From d0863326771581f156a08a8614848745b851201b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Jan 2022 16:47:52 +0100 Subject: [PATCH] First working prototype --- .../java/pgp/cert_d/FileLockingMechanism.java | 92 ++++ .../java/pgp/cert_d/LockingMechanism.java | 30 ++ .../src/main/java/pgp/cert_d/OSUtil.java | 4 + .../cert_d/SharedPGPCertificateDirectory.java | 24 +- .../SharedPGPCertificateDirectoryImpl.java | 421 +++++++++++------- .../src/main/java/pgp/cert_d/SpecialName.java | 4 + .../cert_d/exception/BadDataException.java | 4 + .../cert_d/exception/BadNameException.java | 4 + .../cert_d/exception/NotAStoreException.java | 4 + .../pgp/cert_d/exception/package-info.java | 10 + .../main/java/pgp/cert_d/package-info.java | 10 + .../pgp/certificate_store/Certificate.java | 33 ++ .../certificate_store/CertificateStore.java | 18 +- .../main/java/pgp/certificate_store/Item.java | 43 -- .../pgp/certificate_store/MergeCallback.java | 17 +- .../pgp/certificate_store/ParserBackend.java | 14 + .../pgp/certificate_store/package-info.java | 8 + pgpainless-cert-d/build.gradle | 28 ++ .../SharedPGPCertificateDirectoryTest.java | 99 ++++ pgpainless-core/build.gradle | 1 + .../certificate_store/CertificateParser.java | 49 ++ .../certificate_store/package-info.java | 8 + settings.gradle | 1 + 23 files changed, 707 insertions(+), 219 deletions(-) create mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/FileLockingMechanism.java create mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/LockingMechanism.java create mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/exception/package-info.java create mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/package-info.java create mode 100644 pgp-certificate-store/src/main/java/pgp/certificate_store/Certificate.java delete mode 100644 pgp-certificate-store/src/main/java/pgp/certificate_store/Item.java create mode 100644 pgp-certificate-store/src/main/java/pgp/certificate_store/ParserBackend.java create mode 100644 pgp-certificate-store/src/main/java/pgp/certificate_store/package-info.java create mode 100644 pgpainless-cert-d/build.gradle create mode 100644 pgpainless-cert-d/src/test/java/org/pgpainless/cert_d/SharedPGPCertificateDirectoryTest.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/certificate_store/CertificateParser.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/certificate_store/package-info.java diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/FileLockingMechanism.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/FileLockingMechanism.java new file mode 100644 index 00000000..785e5f23 --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/FileLockingMechanism.java @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// 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(); + } +} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/LockingMechanism.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/LockingMechanism.java new file mode 100644 index 00000000..520a857d --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/LockingMechanism.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// 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; + +} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/OSUtil.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/OSUtil.java index d2459697..1accf635 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/OSUtil.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/OSUtil.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package pgp.cert_d; import java.io.File; diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectory.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectory.java index 7e5cfe71..da2f896e 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectory.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectory.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// 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 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 items(); Iterator fingerprints(); } diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectoryImpl.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectoryImpl.java index 1b5041f4..a75d2b50 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectoryImpl.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectoryImpl.java @@ -1,33 +1,44 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// 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 items() { + return new Iterator() { + + private final Queue> certificateQueue = new SynchronousQueue<>(); + + // Constructor... wtf. + { + for (SpecialName specialName : SpecialName.values()) { + File certFile = getCertFile(specialName); + if (certFile.exists()) { + certificateQueue.add( + new Lazy() { + @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() { + @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 items() { - return null; + private abstract static class Lazy { + abstract E get() throws BadDataException; } @Override public Iterator 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 certificates = items(); + return new Iterator() { + @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; - } - } + }; } } diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/SpecialName.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/SpecialName.java index 7b08269d..874d8996 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/SpecialName.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/SpecialName.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package pgp.cert_d; import java.util.HashMap; diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/BadDataException.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/BadDataException.java index 56a1d2bd..7dc8b7d9 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/BadDataException.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/BadDataException.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package pgp.cert_d.exception; /** diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/BadNameException.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/BadNameException.java index 5d398c36..a4695c0c 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/BadNameException.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/BadNameException.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package pgp.cert_d.exception; /** diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/NotAStoreException.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/NotAStoreException.java index e03561ea..734de499 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/NotAStoreException.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/NotAStoreException.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package pgp.cert_d.exception; /** diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/package-info.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/package-info.java new file mode 100644 index 00000000..9d362808 --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/package-info.java @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Exceptions defined by the Shared PGP Certificate Directory. + * + * @see Failure Modes + */ +package pgp.cert_d.exception; diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/package-info.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/package-info.java new file mode 100644 index 00000000..030e7fdd --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/package-info.java @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * An implementation of the Shared PGP Certificate Directory for java. + * + * @see Shared PGP Certificate Directory + */ +package pgp.cert_d; diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/Certificate.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/Certificate.java new file mode 100644 index 00000000..b2d5015a --- /dev/null +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/Certificate.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.certificate_store; + +import java.io.IOException; +import java.io.InputStream; + +public abstract class Certificate { + /** + * Return the fingerprint of the certificate as 40 lowercase hex characters. + * TODO: Allow OpenPGP V5 fingerprints + * + * @return fingerprint + */ + public abstract String getFingerprint(); + + /** + * Return an {@link InputStream} of the binary representation of the certificate. + * + * @return input stream + */ + public abstract InputStream getInputStream() throws IOException; + + /** + * Return a tag of the certificate. + * The tag is a checksum calculated over the binary representation of the certificate. + * + * @return tag + */ + public abstract String getTag() throws IOException; +} diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateStore.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateStore.java index 459a6f18..6334d3f7 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateStore.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateStore.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package pgp.certificate_store; import java.io.IOException; @@ -6,19 +10,19 @@ import java.util.Iterator; public interface CertificateStore { - Item get(String identifier) throws IOException; + Certificate get(String identifier) throws IOException; - Item getIfChanged(String identifier, String tag) throws IOException; + Certificate getIfChanged(String identifier, String tag) throws IOException; - Item insert(InputStream data, MergeCallback merge) throws IOException; + Certificate insert(InputStream data, MergeCallback merge) throws IOException; - Item tryInsert(InputStream data, MergeCallback merge) throws IOException; + Certificate tryInsert(InputStream data, MergeCallback merge) throws IOException; - Item insertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException; + Certificate insertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException; - Item tryInsertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException; + Certificate tryInsertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException; - Iterator items(); + Iterator items(); Iterator fingerprints(); } diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/Item.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/Item.java deleted file mode 100644 index 28df9613..00000000 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/Item.java +++ /dev/null @@ -1,43 +0,0 @@ -package pgp.certificate_store; - -import java.io.InputStream; - -public class Item { - - private final String fingerprint; - private final String tag; - private final InputStream data; - - public Item(String fingerprint, String tag, InputStream data) { - this.fingerprint = fingerprint; - this.tag = tag; - this.data = data; - } - - /** - * Return the fingerprint of the certificate. - * - * @return certificate fingerprint - */ - public String getFingerprint() { - return fingerprint; - } - - /** - * Return a tag used to check if the certificate was changed between retrievals. - * - * @return tag - */ - public String getTag() { - return tag; - } - - /** - * Return an {@link InputStream} containing the certificate data. - * - * @return data - */ - public InputStream getData() { - return data; - } -} diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/MergeCallback.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/MergeCallback.java index 468d2e5c..b88d5827 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/MergeCallback.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/MergeCallback.java @@ -1,7 +1,8 @@ -package pgp.certificate_store; +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 -import java.io.InputStream; -import java.io.OutputStream; +package pgp.certificate_store; /** * Merge a given certificate (update) with an existing certificate. @@ -11,12 +12,12 @@ public interface MergeCallback { /** * Merge the given certificate data with the existing certificate and return the result. * - * If no existing certificate is found (i.e. existing is null), this method returns the binary representation of data. + * If no existing certificate is found (i.e. existing is null), this method returns the unmodified data. * - * @param data input stream containing the certificate - * @param existing optional input stream containing an already existing copy of the certificate - * @return output stream containing the binary representation of the merged certificate + * @param data certificate + * @param existing optional already existing copy of the certificate + * @return merged certificate */ - OutputStream merge(InputStream data, InputStream existing); + Certificate merge(Certificate data, Certificate existing); } diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/ParserBackend.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/ParserBackend.java new file mode 100644 index 00000000..7bbc0f2b --- /dev/null +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/ParserBackend.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.certificate_store; + +import java.io.IOException; +import java.io.InputStream; + +public interface ParserBackend { + + Certificate readCertificate(InputStream inputStream) throws IOException; + +} diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/package-info.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/package-info.java new file mode 100644 index 00000000..39164d41 --- /dev/null +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Abstract definitions of an OpenPGP certificate store. + */ +package pgp.certificate_store; diff --git a/pgpainless-cert-d/build.gradle b/pgpainless-cert-d/build.gradle new file mode 100644 index 00000000..9d5ecb27 --- /dev/null +++ b/pgpainless-cert-d/build.gradle @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +plugins { + id 'java-library' +} + +group 'org.pgpainless' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + // Logging + testImplementation "ch.qos.logback:logback-classic:$logbackVersion" + + api project(":pgpainless-core") + api project(":pgp-cert-d-java") +} + +test { + useJUnitPlatform() +} diff --git a/pgpainless-cert-d/src/test/java/org/pgpainless/cert_d/SharedPGPCertificateDirectoryTest.java b/pgpainless-cert-d/src/test/java/org/pgpainless/cert_d/SharedPGPCertificateDirectoryTest.java new file mode 100644 index 00000000..32fda258 --- /dev/null +++ b/pgpainless-cert-d/src/test/java/org/pgpainless/cert_d/SharedPGPCertificateDirectoryTest.java @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cert_d; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.platform.commons.logging.Logger; +import org.junit.platform.commons.logging.LoggerFactory; +import org.pgpainless.PGPainless; +import org.pgpainless.certificate_store.CertificateParser; +import org.pgpainless.key.OpenPgpFingerprint; +import pgp.cert_d.FileLockingMechanism; +import pgp.cert_d.LockingMechanism; +import pgp.cert_d.SharedPGPCertificateDirectory; +import pgp.cert_d.SharedPGPCertificateDirectoryImpl; +import pgp.cert_d.exception.BadDataException; +import pgp.cert_d.exception.BadNameException; +import pgp.cert_d.exception.NotAStoreException; +import pgp.certificate_store.Certificate; +import pgp.certificate_store.MergeCallback; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class SharedPGPCertificateDirectoryTest { + + Logger logger = LoggerFactory.getLogger(SharedPGPCertificateDirectoryTest.class); + SharedPGPCertificateDirectory directory; + + private static MergeCallback dummyMerge = new MergeCallback() { + @Override + public Certificate merge(Certificate data, Certificate existing) { + return data; + } + }; + + @BeforeEach + public void beforeEach() throws IOException, NotAStoreException { + File tempDir = Files.createTempDirectory("pgp.cert.d-").toFile(); + tempDir.deleteOnExit(); + directory = new SharedPGPCertificateDirectoryImpl(tempDir, new CertificateParser()); + } + + @Test + public void simpleInsertGet() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException, BadDataException, InterruptedException, BadNameException { + logger.info(() -> "simpleInsertGet: " + ((SharedPGPCertificateDirectoryImpl) directory).getBaseDirectory().getAbsolutePath()); + PGPSecretKeyRing key = PGPainless.generateKeyRing().modernKeyRing("Alice", null); + PGPPublicKeyRing cert = PGPainless.extractCertificate(key); + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(cert); + ByteArrayInputStream certIn = new ByteArrayInputStream(cert.getEncoded()); + + // standard case: get() is null + assertNull(directory.get(fingerprint.toString().toLowerCase())); + + // insert and check returned certs fingerprint + Certificate certificate = directory.insert(certIn, dummyMerge); + assertEquals(fingerprint.toString().toLowerCase(), certificate.getFingerprint()); + + // getIfChanged + assertNull(directory.getIfChanged(certificate.getFingerprint(), certificate.getTag())); + assertNotNull(directory.getIfChanged(certificate.getFingerprint(), "invalidTag")); + + // tryInsert + certIn = new ByteArrayInputStream(cert.getEncoded()); + assertNotNull(directory.tryInsert(certIn, dummyMerge)); + } + + @Test + public void tryInsertFailsWithLockedStore() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException, BadDataException, InterruptedException { + SharedPGPCertificateDirectoryImpl fileDirectory = (SharedPGPCertificateDirectoryImpl) directory; + logger.info(() -> "tryInsertFailsWithLockedStore: " + fileDirectory.getBaseDirectory().getAbsolutePath()); + PGPSecretKeyRing key = PGPainless.generateKeyRing().modernKeyRing("Alice", null); + PGPPublicKeyRing cert = PGPainless.extractCertificate(key); + ByteArrayInputStream certIn = new ByteArrayInputStream(cert.getEncoded()); + + File lockFile = new File(fileDirectory.getBaseDirectory(), "writelock"); + LockingMechanism lock = new FileLockingMechanism(lockFile); + lock.lockDirectory(); + + assertNull(directory.tryInsert(certIn, dummyMerge)); + + lock.releaseDirectory(); + + assertNotNull(directory.tryInsert(certIn, dummyMerge)); + } +} diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index e6947458..2d19f358 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -17,6 +17,7 @@ dependencies { api "org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion" api "org.bouncycastle:bcpg-jdk15on:$bouncyCastleVersion" + api project(":pgp-certificate-store") // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' diff --git a/pgpainless-core/src/main/java/org/pgpainless/certificate_store/CertificateParser.java b/pgpainless-core/src/main/java/org/pgpainless/certificate_store/CertificateParser.java new file mode 100644 index 00000000..4b1b3de2 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/certificate_store/CertificateParser.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.certificate_store; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.util.encoders.Base64; +import org.pgpainless.PGPainless; +import org.pgpainless.key.OpenPgpFingerprint; +import pgp.certificate_store.Certificate; +import pgp.certificate_store.ParserBackend; + +public class CertificateParser implements ParserBackend { + + @Override + public Certificate readCertificate(InputStream inputStream) throws IOException { + final PGPPublicKeyRing certificate = PGPainless.readKeyRing().publicKeyRing(inputStream); + return new Certificate() { + @Override + public String getFingerprint() { + return OpenPgpFingerprint.of(certificate).toString().toLowerCase(); + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(certificate.getEncoded()); + } + + @Override + public String getTag() throws IOException { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("No MessageDigest for SHA-256 instantiated, although BC is on the classpath: " + e.getMessage()); + } + digest.update(certificate.getEncoded()); + return Base64.toBase64String(digest.digest()); + } + }; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/certificate_store/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/certificate_store/package-info.java new file mode 100644 index 00000000..1c6f056a --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/certificate_store/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Implementations of the module pgp-certificate-store using pgpainless-core. + */ +package org.pgpainless.certificate_store; diff --git a/settings.gradle b/settings.gradle index 2a244920..954cb07f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,6 +7,7 @@ rootProject.name = 'PGPainless' include 'pgpainless-core', 'pgpainless-sop', 'pgpainless-cli', + 'pgpainless-cert-d', 'pgp-certificate-store', 'pgp-cert-d-java'