From 7c39781d15f6f360a58a2a7a5c4bb197d0f0ba3d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 9 Aug 2022 17:50:15 +0200 Subject: [PATCH] Rewrite of PGPCertificateDirectory using more flexible backend --- .../main/java/pgp/cert_d/BackendProvider.java | 16 - ...gSharedPGPCertificateDirectoryWrapper.java | 249 ----------- .../FileBasedCertificateDirectoryBackend.java | 405 ++++++++++++++++++ .../java/pgp/cert_d/FileLockingMechanism.java | 96 ----- .../java/pgp/cert_d/FilenameResolver.java | 89 ---- .../InMemoryCertificateDirectoryBackend.java | 116 +++++ .../java/pgp/cert_d/LockingMechanism.java | 37 -- .../pgp/cert_d/PGPCertificateDirectories.java | 33 ++ .../pgp/cert_d/PGPCertificateDirectory.java | 194 +++++++++ .../ReadOnlyPGPCertificateDirectory.java | 28 ++ .../cert_d/SharedPGPCertificateDirectory.java | 61 --- .../SharedPGPCertificateDirectoryImpl.java | 405 ------------------ .../WritingPGPCertificateDirectory.java | 39 ++ .../java/pgp/cert_d/FilenameResolverTest.java | 4 +- .../pgp/certificate_store/Certificate.java | 32 +- .../CertificateDirectory.java | 30 +- .../certificate_store/CertificateMerger.java | 27 -- .../main/java/pgp/certificate_store/Key.java | 16 +- .../pgp/certificate_store/KeyMaterial.java | 23 + .../certificate_store/KeyMaterialMerger.java | 25 ++ ...end.java => KeyMaterialReaderBackend.java} | 2 +- .../java/pgp/certificate_store/KeyMerger.java | 25 -- 22 files changed, 889 insertions(+), 1063 deletions(-) delete mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/BackendProvider.java delete mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/CachingSharedPGPCertificateDirectoryWrapper.java create mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/FileBasedCertificateDirectoryBackend.java delete mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/FileLockingMechanism.java delete mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/FilenameResolver.java create mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/InMemoryCertificateDirectoryBackend.java delete 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/PGPCertificateDirectories.java create mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateDirectory.java create mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/ReadOnlyPGPCertificateDirectory.java delete mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectory.java delete mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectoryImpl.java create mode 100644 pgp-cert-d-java/src/main/java/pgp/cert_d/WritingPGPCertificateDirectory.java delete mode 100644 pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateMerger.java create mode 100644 pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMaterialMerger.java rename pgp-certificate-store/src/main/java/pgp/certificate_store/{KeyReaderBackend.java => KeyMaterialReaderBackend.java} (94%) delete mode 100644 pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMerger.java diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/BackendProvider.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/BackendProvider.java deleted file mode 100644 index ff93dd2..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/BackendProvider.java +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d; - -import pgp.certificate_store.CertificateMerger; -import pgp.certificate_store.KeyReaderBackend; - -public abstract class BackendProvider { - - public abstract KeyReaderBackend provideKeyReaderBackend(); - - public abstract CertificateMerger provideDefaultMergeCallback(); - -} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/CachingSharedPGPCertificateDirectoryWrapper.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/CachingSharedPGPCertificateDirectoryWrapper.java deleted file mode 100644 index d8bb635..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/CachingSharedPGPCertificateDirectoryWrapper.java +++ /dev/null @@ -1,249 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d; - -import pgp.certificate_store.Key; -import pgp.certificate_store.KeyMerger; -import pgp.certificate_store.exception.BadDataException; -import pgp.certificate_store.exception.BadNameException; -import pgp.certificate_store.Certificate; -import pgp.certificate_store.CertificateMerger; - -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -/** - * Caching wrapper for {@link SharedPGPCertificateDirectory} implementations. - */ -public class CachingSharedPGPCertificateDirectoryWrapper - implements SharedPGPCertificateDirectory { - - private static final Map certTagMap = new HashMap<>(); - private static final Map keyTagMap = new HashMap<>(); - private static final Map certificateMap = new HashMap<>(); - private static final Map keyMap = new HashMap<>(); - private final SharedPGPCertificateDirectory underlyingCertificateDirectory; - - public CachingSharedPGPCertificateDirectoryWrapper(SharedPGPCertificateDirectory wrapped) { - this.underlyingCertificateDirectory = wrapped; - } - - /** - * Store the given certificate under the given identifier into the cache. - * - * @param identifier fingerprint or special name - * @param certificate certificate - */ - private void remember(String identifier, Certificate certificate) { - certificateMap.put(identifier, certificate); - try { - certTagMap.put(identifier, certificate.getTag()); - } catch (IOException e) { - certTagMap.put(identifier, null); - } - } - - /** - * Store the given key under the given identifier into the cache. - * - * @param identifier fingerprint or special name - * @param key key - */ - private void remember(String identifier, Key key) { - keyMap.put(identifier, key); - try { - keyTagMap.put(identifier, key.getTag()); - } catch (IOException e) { - keyTagMap.put(identifier, null); - } - } - /** - * Returns true, if the cached tag differs from the provided tag. - * - * @param identifier fingerprint or special name - * @param tag tag - * @return true if cached tag differs, false otherwise - */ - private boolean certTagChanged(String identifier, String tag) { - String tack = certTagMap.get(identifier); - return !tagEquals(tag, tack); - } - - private boolean keyTagChanged(String identifier, String tag) { - String tack = keyTagMap.get(identifier); - return !tagEquals(tag, tack); - } - - /** - * Return true, if tag and tack are equal, false otherwise. - * @param tag tag - * @param tack other tag - * @return true if equal - */ - private static boolean tagEquals(String tag, String tack) { - return (tag == null && tack == null) - || tag != null && tag.equals(tack); - } - - /** - * Clear the cache. - */ - public void invalidate() { - certificateMap.clear(); - certTagMap.clear(); - } - - @Override - public LockingMechanism getLock() { - return underlyingCertificateDirectory.getLock(); - } - - @Override - public Certificate getByFingerprint(String fingerprint) - throws IOException, BadNameException, BadDataException { - Certificate certificate = certificateMap.get(fingerprint); - if (certificate == null) { - certificate = underlyingCertificateDirectory.getByFingerprint(fingerprint); - if (certificate != null) { - remember(fingerprint, certificate); - } - } - - return certificate; - } - - @Override - public Certificate getBySpecialName(String specialName) - throws IOException, BadNameException, BadDataException { - Certificate certificate = certificateMap.get(specialName); - if (certificate == null) { - certificate = underlyingCertificateDirectory.getBySpecialName(specialName); - if (certificate != null) { - remember(specialName, certificate); - } - } - - return certificate; - } - - @Override - public Certificate getByFingerprintIfChanged(String fingerprint, String tag) - throws IOException, BadNameException, BadDataException { - if (certTagChanged(fingerprint, tag)) { - return getByFingerprint(fingerprint); - } - return null; - } - - @Override - public Certificate getBySpecialNameIfChanged(String specialName, String tag) - throws IOException, BadNameException, BadDataException { - if (certTagChanged(specialName, tag)) { - return getBySpecialName(specialName); - } - return null; - } - - @Override - public Key getTrustRoot() throws IOException, BadDataException { - Key key = keyMap.get(SpecialNames.TRUST_ROOT); - if (key == null) { - key = underlyingCertificateDirectory.getTrustRoot(); - if (key != null) { - remember(SpecialNames.TRUST_ROOT, key); - } - } - return key; - } - - @Override - public Key getTrustRootIfChanged(String tag) throws IOException, BadDataException { - if (keyTagChanged(SpecialNames.TRUST_ROOT, tag)) { - return getTrustRoot(); - } - return null; - } - - @Override - public Certificate insert(InputStream data, CertificateMerger merge) - throws IOException, BadDataException, InterruptedException { - Certificate certificate = underlyingCertificateDirectory.insert(data, merge); - remember(certificate.getFingerprint(), certificate); - return certificate; - } - - @Override - public Certificate tryInsert(InputStream data, CertificateMerger merge) - throws IOException, BadDataException { - Certificate certificate = underlyingCertificateDirectory.tryInsert(data, merge); - if (certificate != null) { - remember(certificate.getFingerprint(), certificate); - } - return certificate; - } - - @Override - public Key insertTrustRoot(InputStream data, KeyMerger merge) throws IOException, BadDataException, InterruptedException { - Key key = underlyingCertificateDirectory.insertTrustRoot(data, merge); - remember(SpecialNames.TRUST_ROOT, key); - return key; - } - - @Override - public Key tryInsertTrustRoot(InputStream data, KeyMerger merge) throws IOException, BadDataException { - Key key = underlyingCertificateDirectory.tryInsertTrustRoot(data, merge); - if (key != null) { - remember(SpecialNames.TRUST_ROOT, key); - } - return key; - } - - @Override - public Certificate insertWithSpecialName(String specialName, InputStream data, CertificateMerger merge) - throws IOException, BadDataException, BadNameException, InterruptedException { - Certificate certificate = underlyingCertificateDirectory.insertWithSpecialName(specialName, data, merge); - remember(specialName, certificate); - return certificate; - } - - @Override - public Certificate tryInsertWithSpecialName(String specialName, InputStream data, CertificateMerger merge) - throws IOException, BadDataException, BadNameException { - Certificate certificate = underlyingCertificateDirectory.tryInsertWithSpecialName(specialName, data, merge); - if (certificate != null) { - remember(specialName, certificate); - } - return certificate; - } - - @Override - public Iterator items() { - - Iterator iterator = underlyingCertificateDirectory.items(); - - return new Iterator() { - @Override - public boolean hasNext() { - return iterator.hasNext(); - } - - @Override - public Certificate next() { - Certificate certificate = iterator.next(); - remember(certificate.getFingerprint(), certificate); - return certificate; - } - }; - } - - @Override - public Iterator fingerprints() { - return underlyingCertificateDirectory.fingerprints(); - } - -} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/FileBasedCertificateDirectoryBackend.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/FileBasedCertificateDirectoryBackend.java new file mode 100644 index 0000000..8b112cb --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/FileBasedCertificateDirectoryBackend.java @@ -0,0 +1,405 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.cert_d; + +import pgp.certificate_store.Certificate; +import pgp.certificate_store.KeyMaterial; +import pgp.certificate_store.KeyMaterialMerger; +import pgp.certificate_store.KeyMaterialReaderBackend; +import pgp.certificate_store.exception.BadDataException; +import pgp.certificate_store.exception.BadNameException; +import pgp.certificate_store.exception.NotAStoreException; + +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.nio.channels.OverlappingFileLockException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Pattern; + +public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirectory.Backend { + + private abstract static class Lazy { + abstract E get() throws BadDataException; + } + + private static class FileLockingMechanism implements PGPCertificateDirectory.LockingMechanism { + + private final File lockFile; + private RandomAccessFile randomAccessFile; + private FileLock fileLock; + + FileLockingMechanism(File lockFile) { + this.lockFile = lockFile; + } + + public static FileLockingMechanism defaultDirectoryFileLock(File baseDirectory) { + return new FileLockingMechanism(new File(baseDirectory, "writelock")); + } + + @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 boolean isLocked() { + return randomAccessFile != null; + } + + @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(); + } + } + + private final File baseDirectory; + private final PGPCertificateDirectory.LockingMechanism lock; + private final FilenameResolver resolver; + private final KeyMaterialReaderBackend reader; + + public FileBasedCertificateDirectoryBackend(File baseDirectory, KeyMaterialReaderBackend reader) throws NotAStoreException { + this.baseDirectory = baseDirectory; + this.resolver = new FilenameResolver(baseDirectory); + + if (!baseDirectory.exists()) { + if (!baseDirectory.mkdirs()) { + throw new NotAStoreException("Cannot create base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "'"); + } + } else { + if (baseDirectory.isFile()) { + throw new NotAStoreException("Base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "' appears to be a file."); + } + } + this.lock = FileLockingMechanism.defaultDirectoryFileLock(baseDirectory); + this.reader = reader; + } + + @Override + public PGPCertificateDirectory.LockingMechanism getLock() { + return lock; + } + + @Override + public Certificate readByFingerprint(String fingerprint) throws BadNameException, IOException, BadDataException { + File certFile = resolver.getCertFileByFingerprint(fingerprint); + if (!certFile.exists()) { + return null; + } + + FileInputStream fileIn = new FileInputStream(certFile); + BufferedInputStream bufferedIn = new BufferedInputStream(fileIn); + + Certificate certificate = reader.read(bufferedIn).asCertificate(); + if (!certificate.getFingerprint().equals(fingerprint)) { + // TODO: Figure out more suitable exception + throw new BadDataException(); + } + + return certificate; + } + + @Override + public KeyMaterial readBySpecialName(String specialName) throws BadNameException, IOException, BadDataException { + File certFile = resolver.getCertFileBySpecialName(specialName); + if (!certFile.exists()) { + return null; + } + + FileInputStream fileIn = new FileInputStream(certFile); + BufferedInputStream bufferedIn = new BufferedInputStream(fileIn); + KeyMaterial keyMaterial = reader.read(bufferedIn); + + return keyMaterial; + } + + @Override + public Iterator readItems() { + return new Iterator() { + + private final List> certificateQueue = Collections.synchronizedList(new ArrayList<>()); + + // Constructor... wtf. + { + 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 = reader.read(new FileInputStream(certFile)).asCertificate(); + if (!(subdirectory.getName() + certFile.getName()).equals(certificate.getFingerprint())) { + throw new BadDataException(); + } + return certificate; + } catch (IOException e) { + throw new AssertionError("File got deleted."); + } + } + }); + } + } + } + + @Override + public boolean hasNext() { + return !certificateQueue.isEmpty(); + } + + @Override + public Certificate next() { + try { + return certificateQueue.remove(0).get(); + } catch (BadDataException e) { + throw new AssertionError("Could not retrieve item: " + e.getMessage()); + } + } + }; + } + + @Override + public KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge) throws BadDataException, IOException { + KeyMaterial newCertificate = reader.read(data); + KeyMaterial existingCertificate; + File certFile; + try { + existingCertificate = readBySpecialName(SpecialNames.TRUST_ROOT); + certFile = resolver.getCertFileBySpecialName(SpecialNames.TRUST_ROOT); + } catch (BadNameException e) { + throw new BadDataException(); + } + + if (existingCertificate != null && !newCertificate.getTag().equals(existingCertificate.getTag())) { + newCertificate = merge.merge(newCertificate, existingCertificate); + } + + writeToFile(newCertificate.getInputStream(), certFile); + + return newCertificate; + } + + @Override + public Certificate doInsert(InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException { + KeyMaterial newCertificate = reader.read(data); + Certificate existingCertificate; + File certFile; + try { + existingCertificate = readByFingerprint(newCertificate.getFingerprint()); + certFile = resolver.getCertFileByFingerprint(newCertificate.getFingerprint()); + } catch (BadNameException e) { + throw new BadDataException(); + } + + if (existingCertificate != null && !newCertificate.getTag().equals(existingCertificate.getTag())) { + newCertificate = merge.merge(newCertificate, existingCertificate); + } + + writeToFile(newCertificate.getInputStream(), certFile); + + return newCertificate.asCertificate(); + } + + @Override + public Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException, BadNameException { + KeyMaterial newCertificate = reader.read(data); + KeyMaterial existingCertificate; + File certFile; + try { + existingCertificate = readBySpecialName(specialName); + certFile = resolver.getCertFileBySpecialName(specialName); + } catch (BadNameException e) { + throw new BadDataException(); + } + + if (existingCertificate != null && !newCertificate.getTag().equals(existingCertificate.getTag())) { + newCertificate = merge.merge(newCertificate, existingCertificate); + } + + writeToFile(newCertificate.getInputStream(), certFile); + + return newCertificate.asCertificate(); + } + + private void writeToFile(InputStream inputStream, File certFile) + throws IOException { + certFile.getParentFile().mkdirs(); + if (!certFile.exists() && !certFile.createNewFile()) { + throw new IOException("Could not create cert file " + certFile.getAbsolutePath()); + } + + FileOutputStream fileOut = new FileOutputStream(certFile); + + byte[] buffer = new byte[4096]; + int read; + while ((read = inputStream.read(buffer)) != -1) { + fileOut.write(buffer, 0, read); + } + + inputStream.close(); + fileOut.close(); + } + + public static class FilenameResolver { + + private final File baseDirectory; + private final Pattern openPgpV4FingerprintPattern = Pattern.compile("^[a-f0-9]{40}$"); + + public FilenameResolver(File baseDirectory) { + this.baseDirectory = baseDirectory; + } + + public File getBaseDirectory() { + return baseDirectory; + } + + /** + * Calculate the file location for the certificate addressed by the given + * lowercase hexadecimal OpenPGP fingerprint. + * + * @param fingerprint fingerprint + * @return absolute certificate file location + * + * @throws BadNameException if the given fingerprint string is not a fingerprint + */ + public File getCertFileByFingerprint(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; + } + + /** + * Calculate the file location for the certification addressed using the given special name. + * For known special names, see {@link SpecialNames}. + * + * @param specialName special name (e.g. "trust-root") + * @return absolute certificate file location + * + * @throws BadNameException in case the given special name is not known + */ + public File getCertFileBySpecialName(String specialName) + throws BadNameException { + if (!isSpecialName(specialName)) { + throw new BadNameException(String.format("%s is not a known special name", specialName)); + } + + return new File(getBaseDirectory(), specialName); + } + + /** + * Calculate the file location for the key addressed using the given special name. + * For known special names, see {@link SpecialNames}. + * + * @param specialName special name (e.g. "trust-root") + * @return absolute key file location + * + * @throws BadNameException in case the given special name is not known + */ + public File getKeyFileBySpecialName(String specialName) + throws BadNameException { + if (!isSpecialName(specialName)) { + throw new BadNameException(String.format("%s is not a known special name", specialName)); + } + + return new File(getBaseDirectory(), specialName + ".key"); + } + + private boolean isFingerprint(String fingerprint) { + return openPgpV4FingerprintPattern.matcher(fingerprint).matches(); + } + + private boolean isSpecialName(String specialName) { + return SpecialNames.lookupSpecialName(specialName) != null; + } + + } +} 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 deleted file mode 100644 index 2d87c04..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/FileLockingMechanism.java +++ /dev/null @@ -1,96 +0,0 @@ -// 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; - } - - public static FileLockingMechanism defaultDirectoryFileLock(File baseDirectory) { - return new FileLockingMechanism(new File(baseDirectory, "writelock")); - } - - @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/FilenameResolver.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/FilenameResolver.java deleted file mode 100644 index 0bbdbc1..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/FilenameResolver.java +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d; - -import pgp.certificate_store.exception.BadNameException; - -import java.io.File; -import java.util.regex.Pattern; - -public class FilenameResolver { - - private final File baseDirectory; - private final Pattern openPgpV4FingerprintPattern = Pattern.compile("^[a-f0-9]{40}$"); - - public FilenameResolver(File baseDirectory) { - this.baseDirectory = baseDirectory; - } - - public File getBaseDirectory() { - return baseDirectory; - } - - /** - * Calculate the file location for the certificate addressed by the given - * lowercase hexadecimal OpenPGP fingerprint. - * - * @param fingerprint fingerprint - * @return absolute certificate file location - * - * @throws BadNameException if the given fingerprint string is not a fingerprint - */ - public File getCertFileByFingerprint(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; - } - - /** - * Calculate the file location for the certification addressed using the given special name. - * For known special names, see {@link SpecialNames}. - * - * @param specialName special name (e.g. "trust-root") - * @return absolute certificate file location - * - * @throws BadNameException in case the given special name is not known - */ - public File getCertFileBySpecialName(String specialName) - throws BadNameException { - if (!isSpecialName(specialName)) { - throw new BadNameException(String.format("%s is not a known special name", specialName)); - } - - return new File(getBaseDirectory(), specialName); - } - - /** - * Calculate the file location for the key addressed using the given special name. - * For known special names, see {@link SpecialNames}. - * - * @param specialName special name (e.g. "trust-root") - * @return absolute key file location - * - * @throws BadNameException in case the given special name is not known - */ - public File getKeyFileBySpecialName(String specialName) - throws BadNameException { - if (!isSpecialName(specialName)) { - throw new BadNameException(String.format("%s is not a known special name", specialName)); - } - - return new File(getBaseDirectory(), specialName + ".key"); - } - - private boolean isFingerprint(String fingerprint) { - return openPgpV4FingerprintPattern.matcher(fingerprint).matches(); - } - - private boolean isSpecialName(String specialName) { - return SpecialNames.lookupSpecialName(specialName) != null; - } - -} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/InMemoryCertificateDirectoryBackend.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/InMemoryCertificateDirectoryBackend.java new file mode 100644 index 0000000..fe7cc89 --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/InMemoryCertificateDirectoryBackend.java @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.cert_d; + +import pgp.certificate_store.Certificate; +import pgp.certificate_store.KeyMaterial; +import pgp.certificate_store.KeyMaterialMerger; +import pgp.certificate_store.KeyMaterialReaderBackend; +import pgp.certificate_store.exception.BadDataException; +import pgp.certificate_store.exception.BadNameException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class InMemoryCertificateDirectoryBackend implements PGPCertificateDirectory.Backend { + + protected static class ObjectLockingMechanism implements PGPCertificateDirectory.LockingMechanism { + + private boolean locked = false; + + @Override + public synchronized void lockDirectory() throws InterruptedException { + if (isLocked()) { + wait(); + } + locked = true; + } + + @Override + public synchronized boolean tryLockDirectory() { + if (isLocked()) { + return false; + } + locked = true; + return true; + } + + @Override + public synchronized boolean isLocked() { + return locked; + } + + @Override + public synchronized void releaseDirectory() { + locked = false; + notify(); + } + } + + + private final Map certificateFingerprintMap = new HashMap<>(); + private final Map keyMaterialSpecialNameMap = new HashMap<>(); + private final PGPCertificateDirectory.LockingMechanism lock = new ObjectLockingMechanism(); + private final KeyMaterialReaderBackend reader; + + public InMemoryCertificateDirectoryBackend(KeyMaterialReaderBackend reader) { + this.reader = reader; + } + + @Override + public PGPCertificateDirectory.LockingMechanism getLock() { + return lock; + } + + @Override + public Certificate readByFingerprint(String fingerprint) { + return certificateFingerprintMap.get(fingerprint); + } + + + @Override + public KeyMaterial readBySpecialName(String specialName) { + return keyMaterialSpecialNameMap.get(specialName); + } + + @Override + public Iterator readItems() { + return certificateFingerprintMap.values().iterator(); + } + + @Override + public KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge) + throws BadDataException, IOException { + KeyMaterial update = reader.read(data); + KeyMaterial existing = readBySpecialName(SpecialNames.TRUST_ROOT); + KeyMaterial merged = merge.merge(update, existing); + keyMaterialSpecialNameMap.put(SpecialNames.TRUST_ROOT, merged); + return merged; + } + + + @Override + public Certificate doInsert(InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException { + KeyMaterial update = reader.read(data); + Certificate existing = readByFingerprint(update.getFingerprint()); + Certificate merged = merge.merge(update, existing).asCertificate(); + certificateFingerprintMap.put(update.getFingerprint(), merged); + return merged; + } + + @Override + public Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException, BadNameException { + KeyMaterial keyMaterial = reader.read(data); + KeyMaterial existing = readBySpecialName(specialName); + KeyMaterial merged = merge.merge(keyMaterial, existing); + keyMaterialSpecialNameMap.put(specialName, merged); + return merged.asCertificate(); + } +} 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 deleted file mode 100644 index 92e196d..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/LockingMechanism.java +++ /dev/null @@ -1,37 +0,0 @@ -// 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. - * - * @throws IOException in case of an IO error - * @throws InterruptedException if the thread gets interrupted - */ - 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 - * - * @throws IOException in case of an IO error - */ - boolean tryLockDirectory() throws IOException; - - /** - * Release the directory write-lock acquired via {@link #lockDirectory()}. - * - * @throws IOException in case of an IO error - */ - void releaseDirectory() throws IOException; - -} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateDirectories.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateDirectories.java new file mode 100644 index 0000000..b75ac43 --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateDirectories.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.cert_d; + +import pgp.certificate_store.KeyMaterialReaderBackend; +import pgp.certificate_store.exception.NotAStoreException; + +import java.io.File; + +public final class PGPCertificateDirectories { + + private PGPCertificateDirectories() { + + } + + public static PGPCertificateDirectory inMemoryCertificateDirectory(KeyMaterialReaderBackend keyReader) { + return new PGPCertificateDirectory(new InMemoryCertificateDirectoryBackend(keyReader)); + } + + public static PGPCertificateDirectory defaultFileBasedCertificateDirectory(KeyMaterialReaderBackend keyReader) + throws NotAStoreException { + return fileBasedCertificateDirectory(keyReader, BaseDirectoryProvider.getDefaultBaseDir()); + } + + public static PGPCertificateDirectory fileBasedCertificateDirectory( + KeyMaterialReaderBackend keyReader, File baseDirectory) + throws NotAStoreException { + return new PGPCertificateDirectory( + new FileBasedCertificateDirectoryBackend(baseDirectory, keyReader)); + } +} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateDirectory.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateDirectory.java new file mode 100644 index 0000000..ef7fc5d --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateDirectory.java @@ -0,0 +1,194 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.cert_d; + +import pgp.certificate_store.Certificate; +import pgp.certificate_store.KeyMaterial; +import pgp.certificate_store.KeyMaterialMerger; +import pgp.certificate_store.exception.BadDataException; +import pgp.certificate_store.exception.BadNameException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; + +public class PGPCertificateDirectory + implements ReadOnlyPGPCertificateDirectory, WritingPGPCertificateDirectory { + + private final Backend backend; + + public PGPCertificateDirectory(Backend backend) { + this.backend = backend; + } + + @Override + public Certificate getByFingerprint(String fingerprint) throws BadDataException, BadNameException, IOException { + return backend.readByFingerprint(fingerprint); + } + + @Override + public Certificate getBySpecialName(String specialName) + throws BadNameException, BadDataException, IOException { + KeyMaterial keyMaterial = backend.readBySpecialName(specialName); + if (keyMaterial != null) { + return keyMaterial.asCertificate(); + } + return null; + } + + @Override + public Certificate getTrustRootCertificate() + throws IOException, BadDataException { + try { + return getBySpecialName(SpecialNames.TRUST_ROOT); + } catch (BadNameException e) { + throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is an implementation MUST"); + } + } + + @Override + public Iterator items() { + return backend.readItems(); + } + + @Override + public Iterator fingerprints() { + Iterator certs = items(); + return new Iterator() { + @Override + public boolean hasNext() { + return certs.hasNext(); + } + + @Override + public String next() { + return certs.next().getFingerprint(); + } + }; + } + + @Override + public KeyMaterial getTrustRoot() throws IOException, BadDataException { + try { + return backend.readBySpecialName(SpecialNames.TRUST_ROOT); + } catch (BadNameException e) { + throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is implementation MUST"); + } + } + + @Override + public KeyMaterial insertTrustRoot(InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException, InterruptedException { + backend.getLock().lockDirectory(); + KeyMaterial inserted = backend.doInsertTrustRoot(data, merge); + backend.getLock().releaseDirectory(); + return inserted; + } + + @Override + public KeyMaterial tryInsertTrustRoot(InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException { + if (!backend.getLock().tryLockDirectory()) { + return null; + } + KeyMaterial inserted = backend.doInsertTrustRoot(data, merge); + backend.getLock().releaseDirectory(); + return inserted; + } + + + + @Override + public Certificate insert(InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException, InterruptedException { + backend.getLock().lockDirectory(); + Certificate inserted = backend.doInsert(data, merge); + backend.getLock().releaseDirectory(); + return inserted; + } + + @Override + public Certificate tryInsert(InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException { + if (!backend.getLock().tryLockDirectory()) { + return null; + } + Certificate inserted = backend.doInsert(data, merge); + backend.getLock().releaseDirectory(); + return inserted; + } + + @Override + public Certificate insertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException, BadNameException, InterruptedException { + backend.getLock().lockDirectory(); + Certificate inserted = backend.doInsertWithSpecialName(specialName, data, merge); + backend.getLock().releaseDirectory(); + return inserted; + } + + @Override + public Certificate tryInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException, BadNameException { + if (!backend.getLock().tryLockDirectory()) { + return null; + } + Certificate inserted = backend.doInsertWithSpecialName(specialName, data, merge); + backend.getLock().releaseDirectory(); + return inserted; + } + + public interface Backend { + + LockingMechanism getLock(); + + Certificate readByFingerprint(String fingerprint) throws BadNameException, IOException, BadDataException; + + KeyMaterial readBySpecialName(String specialName) throws BadNameException, IOException, BadDataException; + + Iterator readItems(); + + KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge) + throws BadDataException, IOException; + + Certificate doInsert(InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException; + + Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException, BadNameException; + } + + public interface LockingMechanism { + + /** + * Lock the store for writes. + * Readers can continue to use the store and will always see consistent certs. + * + * @throws IOException in case of an IO error + * @throws InterruptedException if the thread gets interrupted + */ + 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 + * + * @throws IOException in case of an IO error + */ + boolean tryLockDirectory() throws IOException; + + boolean isLocked(); + + /** + * Release the directory write-lock acquired via {@link #lockDirectory()}. + * + * @throws IOException in case of an IO error + */ + void releaseDirectory() throws IOException; + + } +} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/ReadOnlyPGPCertificateDirectory.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/ReadOnlyPGPCertificateDirectory.java new file mode 100644 index 0000000..07ad7de --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/ReadOnlyPGPCertificateDirectory.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.cert_d; + +import pgp.certificate_store.Certificate; +import pgp.certificate_store.exception.BadDataException; +import pgp.certificate_store.exception.BadNameException; + +import java.io.IOException; +import java.util.Iterator; + +public interface ReadOnlyPGPCertificateDirectory { + + Certificate getTrustRootCertificate() + throws IOException, BadDataException; + + Certificate getByFingerprint(String fingerprint) + throws IOException, BadNameException, BadDataException; + + Certificate getBySpecialName(String specialName) + throws IOException, BadNameException, BadDataException; + + Iterator items(); + + Iterator fingerprints(); +} 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 deleted file mode 100644 index b6b83f4..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectory.java +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Iterator; - -import pgp.certificate_store.Key; -import pgp.certificate_store.KeyMerger; -import pgp.certificate_store.exception.BadDataException; -import pgp.certificate_store.exception.BadNameException; -import pgp.certificate_store.Certificate; -import pgp.certificate_store.CertificateMerger; - -public interface SharedPGPCertificateDirectory { - - LockingMechanism getLock(); - - Certificate getByFingerprint(String fingerprint) - throws IOException, BadNameException, BadDataException; - - Certificate getBySpecialName(String specialName) - throws IOException, BadNameException, BadDataException; - - Key getTrustRoot() - throws IOException, BadDataException; - - Key getTrustRootIfChanged(String tag) - throws IOException, BadDataException; - - Key insertTrustRoot(InputStream data, KeyMerger merge) - throws IOException, BadDataException, InterruptedException; - - Key tryInsertTrustRoot(InputStream data, KeyMerger merge) - throws IOException, BadDataException; - - Certificate getByFingerprintIfChanged(String fingerprint, String tag) - throws IOException, BadNameException, BadDataException; - - Certificate getBySpecialNameIfChanged(String specialName, String tag) - throws IOException, BadNameException, BadDataException; - - Certificate insert(InputStream data, CertificateMerger merge) - throws IOException, BadDataException, InterruptedException; - - Certificate tryInsert(InputStream data, CertificateMerger merge) - throws IOException, BadDataException; - - Certificate insertWithSpecialName(String specialName, InputStream data, CertificateMerger merge) - throws IOException, BadDataException, BadNameException, InterruptedException; - - Certificate tryInsertWithSpecialName(String specialName, InputStream data, CertificateMerger 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 deleted file mode 100644 index b44bdcf..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectoryImpl.java +++ /dev/null @@ -1,405 +0,0 @@ -// 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.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -import pgp.certificate_store.Key; -import pgp.certificate_store.KeyMaterial; -import pgp.certificate_store.KeyMerger; -import pgp.certificate_store.KeyReaderBackend; -import pgp.certificate_store.exception.BadDataException; -import pgp.certificate_store.exception.BadNameException; -import pgp.certificate_store.exception.NotAStoreException; -import pgp.certificate_store.Certificate; -import pgp.certificate_store.CertificateMerger; - -public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDirectory { - - private final FilenameResolver resolver; - private final LockingMechanism writeLock; - private final KeyReaderBackend keyReaderBackend; - - public SharedPGPCertificateDirectoryImpl(BackendProvider backendProvider) - throws NotAStoreException { - this(backendProvider.provideKeyReaderBackend()); - } - - public SharedPGPCertificateDirectoryImpl(KeyReaderBackend keyReaderBackend) - throws NotAStoreException { - this( - BaseDirectoryProvider.getDefaultBaseDir(), - keyReaderBackend); - } - - public SharedPGPCertificateDirectoryImpl(File baseDirectory, - KeyReaderBackend keyReaderBackend) - throws NotAStoreException { - this( - keyReaderBackend, - new FilenameResolver(baseDirectory), - FileLockingMechanism.defaultDirectoryFileLock(baseDirectory)); - } - - public SharedPGPCertificateDirectoryImpl( - KeyReaderBackend keyReaderBackend, - FilenameResolver filenameResolver, - LockingMechanism writeLock) - throws NotAStoreException { - this.keyReaderBackend = keyReaderBackend; - this.resolver = filenameResolver; - this.writeLock = writeLock; - - File baseDirectory = resolver.getBaseDirectory(); - if (!baseDirectory.exists()) { - if (!baseDirectory.mkdirs()) { - throw new NotAStoreException("Cannot create base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "'"); - } - } else { - if (baseDirectory.isFile()) { - throw new NotAStoreException("Base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "' appears to be a file."); - } - } - } - - @Override - public LockingMechanism getLock() { - return writeLock; - } - - @Override - public Certificate getByFingerprint(String fingerprint) - throws IOException, BadNameException, BadDataException { - File certFile = resolver.getCertFileByFingerprint(fingerprint); - if (!certFile.exists()) { - return null; - } - - FileInputStream fileIn = new FileInputStream(certFile); - BufferedInputStream bufferedIn = new BufferedInputStream(fileIn); - - Certificate certificate = readCertificate(bufferedIn); - if (!certificate.getFingerprint().equals(fingerprint)) { - // TODO: Figure out more suitable exception - throw new BadDataException(); - } - - return certificate; - } - - private Certificate readCertificate(InputStream inputStream) throws BadDataException, IOException { - KeyMaterial record = keyReaderBackend.read(inputStream); - Certificate certificate = null; - if (record instanceof Certificate) { - certificate = (Certificate) record; - } else if (record instanceof Key) { - Key key = (Key) record; - certificate = key.getCertificate(); - } else { - throw new BadDataException(); - } - return certificate; - } - - private Key readKey(InputStream inputStream) throws BadDataException, IOException { - KeyMaterial record = keyReaderBackend.read(inputStream); - if (record instanceof Key) { - return (Key) record; - } - throw new BadDataException(); - } - - @Override - public Certificate getBySpecialName(String specialName) - throws IOException, BadNameException, BadDataException { - File certFile = resolver.getCertFileBySpecialName(specialName); - if (!certFile.exists()) { - return null; - } - - FileInputStream fileIn = new FileInputStream(certFile); - BufferedInputStream bufferedIn = new BufferedInputStream(fileIn); - Certificate certificate = readCertificate(bufferedIn); - - return certificate; - } - - @Override - public Certificate getByFingerprintIfChanged(String fingerprint, String tag) - throws IOException, BadNameException, BadDataException { - Certificate certificate = getByFingerprint(fingerprint); - if (certificate.getTag().equals(tag)) { - return null; - } - return certificate; - } - - @Override - public Certificate getBySpecialNameIfChanged(String specialName, String tag) - throws IOException, BadNameException, BadDataException { - Certificate certificate = getBySpecialName(specialName); - if (certificate.getTag().equals(tag)) { - return null; - } - return certificate; - } - - @Override - public Key getTrustRoot() throws IOException, BadDataException { - File keyFile; - try { - keyFile = resolver.getKeyFileBySpecialName(SpecialNames.TRUST_ROOT); - } catch (BadNameException e) { - throw new RuntimeException("trust-root MUST be a known special name", e); - } - - if (!keyFile.exists()) { - return null; - } - FileInputStream fileIn = new FileInputStream(keyFile); - BufferedInputStream bufferedInputStream = new BufferedInputStream(fileIn); - Key key = readKey(bufferedInputStream); - - return key; - } - - @Override - public Key getTrustRootIfChanged(String tag) throws IOException, BadDataException { - // TODO: The tag is likely intended for performance improvements, - // so really we should look it up somewhere without the need to parse the whole key. - Key key = getTrustRoot(); - if (key.getTag().equals(tag)) { - return null; - } - return key; - } - - @Override - public Certificate insert(InputStream data, CertificateMerger merge) - throws IOException, BadDataException, InterruptedException { - writeLock.lockDirectory(); - - Certificate certificate = _insert(data, merge); - - writeLock.releaseDirectory(); - return certificate; - } - - @Override - public Certificate tryInsert(InputStream data, CertificateMerger merge) - throws IOException, BadDataException { - if (!writeLock.tryLockDirectory()) { - return null; - } - - Certificate certificate = _insert(data, merge); - - writeLock.releaseDirectory(); - return certificate; - } - - private Certificate _insert(InputStream data, CertificateMerger merge) - throws IOException, BadDataException { - Certificate newCertificate = readCertificate(data); - Certificate existingCertificate; - File certFile; - try { - existingCertificate = getByFingerprint(newCertificate.getFingerprint()); - certFile = resolver.getCertFileByFingerprint(newCertificate.getFingerprint()); - } catch (BadNameException e) { - throw new BadDataException(); - } - - if (existingCertificate != null && !existingCertificate.getTag().equals(newCertificate.getTag())) { - newCertificate = merge.merge(newCertificate, existingCertificate); - } - - writeToFile(newCertificate.getInputStream(), certFile); - - return newCertificate; - } - - private Key _insertTrustRoot(InputStream data, KeyMerger merge) - throws IOException, BadDataException { - Key newKey = readKey(data); - Key existingKey; - File keyFile; - try { - existingKey = getTrustRoot(); - keyFile = resolver.getKeyFileBySpecialName(SpecialNames.TRUST_ROOT); - } catch (BadNameException e) { - throw new RuntimeException(String.format("trust-root MUST be known special name.", e)); - } - - if (existingKey != null && !existingKey.getTag().equals(newKey.getTag())) { - newKey = merge.merge(newKey, existingKey); - } - - writeToFile(newKey.getInputStream(), keyFile); - - return newKey; - } - - @Override - public Key insertTrustRoot(InputStream data, KeyMerger merge) throws IOException, BadDataException, InterruptedException { - writeLock.lockDirectory(); - - Key key = _insertTrustRoot(data, merge); - - writeLock.releaseDirectory(); - return key; - } - - @Override - public Key tryInsertTrustRoot(InputStream data, KeyMerger merge) throws IOException, BadDataException { - return null; - } - - private void writeToFile(InputStream inputStream, File certFile) - throws IOException { - certFile.getParentFile().mkdirs(); - if (!certFile.exists() && !certFile.createNewFile()) { - throw new IOException("Could not create cert file " + certFile.getAbsolutePath()); - } - - FileOutputStream fileOut = new FileOutputStream(certFile); - - byte[] buffer = new byte[4096]; - int read; - while ((read = inputStream.read(buffer)) != -1) { - fileOut.write(buffer, 0, read); - } - - inputStream.close(); - fileOut.close(); - } - - @Override - public Certificate insertWithSpecialName(String specialName, InputStream data, CertificateMerger merge) - throws IOException, BadNameException, BadDataException, InterruptedException { - writeLock.lockDirectory(); - - Certificate certificate = _insertSpecial(specialName, data, merge); - - writeLock.releaseDirectory(); - return certificate; - } - - @Override - public Certificate tryInsertWithSpecialName(String specialName, InputStream data, CertificateMerger merge) - throws IOException, BadNameException, BadDataException { - if (!writeLock.tryLockDirectory()) { - return null; - } - - Certificate certificate = _insertSpecial(specialName, data, merge); - - writeLock.releaseDirectory(); - return certificate; - } - - private Certificate _insertSpecial(String specialName, InputStream data, CertificateMerger merge) - throws IOException, BadNameException, BadDataException { - Certificate newCertificate = readCertificate(data); - Certificate existingCertificate = getBySpecialName(specialName); - File certFile = resolver.getCertFileBySpecialName(specialName); - - if (existingCertificate != null && !existingCertificate.getTag().equals(newCertificate.getTag())) { - newCertificate = merge.merge(newCertificate, existingCertificate); - } - - writeToFile(newCertificate.getInputStream(), certFile); - - return newCertificate; - } - - @Override - public Iterator items() { - return new Iterator() { - - private final List> certificateQueue = Collections.synchronizedList(new ArrayList<>()); - - // Constructor... wtf. - { - File[] subdirectories = resolver.getBaseDirectory().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 = 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."); - } - } - }); - } - } - } - - @Override - public boolean hasNext() { - return !certificateQueue.isEmpty(); - } - - @Override - public Certificate next() { - try { - return certificateQueue.remove(0).get(); - } catch (BadDataException e) { - throw new AssertionError("Could not retrieve item: " + e.getMessage()); - } - } - }; - } - - private abstract static class Lazy { - abstract E get() throws BadDataException; - } - - @Override - public Iterator fingerprints() { - Iterator certificates = items(); - return new Iterator() { - @Override - public boolean hasNext() { - return certificates.hasNext(); - } - - @Override - public String next() { - return certificates.next().getFingerprint(); - } - }; - } -} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/WritingPGPCertificateDirectory.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/WritingPGPCertificateDirectory.java new file mode 100644 index 0000000..2ac60f7 --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/WritingPGPCertificateDirectory.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.cert_d; + +import pgp.certificate_store.Certificate; +import pgp.certificate_store.KeyMaterial; +import pgp.certificate_store.KeyMaterialMerger; +import pgp.certificate_store.exception.BadDataException; +import pgp.certificate_store.exception.BadNameException; + +import java.io.IOException; +import java.io.InputStream; + +public interface WritingPGPCertificateDirectory { + + KeyMaterial getTrustRoot() + throws IOException, BadDataException; + + KeyMaterial insertTrustRoot(InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException, InterruptedException; + + KeyMaterial tryInsertTrustRoot(InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException; + + Certificate insert(InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException, InterruptedException; + + Certificate tryInsert(InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException; + + Certificate insertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException, BadNameException, InterruptedException; + + Certificate tryInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) + throws IOException, BadDataException, BadNameException; + +} diff --git a/pgp-cert-d-java/src/test/java/pgp/cert_d/FilenameResolverTest.java b/pgp-cert-d-java/src/test/java/pgp/cert_d/FilenameResolverTest.java index 1d534a4..ee617b3 100644 --- a/pgp-cert-d-java/src/test/java/pgp/cert_d/FilenameResolverTest.java +++ b/pgp-cert-d-java/src/test/java/pgp/cert_d/FilenameResolverTest.java @@ -18,13 +18,13 @@ import static org.junit.jupiter.api.Assertions.assertThrows; public class FilenameResolverTest { private File baseDir; - private FilenameResolver resolver; + private FileBasedCertificateDirectoryBackend.FilenameResolver resolver; @BeforeEach public void setup() throws IOException { baseDir = Files.createTempDirectory("filenameresolver").toFile(); baseDir.deleteOnExit(); - resolver = new FilenameResolver(baseDir); + resolver = new FileBasedCertificateDirectoryBackend.FilenameResolver(baseDir); } @Test 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 index f6501e8..444f2ee 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/Certificate.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/Certificate.java @@ -4,37 +4,13 @@ package pgp.certificate_store; -import java.io.IOException; -import java.io.InputStream; -import java.util.Set; - /** * OpenPGP certificate (public key). */ public abstract class Certificate implements KeyMaterial { - /** - * Return an {@link InputStream} of the binary representation of the certificate. - * - * @return input stream - * @throws IOException in case of an IO error - */ - 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 - * @throws IOException in case of an IO error - */ - public abstract String getTag() throws IOException; - - /** - * Return a {@link Set} containing key-ids of subkeys. - * - * @return subkeys - * @throws IOException in case of an IO error - */ - public abstract Set getSubkeyIds() throws IOException; + @Override + public Certificate asCertificate() { + return this; + } } diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateDirectory.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateDirectory.java index a2a66df..1bb9eb8 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateDirectory.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateDirectory.java @@ -54,12 +54,12 @@ public interface CertificateDirectory { /** * Insert a certificate into the store. - * If an instance of the certificate is already present in the store, the given {@link CertificateMerger} will be + * If an instance of the certificate is already present in the store, the given {@link KeyMaterialMerger} will be * used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate * will be stored in the store and returned. * * This method will block until a write-lock on the store can be acquired. If you cannot afford blocking, - * consider to use {@link #tryInsertCertificate(InputStream, CertificateMerger)} instead. + * consider to use {@link #tryInsertCertificate(InputStream, KeyMaterialMerger)} instead. * * @param data input stream containing the new certificate instance * @param merge callback for merging with an existing certificate instance @@ -69,12 +69,12 @@ public interface CertificateDirectory { * @throws InterruptedException in case the inserting thread gets interrupted * @throws BadDataException if the data stream does not contain valid OpenPGP data */ - Certificate insertCertificate(InputStream data, CertificateMerger merge) + Certificate insertCertificate(InputStream data, KeyMaterialMerger merge) throws IOException, InterruptedException, BadDataException; /** * Insert a certificate into the store. - * If an instance of the certificate is already present in the store, the given {@link CertificateMerger} will be + * If an instance of the certificate is already present in the store, the given {@link KeyMaterialMerger} will be * used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate * will be stored in the store and returned. * @@ -90,19 +90,19 @@ public interface CertificateDirectory { * @throws IOException in case of an IO-error * @throws BadDataException if the data stream does not contain valid OpenPGP data */ - Certificate tryInsertCertificate(InputStream data, CertificateMerger merge) + Certificate tryInsertCertificate(InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException; /** * Insert a certificate into the store. * The certificate will be stored under the given special name instead of its fingerprint. * - * If an instance of the certificate is already present under the special name in the store, the given {@link CertificateMerger} will be + * If an instance of the certificate is already present under the special name in the store, the given {@link KeyMaterialMerger} will be * used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate * will be stored in the store and returned. * * This method will block until a write-lock on the store can be acquired. If you cannot afford blocking, - * consider to use {@link #tryInsertCertificateBySpecialName(String, InputStream, CertificateMerger)} instead. + * consider to use {@link #tryInsertCertificateBySpecialName(String, InputStream, KeyMaterialMerger)} instead. * * @param specialName special name of the certificate * @param data input stream containing the new certificate instance @@ -114,14 +114,14 @@ public interface CertificateDirectory { * @throws BadDataException if the certificate file does not contain valid OpenPGP data * @throws BadNameException if the special name is unknown */ - Certificate insertCertificateBySpecialName(String specialName, InputStream data, CertificateMerger merge) + Certificate insertCertificateBySpecialName(String specialName, InputStream data, KeyMaterialMerger merge) throws IOException, InterruptedException, BadDataException, BadNameException; /** * Insert a certificate into the store. * The certificate will be stored under the given special name instead of its fingerprint. * - * If an instance of the certificate is already present under the special name in the store, the given {@link CertificateMerger} will be + * If an instance of the certificate is already present under the special name in the store, the given {@link KeyMaterialMerger} will be * used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate * will be stored in the store and returned. * @@ -139,7 +139,7 @@ public interface CertificateDirectory { * @throws BadDataException if the data stream does not contain valid OpenPGP data * @throws BadNameException if the special name is not known */ - Certificate tryInsertCertificateBySpecialName(String specialName, InputStream data, CertificateMerger merge) + Certificate tryInsertCertificateBySpecialName(String specialName, InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException, BadNameException; /** @@ -186,7 +186,7 @@ public interface CertificateDirectory { /** * Insert the given trust-root key into the store. - * If the key store already holds a trust-root key, the given {@link KeyMerger} callback will be used to merge + * If the key store already holds a trust-root key, the given {@link KeyMaterialMerger} callback will be used to merge * the two instances into one {@link Key}. The result will be stored in the store and returned. * * This method will not block. Instead, if the store is already write-locked, this method will simply return null @@ -202,16 +202,16 @@ public interface CertificateDirectory { * @throws InterruptedException in case the inserting thread gets interrupted * @throws BadDataException if the data stream does not contain a valid OpenPGP key */ - Key insertTrustRoot(InputStream data, KeyMerger keyMerger) + Key insertTrustRoot(InputStream data, KeyMaterialMerger keyMerger) throws IOException, InterruptedException, BadDataException; /** * Insert the given trust-root key into the store. - * If the key store already holds a trust-root key, the given {@link KeyMerger} callback will be used to merge + * If the key store already holds a trust-root key, the given {@link KeyMaterialMerger} callback will be used to merge * the two instances into one {@link Key}. The result will be stored in the store and returned. * * This method will block until a write-lock on the store can be acquired. If you cannot afford blocking, - * consider using {@link #tryInsertTrustRoot(InputStream, KeyMerger)} instead. + * consider using {@link #tryInsertTrustRoot(InputStream, KeyMaterialMerger)} instead. * * @param data input stream containing the new trust-root key * @param keyMerger callback for merging with an existing key instance @@ -220,6 +220,6 @@ public interface CertificateDirectory { * @throws IOException in case of an IO error * @throws BadDataException if the data stream does not contain a valid OpenPGP key */ - Key tryInsertTrustRoot(InputStream data, KeyMerger keyMerger) + Key tryInsertTrustRoot(InputStream data, KeyMaterialMerger keyMerger) throws IOException, BadDataException; } diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateMerger.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateMerger.java deleted file mode 100644 index 9659003..0000000 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateMerger.java +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.certificate_store; - -import java.io.IOException; - -/** - * Merge a given certificate (update) with an existing certificate. - */ -public interface CertificateMerger { - - /** - * 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 unmodified data. - * - * @param data certificate - * @param existing optional already existing copy of the certificate - * @return merged certificate - * - * @throws IOException in case of an IO error - */ - Certificate merge(Certificate data, Certificate existing) throws IOException; - -} diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/Key.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/Key.java index 6425468..75a93f6 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/Key.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/Key.java @@ -4,9 +4,6 @@ package pgp.certificate_store; -import java.io.IOException; -import java.io.InputStream; - /** * OpenPGP key (secret key). */ @@ -19,14 +16,9 @@ public abstract class Key implements KeyMaterial { */ public abstract Certificate getCertificate(); - /** - * Return an {@link InputStream} of the binary representation of the secret key. - * - * @return input stream - * @throws IOException in case of an IO error - */ - public abstract InputStream getInputStream() throws IOException; - - public abstract String getTag() throws IOException; + @Override + public Certificate asCertificate() { + return getCertificate(); + } } diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMaterial.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMaterial.java index d940a25..7edce0c 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMaterial.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMaterial.java @@ -4,6 +4,10 @@ package pgp.certificate_store; +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; + public interface KeyMaterial { /** @@ -14,4 +18,23 @@ public interface KeyMaterial { */ String getFingerprint(); + Certificate asCertificate(); + + /** + * Return an {@link InputStream} of the binary representation of the secret key. + * + * @return input stream + * @throws IOException in case of an IO error + */ + InputStream getInputStream() throws IOException; + + String getTag() throws IOException; + + /** + * Return a {@link Set} containing key-ids of subkeys. + * + * @return subkeys + * @throws IOException in case of an IO error + */ + Set getSubkeyIds() throws IOException; } diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMaterialMerger.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMaterialMerger.java new file mode 100644 index 0000000..4cef3c8 --- /dev/null +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMaterialMerger.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.certificate_store; + +import java.io.IOException; + +/** + * Merge a given {@link Key} (update) with an existing {@link Key}. + */ +public interface KeyMaterialMerger { + + /** + * Merge the given key material with an existing copy and return the result. + * If no existing {@link KeyMaterial} is found (i.e. if existing is null), this method returns the unmodified data. + * + * @param data key material + * @param existing optional already existing copy of the key material + * @return merged key material + * + * @throws IOException in case of an IO error + */ + KeyMaterial merge(KeyMaterial data, KeyMaterial existing) throws IOException; +} diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/KeyReaderBackend.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMaterialReaderBackend.java similarity index 94% rename from pgp-certificate-store/src/main/java/pgp/certificate_store/KeyReaderBackend.java rename to pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMaterialReaderBackend.java index d99d8e2..c45f774 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/KeyReaderBackend.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMaterialReaderBackend.java @@ -9,7 +9,7 @@ import pgp.certificate_store.exception.BadDataException; import java.io.IOException; import java.io.InputStream; -public interface KeyReaderBackend { +public interface KeyMaterialReaderBackend { /** * Read a {@link KeyMaterial} (either {@link Key} or {@link Certificate}) from the given {@link InputStream}. diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMerger.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMerger.java deleted file mode 100644 index 3a7a7c5..0000000 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/KeyMerger.java +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.certificate_store; - -import java.io.IOException; - -/** - * Merge a given {@link Key} (update) with an existing {@link Key}. - */ -public interface KeyMerger { - - /** - * Merge the given key data with the existing {@link Key} and return the result. - * If no existing {@link Key} is found (i.e. if existing is null), this method returns the unmodified data. - * - * @param data key - * @param existing optional already existing copy of the key - * @return merged key - * - * @throws IOException in case of an IO error - */ - Key merge(Key data, Key existing) throws IOException; -}