1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-12-16 09:11:08 +01:00

First working prototype

This commit is contained in:
Paul Schaub 2022-01-24 16:47:52 +01:00
parent 3a690079fe
commit 50243aa6b6
23 changed files with 707 additions and 219 deletions

View file

@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
public class FileLockingMechanism implements LockingMechanism {
private final File lockFile;
private RandomAccessFile randomAccessFile;
private FileLock fileLock;
public FileLockingMechanism(File lockFile) {
this.lockFile = lockFile;
}
@Override
public synchronized void lockDirectory() throws IOException, InterruptedException {
if (randomAccessFile != null) {
// we own the lock already. Let's wait...
this.wait();
}
try {
randomAccessFile = new RandomAccessFile(lockFile, "rw");
} catch (FileNotFoundException e) {
lockFile.createNewFile();
randomAccessFile = new RandomAccessFile(lockFile, "rw");
}
fileLock = randomAccessFile.getChannel().lock();
}
@Override
public synchronized boolean tryLockDirectory() throws IOException {
if (randomAccessFile != null) {
// We already locked the directory for another write operation.
// We fail, since we have not yet released the lock from the other operation.
return false;
}
try {
randomAccessFile = new RandomAccessFile(lockFile, "rw");
} catch (FileNotFoundException e) {
lockFile.createNewFile();
randomAccessFile = new RandomAccessFile(lockFile, "rw");
}
try {
fileLock = randomAccessFile.getChannel().tryLock();
if (fileLock == null) {
// try-lock failed, file is locked by another process.
randomAccessFile.close();
randomAccessFile = null;
return false;
}
} catch (OverlappingFileLockException e) {
// Some other object is holding the lock.
randomAccessFile.close();
randomAccessFile = null;
return false;
}
return true;
}
@Override
public synchronized void releaseDirectory() throws IOException {
// unlock file
if (fileLock != null) {
fileLock.release();
fileLock = null;
}
// close file
if (randomAccessFile != null) {
randomAccessFile.close();
randomAccessFile = null;
}
// delete file
if (lockFile.exists()) {
lockFile.delete();
}
// notify waiters
this.notify();
}
}

View file

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.io.IOException;
public interface LockingMechanism {
/**
* Lock the store for writes.
* Readers can continue to use the store and will always see consistent certs.
*/
void lockDirectory() throws IOException, InterruptedException;
/**
* Try top lock the store for writes.
* Return false without locking the store in case the store was already locked.
*
* @return true if locking succeeded, false otherwise
*/
boolean tryLockDirectory() throws IOException;
/**
* Release the directory write-lock acquired via {@link #lockDirectory()}.
*/
void releaseDirectory() throws IOException;
}

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.io.File;

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.io.IOException;
@ -6,24 +10,28 @@ import java.util.Iterator;
import pgp.cert_d.exception.BadDataException;
import pgp.cert_d.exception.BadNameException;
import pgp.certificate_store.Item;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.MergeCallback;
public interface SharedPGPCertificateDirectory {
Item get(String identifier) throws IOException, BadNameException;
Certificate get(String fingerprint) throws IOException, BadNameException;
Item getIfChanged(String identifier, String tag) throws IOException, BadNameException;
Certificate get(SpecialName specialName) throws IOException, BadNameException;
Item insert(InputStream data, MergeCallback merge) throws IOException, BadDataException;
Certificate getIfChanged(String fingerprint, String tag) throws IOException, BadNameException;
Item tryInsert(InputStream data, MergeCallback merge) throws IOException, BadDataException;
Certificate getIfChanged(SpecialName specialName, String tag) throws IOException, BadNameException;
Item insertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException, BadDataException, BadNameException;
Certificate insert(InputStream data, MergeCallback merge) throws IOException, BadDataException, InterruptedException;
Item tryInsertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException, BadDataException, BadNameException;
Certificate tryInsert(InputStream data, MergeCallback merge) throws IOException, BadDataException;
Iterator<Item> items();
Certificate insertSpecial(SpecialName specialName, InputStream data, MergeCallback merge) throws IOException, BadDataException, BadNameException, InterruptedException;
Certificate tryInsertSpecial(SpecialName specialName, InputStream data, MergeCallback merge) throws IOException, BadDataException, BadNameException;
Iterator<Certificate> items();
Iterator<String> fingerprints();
}

