diff --git a/.reuse/dep5 b/.reuse/dep5 index 3bc4763..2853568 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -14,7 +14,3 @@ Files: gradle* Copyright: 2015 the original author or authors. License: Apache-2.0 -# Woodpecker build files -Files: .woodpecker/* -Copyright: 2022 the original author or authors. -License: Apache-2.0 diff --git a/.woodpecker/.build.yml b/.woodpecker/.build.yml deleted file mode 100644 index f504b44..0000000 --- a/.woodpecker/.build.yml +++ /dev/null @@ -1,12 +0,0 @@ -pipeline: - run: - image: gradle:7.5-jdk8 - commands: - - git checkout $CI_COMMIT_BRANCH - # Code works - - gradle test - # Code is clean - - gradle check javadocAll - # Code has coverage - - gradle jacocoRootReport coveralls - secrets: [COVERALLS_REPO_TOKEN] diff --git a/.woodpecker/.reuse.yml b/.woodpecker/.reuse.yml deleted file mode 100644 index 58f17e6..0000000 --- a/.woodpecker/.reuse.yml +++ /dev/null @@ -1,7 +0,0 @@ -# Code is licensed properly -# See https://reuse.software/ -pipeline: - reuse: - image: fsfe/reuse:latest - commands: - - reuse lint \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f2bfcca..a78481a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,28 +5,6 @@ SPDX-License-Identifier: CC0-1.0 # Cert-D-Java Changelog -## 0.2.2 -- Bump Bouncy Castle to `1.75` -- Bump `sqlite-jdbc` to `3.42.0.0` - -## 0.2.1 -- Throw `NoSuchElementException` when querying non-existent certificates - -## 0.2.0 -- `pgp-certificate-store`: - - Rework `Certificate`, `Key` to inherit from `KeyMaterial` - - Rename `CertificateReaderBackend` to `KeyMaterialReaderBackend` - - Rename `CertificateMerger` to `KeyMaterialMerger` - - Rework `PGPCertificateStore` class -- `pgp-cert-d-java`: - - Increase minimum Android API level to 26 - - Add `PGPCertificateDirectories` factory class - - Rework `PGPCertificateDirectory` class by separating out backend logic - - Split interface into `ReadOnlyPGPCertificateDirectory` and `WritingPGPCertificateDirectory` - - `FileBasedCertificateDirectoryBackend`: Calculate tag based on file attributes (inode) -- `pgp-cert-d-java-jdbc-sqlite-lookup`: - - Add `DatabaseSubkeyLookupFactory` - ## 0.1.1 - Bump `slf4j` to `1.7.36` - Bump `logback` to `1.2.11` diff --git a/README.md b/README.md index 4dde81f..588d836 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,10 @@ SPDX-License-Identifier: Apache-2.0 --> # Shared PGP Certificate Directory for Java -[![status-badge](https://ci.codeberg.org/api/badges/PGPainless/cert-d-java/status.svg?branch=main)](https://ci.codeberg.org/PGPainless/cert-d-java) -[![Coverage Status](https://coveralls.io/repos/github/pgpainless/cert-d-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/cert-d-java?branch=main) -[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/cert-d-java)](https://api.reuse.software/info/github.com/pgpainless/cert-d-java) This repository contains a number of modules defining OpenPGP certificate storage for Java and Android applications. -The module [pgp-certificate-store](pgp-certificate-store) defines generalized +The module [pgp-certificate-store](pgp-certificate-store] defines generalized interfaces for OpenPGP Certificate storage. It can be used by applications and libraries such as [PGPainless](https://pgpainless.org/) for certificate management. diff --git a/pgp-cert-d-java-jdbc-sqlite-lookup/src/main/java/pgp/cert_d/jdbc/sqlite/DatabaseSubkeyLookup.java b/pgp-cert-d-java-jdbc-sqlite-lookup/src/main/java/pgp/cert_d/jdbc/sqlite/DatabaseSubkeyLookup.java index cc56b6d..f35d7c8 100644 --- a/pgp-cert-d-java-jdbc-sqlite-lookup/src/main/java/pgp/cert_d/jdbc/sqlite/DatabaseSubkeyLookup.java +++ b/pgp-cert-d-java-jdbc-sqlite-lookup/src/main/java/pgp/cert_d/jdbc/sqlite/DatabaseSubkeyLookup.java @@ -11,7 +11,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import pgp.cert_d.subkey_lookup.SubkeyLookup; +import pgp.certificate_store.SubkeyLookup; public class DatabaseSubkeyLookup implements SubkeyLookup { diff --git a/pgp-cert-d-java-jdbc-sqlite-lookup/src/main/java/pgp/cert_d/jdbc/sqlite/DatabaseSubkeyLookupFactory.java b/pgp-cert-d-java-jdbc-sqlite-lookup/src/main/java/pgp/cert_d/jdbc/sqlite/DatabaseSubkeyLookupFactory.java deleted file mode 100644 index d0a259a..0000000 --- a/pgp-cert-d-java-jdbc-sqlite-lookup/src/main/java/pgp/cert_d/jdbc/sqlite/DatabaseSubkeyLookupFactory.java +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d.jdbc.sqlite; - -import pgp.cert_d.subkey_lookup.SubkeyLookup; -import pgp.cert_d.subkey_lookup.SubkeyLookupFactory; - -import java.io.File; -import java.io.IOException; -import java.sql.SQLException; - -/** - * Implementation of {@link SubkeyLookupFactory} which creates a SQLite-based {@link DatabaseSubkeyLookup}. - */ -public class DatabaseSubkeyLookupFactory implements SubkeyLookupFactory { - - private String databaseName; - - public DatabaseSubkeyLookupFactory() { - this("_pgpainless_subkey_map.db"); - } - - public DatabaseSubkeyLookupFactory(String databaseName) { - this.databaseName = databaseName; - } - - @Override - public SubkeyLookup createFileBasedInstance(File baseDirectory) { - File databaseFile = new File(baseDirectory, databaseName); - SubkeyLookupDao dao; - try { - if (!databaseFile.exists()) { - databaseFile.createNewFile(); - } - dao = SqliteSubkeyLookupDaoImpl.forDatabaseFile(databaseFile); - } catch (SQLException | IOException e) { - throw new RuntimeException(e); - } - return new DatabaseSubkeyLookup(dao); - } -} diff --git a/pgp-cert-d-java-jdbc-sqlite-lookup/src/main/java/pgp/cert_d/jdbc/sqlite/package-info.java b/pgp-cert-d-java-jdbc-sqlite-lookup/src/main/java/pgp/cert_d/jdbc/sqlite/package-info.java index 74bf428..cf7f9af 100644 --- a/pgp-cert-d-java-jdbc-sqlite-lookup/src/main/java/pgp/cert_d/jdbc/sqlite/package-info.java +++ b/pgp-cert-d-java-jdbc-sqlite-lookup/src/main/java/pgp/cert_d/jdbc/sqlite/package-info.java @@ -3,6 +3,6 @@ // SPDX-License-Identifier: Apache-2.0 /** - * Implementation of a {@link pgp.cert_d.subkey_lookup.SubkeyLookup} mechanism using an SQLite Database. + * Implementation of a {@link pgp.certificate_store.SubkeyLookup} mechanism using an SQLite Database. */ package pgp.cert_d.jdbc.sqlite; diff --git a/pgp-cert-d-java-jdbc-sqlite-lookup/src/test/java/pgp/cert_d/jdbc/sqlite/SqliteSubkeyLookupTest.java b/pgp-cert-d-java-jdbc-sqlite-lookup/src/test/java/pgp/cert_d/jdbc/sqlite/SqliteSubkeyLookupTest.java index 5539d12..b71337c 100644 --- a/pgp-cert-d-java-jdbc-sqlite-lookup/src/test/java/pgp/cert_d/jdbc/sqlite/SqliteSubkeyLookupTest.java +++ b/pgp-cert-d-java-jdbc-sqlite-lookup/src/test/java/pgp/cert_d/jdbc/sqlite/SqliteSubkeyLookupTest.java @@ -22,15 +22,15 @@ import org.junit.jupiter.api.Test; public class SqliteSubkeyLookupTest { - private File tempDir; + private File databaseFile; private DatabaseSubkeyLookup lookup; @BeforeEach - public void setupLookup() throws IOException { - tempDir = Files.createTempDirectory("pgp.cert.d").toFile(); - tempDir.deleteOnExit(); - lookup = (DatabaseSubkeyLookup) new DatabaseSubkeyLookupFactory() - .createFileBasedInstance(tempDir); + public void setupLookup() throws IOException, SQLException { + databaseFile = Files.createTempFile("pgp.cert.d-", "lookup.db").toFile(); + databaseFile.createNewFile(); + databaseFile.deleteOnExit(); + lookup = new DatabaseSubkeyLookup(SqliteSubkeyLookupDaoImpl.forDatabaseFile(databaseFile)); } @Test @@ -55,7 +55,7 @@ public class SqliteSubkeyLookupTest { assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), lookup.getCertificateFingerprintsForSubkeyId(1337)); // do the lookup using a second db instance on the same file - DatabaseSubkeyLookup secondInstance = (DatabaseSubkeyLookup) new DatabaseSubkeyLookupFactory().createFileBasedInstance(tempDir); + DatabaseSubkeyLookup secondInstance = new DatabaseSubkeyLookup(SqliteSubkeyLookupDaoImpl.forDatabaseFile(databaseFile)); assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), secondInstance.getCertificateFingerprintsForSubkeyId(1337)); } diff --git a/pgp-cert-d-java/README.md b/pgp-cert-d-java/README.md index 49302bb..58e29d7 100644 --- a/pgp-cert-d-java/README.md +++ b/pgp-cert-d-java/README.md @@ -13,11 +13,9 @@ Backend-agnostic implementation of the [Shared PGP Certificate Directory Specifi This module implements the non-OpenPGP parts of the spec, e.g. locating the directory, resolving certificate file paths, locking the directory for writes etc. -This library can be used on Android API level 26 and up. - To get a useful implementation, a backend implementation such as `pgpainless-cert-d` is required, which needs to provide support for reading and merging certificates. -`pgp-cert-d-java` can be used as an implementation of `pgp-certificate-store` using the `PGPCertificateStoreAdapter` class. +`pgp-cert-d-java` can be used as an implementation of `pgp-certificate-store`. -Note: This is a library module. For a command line interface, see [pgpainless-cert-d-cli](https://github.com/pgpainless/cert-d-pgpainless/tree/main/pgpainless-cert-d-cli). +Note: This is a library module. For a command line interface, see [pgpainless-cert-d-cli](https://github.com/pgpainless/cert-d-pgpainless/tree/main/pgpainless-cert-d-cli). \ No newline at end of file diff --git a/pgp-cert-d-java/build.gradle b/pgp-cert-d-java/build.gradle index 19f1329..db5ba5c 100644 --- a/pgp-cert-d-java/build.gradle +++ b/pgp-cert-d-java/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'ru.vyarus.animalsniffer' dependencies { // animal sniffer for ensuring Android API compatibility - signature "net.sf.androidscents.signature:android-api-level-${animalsnifferSignatureVersion}@signature" + signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:2.3.3_r2@signature" // JUnit testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" @@ -26,13 +26,11 @@ dependencies { // Logging testImplementation "ch.qos.logback:logback-classic:$logbackVersion" - api project(":pgp-certificate-store") - // SQL Subkey table testImplementation project(":pgp-cert-d-java-jdbc-sqlite-lookup") - testImplementation "org.bouncycastle:bcprov-jdk15to18:$bouncycastleVersion" - testImplementation "org.bouncycastle:bcpg-jdk15to18:$bouncyPgVersion" + // Certificate store + api project(":pgp-certificate-store") } animalsniffer { 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 new file mode 100644 index 0000000..70bc93c --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/BackendProvider.java @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.cert_d; + +import pgp.certificate_store.CertificateReaderBackend; +import pgp.certificate_store.MergeCallback; + +public abstract class BackendProvider { + + public abstract CertificateReaderBackend provideCertificateReaderBackend(); + + public abstract MergeCallback provideDefaultMergeCallback(); + +} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/BaseDirectoryProvider.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/BaseDirectoryProvider.java index 386a5c7..0a24084 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/BaseDirectoryProvider.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/BaseDirectoryProvider.java @@ -6,16 +6,6 @@ package pgp.cert_d; import java.io.File; -/** - * Provider class that is responsible for resolving the pgp.cert.d base directory of the system. - * The result can be overwritten by setting the
PGP_CERT_D
environment variable. - * If this variable is not set, the system-specific default directory will be returned. - * - * On Windows systems, this is
%APPDATA%\pgp.cert.d
. - * On Linux systems it is either
$XDG_DATA_HOME/pgp.cert.d
or, if
$XDG_DATA_HOME
is not set, - * it is
$HOME/.local/share/pgp.cert.d
- * On Mac systems it is
$HOME/Library/Application Support/pgp.cert.d
. - */ public class BaseDirectoryProvider { public static File getDefaultBaseDir() { 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 new file mode 100644 index 0000000..820d55d --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/CachingSharedPGPCertificateDirectoryWrapper.java @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.cert_d; + +import pgp.certificate_store.exception.BadDataException; +import pgp.certificate_store.exception.BadNameException; +import pgp.certificate_store.Certificate; +import pgp.certificate_store.MergeCallback; + +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 tagMap = new HashMap<>(); + private static final Map certificateMap = 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 { + tagMap.put(identifier, certificate.getTag()); + } catch (IOException e) { + tagMap.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 tagChanged(String identifier, String tag) { + String tack = tagMap.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(); + tagMap.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 (tagChanged(fingerprint, tag)) { + return getByFingerprint(fingerprint); + } + return null; + } + + @Override + public Certificate getBySpecialNameIfChanged(String specialName, String tag) + throws IOException, BadNameException, BadDataException { + if (tagChanged(specialName, tag)) { + return getBySpecialName(specialName); + } + return null; + } + + @Override + public Certificate insert(InputStream data, MergeCallback merge) + throws IOException, BadDataException, InterruptedException { + Certificate certificate = underlyingCertificateDirectory.insert(data, merge); + remember(certificate.getFingerprint(), certificate); + return certificate; + } + + @Override + public Certificate tryInsert(InputStream data, MergeCallback merge) + throws IOException, BadDataException { + Certificate certificate = underlyingCertificateDirectory.tryInsert(data, merge); + if (certificate != null) { + remember(certificate.getFingerprint(), certificate); + } + return certificate; + } + + @Override + public Certificate insertWithSpecialName(String specialName, InputStream data, MergeCallback 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, MergeCallback 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/FileLockingMechanism.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/FileLockingMechanism.java new file mode 100644 index 0000000..2d87c04 --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/FileLockingMechanism.java @@ -0,0 +1,96 @@ +// 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 new file mode 100644 index 0000000..81ffc86 --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/FilenameResolver.java @@ -0,0 +1,70 @@ +// 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(); + } + + return new File(getBaseDirectory(), specialName); + } + + 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/subkey_lookup/InMemorySubkeyLookup.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/InMemorySubkeyLookup.java similarity index 94% rename from pgp-cert-d-java/src/main/java/pgp/cert_d/subkey_lookup/InMemorySubkeyLookup.java rename to pgp-cert-d-java/src/main/java/pgp/cert_d/InMemorySubkeyLookup.java index a42f652..1cd7862 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/subkey_lookup/InMemorySubkeyLookup.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/InMemorySubkeyLookup.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package pgp.cert_d.subkey_lookup; +package pgp.cert_d; import java.util.Collections; import java.util.HashMap; @@ -11,6 +11,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import pgp.certificate_store.SubkeyLookup; + public class InMemorySubkeyLookup implements SubkeyLookup { private static final Map> subkeyMap = new HashMap<>(); @@ -28,7 +30,6 @@ public class InMemorySubkeyLookup implements SubkeyLookup { public void storeCertificateSubkeyIds(String certificate, List subkeyIds) { for (long subkeyId : subkeyIds) { Set certificates = subkeyMap.get(subkeyId); - // noinspection Java8MapApi if (certificates == null) { certificates = new HashSet<>(); subkeyMap.put(subkeyId, certificates); 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 0000000..92e196d --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/LockingMechanism.java @@ -0,0 +1,37 @@ +// 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 deleted file mode 100644 index 50f6908..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateDirectories.java +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d; - -import pgp.cert_d.backend.FileBasedCertificateDirectoryBackend; -import pgp.cert_d.backend.InMemoryCertificateDirectoryBackend; -import pgp.cert_d.subkey_lookup.InMemorySubkeyLookup; -import pgp.cert_d.subkey_lookup.SubkeyLookup; -import pgp.certificate_store.certificate.KeyMaterialReaderBackend; -import pgp.certificate_store.exception.NotAStoreException; - -import java.io.File; - -/** - * Static factory methods that return implementations of the {@link PGPCertificateDirectory} class. - */ -public final class PGPCertificateDirectories { - - private PGPCertificateDirectories() { - - } - - public static PGPCertificateDirectory inMemoryCertificateDirectory( - KeyMaterialReaderBackend keyReader) { - return new PGPCertificateDirectory( - new InMemoryCertificateDirectoryBackend(keyReader), new InMemorySubkeyLookup()); - } - - public static PGPCertificateDirectory defaultFileBasedCertificateDirectory( - KeyMaterialReaderBackend keyReader, - SubkeyLookup subkeyLookup) - throws NotAStoreException { - return fileBasedCertificateDirectory(keyReader, BaseDirectoryProvider.getDefaultBaseDir(), subkeyLookup); - } - - public static PGPCertificateDirectory fileBasedCertificateDirectory( - KeyMaterialReaderBackend keyReader, - File baseDirectory, - SubkeyLookup subkeyLookup) - throws NotAStoreException { - return new PGPCertificateDirectory( - new FileBasedCertificateDirectoryBackend(baseDirectory, keyReader), subkeyLookup); - } -} 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 deleted file mode 100644 index 87d2512..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateDirectory.java +++ /dev/null @@ -1,367 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d; - -import pgp.cert_d.subkey_lookup.SubkeyLookup; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.certificate.KeyMaterial; -import pgp.certificate_store.certificate.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; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Set; -import java.util.regex.Pattern; - -/** - * Implementation of the Shared PGP Certificate Directory. - * - * @see Shared PGP Certificate Directory Specification - */ -public class PGPCertificateDirectory - implements ReadOnlyPGPCertificateDirectory, WritingPGPCertificateDirectory, SubkeyLookup { - - final Backend backend; - final SubkeyLookup subkeyLookup; - private final Pattern openPgpV4FingerprintPattern = Pattern.compile("^[a-f0-9]{40}$"); - - /** - * Constructor for a PGP certificate directory. - * - * @param backend storage backend - * @param subkeyLookup subkey lookup mechanism to map subkey-ids to certificates - */ - public PGPCertificateDirectory(Backend backend, SubkeyLookup subkeyLookup) { - this.backend = backend; - this.subkeyLookup = subkeyLookup; - } - - @Override - public Certificate getByFingerprint(String fingerprint) throws BadDataException, BadNameException, IOException { - if (!openPgpV4FingerprintPattern.matcher(fingerprint).matches()) { - throw new BadNameException(); - } - Certificate certificate = backend.readByFingerprint(fingerprint); - if (certificate == null) { - throw new NoSuchElementException(); - } - return certificate; - } - - @Override - public Certificate getByFingerprintIfChanged(String fingerprint, long tag) - throws IOException, BadNameException, BadDataException { - if (!Objects.equals(tag, backend.getTagForFingerprint(fingerprint))) { - return getByFingerprint(fingerprint); - } - return null; - } - - - @Override - public Certificate getBySpecialName(String specialName) - throws BadNameException, BadDataException, IOException { - KeyMaterial keyMaterial = backend.readBySpecialName(specialName); - if (keyMaterial != null) { - return keyMaterial.asCertificate(); - } - throw new NoSuchElementException(); - } - - @Override - public Certificate getBySpecialNameIfChanged(String specialName, long tag) - throws IOException, BadNameException, BadDataException { - if (!Objects.equals(tag, backend.getTagForSpecialName(specialName))) { - return getBySpecialName(specialName); - } - 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 Certificate getTrustRootCertificateIfChanged(long tag) throws IOException, BadDataException { - try { - return getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag); - } 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 { - KeyMaterial keyMaterial = backend.readBySpecialName(SpecialNames.TRUST_ROOT); - if (keyMaterial == null) { - throw new NoSuchElementException(); - } - return keyMaterial; - } 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); - subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds()); - 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); - subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds()); - 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); - subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds()); - 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); - subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds()); - 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); - subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds()); - 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); - subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds()); - backend.getLock().releaseDirectory(); - return inserted; - } - - @Override - public Set getCertificateFingerprintsForSubkeyId(long subkeyId) throws IOException { - return subkeyLookup.getCertificateFingerprintsForSubkeyId(subkeyId); - } - - @Override - public void storeCertificateSubkeyIds(String certificate, List subkeyIds) throws IOException { - subkeyLookup.storeCertificateSubkeyIds(certificate, subkeyIds); - } - - /** - * Storage backend. - */ - public interface Backend { - - /** - * Get the locking mechanism to write-lock the backend. - * - * @return lock - */ - LockingMechanism getLock(); - - /** - * Read a {@link Certificate} by its OpenPGP fingerprint. - * - * @param fingerprint fingerprint - * @return certificate - * - * @throws BadNameException if the fingerprint is malformed - * @throws IOException in case of an IO error - * @throws BadDataException if the certificate contains bad data - */ - Certificate readByFingerprint(String fingerprint) throws BadNameException, IOException, BadDataException; - - /** - * Read a {@link Certificate} or {@link pgp.certificate_store.certificate.Key} by the given special name. - * - * @param specialName special name - * @return certificate or key - * - * @throws BadNameException if the special name is not known - * @throws IOException in case of an IO error - * @throws BadDataException if the certificate contains bad data - */ - KeyMaterial readBySpecialName(String specialName) throws BadNameException, IOException, BadDataException; - - /** - * Return an {@link Iterator} of all {@link Certificate Certificates} in the store, except for certificates - * stored under a special name. - * - * @return iterator - */ - Iterator readItems(); - - /** - * Insert a {@link pgp.certificate_store.certificate.Key} or {@link Certificate} as trust-root. - * - * @param data input stream containing the key material - * @param merge callback to merge the key material with existing key material - * @return merged or inserted key material - * - * @throws BadDataException if the data stream or existing key material contains bad data - * @throws IOException in case of an IO error - */ - KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge) - throws BadDataException, IOException; - - /** - * Insert a {@link Certificate} identified by its fingerprint into the directory. - * - * @param data input stream containing the certificate data - * @param merge callback to merge the certificate with existing key material - * @return merged or inserted certificate - * - * @throws IOException in case of an IO error - * @throws BadDataException if the data stream or existing certificate contains bad data - */ - Certificate doInsert(InputStream data, KeyMaterialMerger merge) - throws IOException, BadDataException; - - /** - * Insert a {@link pgp.certificate_store.certificate.Key} or {@link Certificate} under the given special name. - * - * @param specialName special name to identify the key material with - * @param data data stream containing the key or certificate - * @param merge callback to merge the key/certificate with existing key material - * @return certificate component of the merged or inserted key material - * - * @throws IOException in case of an IO error - * @throws BadDataException if the data stream or existing key material contains bad data - * @throws BadNameException if the special name is not known - */ - Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) - throws IOException, BadDataException, BadNameException; - - /** - * Calculate the tag of the certificate with the given fingerprint. - * - * @param fingerprint fingerprint - * @return tag - * - * @throws BadNameException if the fingerprint is malformed - * @throws IOException in case of an IO error - * @throws IllegalArgumentException if the certificate does not exist - */ - Long getTagForFingerprint(String fingerprint) throws BadNameException, IOException; - - /** - * Calculate the tag of the certificate identified by the given special name. - * - * @param specialName special name - * @return tag - * - * @throws BadNameException if the special name is not known - * @throws IOException in case of an IO error - * @throws IllegalArgumentException if the certificate or key does not exist - */ - Long getTagForSpecialName(String specialName) throws BadNameException, IOException; - } - - /** - * Interface for a write-locking mechanism. - */ - 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; - - /** - * Return true if the lock is in locked state. - * - * @return true if locked - */ - 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/PGPCertificateStoreAdapter.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateStoreAdapter.java deleted file mode 100644 index 972d3aa..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateStoreAdapter.java +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d; - -import pgp.certificate_store.PGPCertificateStore; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.certificate.KeyMaterialMerger; -import pgp.certificate_store.exception.BadDataException; -import pgp.certificate_store.exception.BadNameException; - -import java.io.IOException; -import java.io.InputStream; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; - -/** - * Adapter class to adapt a {@link PGPCertificateDirectory} to the {@link PGPCertificateStore} interface. - */ -public class PGPCertificateStoreAdapter implements PGPCertificateStore { - - private final PGPCertificateDirectory directory; - - public PGPCertificateStoreAdapter(PGPCertificateDirectory directory) { - this.directory = directory; - } - - @Override - public Certificate getCertificate(String identifier) - throws IOException, BadNameException, BadDataException { - if (SpecialNames.lookupSpecialName(identifier) != null) { - return directory.getBySpecialName(identifier); - } else { - return directory.getByFingerprint(identifier.toLowerCase()); - } - } - - @Override - public Certificate getCertificateIfChanged(String identifier, Long tag) - throws IOException, BadNameException, BadDataException { - if (SpecialNames.lookupSpecialName(identifier) != null) { - return directory.getBySpecialNameIfChanged(identifier, tag); - } else { - return directory.getByFingerprintIfChanged(identifier.toLowerCase(), tag); - } - } - - @Override - public Iterator getCertificatesBySubkeyId(long subkeyId) - throws IOException, BadDataException { - Set fingerprints = directory.getCertificateFingerprintsForSubkeyId(subkeyId); - Set certificates = new HashSet<>(); - for (String fingerprint : fingerprints) { - try { - certificates.add(directory.getByFingerprint(fingerprint)); - } catch (BadNameException e) { - throw new RuntimeException(e); - } - } - return certificates.iterator(); - } - - @Override - public Certificate insertCertificate(InputStream data, KeyMaterialMerger merge) - throws IOException, InterruptedException, BadDataException { - Certificate certificate = directory.insert(data, merge); - return certificate; - } - - @Override - public Certificate insertCertificateBySpecialName(String specialName, InputStream data, KeyMaterialMerger merge) - throws IOException, InterruptedException, BadDataException, BadNameException { - return directory.insertWithSpecialName(specialName, data, merge); - } - - @Override - public Iterator getCertificates() { - return directory.items(); - } - - @Override - public Iterator getFingerprints() { - return directory.fingerprints(); - } -} 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 deleted file mode 100644 index 1b27ffb..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/ReadOnlyPGPCertificateDirectory.java +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d; - -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.exception.BadDataException; -import pgp.certificate_store.exception.BadNameException; - -import java.io.IOException; -import java.util.Iterator; -import java.util.NoSuchElementException; - -/** - * Interface for a read-only OpenPGP certificate directory. - */ -public interface ReadOnlyPGPCertificateDirectory { - - /** - * Get the trust-root certificate. This is a certificate which is stored under the special name - *
trust-root
. - * - * @return trust-root certificate - * - * @throws IOException in case of an IO error - * @throws BadDataException if the certificate contains bad data - * @throws NoSuchElementException if no such certificate is found - */ - Certificate getTrustRootCertificate() - throws IOException, BadDataException; - - /** - * Get the trust-root certificate if it has changed. - * This method uses the
tag
to calculate if the certificate might have changed. - * If the computed tag equals the given tag, the certificate has not changed, so
null
is returned. - * Otherwise. the changed certificate is returned. - * - * @param tag tag - * @return changed certificate, or null if the certificate is unchanged. - * - * @throws IOException in case of an IO error - * @throws BadDataException if the certificate contains bad data - * @throws NoSuchElementException if no such certificate is found - */ - Certificate getTrustRootCertificateIfChanged(long tag) - throws IOException, BadDataException; - - /** - * Get the certificate identified by the given fingerprint. - * - * @param fingerprint lower-case fingerprint of the certificate - * @return certificate - * - * @throws IOException in case of an IO error - * @throws BadNameException if the fingerprint is malformed - * @throws BadDataException if the certificate contains bad data - * @throws NoSuchElementException if no such certificate is found - */ - Certificate getByFingerprint(String fingerprint) - throws IOException, BadNameException, BadDataException; - - /** - * Get the certificate identified by the given fingerprint if it has changed. - * This method uses the
tag
to calculate, if the certificate might have changed. - * If the computed tag equals the given tag, the certificate has not changed, so
null
is returned. - * Otherwise, the changed certificate is returned. - * - * @param fingerprint lower-case fingerprint of the certificate - * @param tag tag - * @return certificate or null if the certificate has not been changed - * - * @throws IOException in case of an IO error - * @throws BadNameException if the fingerprint is malformed - * @throws BadDataException if the certificate contains bad data - * @throws NoSuchElementException if no such certificate is found - */ - Certificate getByFingerprintIfChanged(String fingerprint, long tag) - throws IOException, BadNameException, BadDataException; - - /** - * Get the certificate identified by the given special name. - * - * @param specialName special name - * @return certificate - * - * @throws IOException in case of an IO error - * @throws BadNameException if the special name is not known - * @throws BadDataException if the certificate contains bad data - * @throws NoSuchElementException if no such certificate is found - */ - Certificate getBySpecialName(String specialName) - throws IOException, BadNameException, BadDataException; - - /** - * Get the certificate identified by the given special name or null, if it has not been changed. - * This method uses the
tag
to calculate, if the certificate might have changed. - * If the computed tag equals the given tag, the certificate has not changed, so
null
is returned. - * Otherwise, the changed certificate is returned. - * - * @param specialName special name - * @param tag tag - * @return certificate or null - * - * @throws IOException in case of an IO error - * @throws BadNameException if the special name is not known - * @throws BadDataException if the certificate contains bad data - * @throws NoSuchElementException if no such certificate is found - */ - Certificate getBySpecialNameIfChanged(String specialName, long tag) - throws IOException, BadNameException, BadDataException; - - /** - * Get all certificates in the directory, except for certificates which are stored by special name. - * - * @return iterator of certificates - */ - Iterator items(); - - /** - * Get the fingerprints of all certificates in the directory, except for certificates which are stored by - * special name. - * - * @return iterator of fingerprints - */ - 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 new file mode 100644 index 0000000..9c80965 --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectory.java @@ -0,0 +1,47 @@ +// 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.exception.BadDataException; +import pgp.certificate_store.exception.BadNameException; +import pgp.certificate_store.Certificate; +import pgp.certificate_store.MergeCallback; + +public interface SharedPGPCertificateDirectory { + + LockingMechanism getLock(); + + Certificate getByFingerprint(String fingerprint) + throws IOException, BadNameException, BadDataException; + + Certificate getBySpecialName(String specialName) + throws IOException, BadNameException, 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, MergeCallback merge) + throws IOException, BadDataException, InterruptedException; + + Certificate tryInsert(InputStream data, MergeCallback merge) + throws IOException, BadDataException; + + Certificate insertWithSpecialName(String specialName, InputStream data, MergeCallback merge) + throws IOException, BadDataException, BadNameException, InterruptedException; + + Certificate tryInsertWithSpecialName(String 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 new file mode 100644 index 0000000..7b0a917 --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectoryImpl.java @@ -0,0 +1,314 @@ +// 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.exception.BadDataException; +import pgp.certificate_store.exception.BadNameException; +import pgp.certificate_store.exception.NotAStoreException; +import pgp.certificate_store.Certificate; +import pgp.certificate_store.CertificateReaderBackend; +import pgp.certificate_store.MergeCallback; + +public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDirectory { + + private final FilenameResolver resolver; + private final LockingMechanism writeLock; + private final CertificateReaderBackend certificateReaderBackend; + + public SharedPGPCertificateDirectoryImpl(BackendProvider backendProvider) + throws NotAStoreException { + this(backendProvider.provideCertificateReaderBackend()); + } + + public SharedPGPCertificateDirectoryImpl(CertificateReaderBackend certificateReaderBackend) + throws NotAStoreException { + this( + BaseDirectoryProvider.getDefaultBaseDir(), + certificateReaderBackend); + } + + public SharedPGPCertificateDirectoryImpl(File baseDirectory, CertificateReaderBackend certificateReaderBackend) + throws NotAStoreException { + this( + certificateReaderBackend, + new FilenameResolver(baseDirectory), + FileLockingMechanism.defaultDirectoryFileLock(baseDirectory)); + } + + public SharedPGPCertificateDirectoryImpl( + CertificateReaderBackend certificateReaderBackend, + FilenameResolver filenameResolver, + LockingMechanism writeLock) + throws NotAStoreException { + this.certificateReaderBackend = certificateReaderBackend; + 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 = certificateReaderBackend.readCertificate(bufferedIn); + + if (!certificate.getFingerprint().equals(fingerprint)) { + // TODO: Figure out more suitable exception + throw new BadDataException(); + } + + return certificate; + } + + @Override + public Certificate getBySpecialName(String specialName) + throws IOException, BadNameException { + File certFile = resolver.getCertFileBySpecialName(specialName); + if (!certFile.exists()) { + return null; + } + + FileInputStream fileIn = new FileInputStream(certFile); + BufferedInputStream bufferedIn = new BufferedInputStream(fileIn); + Certificate certificate = certificateReaderBackend.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 { + Certificate certificate = getBySpecialName(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 = certificateReaderBackend.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); + } + + 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 insertWithSpecialName(String 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 tryInsertWithSpecialName(String 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(String specialName, InputStream data, MergeCallback merge) + throws IOException, BadNameException, BadDataException { + Certificate newCertificate = certificateReaderBackend.readCertificate(data); + Certificate existingCertificate = getBySpecialName(specialName); + File certFile = resolver.getCertFileBySpecialName(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 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 = certificateReaderBackend.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/SpecialNames.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/SpecialNames.java index 622b2de..682d834 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/SpecialNames.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/SpecialNames.java @@ -9,14 +9,11 @@ import java.util.Map; public class SpecialNames { - public static final String TRUST_ROOT = "trust-root"; - - // Map to allow for potentially upper- and lowercase variants of the same special name private static final Map SPECIAL_NAMES = new HashMap<>(); static { - SPECIAL_NAMES.put("TRUST-ROOT", TRUST_ROOT); // TODO: Remove - SPECIAL_NAMES.put(TRUST_ROOT, TRUST_ROOT); + SPECIAL_NAMES.put("TRUST-ROOT", "trust-root"); // TODO: Remove + SPECIAL_NAMES.put("trust-root", "trust-root"); } public static String lookupSpecialName(String specialName) { 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 deleted file mode 100644 index e5371fc..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/WritingPGPCertificateDirectory.java +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d; - -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.certificate.KeyMaterial; -import pgp.certificate_store.certificate.KeyMaterialMerger; -import pgp.certificate_store.exception.BadDataException; -import pgp.certificate_store.exception.BadNameException; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Interface for a writing OpenPGP certificate directory. - */ -public interface WritingPGPCertificateDirectory { - - /** - * Return the certificate or key identified by the special name
trust-root
. - * - * @return trust-root key or certificate - * - * @throws IOException in case of an IO error - * @throws BadDataException if the certificate contains bad data - */ - KeyMaterial getTrustRoot() - throws IOException, BadDataException; - - /** - * Insert a key or certificate under the special name
trust-root
. - * This method blocks until the key material has been written. - * - * @param data input stream containing the key or certificate - * @param merge key material merger to merge the key or certificate with existing key material - * @return the merged or inserted key or certificate - * - * @throws IOException in case of an IO error - * @throws BadDataException if the data stream or the existing trust-root key material contains bad data - * @throws InterruptedException if the thread is interrupted - */ - KeyMaterial insertTrustRoot(InputStream data, KeyMaterialMerger merge) - throws IOException, BadDataException, InterruptedException; - - /** - * Insert a key or certificate under the special name
trust-root
. - * Contrary to {@link #insertTrustRoot(InputStream, KeyMaterialMerger)}, this method does not block. - * Instead, it returns null if the write-lock cannot be obtained. - * - * @param data input stream containing the key or certificate - * @param merge key material merger to merge the key or certificate with existing key material - * @return the merged or inserted key or certificate, or null if the write-lock cannot be obtained - * - * @throws IOException in case of an IO error - * @throws BadDataException if the thread is interrupted - */ - KeyMaterial tryInsertTrustRoot(InputStream data, KeyMaterialMerger merge) - throws IOException, BadDataException; - - /** - * Insert a certificate identified by its fingerprint. - * This method blocks until the certificate has been written. - * - * @param data input stream containing the certificate data - * @param merge merge callback to merge the certificate with existing certificate material - * @return the merged or inserted certificate - * - * @throws IOException in case of an IO error - * @throws BadDataException if the data stream or existing certificate contains bad data - * @throws InterruptedException if the thread is interrupted - */ - Certificate insert(InputStream data, KeyMaterialMerger merge) - throws IOException, BadDataException, InterruptedException; - - /** - * Insert a certificate identified by its fingerprint. - * Contrary to {@link #insert(InputStream, KeyMaterialMerger)}, this method does not block. - * Instead, it returns null if the write-lock cannot be obtained. - * - * @param data input stream containing the certificate data - * @param merge merge callback to merge the certificate with existing certificate material - * @return the merged or inserted certificate - * - * @throws IOException in case of an IO error - * @throws BadDataException if the data stream or existing certificate contains bad data - */ - Certificate tryInsert(InputStream data, KeyMaterialMerger merge) - throws IOException, BadDataException; - - /** - * Insert a certificate or key under the given special name. - * This method blocks until the certificate/key has been written. - * - * @param specialName special name under which the key material shall be inserted - * @param data input stream containing the key/certificate data - * @param merge callback to merge the key/certificate with existing key material - * @return certificate component of the merged or inserted key material data - * - * @throws IOException in case of an IO error - * @throws BadDataException if the data stream or the existing certificate contains bad data - * @throws BadNameException if the special name is not known - * @throws InterruptedException if the thread is interrupted - */ - Certificate insertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) - throws IOException, BadDataException, BadNameException, InterruptedException; - - /** - * Insert a certificate or key under the given special name. - * Contrary to {@link #insertWithSpecialName(String, InputStream, KeyMaterialMerger)}, this method does not block. - * Instead, it returns null if the write-lock cannot be obtained. - * - * @param specialName special name under which the key material shall be inserted - * @param data input stream containing the key material - * @param merge callback to merge the key/certificate with existing key material - * @return certificate component of the merged or inserted key material - * - * @throws IOException in case of an IO error - * @throws BadDataException if the data stream or existing key material contains bad data - * @throws BadNameException if the special name is not known - */ - Certificate tryInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) - throws IOException, BadDataException, BadNameException; - -} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/FileBasedCertificateDirectoryBackend.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/FileBasedCertificateDirectoryBackend.java deleted file mode 100644 index 08ac356..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/FileBasedCertificateDirectoryBackend.java +++ /dev/null @@ -1,448 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d.backend; - -import pgp.cert_d.PGPCertificateDirectory; -import pgp.cert_d.SpecialNames; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.certificate.Key; -import pgp.certificate_store.certificate.KeyMaterial; -import pgp.certificate_store.certificate.KeyMaterialMerger; -import pgp.certificate_store.certificate.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.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.regex.Pattern; - -/** - * Implementation of {@link PGPCertificateDirectory.Backend} which stores certificates in a directory structure. - * - * @see Shared PGP Certificate Directory - */ -public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirectory.Backend { - - private abstract static class Lazy { - abstract E get() throws BadDataException; - } - - /** - * Locking mechanism which uses a lock file to synchronize write-access to the store. - */ - 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; - } - - long tag = getTagForFingerprint(fingerprint); - - FileInputStream fileIn = new FileInputStream(certFile); - BufferedInputStream bufferedIn = new BufferedInputStream(fileIn); - - Certificate certificate = reader.read(bufferedIn, tag).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; - } - - long tag = getTagForSpecialName(specialName); - - FileInputStream fileIn = new FileInputStream(certFile); - BufferedInputStream bufferedIn = new BufferedInputStream(fileIn); - KeyMaterial keyMaterial = reader.read(bufferedIn, tag); - - 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}$"); - } - }); - - if (subdirectories == null) { - subdirectories = new File[0]; - } - - 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}$"); - } - }); - - if (files == null) { - files = new File[0]; - } - - for (File certFile : files) { - certificateQueue.add(new Lazy() { - @Override - Certificate get() throws BadDataException { - try { - long tag = getTag(certFile); - Certificate certificate = reader.read(new FileInputStream(certFile), tag).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, null); - 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 = merge.merge(newCertificate, existingCertificate); - } - - long tag = writeToFile(newCertificate.getInputStream(), certFile); - if (newCertificate instanceof Key) { - newCertificate = new Key((Key) newCertificate, tag); - } else { - newCertificate = new Certificate((Certificate) newCertificate, tag); - } - return newCertificate; - } - - @Override - public Certificate doInsert(InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException { - KeyMaterial newCertificate = reader.read(data, null); - Certificate existingCertificate; - File certFile; - try { - existingCertificate = readByFingerprint(newCertificate.getFingerprint()); - certFile = resolver.getCertFileByFingerprint(newCertificate.getFingerprint()); - } catch (BadNameException e) { - throw new BadDataException(); - } - - if (existingCertificate != null) { - newCertificate = merge.merge(newCertificate, existingCertificate); - } - - long tag = writeToFile(newCertificate.getInputStream(), certFile); - return new Certificate(newCertificate.asCertificate(), tag); - } - - @Override - public Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException, BadNameException { - KeyMaterial newCertificate = reader.read(data, null); - KeyMaterial existingCertificate; - File certFile; - try { - existingCertificate = readBySpecialName(specialName); - certFile = resolver.getCertFileBySpecialName(specialName); - } catch (BadNameException e) { - throw new BadDataException(); - } - - if (existingCertificate != null) { - newCertificate = merge.merge(newCertificate, existingCertificate); - } - - long tag = writeToFile(newCertificate.getInputStream(), certFile); - return new Certificate(newCertificate.asCertificate(), tag); - } - - @Override - public Long getTagForFingerprint(String fingerprint) throws BadNameException, IOException { - File file = resolver.getCertFileByFingerprint(fingerprint); - return getTag(file); - } - - @Override - public Long getTagForSpecialName(String specialName) throws BadNameException, IOException { - File file = resolver.getCertFileBySpecialName(specialName); - return getTag(file); - } - - private Long getTag(File file) throws IOException { - if (!file.exists()) { - throw new NoSuchElementException(); - } - Path path = file.toPath(); - BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); - - // On UNIX file systems, for example, fileKey() will return the device ID and inode - int fileId = attrs.fileKey().hashCode(); - long lastMod = attrs.lastModifiedTime().toMillis(); - - return lastMod + (11L * fileId); - } - - private long 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(); - return getTag(certFile); - } - - /** - * Class to resolve file names from certificate fingerprints / special names. - */ - public static class FilenameResolver { - - private final File baseDirectory; - // matches v4 and v5 fingerprints (v4 = 40 hex chars, v5 = 64 hex chars) - private final Pattern openPgpFingerprint = Pattern.compile("^[a-f0-9]{40}([a-f0-9]{24})?$"); - - 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); - } - - private boolean isFingerprint(String fingerprint) { - return openPgpFingerprint.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/backend/InMemoryCertificateDirectoryBackend.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/InMemoryCertificateDirectoryBackend.java deleted file mode 100644 index 90797af..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/InMemoryCertificateDirectoryBackend.java +++ /dev/null @@ -1,170 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d.backend; - -import pgp.cert_d.PGPCertificateDirectory; -import pgp.cert_d.SpecialNames; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.certificate.Key; -import pgp.certificate_store.certificate.KeyMaterial; -import pgp.certificate_store.certificate.KeyMaterialMerger; -import pgp.certificate_store.certificate.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; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Implementation of the {@link PGPCertificateDirectory.Backend} which stores key material in-memory. - * It uses object locking with {@link #wait()} and {@link #notify()} to synchronize write-access. - */ -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; - private final AtomicLong nonce = new AtomicLong(1); - - 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) throws BadNameException { - if (SpecialNames.lookupSpecialName(specialName) == null) { - throw new BadNameException("Invalid special name " + 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, null); - KeyMaterial existing = null; - try { - existing = readBySpecialName(SpecialNames.TRUST_ROOT); - } catch (BadNameException e) { - // Does not happen - throw new RuntimeException(e); - } - KeyMaterial merged = merge.merge(update, existing); - if (merged instanceof Key) { - merged = new Key((Key) merged, newTag()); - } else { - merged = new Certificate((Certificate) merged, newTag()); - } - keyMaterialSpecialNameMap.put(SpecialNames.TRUST_ROOT, merged); - return merged; - } - - - @Override - public Certificate doInsert(InputStream data, KeyMaterialMerger merge) - throws IOException, BadDataException { - KeyMaterial update = reader.read(data, null); - Certificate existing = readByFingerprint(update.getFingerprint()); - Certificate merged = merge.merge(update, existing).asCertificate(); - merged = new Certificate(merged, newTag()); - 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, null); - KeyMaterial existing = readBySpecialName(specialName); - KeyMaterial merged = merge.merge(keyMaterial, existing); - if (merged instanceof Key) { - merged = new Key((Key) merged, newTag()); - } else { - merged = new Certificate((Certificate) merged, newTag()); - } - keyMaterialSpecialNameMap.put(specialName, merged); - return merged.asCertificate(); - } - - @Override - public Long getTagForFingerprint(String fingerprint) throws BadNameException, IOException { - Certificate certificate = certificateFingerprintMap.get(fingerprint); - if (certificate == null) { - return null; - } - return certificate.getTag(); - } - - @Override - public Long getTagForSpecialName(String specialName) throws BadNameException, IOException { - if (SpecialNames.lookupSpecialName(specialName) == null) { - throw new BadNameException("Invalid special name " + specialName); - } - KeyMaterial tagged = keyMaterialSpecialNameMap.get(specialName); - if (tagged == null) { - return null; - } - return tagged.getTag(); - } - - private Long newTag() { - return System.currentTimeMillis() + nonce.incrementAndGet(); - } -} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/package-info.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/package-info.java deleted file mode 100644 index bf02c7c..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Storage Backends. - */ -package pgp.cert_d.backend; diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/subkey_lookup/InMemorySubkeyLookupFactory.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/subkey_lookup/InMemorySubkeyLookupFactory.java deleted file mode 100644 index 98e006c..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/subkey_lookup/InMemorySubkeyLookupFactory.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d.subkey_lookup; - -import java.io.File; - -/** - * Factory class to instantiate {@link InMemorySubkeyLookup} objects. - */ -public class InMemorySubkeyLookupFactory implements SubkeyLookupFactory { - @Override - public SubkeyLookup createFileBasedInstance(File baseDirectory) { - return new InMemorySubkeyLookup(); - } -} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/subkey_lookup/SubkeyLookupFactory.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/subkey_lookup/SubkeyLookupFactory.java deleted file mode 100644 index a958b84..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/subkey_lookup/SubkeyLookupFactory.java +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d.subkey_lookup; - -import java.io.File; - -/** - * Factory class to instantiate different {@link SubkeyLookup} implementations. - */ -public interface SubkeyLookupFactory { - - /** - * Create a new {@link SubkeyLookup} instance that lives in the given baseDirectory. - * - * @param baseDirectory base directory - * @return subkey lookup - */ - SubkeyLookup createFileBasedInstance(File baseDirectory); -} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/subkey_lookup/package-info.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/subkey_lookup/package-info.java deleted file mode 100644 index eb0a8b5..0000000 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/subkey_lookup/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Subkey Lookup functionality. - */ -package pgp.cert_d.subkey_lookup; diff --git a/pgp-cert-d-java/src/test/java/pgp/cert_d/BaseDirectoryProviderTest.java b/pgp-cert-d-java/src/test/java/pgp/cert_d/BaseDirectoryProviderTest.java index f6af1be..357ba3f 100644 --- a/pgp-cert-d-java/src/test/java/pgp/cert_d/BaseDirectoryProviderTest.java +++ b/pgp-cert-d-java/src/test/java/pgp/cert_d/BaseDirectoryProviderTest.java @@ -18,7 +18,7 @@ public class BaseDirectoryProviderTest { public void testGetDefaultBaseDir_Linux() { assumeTrue(System.getProperty("os.name").equalsIgnoreCase("linux")); File baseDir = BaseDirectoryProvider.getDefaultBaseDirForOS("linux"); - assertTrue(baseDir.getAbsolutePath().endsWith("pgp.cert.d")); + assertTrue(baseDir.getAbsolutePath().endsWith("/.local/share/pgp.cert.d")); } @Test diff --git a/pgp-cert-d-java/src/test/java/pgp/cert_d/FileBasedPGPCertificateDirectoryTest.java b/pgp-cert-d-java/src/test/java/pgp/cert_d/FileBasedPGPCertificateDirectoryTest.java deleted file mode 100644 index 8701e33..0000000 --- a/pgp-cert-d-java/src/test/java/pgp/cert_d/FileBasedPGPCertificateDirectoryTest.java +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d; - -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Test; -import pgp.cert_d.backend.FileBasedCertificateDirectoryBackend; -import pgp.cert_d.dummy.TestKeyMaterialMerger; -import pgp.cert_d.dummy.TestKeyMaterialReaderBackend; -import pgp.cert_d.subkey_lookup.InMemorySubkeyLookup; -import pgp.cert_d.subkey_lookup.SubkeyLookup; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.certificate.KeyMaterialMerger; -import pgp.certificate_store.certificate.KeyMaterialReaderBackend; -import pgp.certificate_store.exception.BadDataException; -import pgp.certificate_store.exception.BadNameException; -import pgp.certificate_store.exception.NotAStoreException; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.file.Files; - -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assumptions.assumeFalse; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - -public class FileBasedPGPCertificateDirectoryTest { - - private static final KeyMaterialMerger merger = new TestKeyMaterialMerger(); - @Test - public void testFileBasedCertificateDirectoryTagChangesWhenFileChanges() - throws IOException, NotAStoreException, BadDataException, InterruptedException, BadNameException { - File tempDir = Files.createTempDirectory("file-based-changes").toFile(); - tempDir.deleteOnExit(); - PGPCertificateDirectory directory = PGPCertificateDirectories.fileBasedCertificateDirectory( - new TestKeyMaterialReaderBackend(), - tempDir, - new InMemorySubkeyLookup()); - FileBasedCertificateDirectoryBackend.FilenameResolver resolver = - new FileBasedCertificateDirectoryBackend.FilenameResolver(tempDir); - - // Insert certificate - Certificate certificate = directory.insert(TestKeys.getCedricCert(), merger); - Long tag = certificate.getTag(); - assertNotNull(tag); - assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag)); - - Long oldTag = tag; - - Thread.sleep(10); - // Change the file on disk directly, this invalidates the tag due to changed modification date - File certFile = resolver.getCertFileByFingerprint(certificate.getFingerprint()); - FileOutputStream fileOut = new FileOutputStream(certFile); - Streams.pipeAll(certificate.getInputStream(), fileOut); - fileOut.write("\n".getBytes()); - fileOut.close(); - - // Old invalidated tag indicates a change, so the modified certificate is returned - certificate = directory.getByFingerprintIfChanged(certificate.getFingerprint(), oldTag); - assertNotNull(certificate); - - // new tag is valid - tag = certificate.getTag(); - assertNotEquals(oldTag, tag); - assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag)); - } - - @Test - public void fileBasedStoreInWriteProtectedAreaThrows() { - File root = new File("/"); - assumeTrue(root.exists(), "This test only runs on unix-like systems"); - File baseDirectory = new File(root, "pgp.cert.d"); - assumeFalse(baseDirectory.mkdirs(), "This test assumes that we cannot create dirs in /"); - - KeyMaterialReaderBackend reader = new TestKeyMaterialReaderBackend(); - SubkeyLookup lookup = new InMemorySubkeyLookup(); - assertThrows(NotAStoreException.class, () -> PGPCertificateDirectories.fileBasedCertificateDirectory( - reader, baseDirectory, lookup)); - } - - @Test - public void fileBasedStoreOnFileThrows() - throws IOException { - File tempDir = Files.createTempDirectory("containsAFile").toFile(); - tempDir.deleteOnExit(); - File baseDir = new File(tempDir, "pgp.cert.d"); - baseDir.createNewFile(); // this is a file, not a dir - - KeyMaterialReaderBackend reader = new TestKeyMaterialReaderBackend(); - SubkeyLookup lookup = new InMemorySubkeyLookup(); - assertThrows(NotAStoreException.class, () -> PGPCertificateDirectories.fileBasedCertificateDirectory( - reader, baseDir, lookup)); - } - - @Test - public void testCertificateStoredUnderWrongFingerprintThrowsBadData() - throws IOException, NotAStoreException, BadDataException, InterruptedException, BadNameException { - File tempDir = Files.createTempDirectory("wrong-fingerprint").toFile(); - tempDir.deleteOnExit(); - PGPCertificateDirectory directory = PGPCertificateDirectories.fileBasedCertificateDirectory( - new TestKeyMaterialReaderBackend(), - tempDir, - new InMemorySubkeyLookup()); - FileBasedCertificateDirectoryBackend.FilenameResolver resolver = - new FileBasedCertificateDirectoryBackend.FilenameResolver(tempDir); - - // Insert Rons certificate - directory.insert(TestKeys.getRonCert(), merger); - - // Copy Rons cert to Cedrics cert file - File ronCert = resolver.getCertFileByFingerprint(TestKeys.RON_FP); - FileInputStream inputStream = new FileInputStream(ronCert); - File cedricCert = resolver.getCertFileByFingerprint(TestKeys.CEDRIC_FP); - cedricCert.getParentFile().mkdirs(); - cedricCert.createNewFile(); - FileOutputStream outputStream = new FileOutputStream(cedricCert); - Streams.pipeAll(inputStream, outputStream); - inputStream.close(); - outputStream.close(); - - // Reading cedrics cert will fail, as it has Rons fingerprint - assertThrows(BadDataException.class, () -> directory.getByFingerprint(TestKeys.CEDRIC_FP)); - } -} 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 ee27bf0..1d534a4 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 @@ -6,7 +6,6 @@ package pgp.cert_d; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import pgp.cert_d.backend.FileBasedCertificateDirectoryBackend; import pgp.certificate_store.exception.BadNameException; import java.io.File; @@ -19,13 +18,13 @@ import static org.junit.jupiter.api.Assertions.assertThrows; public class FilenameResolverTest { private File baseDir; - private FileBasedCertificateDirectoryBackend.FilenameResolver resolver; + private FilenameResolver resolver; @BeforeEach public void setup() throws IOException { baseDir = Files.createTempDirectory("filenameresolver").toFile(); baseDir.deleteOnExit(); - resolver = new FileBasedCertificateDirectoryBackend.FilenameResolver(baseDir); + resolver = new FilenameResolver(baseDir); } @Test diff --git a/pgp-cert-d-java/src/test/java/pgp/cert_d/PGPCertificateDirectoryTest.java b/pgp-cert-d-java/src/test/java/pgp/cert_d/PGPCertificateDirectoryTest.java deleted file mode 100644 index f6bd858..0000000 --- a/pgp-cert-d-java/src/test/java/pgp/cert_d/PGPCertificateDirectoryTest.java +++ /dev/null @@ -1,311 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d; - -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Named; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import pgp.cert_d.dummy.TestKeyMaterialMerger; -import pgp.cert_d.dummy.TestKeyMaterialReaderBackend; -import pgp.cert_d.subkey_lookup.InMemorySubkeyLookup; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.certificate.Key; -import pgp.certificate_store.certificate.KeyMaterial; -import pgp.certificate_store.certificate.KeyMaterialMerger; -import pgp.certificate_store.exception.BadDataException; -import pgp.certificate_store.exception.BadNameException; -import pgp.certificate_store.exception.NotAStoreException; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.Files; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.Set; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static pgp.cert_d.TestKeys.CEDRIC_FP; -import static pgp.cert_d.TestKeys.HARRY_FP; -import static pgp.cert_d.TestKeys.RON_FP; - -public class PGPCertificateDirectoryTest { - - - private static final KeyMaterialMerger merger = new TestKeyMaterialMerger(); - - private static Stream provideTestSubjects() - throws IOException, NotAStoreException { - PGPCertificateDirectory inMemory = PGPCertificateDirectories.inMemoryCertificateDirectory( - new TestKeyMaterialReaderBackend()); - - File tempDir = Files.createTempDirectory("pgp-cert-d-test").toFile(); - tempDir.deleteOnExit(); - PGPCertificateDirectory fileBased = PGPCertificateDirectories.fileBasedCertificateDirectory( - new TestKeyMaterialReaderBackend(), - tempDir, - new InMemorySubkeyLookup()); - - return Stream.of( - Arguments.of(Named.of("InMemoryCertificateDirectory", inMemory)), - Arguments.of(Named.of("FileBasedCertificateDirectory", fileBased))); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void getNonExistentCertByFingerprintThrowsNoSuchElementException(PGPCertificateDirectory directory) { - assertThrows(NoSuchElementException.class, () -> - directory.getByFingerprint("0000000000000000000000000000000000000000")); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void getNonExistentCertByFingerprintIfChangedThrowsNoSuchElementException(PGPCertificateDirectory directory) { - assertThrows(NoSuchElementException.class, () -> - directory.getByFingerprintIfChanged("0000000000000000000000000000000000000000", 12)); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void getNonExistentCertBySpecialNameThrowsNoSuchElementException(PGPCertificateDirectory directory) { - assertThrows(NoSuchElementException.class, () -> - directory.getBySpecialName(SpecialNames.TRUST_ROOT)); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void getNonExistentCertBySpecialNameIfChangedThrowsNoSuchElementException(PGPCertificateDirectory directory) { - assertThrows(NoSuchElementException.class, () -> - directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, 12)); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void getNonExistentTrustRootThrowsNoSuchElementException(PGPCertificateDirectory directory) { - assertThrows(NoSuchElementException.class, () -> - directory.getTrustRoot()); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void getNonExistentTrustRootIfChangedThrowsNoSuchElementException(PGPCertificateDirectory directory) { - assertThrows(NoSuchElementException.class, () -> - directory.getTrustRootCertificateIfChanged(12)); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void getNonExistentTrustRootCertificateThrowsNoSuchElementException(PGPCertificateDirectory directory) { - assertThrows(NoSuchElementException.class, () -> - directory.getTrustRootCertificate()); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void lockDirectoryAndTryInsertWillFail(PGPCertificateDirectory directory) - throws IOException, InterruptedException, BadDataException { - // Manually lock the dir - assertFalse(directory.backend.getLock().isLocked()); - directory.backend.getLock().lockDirectory(); - assertTrue(directory.backend.getLock().isLocked()); - assertFalse(directory.backend.getLock().tryLockDirectory()); - - Certificate inserted = directory.tryInsert(TestKeys.getCedricCert(), merger); - assertNull(inserted); - - directory.backend.getLock().releaseDirectory(); - inserted = directory.tryInsert(TestKeys.getCedricCert(), merger); - assertNotNull(inserted); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void lockDirectoryAndTryInsertTrustRootWillFail(PGPCertificateDirectory directory) - throws IOException, InterruptedException, BadDataException { - // Manually lock the dir - assertFalse(directory.backend.getLock().isLocked()); - directory.backend.getLock().lockDirectory(); - assertTrue(directory.backend.getLock().isLocked()); - - KeyMaterial inserted = directory.tryInsertTrustRoot(TestKeys.getHarryKey(), merger); - assertNull(inserted); - - directory.backend.getLock().releaseDirectory(); - inserted = directory.tryInsertTrustRoot(TestKeys.getHarryKey(), merger); - assertNotNull(inserted); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void lockDirectoryAndTryInsertWithSpecialNameWillFail(PGPCertificateDirectory directory) - throws IOException, InterruptedException, BadDataException, BadNameException { - // Manually lock the dir - assertFalse(directory.backend.getLock().isLocked()); - directory.backend.getLock().lockDirectory(); - assertTrue(directory.backend.getLock().isLocked()); - - Certificate inserted = directory.tryInsertWithSpecialName(SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger); - assertNull(inserted); - - directory.backend.getLock().releaseDirectory(); - inserted = directory.tryInsertWithSpecialName(SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger); - assertNotNull(inserted); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void getByInvalidNameFails(PGPCertificateDirectory directory) { - assertThrows(BadNameException.class, () -> directory.getBySpecialName("invalid")); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void testInsertAndGetSingleCert(PGPCertificateDirectory directory) - throws BadDataException, IOException, InterruptedException, BadNameException { - assertThrows(NoSuchElementException.class, () -> directory.getByFingerprint(CEDRIC_FP), "Empty directory MUST NOT contain certificate"); - - Certificate certificate = directory.insert(TestKeys.getCedricCert(), merger); - assertEquals(CEDRIC_FP, certificate.getFingerprint(), "Fingerprint of inserted cert MUST match"); - - Certificate get = directory.getByFingerprint(CEDRIC_FP); - assertEquals(CEDRIC_FP, get.getFingerprint(), "Fingerprint of retrieved cert MUST match"); - - byte[] expected = TestKeys.CEDRIC_CERT.getBytes(Charset.forName("UTF8")); - ByteArrayOutputStream actual = new ByteArrayOutputStream(); - Streams.pipeAll(get.getInputStream(), actual); - assertArrayEquals(expected, actual.toByteArray(), "InputStream of cert MUST match what we gave in"); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void testInsertAndGetTrustRootAndCert(PGPCertificateDirectory directory) - throws BadDataException, IOException, InterruptedException { - assertThrows(NoSuchElementException.class, () -> directory.getTrustRoot()); - - KeyMaterial trustRootMaterial = directory.insertTrustRoot( - TestKeys.getHarryKey(), merger); - assertNotNull(trustRootMaterial); - assertTrue(trustRootMaterial instanceof Key); - assertEquals(HARRY_FP, trustRootMaterial.getFingerprint()); - - Key trustRoot = (Key) directory.getTrustRoot(); - assertEquals(HARRY_FP, trustRoot.getFingerprint()); - Certificate trustRootCert = directory.getTrustRootCertificate(); - assertEquals(HARRY_FP, trustRootCert.getFingerprint()); - - directory.tryInsert(TestKeys.getRonCert(), merger); - directory.insert(TestKeys.getCedricCert(), merger); - - Set expected = new HashSet<>(Arrays.asList(RON_FP, CEDRIC_FP)); - - Set actual = new HashSet<>(); - Iterator fingerprints = directory.fingerprints(); - actual.add(fingerprints.next()); - actual.add(fingerprints.next()); - assertFalse(fingerprints.hasNext()); - - assertEquals(expected, actual); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void testGetTrustRootIfChanged(PGPCertificateDirectory directory) - throws BadDataException, IOException, InterruptedException { - KeyMaterial trustRootMaterial = directory.insertTrustRoot( - TestKeys.getHarryKey(), merger); - - assertNotNull(trustRootMaterial.getTag()); - Long tag = trustRootMaterial.getTag(); - assertNull(directory.getTrustRootCertificateIfChanged(tag)); - assertNotNull(directory.getTrustRootCertificateIfChanged(tag + 1)); - - Long oldTag = tag; - Thread.sleep(10); - // "update" key - trustRootMaterial = directory.insertTrustRoot( - TestKeys.getHarryKey(), merger); - tag = trustRootMaterial.getTag(); - - assertNotEquals(oldTag, tag); - assertNotNull(directory.getTrustRootCertificateIfChanged(oldTag)); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void testGetBySpecialNameIfChanged(PGPCertificateDirectory directory) - throws BadDataException, IOException, InterruptedException, BadNameException { - KeyMaterial specialName = directory.insertWithSpecialName(SpecialNames.TRUST_ROOT, - TestKeys.getHarryKey(), merger); - - assertNotNull(specialName.getTag()); - Long tag = specialName.getTag(); - assertNull(directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag)); - assertNotNull(directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag + 1)); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void testGetByFingerprintIfChanged(PGPCertificateDirectory directory) - throws BadDataException, IOException, InterruptedException, BadNameException { - Certificate certificate = directory.insert(TestKeys.getCedricCert(), merger); - Long tag = certificate.getTag(); - assertNotNull(tag); - - assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag)); - assertNotNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag + 1)); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void testOverwriteTrustRoot(PGPCertificateDirectory directory) - throws BadDataException, IOException, InterruptedException { - directory.insertTrustRoot(TestKeys.getHarryKey(), merger); - assertEquals(HARRY_FP, directory.getTrustRoot().getFingerprint()); - assertTrue(directory.getTrustRoot() instanceof Key); - - directory.insertTrustRoot(TestKeys.getCedricCert(), merger); - assertEquals(CEDRIC_FP, directory.getTrustRoot().getFingerprint()); - assertTrue(directory.getTrustRoot() instanceof Certificate); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void testOverwriteSpecialName(PGPCertificateDirectory directory) - throws BadDataException, IOException, InterruptedException, BadNameException { - directory.insertWithSpecialName(SpecialNames.TRUST_ROOT, TestKeys.getRonCert(), merger); - KeyMaterial bySpecialName = directory.getBySpecialName(SpecialNames.TRUST_ROOT); - assertEquals(RON_FP, bySpecialName.getFingerprint()); - - directory.insertWithSpecialName(SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger); - assertEquals(HARRY_FP, directory.getBySpecialName(SpecialNames.TRUST_ROOT).getFingerprint()); - } - - @ParameterizedTest - @MethodSource("provideTestSubjects") - public void testOverwriteByFingerprint(PGPCertificateDirectory directory) - throws BadDataException, IOException, InterruptedException, BadNameException { - directory.insert(TestKeys.getRonCert(), merger); - Certificate extracted = directory.getByFingerprint(RON_FP); - assertEquals(RON_FP, extracted.getFingerprint()); - - directory.insert(TestKeys.getRonCert(), merger); - extracted = directory.getByFingerprint(RON_FP); - assertEquals(RON_FP, extracted.getFingerprint()); - } - -} diff --git a/pgp-cert-d-java/src/test/java/pgp/cert_d/PGPCertificateStoreAdapterTest.java b/pgp-cert-d-java/src/test/java/pgp/cert_d/PGPCertificateStoreAdapterTest.java deleted file mode 100644 index 79b5fed..0000000 --- a/pgp-cert-d-java/src/test/java/pgp/cert_d/PGPCertificateStoreAdapterTest.java +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import pgp.cert_d.backend.InMemoryCertificateDirectoryBackend; -import pgp.cert_d.dummy.TestKeyMaterialMerger; -import pgp.cert_d.dummy.TestKeyMaterialReaderBackend; -import pgp.cert_d.subkey_lookup.InMemorySubkeyLookupFactory; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.exception.BadDataException; -import pgp.certificate_store.exception.BadNameException; - -import java.io.IOException; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class PGPCertificateStoreAdapterTest { - - private PGPCertificateDirectory directory; - private PGPCertificateStoreAdapter adapter; - - private static final TestKeyMaterialMerger merger = new TestKeyMaterialMerger(); - - @BeforeEach - public void setup() { - directory = new PGPCertificateDirectory( - new InMemoryCertificateDirectoryBackend(new TestKeyMaterialReaderBackend()), - new InMemorySubkeyLookupFactory().createFileBasedInstance(null)); - adapter = new PGPCertificateStoreAdapter(directory); - } - - @Test - public void testBadFPWithInvalidCharsYieldsBadNameException() { - assertThrows(BadNameException.class, () -> adapter.getCertificate("XYZ78fd17f207fdf62f7976c4e9d98917ad84522")); - } - - @Test - public void testBadFPWithTooFewCharsYieldsBadNameException() { - assertThrows(BadNameException.class, () -> adapter.getCertificate("23578fd17f207fdf62f7976c4e9d98917ad")); - } - - @Test - public void testInsertGetCertificate() - throws BadDataException, IOException, InterruptedException, BadNameException { - assertThrows(NoSuchElementException.class, () -> adapter.getCertificate(TestKeys.CEDRIC_FP)); - assertFalse(adapter.getCertificates().hasNext()); - - Certificate certificate = adapter.insertCertificate(TestKeys.getCedricCert(), merger); - assertNotNull(certificate); - assertEquals(TestKeys.CEDRIC_FP, certificate.getFingerprint()); - - certificate = adapter.getCertificate(TestKeys.CEDRIC_FP.toUpperCase()); - assertEquals(TestKeys.CEDRIC_FP, certificate.getFingerprint(), "We can also fetch with uppercase fps"); - - Iterator fingerprints = adapter.getFingerprints(); - assertEquals(TestKeys.CEDRIC_FP, fingerprints.next()); - assertFalse(fingerprints.hasNext()); - } - - @Test - public void testInsertGetTrustRoot() - throws BadDataException, BadNameException, IOException, InterruptedException { - assertThrows(NoSuchElementException.class, () -> adapter.getCertificate(SpecialNames.TRUST_ROOT)); - - Certificate certificate = adapter.insertCertificateBySpecialName( - SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger); - assertNotNull(certificate); - assertEquals(TestKeys.HARRY_FP, certificate.getFingerprint()); - - assertFalse(adapter.getCertificates().hasNext(), "Special-named certs are not returned by getCertificates()"); - assertFalse(adapter.getFingerprints().hasNext()); - } - - @Test - public void testGetCertificateIfChanged() - throws BadDataException, IOException, InterruptedException, BadNameException { - Certificate certificate = adapter.insertCertificate(TestKeys.getRonCert(), merger); - Long tag = certificate.getTag(); - - assertNull(adapter.getCertificateIfChanged(TestKeys.RON_FP, tag), "Cert has not changed, tag is still valid"); - assertNotNull(adapter.getCertificateIfChanged(TestKeys.RON_FP, tag + 1)); - } - - @Test - public void testGetTrustRootIfChanged() - throws BadDataException, BadNameException, IOException, InterruptedException { - Certificate certificate = adapter.insertCertificateBySpecialName(SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger); - Long tag = certificate.getTag(); - - assertNull(adapter.getCertificateIfChanged(SpecialNames.TRUST_ROOT, tag)); - assertNotNull(adapter.getCertificateIfChanged(SpecialNames.TRUST_ROOT, tag * 2)); - } - - @Test - public void testGetCertificateBySubkeyId() - throws BadDataException, IOException, InterruptedException { - // Insert some certs - adapter.insertCertificate(TestKeys.getCedricCert(), merger); - adapter.insertCertificate(TestKeys.getHarryKey(), merger); - // Now insert Ron - Certificate certificate = adapter.insertCertificate(TestKeys.getRonCert(), merger); - List subkeyIds = certificate.getSubkeyIds(); - - assertFalse(adapter.getCertificatesBySubkeyId(0).hasNext()); - - for (Long subkeyId : subkeyIds) { - Iterator certsWithSubkey = adapter.getCertificatesBySubkeyId(subkeyId); - Certificate certWithSubkey = certsWithSubkey.next(); - assertFalse(certsWithSubkey.hasNext()); - - assertEquals(TestKeys.RON_FP, certWithSubkey.getFingerprint()); - } - } -} diff --git a/pgp-cert-d-java/src/test/java/pgp/cert_d/SubkeyLookupTest.java b/pgp-cert-d-java/src/test/java/pgp/cert_d/SubkeyLookupTest.java index d05a149..03ad9c8 100644 --- a/pgp-cert-d-java/src/test/java/pgp/cert_d/SubkeyLookupTest.java +++ b/pgp-cert-d-java/src/test/java/pgp/cert_d/SubkeyLookupTest.java @@ -24,8 +24,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import pgp.cert_d.jdbc.sqlite.DatabaseSubkeyLookup; import pgp.cert_d.jdbc.sqlite.SqliteSubkeyLookupDaoImpl; -import pgp.cert_d.subkey_lookup.InMemorySubkeyLookup; -import pgp.cert_d.subkey_lookup.SubkeyLookup; +import pgp.certificate_store.SubkeyLookup; public class SubkeyLookupTest { diff --git a/pgp-cert-d-java/src/test/java/pgp/cert_d/TestKeys.java b/pgp-cert-d-java/src/test/java/pgp/cert_d/TestKeys.java deleted file mode 100644 index fd2d8f1..0000000 --- a/pgp-cert-d-java/src/test/java/pgp/cert_d/TestKeys.java +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.nio.charset.Charset; - -public class TestKeys { - @SuppressWarnings("CharsetObjectCanBeUsed") - private static final Charset UTF8 = Charset.forName("UTF8"); - - public static final String HARRY_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Comment: 2357 8FD1 7F20 7FDF 62F7 976C 4E9D 9891 7AD8 4522\n" + - "Comment: Harry Potter \n" + - "\n" + - "xVgEYwTP0hYJKwYBBAHaRw8BAQdAPVcWeaMiUVG+vECWpoytSoF3wNJQG/JsnCbj\n" + - "uQtv0REAAP0cS3GCmrIMO/FqNm1FG1mKw4P+mvZ1JBFILN7Laooq7A/QwsARBB8W\n" + - "CgCDBYJjBM/SBYkFn6YAAwsJBwkQTp2YkXrYRSJHFAAAAAAAHgAgc2FsdEBub3Rh\n" + - "dGlvbnMuc2VxdW9pYS1wZ3Aub3JnRSvJhQu9P/3bpFqFdB2c5Mfg9JIdyic1tsAt\n" + - "lZ7o4k4DFQoIApsBAh4BFiEEI1eP0X8gf99i95dsTp2YkXrYRSIAAK2cAP9juDnY\n" + - "qB6XuXVx76MzDlFemqJ/r2TIlN22O33ITp23cQEAiMk/rULVdfmlFi3QBvXgtPI2\n" + - "QQYFI0UnyGLmJSa1cwzNIEhhcnJ5IFBvdHRlciA8aGFycnlAcG90dGVyLm1vcmU+\n" + - "wsAUBBMWCgCGBYJjBM/SBYkFn6YAAwsJBwkQTp2YkXrYRSJHFAAAAAAAHgAgc2Fs\n" + - "dEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn0o9na1p+a9kY3y3+xUSFFnxbuxNM\n" + - "5zvth0SAfJIH2C8DFQoIApkBApsBAh4BFiEEI1eP0X8gf99i95dsTp2YkXrYRSIA\n" + - "AC1zAP0e2qRXH4zCnjvdYwGP0tIY3dwBsm1bvk+wVFHm8h68iwEAh2uyyQ+O5iQH\n" + - "7NN/lV5dUKKsKaimj/vVGpSW3NtFZQDHWARjBM/SFgkrBgEEAdpHDwEBB0BUqcZu\n" + - "VsEO6fmW8q3S5ll9WohcTOWRX7Spg5wS3DIqPgABALzJ9ZImb4U94WqRtftSSaeF\n" + - "0w6rHCn2DiTT8pxjefGQEW7CwMUEGBYKATcFgmMEz9IFiQWfpgAJEE6dmJF62EUi\n" + - "RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ+HPX0u5kyKR\n" + - "5IwErbomgGKVCGuvR6oSKc7CDQYMJS9eApsCvqAEGRYKAG8FgmMEz9IJEKk0hrvR\n" + - "6Jc7RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ8Chba26\n" + - "1nQ6ZEZ/rVH8wMhYznGNa/Ux28sodM04wU6dFiEEli7ijJ6quX9gSoSbqTSGu9Ho\n" + - "lzsAAG1wAQDVvKVWaMOBELROkF72oBH58X6lrOmr08W5FJQxehywhQEAwetpgL1V\n" + - "DNj4qcvuCJJ2agAM1tA22WMPpQQeA5CCgwcWIQQjV4/RfyB/32L3l2xOnZiRethF\n" + - "IgAAsWEA/RfOKexMYEtzlpM71MB9SL+emHXf+w1TNAvBxrifU8bMAPoDmWHkWjZQ\n" + - "N6upbHKssRywPLKCMPLnFYtBNxDrMYr0BMddBGMEz9ISCisGAQQBl1UBBQEBB0CR\n" + - "p5dCIlSpV/EvXX2+YZnZSRtc8eTFXkph8RArNi0QPAMBCAcAAP9seqRo6mbmvS4h\n" + - "fkxmV5zap3wIemzW4iabNU2VbWJbEBALwsAGBBgWCgB4BYJjBM/SBYkFn6YACRBO\n" + - "nZiRethFIkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdx\n" + - "uRLJ/h81azzvGn5zgJ+jdfkdM6iO+f1CLgfnHUH9ugKbDBYhBCNXj9F/IH/fYveX\n" + - "bE6dmJF62EUiAACObgEAk4whKEo2nzpWht65tpFjrEXdakj00mA/P612P2CUdPQB\n" + - "ANNn+VUiu9rtnLcP4NlaUVOwsgN7yyed0orbmG1VvSMF\n" + - "=cBAn\n" + - "-----END PGP PRIVATE KEY BLOCK-----\n"; - public static final String HARRY_FP = "23578fd17f207fdf62f7976c4e9d98917ad84522"; - - public static final String RON_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "Comment: B798 AF18 6BFE 4C19 902D 4950 5647 F001 37EF 4C41\n" + - "Comment: Ron Weasley \n" + - "\n" + - "xjMEYwTRXBYJKwYBBAHaRw8BAQdAPHyiu4nwvo3OY3wLG1tUmS6qeTeT1zd3BrL+\n" + - "6/5Ys3jCwBEEHxYKAIMFgmME0VwFiQWfpgADCwkHCRBWR/ABN+9MQUcUAAAAAAAe\n" + - "ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfEPNi/1ObPMwDwS094Lcyq\n" + - "dRNRk2FRzvhoXKrqF/GHfQMVCggCmwECHgEWIQS3mK8Ya/5MGZAtSVBWR/ABN+9M\n" + - "QQAAR/oBAJWxxUJqOAzYG4uAd6SSF55LZVl00t3bGhgEyGmrB/ppAQCZTpWu0rwU\n" + - "GVv/MoeqRwX+P8sHS4FSu/hSYJpbNwysCM0gUm9uIFdlYXNsZXkgPHJvbkB3ZWFz\n" + - "bGV5LmJ1cnJvdz7CwBQEExYKAIYFgmME0VwFiQWfpgADCwkHCRBWR/ABN+9MQUcU\n" + - "AAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmf43PjsP9w1eGYP\n" + - "CLm6O+K27EQPiCf2cW71QnQ0RunupgMVCggCmQECmwECHgEWIQS3mK8Ya/5MGZAt\n" + - "SVBWR/ABN+9MQQAA7rYA/3U2aaw5PFa9L90PbxygOwFrgIVWLiOpnKfjqDJqEgva\n" + - "AQDxTIbpUYEAYmTpmAm1tiQSlpp9P96vqCMIj2OqtYCNAs4zBGME0VwWCSsGAQQB\n" + - "2kcPAQEHQGzhRPzKRkkce0v1NjuTV2stn8CEMVgnUxsMPtd0h2M9wsDFBBgWCgE3\n" + - "BYJjBNFcBYkFn6YACRBWR/ABN+9MQUcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z\n" + - "ZXF1b2lhLXBncC5vcmd6UNkzsh0jKRPQAKX2PoUhMN4QfhTK9IC6L+QbyL1rFgKb\n" + - "Ar6gBBkWCgBvBYJjBNFcCRCuGMJD3GUsUUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + - "cy5zZXF1b2lhLXBncC5vcmcUTns9+sw7XKKO5ZOYQninRAchypKHbqV2LinV46Hi\n" + - "bxYhBI+SjTgn0fulukOYj64YwkPcZSxRAADZtAEApse3UJi1iuSFvnyXxuYIOm4d\n" + - "0sOaOtd18venqfWGyX4BALf7T7LknMY688vaW6/xkw2fonG6Y5VxreIHlMZAcX0H\n" + - "FiEEt5ivGGv+TBmQLUlQVkfwATfvTEEAAFQ3AQCGSLEt8wgJZXlljPdk1eQ3uvW3\n" + - "VHryNAc3/vbSOvByFAD/WKXY8Pqki2r9XVUW33Q88firoiKVuGmBxklEG3ACjALO\n" + - "OARjBNFcEgorBgEEAZdVAQUBAQdARnMlx3ST0EHPiErN7lOF+lhtJ8FmW9arc46u\n" + - "sHFMgUMDAQgHwsAGBBgWCgB4BYJjBNFcBYkFn6YACRBWR/ABN+9MQUcUAAAAAAAe\n" + - "ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmfv1PKQX1GMihAdj3ftW/yS\n" + - "bnPYdE+0h5rGCuhYl7sjaQKbDBYhBLeYrxhr/kwZkC1JUFZH8AE370xBAABWugEA\n" + - "rWOEHQjzoQkxxsErVEVZjqr05SLMmo6+HMJ/4Sgur10A/0+4FSbaKKNGiCnCMRsZ\n" + - "BEswoD99mUaBXl1nPH+Hg38O\n" + - "=+pb5\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"; - public static final String RON_FP = "b798af186bfe4c19902d49505647f00137ef4c41"; - - public static final String CEDRIC_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "Comment: 5E75 BF20 646B C1A9 8D3B 1BC2 FE9C D472 987C 4021\n" + - "Comment: Cedric Diggory \n" + - "\n" + - "xjMEYwTIyhYJKwYBBAHaRw8BAQdA80cyaoAEfh/ENuHw8XtWqrxDoPQ/x44LQzyO\n" + - "TLhMN+PCwBEEHxYKAIMFgmMEyMoFiQWfpgADCwkHCRD+nNRymHxAIUcUAAAAAAAe\n" + - "ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmf0ckQJQzwOKkXPe8rFP5B+\n" + - "CbAshRG5OKD3Dp+hScGFXgMVCggCmwECHgEWIQRedb8gZGvBqY07G8L+nNRymHxA\n" + - "IQAA9WYBAP5rQCq/W3KV90T/wpxf5pcXoCB4tCC9Gi/1AiuGhQdAAP48PIX9fH+T\n" + - "g7N+tU0xzzCc2nWxG3cIuvGFsg94pKL8As0gQ2VkcmljIERpZ2dvcnkgPGNlZHJp\n" + - "Y0BkaWdnby5yeT7CwBQEExYKAIYFgmMEyMoFiQWfpgADCwkHCRD+nNRymHxAIUcU\n" + - "AAAAAAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdkUL5mF5SwIXja\n" + - "bCxhI3lvqiUURSoLY13K6YvHYLz7bwMVCggCmQECmwECHgEWIQRedb8gZGvBqY07\n" + - "G8L+nNRymHxAIQAA6SwA/jiM8k/Z0ljnHdFxsdoLhdnTZ0yJT/7RxreSZ3aITrDs\n" + - "AP9V8bAYy4hK0C7i4FmNcos3HQs2Si6ee2/EZjo8LqxeCc4zBGMEyMoWCSsGAQQB\n" + - "2kcPAQEHQIu0hKMngTnmIPXlZ/p9WOZmLB0s9v9yZJLdZ5ICKn7jwsDFBBgWCgE3\n" + - "BYJjBMjKBYkFn6YACRD+nNRymHxAIUcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z\n" + - "ZXF1b2lhLXBncC5vcmdCT1SyOVJwTPp4OEDWFNEgxKD12H+Dya9EzOMJ3I9frwKb\n" + - "Ar6gBBkWCgBvBYJjBMjKCRDNPli8d9EIkUcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + - "cy5zZXF1b2lhLXBncC5vcmccLTSNIhZOiNFaTj76iAutuAkUCImFp5ptMICZRo7E\n" + - "TRYhBESzEAYRbxRfM3ub5c0+WLx30QiRAAAZtwD/WRJrSxzJRsnZs4w+QgZjqOZx\n" + - "bOGwGObfbEHaExG0cKEA/R+BFODg5oPOvK9W7n0Kt9O171Po+zXB0UDmBiEhh0YL\n" + - "FiEEXnW/IGRrwamNOxvC/pzUcph8QCEAAEneAQDnOv/cf1/qmjfLnorEi+Z4gRWQ\n" + - "fp3Rp/gI4SLUQxT0PQD/USZIP0bNMGGC1TRQa+8nK6opSqtIvsatt0tQuu178A7O\n" + - "OARjBMjKEgorBgEEAZdVAQUBAQdAazcEUsYtY9f9o4A+ePR7ACMIDScVEUWS83+I\n" + - "SwJQz3QDAQgHwsAGBBgWCgB4BYJjBMjKBYkFn6YACRD+nNRymHxAIUcUAAAAAAAe\n" + - "ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmc/qxMatwD+6zaKDZGlVdn/\n" + - "TWumSgLtuyYonaOupIfMEAKbDBYhBF51vyBka8GpjTsbwv6c1HKYfEAhAADPiwEA\n" + - "vQ7fTnAHcdZlMVnNPkc0pZSp1+kO5Z789I5Pp4HloNIBAMoC84ja83PjvcpIyxgR\n" + - "kspLC9BliezVbFSHIK9NQ/wC\n" + - "=VemI\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"; - public static final String CEDRIC_FP = "5e75bf20646bc1a98d3b1bc2fe9cd472987c4021"; - - public static InputStream getHarryKey() { - return new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8)); - } - - public static InputStream getRonCert() { - return new ByteArrayInputStream(RON_CERT.getBytes(UTF8)); - } - - public static InputStream getCedricCert() { - return new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)); - } -} diff --git a/pgp-cert-d-java/src/test/java/pgp/cert_d/dummy/TestKeyMaterialMerger.java b/pgp-cert-d-java/src/test/java/pgp/cert_d/dummy/TestKeyMaterialMerger.java deleted file mode 100644 index 8ca24c3..0000000 --- a/pgp-cert-d-java/src/test/java/pgp/cert_d/dummy/TestKeyMaterialMerger.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d.dummy; - -import pgp.certificate_store.certificate.KeyMaterial; -import pgp.certificate_store.certificate.KeyMaterialMerger; - -import java.io.IOException; - -public class TestKeyMaterialMerger implements KeyMaterialMerger { - @Override - public KeyMaterial merge(KeyMaterial data, KeyMaterial existing) throws IOException { - return data; - } -} diff --git a/pgp-cert-d-java/src/test/java/pgp/cert_d/dummy/TestKeyMaterialReaderBackend.java b/pgp-cert-d-java/src/test/java/pgp/cert_d/dummy/TestKeyMaterialReaderBackend.java deleted file mode 100644 index 2ef392d..0000000 --- a/pgp-cert-d-java/src/test/java/pgp/cert_d/dummy/TestKeyMaterialReaderBackend.java +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.cert_d.dummy; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; -import org.bouncycastle.util.encoders.Hex; -import org.bouncycastle.util.io.Streams; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.certificate.Key; -import pgp.certificate_store.certificate.KeyMaterial; -import pgp.certificate_store.certificate.KeyMaterialReaderBackend; -import pgp.certificate_store.exception.BadDataException; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -public class TestKeyMaterialReaderBackend implements KeyMaterialReaderBackend { - - KeyFingerPrintCalculator fpCalc = new BcKeyFingerprintCalculator(); - - @Override - public KeyMaterial read(InputStream data, Long tag) throws IOException, BadDataException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(data, out); - - try { - return readKey(new ByteArrayInputStream(out.toByteArray()), tag); - } catch (IOException | PGPException e) { - try { - return readCertificate(new ByteArrayInputStream(out.toByteArray()), tag); - } catch (IOException e1) { - throw new BadDataException(); - } - } - } - - private Key readKey(InputStream inputStream, Long tag) throws IOException, PGPException { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - Streams.pipeAll(inputStream, buffer); - inputStream.close(); - - InputStream decoderStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(buffer.toByteArray())); - - PGPSecretKeyRing secretKeys = new PGPSecretKeyRing(decoderStream, fpCalc); - PGPPublicKeyRing cert = extractCert(secretKeys); - ByteArrayInputStream encoded = new ByteArrayInputStream(cert.getEncoded()); - Certificate certificate = readCertificate(encoded, tag); - - return new Key(buffer.toByteArray(), certificate, tag); - } - - private Certificate readCertificate(InputStream inputStream, Long tag) throws IOException { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - Streams.pipeAll(inputStream, buffer); - ByteArrayInputStream in = new ByteArrayInputStream(buffer.toByteArray()); - InputStream decoderStream = PGPUtil.getDecoderStream(in); - - PGPPublicKeyRing cert = new PGPPublicKeyRing(decoderStream, fpCalc); - String fingerprint = Hex.toHexString(cert.getPublicKey().getFingerprint()).toLowerCase(); - List subKeyIds = getSubkeyIds(cert); - return new Certificate(buffer.toByteArray(), fingerprint, subKeyIds, tag); - } - - private PGPPublicKeyRing extractCert(PGPSecretKeyRing secretKeys) { - List publicKeyList = new ArrayList<>(); - Iterator publicKeyIterator = secretKeys.getPublicKeys(); - while (publicKeyIterator.hasNext()) { - publicKeyList.add(publicKeyIterator.next()); - } - return new PGPPublicKeyRing(publicKeyList); - } - - private static List getSubkeyIds(PGPKeyRing keyRing) { - List keyIds = new ArrayList<>(); - Iterator keys = keyRing.getPublicKeys(); - while (keys.hasNext()) { - keyIds.add(keys.next().getKeyID()); - } - return keyIds; - } -} diff --git a/pgp-certificate-store/build.gradle b/pgp-certificate-store/build.gradle index c994a8b..f2d7d7a 100644 --- a/pgp-certificate-store/build.gradle +++ b/pgp-certificate-store/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'ru.vyarus.animalsniffer' dependencies { // animal sniffer for ensuring Android API compatibility - signature "net.sf.androidscents.signature:android-api-level-${animalsnifferSignatureVersion}@signature" + signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:2.3.3_r2@signature" // JUnit for testing testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/AbstractCertificateStore.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/AbstractCertificateStore.java new file mode 100644 index 0000000..3bd394e --- /dev/null +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/AbstractCertificateStore.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.certificate_store; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pgp.certificate_store.exception.BadDataException; +import pgp.certificate_store.exception.BadNameException; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public abstract class AbstractCertificateStore implements CertificateStore { + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractCertificateStore.class); + + public Set getCertificatesBySubkeyId(long subkeyId) + throws IOException { + Set identifiers = getCertificateFingerprintsForSubkeyId(subkeyId); + if (identifiers.isEmpty()) { + return Collections.emptySet(); + } + + Set certificates = new HashSet<>(); + for (String identifier : identifiers) { + try { + certificates.add(getCertificate(identifier)); + } catch (BadNameException | BadDataException e) { + LOGGER.warn("Could not read certificate.", e); + } + } + + return certificates; + } +} 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 0000000..ea3f363 --- /dev/null +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/Certificate.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.certificate_store; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; + +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 + * @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; +} 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 new file mode 100644 index 0000000..006970c --- /dev/null +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateDirectory.java @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.certificate_store; + +import pgp.certificate_store.exception.BadDataException; +import pgp.certificate_store.exception.BadNameException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; + +/** + * Certificate storage definition. + * This interface defines methods to insert and retrieve {@link Certificate Certificates} to and from a store. + * + * {@link Certificate Certificates} are hereby identified by identifiers. An identifier can either be a fingerprint + * or a special name. Special names are implementation-defined identifiers for certificates. + * + * Fingerprints are expected to be hexadecimal lowercase character sequences. + */ +public interface CertificateDirectory { + + /** + * Return the certificate that matches the given identifier. + * If no matching certificate can be found, return null. + * + * @param identifier identifier for a certificate. + * @return certificate or null + * + * @throws IOException in case of an IO-error + * @throws BadNameException if the identifier is invalid + * @throws BadDataException if the certificate file contains invalid data + */ + Certificate getCertificate(String identifier) + throws IOException, BadNameException, BadDataException; + + /** + * Return the certificate that matches the given identifier, but only iff it changed since the last invocation. + * To compare the certificate against its last returned result, the given tag is used. + * If the tag of the currently found certificate matches the given argument, return null. + * + * @param identifier identifier for a certificate + * @param tag tag to compare freshness + * @return changed certificate or null + * + * @throws IOException in case of an IO-error + * @throws BadNameException if the identifier is invalid + * @throws BadDataException if the certificate file contains invalid data + */ + Certificate getCertificateIfChanged(String identifier, String tag) + throws IOException, BadNameException, BadDataException; + + /** + * Insert a certificate into the store. + * If an instance of the certificate is already present in the store, the given {@link MergeCallback} 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, MergeCallback)} instead. + * + * @param data input stream containing the new certificate instance + * @param merge callback for merging with an existing certificate instance + * @return merged certificate + * + * @throws IOException in case of an IO-error + * @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, MergeCallback 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 MergeCallback} 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 not block. Instead, if the store is already write-locked, this method will simply return null + * without any writing. + * However, if the write-lock is available, this method will acquire the lock, write to the store, release the lock + * and return the written certificate. + * + * @param data input stream containing the new certificate instance + * @param merge callback for merging with an existing certificate instance + * @return merged certificate or null if the store cannot be locked + * + * @throws IOException in case of an IO-error + * @throws BadDataException if the data stream does not contain valid OpenPGP data + */ + Certificate tryInsertCertificate(InputStream data, MergeCallback 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 MergeCallback} 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, MergeCallback)} instead. + * + * @param specialName special name of the certificate + * @param data input stream containing the new certificate instance + * @param merge callback for merging with an existing certificate instance + * @return merged certificate or null if the store cannot be locked + * + * @throws IOException in case of an IO-error + * @throws InterruptedException if the thread is interrupted + * @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, MergeCallback 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 MergeCallback} 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 not block. Instead, if the store is already write-locked, this method will simply return null + * without any writing. + * However, if the write-lock is available, this method will acquire the lock, write to the store, release the lock + * and return the written certificate. + * + * @param specialName special name for the certificate + * @param data input stream containing the new certificate instance + * @param merge callback for merging with an existing certificate instance + * @return merged certificate or null if the store cannot be locked + * + * @throws IOException in case of an IO-error + * @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, MergeCallback merge) + throws IOException, BadDataException, BadNameException; + + /** + * Return an {@link Iterator} containing all certificates in the store. + * The iterator will contain both certificates addressed by special names and by fingerprints. + * + * @return certificates + */ + Iterator getCertificates(); + + /** + * Return an {@link Iterator} containing all certificate fingerprints from the store. + * Note that this only includes the fingerprints of certificate primary keys, not those of subkeys. + * + * @return fingerprints + */ + Iterator getFingerprints(); +} diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateReaderBackend.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateReaderBackend.java new file mode 100644 index 0000000..c16b111 --- /dev/null +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateReaderBackend.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.certificate_store; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Interface definition for a class that can read {@link Certificate Certificates} from binary + * {@link InputStream InputStreams}. + */ +public interface CertificateReaderBackend { + + /** + * Read a {@link Certificate} from the given {@link InputStream}. + * + * @param inputStream input stream containing the binary representation of the certificate. + * @return certificate object + * + * @throws IOException in case of an IO error + */ + Certificate readCertificate(InputStream inputStream) 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 new file mode 100644 index 0000000..a8325ee --- /dev/null +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateStore.java @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package pgp.certificate_store; + +public interface CertificateStore extends CertificateDirectory, SubkeyLookup { + +} 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 new file mode 100644 index 0000000..9c9f162 --- /dev/null +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/MergeCallback.java @@ -0,0 +1,27 @@ +// 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 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 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/PGPCertificateStore.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/PGPCertificateStore.java deleted file mode 100644 index 251229f..0000000 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/PGPCertificateStore.java +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.certificate_store; - -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.certificate.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; -import java.util.NoSuchElementException; - -/** - * Interface for an OpenPGP certificate (public key) store. - */ -public interface PGPCertificateStore { - - /** - * Return the certificate that matches the given identifier. - * - * @param identifier identifier for a certificate. - * @return certificate or null - * - * @throws IOException in case of an IO-error - * @throws BadNameException if the identifier is invalid - * @throws BadDataException if the certificate file contains invalid data - * @throws NoSuchElementException if no such certificate is found - */ - Certificate getCertificate(String identifier) - throws IOException, BadNameException, BadDataException; - - /** - * Return the certificate that matches the given identifier, but only if it has been changed. - * Whether it has been changed is determined by calculating the tag in the directory - * (e.g. by looking at the inode and last modification date) and comparing the result with the tag provided by - * the caller. - * - * @param identifier certificate identifier - * @param tag tag by the caller - * @return certificate if it has been changed, null otherwise - * - * @throws IOException in case of an IO-error - * @throws BadNameException if the identifier is invalid - * @throws BadDataException if the certificate file contains invalid data - * @throws NoSuchElementException if no such certificate is found - */ - Certificate getCertificateIfChanged(String identifier, Long tag) - throws IOException, BadNameException, BadDataException; - - /** - * Return an {@link Iterator} over all certificates in the store that contain a subkey with the given - * subkey id. - * @param subkeyId id of the subkey - * @return iterator - * - * @throws IOException in case of an IO error - * @throws BadDataException if any of the certificate files contains invalid data - */ - Iterator getCertificatesBySubkeyId(long subkeyId) - throws IOException, BadDataException; - - /** - * Insert a certificate into the store. - * 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. - * - * @param data input stream containing the new certificate instance - * @param merge callback for merging with an existing certificate instance - * @return merged certificate - * - * @throws IOException in case of an IO-error - * @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, KeyMaterialMerger merge) - throws IOException, InterruptedException, 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 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. - * - * @param specialName special name of the certificate - * @param data input stream containing the new certificate instance - * @param merge callback for merging with an existing certificate instance - * @return merged certificate or null if the store cannot be locked - * - * @throws IOException in case of an IO-error - * @throws InterruptedException if the thread is interrupted - * @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, KeyMaterialMerger merge) - throws IOException, InterruptedException, BadDataException, BadNameException; - - /** - * Return an {@link Iterator} containing all certificates in the store. - * The iterator will contain both certificates addressed by special names and by fingerprints. - * - * @return certificates - */ - Iterator getCertificates(); - - /** - * Return an {@link Iterator} containing all certificate fingerprints from the store. - * Note that this only includes the fingerprints of certificate primary keys, not those of subkeys. - * - * @return fingerprints - */ - Iterator getFingerprints(); -} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/subkey_lookup/SubkeyLookup.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/SubkeyLookup.java similarity index 96% rename from pgp-cert-d-java/src/main/java/pgp/cert_d/subkey_lookup/SubkeyLookup.java rename to pgp-certificate-store/src/main/java/pgp/certificate_store/SubkeyLookup.java index 05fe766..55d03e6 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/subkey_lookup/SubkeyLookup.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/SubkeyLookup.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package pgp.cert_d.subkey_lookup; +package pgp.certificate_store; import java.io.IOException; import java.util.List; diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/Certificate.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/Certificate.java deleted file mode 100644 index 175f739..0000000 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/Certificate.java +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.certificate_store.certificate; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.List; - -/** - * OpenPGP certificate (public key). - */ -public class Certificate implements KeyMaterial { - - private final byte[] bytes; - private final String fingerprint; - private final List subkeyIds; - private final Long tag; - - /** - * Certificate constructor. - * - * @param bytes encoding of the certificate - * @param fingerprint fingerprint (lowercase hex characters) - * @param subkeyIds list of subkey ids - * @param tag tag - */ - public Certificate(byte[] bytes, String fingerprint, List subkeyIds, Long tag) { - this.bytes = bytes; - this.fingerprint = fingerprint; - this.subkeyIds = subkeyIds; - this.tag = tag; - } - - /** - * Copy constructor to assign a new tag to the {@link Certificate}. - * - * @param cert certificate - * @param tag tag - */ - public Certificate(Certificate cert, Long tag) { - this(cert.bytes, cert.fingerprint, cert.subkeyIds, tag); - } - - @Override - public String getFingerprint() { - return fingerprint; - } - - @Override - public Certificate asCertificate() { - return this; - } - - @Override - public InputStream getInputStream() { - return new ByteArrayInputStream(bytes); - } - - @Override - public Long getTag() { - return tag; - } - - @Override - public List getSubkeyIds() { - return subkeyIds; - } -} diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/Key.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/Key.java deleted file mode 100644 index a2a4aef..0000000 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/Key.java +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.certificate_store.certificate; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.List; - -/** - * OpenPGP key (secret key). - */ -public class Key implements KeyMaterial { - - private final byte[] bytes; - private final Certificate certificate; - private final Long tag; - - /** - * Key constructor. - * - * @param bytes encoding of the key - * @param certificate associated certificate - * @param tag tag - */ - public Key(byte[] bytes, Certificate certificate, Long tag) { - this.bytes = bytes; - this.certificate = certificate; - this.tag = tag; - } - - /** - * Copy constructor to change the tag of both the {@link Key} and its {@link Certificate}. - * - * @param key key - * @param tag tag - */ - public Key(Key key, Long tag) { - this(key.bytes, new Certificate(key.certificate, tag), tag); - } - - /** - * Return the certificate part of this OpenPGP key. - * - * @return OpenPGP certificate - */ - public Certificate getCertificate() { - return new Certificate(certificate, getTag()); - } - - @Override - public String getFingerprint() { - return certificate.getFingerprint(); - } - - @Override - public Certificate asCertificate() { - return getCertificate(); - } - - @Override - public InputStream getInputStream() { - return new ByteArrayInputStream(bytes); - } - - @Override - public Long getTag() { - return tag; - } - - @Override - public List getSubkeyIds() { - return certificate.getSubkeyIds(); - } - -} diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterial.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterial.java deleted file mode 100644 index 1438b47..0000000 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterial.java +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.certificate_store.certificate; - -import java.io.InputStream; -import java.util.List; -import java.util.Set; - -public interface KeyMaterial { - - /** - * Return the fingerprint of the certificate as 40 lowercase hex characters. - * TODO: Allow OpenPGP V5 fingerprints - * - * @return fingerprint - */ - String getFingerprint(); - - /** - * Return the {@link Certificate} belonging to this key material. - * If this is already a {@link Certificate}, return this. - * If this is a {@link Key}, extract the {@link Certificate} and return it. - * - * @return certificate - */ - Certificate asCertificate(); - - /** - * Return an {@link InputStream} of the binary representation of the secret key. - * - * @return input stream - */ - InputStream getInputStream(); - - /** - * Return the tag belonging to this key material. - * The tag can be used to keep an application cache in sync with what is in the directory. - * - * @return tag - */ - Long getTag(); - - /** - * Return a {@link Set} containing key-ids of subkeys. - * - * @return subkeys - */ - List getSubkeyIds(); -} diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterialMerger.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterialMerger.java deleted file mode 100644 index 20ad268..0000000 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterialMerger.java +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.certificate_store.certificate; - -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/certificate/KeyMaterialReaderBackend.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterialReaderBackend.java deleted file mode 100644 index c921d64..0000000 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterialReaderBackend.java +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package pgp.certificate_store.certificate; - -import pgp.certificate_store.exception.BadDataException; - -import java.io.IOException; -import java.io.InputStream; - -public interface KeyMaterialReaderBackend { - - /** - * Read a {@link KeyMaterial} (either {@link Key} or {@link Certificate}) from the given {@link InputStream}. - * - * @param data input stream containing the binary representation of the key. - * @param tag tag for the key material. Might be null. - * @return key or certificate object - * - * @throws IOException in case of an IO error - * @throws BadDataException in case that the data stream does not contain a valid OpenPGP key/certificate - */ - KeyMaterial read(InputStream data, Long tag) throws IOException, BadDataException; -} diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/package-info.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/package-info.java deleted file mode 100644 index 5060b18..0000000 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * General OpenPGP Certificate Storage related classes. - */ -package pgp.certificate_store.certificate; diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/exception/package-info.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/exception/package-info.java index 302b1d2..c06ce06 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/exception/package-info.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/exception/package-info.java @@ -3,6 +3,8 @@ // SPDX-License-Identifier: Apache-2.0 /** - * Exceptions. + * Exceptions defined by the Shared PGP Certificate Directory. + * + * @see Failure Modes */ package pgp.certificate_store.exception; diff --git a/settings.gradle b/settings.gradle index ef5cea0..3920c69 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,7 +4,7 @@ rootProject.name = 'cert-d-java' -include 'pgp-certificate-store', - 'pgp-cert-d-java', - 'pgp-cert-d-java-jdbc-sqlite-lookup' +include 'pgp-cert-d-java', + 'pgp-cert-d-java-jdbc-sqlite-lookup', + 'pgp-certificate-store' diff --git a/version.gradle b/version.gradle index 2903026..8731281 100644 --- a/version.gradle +++ b/version.gradle @@ -4,16 +4,13 @@ allprojects { ext { - shortVersion = '0.2.3' - isSnapshot = true - minAndroidSdk = 26 - animalsnifferSignatureVersion = "$minAndroidSdk:8.0.0_r2" + shortVersion = '0.1.1' + isSnapshot = false + minAndroidSdk = 10 javaSourceCompatibility = 1.8 - bouncycastleVersion = '1.75' - bouncyPgVersion = "$bouncycastleVersion" slf4jVersion = '1.7.36' logbackVersion = '1.2.11' junitVersion = '5.8.2' - sqliteJdbcVersion = '3.42.0.0' + sqliteJdbcVersion = '3.36.0.3' } }