View file

@ -1,33 +1,44 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.SynchronousQueue;
import java.util.regex.Pattern;
import pgp.cert_d.exception.BadDataException;
import pgp.cert_d.exception.BadNameException;
import pgp.cert_d.exception.NotAStoreException;
import pgp.certificate_store.Item;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.MergeCallback;
import pgp.certificate_store.ParserBackend;
public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDirectory {
private final File baseDirectory;
private final Pattern openPgpV4FingerprintPattern = Pattern.compile("^[a-f0-9]{40}$");
private final WriteLock writeLock;
private final LockingMechanism writeLock;
private final ParserBackend parserBackend;
public SharedPGPCertificateDirectoryImpl() throws NotAStoreException {
this(OSUtil.getDefaultBaseDir());
public SharedPGPCertificateDirectoryImpl(ParserBackend parserBackend)
throws NotAStoreException {
this(OSUtil.getDefaultBaseDir(), parserBackend);
}
public SharedPGPCertificateDirectoryImpl(File baseDirectory) throws NotAStoreException {
public SharedPGPCertificateDirectoryImpl(File baseDirectory, ParserBackend parserBackend)
throws NotAStoreException {
this.parserBackend = parserBackend;
this.baseDirectory = baseDirectory;
if (!baseDirectory.exists()) {
if (!baseDirectory.mkdirs()) {
@ -38,167 +49,271 @@ public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDi
throw new NotAStoreException("Base directory '" + getBaseDirectory().getAbsolutePath() + "' appears to be a file.");
}
}
writeLock = new WriteLock(new File(getBaseDirectory(), "writelock"));
writeLock = new FileLockingMechanism(new File(getBaseDirectory(), "writelock"));
}
public File getBaseDirectory() {
return baseDirectory;
}
private File getCertFile(String identifier) throws BadNameException {
SpecialName specialName = SpecialName.fromString(identifier);
if (specialName != null) {
// is special name
return new File(getBaseDirectory(), specialName.getValue());
} else {
if (!isFingerprint(identifier)) {
throw new BadNameException();
private File getCertFile(String fingerprint) throws BadNameException {
if (!isFingerprint(fingerprint)) {
throw new BadNameException();
}
// is fingerprint
File subdirectory = new File(getBaseDirectory(), fingerprint.substring(0, 2));
File file = new File(subdirectory, fingerprint.substring(2));
return file;
}
private File getCertFile(SpecialName specialName) {
return new File(getBaseDirectory(), specialName.getValue());
}
private boolean isFingerprint(String fingerprint) {
return openPgpV4FingerprintPattern.matcher(fingerprint).matches();
}
@Override
public Certificate get(String fingerprint) throws IOException, BadNameException {
File certFile = getCertFile(fingerprint);
if (!certFile.exists()) {
return null;
}
FileInputStream fileIn = new FileInputStream(certFile);
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
Certificate certificate = parserBackend.readCertificate(bufferedIn);
if (!certificate.getFingerprint().equals(fingerprint)) {
// TODO: Figure out more suitable exception
throw new BadNameException();
}
return certificate;
}
@Override
public Certificate get(SpecialName specialName) throws IOException {
File certFile = getCertFile(specialName);
if (!certFile.exists()) {
return null;
}
FileInputStream fileIn = new FileInputStream(certFile);
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
Certificate certificate = parserBackend.readCertificate(bufferedIn);
return certificate;
}
@Override
public Certificate getIfChanged(String fingerprint, String tag) throws IOException, BadNameException {
Certificate certificate = get(fingerprint);
if (certificate.getTag().equals(tag)) {
return null;
}
return certificate;
}
@Override
public Certificate getIfChanged(SpecialName specialName, String tag) throws IOException {
Certificate certificate = get(specialName);
if (certificate.getTag().equals(tag)) {
return null;
}
return certificate;
}
@Override
public Certificate insert(InputStream data, MergeCallback merge) throws IOException, BadDataException, InterruptedException {
writeLock.lockDirectory();
Certificate certificate = _insert(data, merge);
writeLock.releaseDirectory();
return certificate;
}
@Override
public Certificate tryInsert(InputStream data, MergeCallback merge) throws IOException, BadDataException {
if (!writeLock.tryLockDirectory()) {
return null;
}
Certificate certificate = _insert(data, merge);
writeLock.releaseDirectory();
return certificate;
}
private Certificate _insert(InputStream data, MergeCallback merge) throws IOException, BadDataException {
Certificate newCertificate = parserBackend.readCertificate(data);
Certificate existingCertificate;
File certFile;
try {
existingCertificate = get(newCertificate.getFingerprint());
certFile = getCertFile(newCertificate.getFingerprint());
} catch (BadNameException e) {
throw new BadDataException();
}
if (existingCertificate != null && !existingCertificate.getTag().equals(newCertificate.getTag())) {
newCertificate = merge.merge(newCertificate, existingCertificate);
}
writeCertificate(newCertificate, certFile);
return newCertificate;
}
private void writeCertificate(Certificate certificate, File certFile) throws IOException {
certFile.getParentFile().mkdirs();
if (!certFile.exists() && !certFile.createNewFile()) {
throw new IOException("Could not create cert file " + certFile.getAbsolutePath());
}
InputStream certIn = certificate.getInputStream();
FileOutputStream fileOut = new FileOutputStream(certFile);
byte[] buffer = new byte[4096];
int read;
while ((read = certIn.read(buffer)) != -1) {
fileOut.write(buffer, 0, read);
}
certIn.close();
fileOut.close();
}
@Override
public Certificate insertSpecial(SpecialName specialName, InputStream data, MergeCallback merge) throws IOException, BadNameException, BadDataException, InterruptedException {
writeLock.lockDirectory();
Certificate certificate = _insertSpecial(specialName, data, merge);
writeLock.releaseDirectory();
return certificate;
}
@Override
public Certificate tryInsertSpecial(SpecialName specialName, InputStream data, MergeCallback merge) throws IOException, BadNameException, BadDataException {
if (!writeLock.tryLockDirectory()) {
return null;
}
Certificate certificate = _insertSpecial(specialName, data, merge);
writeLock.releaseDirectory();
return certificate;
}
private Certificate _insertSpecial(SpecialName specialName, InputStream data, MergeCallback merge) throws IOException, BadNameException, BadDataException {
Certificate newCertificate = parserBackend.readCertificate(data);
Certificate existingCertificate = get(specialName);
File certFile = getCertFile(specialName);
if (existingCertificate != null && !existingCertificate.getTag().equals(newCertificate.getTag())) {
newCertificate = merge.merge(newCertificate, existingCertificate);
}
writeCertificate(newCertificate, certFile);
return newCertificate;
}
@Override
public Iterator<Certificate> items() {
return new Iterator<Certificate>() {
private final Queue<Lazy<Certificate>> certificateQueue = new SynchronousQueue<>();
// Constructor... wtf.
{
for (SpecialName specialName : SpecialName.values()) {
File certFile = getCertFile(specialName);
if (certFile.exists()) {
certificateQueue.add(
new Lazy<Certificate>() {
@Override
Certificate get() {
try {
return parserBackend.readCertificate(new FileInputStream(certFile));
} catch (IOException e) {
throw new AssertionError("File got deleted.");
}
}
});
}
}
File[] subdirectories = baseDirectory.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory() && file.getName().matches("^[a-f0-9]{2}$");
}
});
for (File subdirectory : subdirectories) {
File[] files = subdirectory.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isFile() && file.getName().matches("^[a-f0-9]{38}$");
}
});
for (File certFile : files) {
certificateQueue.add(new Lazy<Certificate>() {
@Override
Certificate get() throws BadDataException {
try {
Certificate certificate = parserBackend.readCertificate(new FileInputStream(certFile));
if (!(subdirectory.getName() + certFile.getName()).equals(certificate.getFingerprint())) {
throw new BadDataException();
}
return certificate;
} catch (IOException e) {
throw new AssertionError("File got deleted.");
}
}
});
}
}
}
// is fingerprint
File subdirectory = new File(getBaseDirectory(), identifier.substring(0, 2));
File file = new File(subdirectory, identifier.substring(2));
return file;
}
@Override
public boolean hasNext() {
return !certificateQueue.isEmpty();
}
@Override
public Certificate next() {
try {
return certificateQueue.poll().get();
} catch (BadDataException e) {
throw new AssertionError("Could not retrieve item: " + e.getMessage());
}
}
};
}
private boolean isFingerprint(String identifier) {
return openPgpV4FingerprintPattern.matcher(identifier).matches();
}
@Override
public Item get(String identifier) throws IOException, BadNameException {
File certFile = getCertFile(identifier);
if (certFile.exists()) {
return new Item(identifier, "TAG", new FileInputStream(certFile));
}
return null;
}
@Override
public Item getIfChanged(String identifier, String tag) throws IOException, BadNameException {
return null;
}
@Override
public Item insert(InputStream data, MergeCallback merge) throws IOException, BadDataException {
writeLock.lock();
Item item = _insert(data, merge);
writeLock.release();
return item;
}
@Override
public Item tryInsert(InputStream data, MergeCallback merge) throws IOException, BadDataException {
if (!writeLock.tryLock()) {
return null;
}
Item item = _insert(data, merge);
writeLock.release();
return item;
}
private Item _insert(InputStream data, MergeCallback merge) throws IOException, BadDataException {
return null;
}
@Override
public Item insertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException, BadNameException, BadDataException {
writeLock.lock();
Item item = _insertSpecial(specialName, data, merge);
writeLock.release();
return item;
}
@Override
public Item tryInsertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException, BadNameException, BadDataException {
if (!writeLock.tryLock()) {
return null;
}
Item item = _insertSpecial(specialName, data, merge);
writeLock.release();
return item;
}
private Item _insertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException, BadNameException, BadDataException {
return null;
}
@Override
public Iterator<Item> items() {
return null;
private abstract static class Lazy<E> {
abstract E get() throws BadDataException;
}
@Override
public Iterator<String> fingerprints() {
return null;
}
public static class WriteLock {
private final File lockFile;
private RandomAccessFile randomAccessFile;
private FileLock fileLock;
public WriteLock(File lockFile) {
this.lockFile = lockFile;
}
public synchronized void lock() throws IOException {
if (randomAccessFile != null) {
throw new IllegalStateException("File already locked.");
Iterator<Certificate> certificates = items();
return new Iterator<String>() {
@Override
public boolean hasNext() {
return certificates.hasNext();
}
try {
randomAccessFile = new RandomAccessFile(lockFile, "rw");
} catch (FileNotFoundException e) {
lockFile.createNewFile();
randomAccessFile = new RandomAccessFile(lockFile, "rw");
@Override
public String next() {
return certificates.next().getFingerprint();
}
fileLock = randomAccessFile.getChannel().lock();
}
public synchronized boolean tryLock() throws IOException {
if (randomAccessFile != null) {
return false;
}
try {
randomAccessFile = new RandomAccessFile(lockFile, "rw");
} catch (FileNotFoundException e) {
lockFile.createNewFile();
randomAccessFile = new RandomAccessFile(lockFile, "rw");
}
fileLock = randomAccessFile.getChannel().tryLock();
if (fileLock == null) {
randomAccessFile.close();
randomAccessFile = null;
return false;
}
return true;
}
public synchronized void release() throws IOException {
if (lockFile.exists()) {
lockFile.delete();
}
if (fileLock != null) {
fileLock.release();
fileLock = null;
}
if (randomAccessFile != null) {
randomAccessFile.close();
randomAccessFile = null;
}
}
};
}
}

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.util.HashMap;

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.exception;
/**

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.exception;
/**

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.exception;
/**

View file

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Exceptions defined by the Shared PGP Certificate Directory.
*
* @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/#name-failure-modes">Failure Modes</a>
*/
package pgp.cert_d.exception;

View file

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* An implementation of the Shared PGP Certificate Directory for java.
*
* @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/">Shared PGP Certificate Directory</a>
*/
package pgp.cert_d;