Merge pull request #1 from pgpainless/rewrite

Rewrite logic to improve ability to set different backends
This commit is contained in:
Paul Schaub 2022-08-24 14:56:17 +02:00 committed by GitHub
commit ecfae83f3a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 2292 additions and 1396 deletions

View file

@ -5,6 +5,18 @@ SPDX-License-Identifier: CC0-1.0
# Cert-D-Java Changelog # Cert-D-Java Changelog
## 0.1.2-SNAPSHOT
- `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`:
- Rework `PGPCertificateDirectory` class by separating out backend logic
- Split interface into `ReadOnlyPGPCertificateDirectory` and `WritingPGPCertificateDirectory`
- `pgp-cert-d-java-jdbc-sqlite-lookup`:
- Add `DatabaseSubkeyLookupFactory`
## 0.1.1 ## 0.1.1
- Bump `slf4j` to `1.7.36` - Bump `slf4j` to `1.7.36`
- Bump `logback` to `1.2.11` - Bump `logback` to `1.2.11`

View file

@ -11,7 +11,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import pgp.certificate_store.SubkeyLookup; import pgp.cert_d.subkey_lookup.SubkeyLookup;
public class DatabaseSubkeyLookup implements SubkeyLookup { public class DatabaseSubkeyLookup implements SubkeyLookup {

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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);
}
}

View file

@ -3,6 +3,6 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
/** /**
* Implementation of a {@link pgp.certificate_store.SubkeyLookup} mechanism using an SQLite Database. * Implementation of a {@link pgp.cert_d.subkey_lookup.SubkeyLookup} mechanism using an SQLite Database.
*/ */
package pgp.cert_d.jdbc.sqlite; package pgp.cert_d.jdbc.sqlite;

View file

@ -22,15 +22,15 @@ import org.junit.jupiter.api.Test;
public class SqliteSubkeyLookupTest { public class SqliteSubkeyLookupTest {
private File databaseFile; private File tempDir;
private DatabaseSubkeyLookup lookup; private DatabaseSubkeyLookup lookup;
@BeforeEach @BeforeEach
public void setupLookup() throws IOException, SQLException { public void setupLookup() throws IOException {
databaseFile = Files.createTempFile("pgp.cert.d-", "lookup.db").toFile(); tempDir = Files.createTempDirectory("pgp.cert.d").toFile();
databaseFile.createNewFile(); tempDir.deleteOnExit();
databaseFile.deleteOnExit(); lookup = (DatabaseSubkeyLookup) new DatabaseSubkeyLookupFactory()
lookup = new DatabaseSubkeyLookup(SqliteSubkeyLookupDaoImpl.forDatabaseFile(databaseFile)); .createFileBasedInstance(tempDir);
} }
@Test @Test
@ -55,7 +55,7 @@ public class SqliteSubkeyLookupTest {
assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), lookup.getCertificateFingerprintsForSubkeyId(1337)); assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), lookup.getCertificateFingerprintsForSubkeyId(1337));
// do the lookup using a second db instance on the same file // do the lookup using a second db instance on the same file
DatabaseSubkeyLookup secondInstance = new DatabaseSubkeyLookup(SqliteSubkeyLookupDaoImpl.forDatabaseFile(databaseFile)); DatabaseSubkeyLookup secondInstance = (DatabaseSubkeyLookup) new DatabaseSubkeyLookupFactory().createFileBasedInstance(tempDir);
assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), secondInstance.getCertificateFingerprintsForSubkeyId(1337)); assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), secondInstance.getCertificateFingerprintsForSubkeyId(1337));
} }

View file

@ -16,7 +16,7 @@ apply plugin: 'ru.vyarus.animalsniffer'
dependencies { dependencies {
// animal sniffer for ensuring Android API compatibility // animal sniffer for ensuring Android API compatibility
signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:2.3.3_r2@signature" signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:8.0.0_r2@signature"
// JUnit // JUnit
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
@ -26,11 +26,13 @@ dependencies {
// Logging // Logging
testImplementation "ch.qos.logback:logback-classic:$logbackVersion" testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
api project(":pgp-certificate-store")
// SQL Subkey table // SQL Subkey table
testImplementation project(":pgp-cert-d-java-jdbc-sqlite-lookup") testImplementation project(":pgp-cert-d-java-jdbc-sqlite-lookup")
// Certificate store testImplementation "org.bouncycastle:bcprov-jdk15to18:$bouncycastleVersion"
api project(":pgp-certificate-store") testImplementation "org.bouncycastle:bcpg-jdk15to18:$bouncycastleVersion"
} }
animalsniffer { animalsniffer {

View file

@ -1,16 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import pgp.certificate_store.CertificateMerger;
import pgp.certificate_store.KeyReaderBackend;
public abstract class BackendProvider {
public abstract KeyReaderBackend provideKeyReaderBackend();
public abstract CertificateMerger provideDefaultMergeCallback();
}

View file

@ -6,6 +6,16 @@ package pgp.cert_d;
import java.io.File; 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 <pre>PGP_CERT_D</pre> environment variable.
* If this variable is not set, the system-specific default directory will be returned.
*
* On Windows systems, this is <pre>%APPDATA%\pgp.cert.d</pre>.
* On Linux systems it is either <pre>$XDG_DATA_HOME/pgp.cert.d</pre> or, if <pre>$XDG_DATA_HOME</pre> is not set,
* it is <pre>$HOME/.local/share/pgp.cert.d</pre>
* On Mac systems it is <pre>$HOME/Library/Application Support/pgp.cert.d</pre>.
*/
public class BaseDirectoryProvider { public class BaseDirectoryProvider {
public static File getDefaultBaseDir() { public static File getDefaultBaseDir() {

View file

@ -1,249 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import pgp.certificate_store.Key;
import pgp.certificate_store.KeyMerger;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.CertificateMerger;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Caching wrapper for {@link SharedPGPCertificateDirectory} implementations.
*/
public class CachingSharedPGPCertificateDirectoryWrapper
implements SharedPGPCertificateDirectory {
private static final Map<String, String> certTagMap = new HashMap<>();
private static final Map<String, String> keyTagMap = new HashMap<>();
private static final Map<String, Certificate> certificateMap = new HashMap<>();
private static final Map<String, Key> keyMap = new HashMap<>();
private final SharedPGPCertificateDirectory underlyingCertificateDirectory;
public CachingSharedPGPCertificateDirectoryWrapper(SharedPGPCertificateDirectory wrapped) {
this.underlyingCertificateDirectory = wrapped;
}
/**
* Store the given certificate under the given identifier into the cache.
*
* @param identifier fingerprint or special name
* @param certificate certificate
*/
private void remember(String identifier, Certificate certificate) {
certificateMap.put(identifier, certificate);
try {
certTagMap.put(identifier, certificate.getTag());
} catch (IOException e) {
certTagMap.put(identifier, null);
}
}
/**
* Store the given key under the given identifier into the cache.
*
* @param identifier fingerprint or special name
* @param key key
*/
private void remember(String identifier, Key key) {
keyMap.put(identifier, key);
try {
keyTagMap.put(identifier, key.getTag());
} catch (IOException e) {
keyTagMap.put(identifier, null);
}
}
/**
* Returns true, if the cached tag differs from the provided tag.
*
* @param identifier fingerprint or special name
* @param tag tag
* @return true if cached tag differs, false otherwise
*/
private boolean certTagChanged(String identifier, String tag) {
String tack = certTagMap.get(identifier);
return !tagEquals(tag, tack);
}
private boolean keyTagChanged(String identifier, String tag) {
String tack = keyTagMap.get(identifier);
return !tagEquals(tag, tack);
}
/**
* Return true, if tag and tack are equal, false otherwise.
* @param tag tag
* @param tack other tag
* @return true if equal
*/
private static boolean tagEquals(String tag, String tack) {
return (tag == null && tack == null)
|| tag != null && tag.equals(tack);
}
/**
* Clear the cache.
*/
public void invalidate() {
certificateMap.clear();
certTagMap.clear();
}
@Override
public LockingMechanism getLock() {
return underlyingCertificateDirectory.getLock();
}
@Override
public Certificate getByFingerprint(String fingerprint)
throws IOException, BadNameException, BadDataException {
Certificate certificate = certificateMap.get(fingerprint);
if (certificate == null) {
certificate = underlyingCertificateDirectory.getByFingerprint(fingerprint);
if (certificate != null) {
remember(fingerprint, certificate);
}
}
return certificate;
}
@Override
public Certificate getBySpecialName(String specialName)
throws IOException, BadNameException, BadDataException {
Certificate certificate = certificateMap.get(specialName);
if (certificate == null) {
certificate = underlyingCertificateDirectory.getBySpecialName(specialName);
if (certificate != null) {
remember(specialName, certificate);
}
}
return certificate;
}
@Override
public Certificate getByFingerprintIfChanged(String fingerprint, String tag)
throws IOException, BadNameException, BadDataException {
if (certTagChanged(fingerprint, tag)) {
return getByFingerprint(fingerprint);
}
return null;
}
@Override
public Certificate getBySpecialNameIfChanged(String specialName, String tag)
throws IOException, BadNameException, BadDataException {
if (certTagChanged(specialName, tag)) {
return getBySpecialName(specialName);
}
return null;
}
@Override
public Key getTrustRoot() throws IOException, BadDataException {
Key key = keyMap.get(SpecialNames.TRUST_ROOT);
if (key == null) {
key = underlyingCertificateDirectory.getTrustRoot();
if (key != null) {
remember(SpecialNames.TRUST_ROOT, key);
}
}
return key;
}
@Override
public Key getTrustRootIfChanged(String tag) throws IOException, BadDataException {
if (keyTagChanged(SpecialNames.TRUST_ROOT, tag)) {
return getTrustRoot();
}
return null;
}
@Override
public Certificate insert(InputStream data, CertificateMerger merge)
throws IOException, BadDataException, InterruptedException {
Certificate certificate = underlyingCertificateDirectory.insert(data, merge);
remember(certificate.getFingerprint(), certificate);
return certificate;
}
@Override
public Certificate tryInsert(InputStream data, CertificateMerger merge)
throws IOException, BadDataException {
Certificate certificate = underlyingCertificateDirectory.tryInsert(data, merge);
if (certificate != null) {
remember(certificate.getFingerprint(), certificate);
}
return certificate;
}
@Override
public Key insertTrustRoot(InputStream data, KeyMerger merge) throws IOException, BadDataException, InterruptedException {
Key key = underlyingCertificateDirectory.insertTrustRoot(data, merge);
remember(SpecialNames.TRUST_ROOT, key);
return key;
}
@Override
public Key tryInsertTrustRoot(InputStream data, KeyMerger merge) throws IOException, BadDataException {
Key key = underlyingCertificateDirectory.tryInsertTrustRoot(data, merge);
if (key != null) {
remember(SpecialNames.TRUST_ROOT, key);
}
return key;
}
@Override
public Certificate insertWithSpecialName(String specialName, InputStream data, CertificateMerger merge)
throws IOException, BadDataException, BadNameException, InterruptedException {
Certificate certificate = underlyingCertificateDirectory.insertWithSpecialName(specialName, data, merge);
remember(specialName, certificate);
return certificate;
}
@Override
public Certificate tryInsertWithSpecialName(String specialName, InputStream data, CertificateMerger merge)
throws IOException, BadDataException, BadNameException {
Certificate certificate = underlyingCertificateDirectory.tryInsertWithSpecialName(specialName, data, merge);
if (certificate != null) {
remember(specialName, certificate);
}
return certificate;
}
@Override
public Iterator<Certificate> items() {
Iterator<Certificate> iterator = underlyingCertificateDirectory.items();
return new Iterator<Certificate>() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public Certificate next() {
Certificate certificate = iterator.next();
remember(certificate.getFingerprint(), certificate);
return certificate;
}
};
}
@Override
public Iterator<String> fingerprints() {
return underlyingCertificateDirectory.fingerprints();
}
}

View file

@ -1,96 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
public class FileLockingMechanism implements LockingMechanism {
private final File lockFile;
private RandomAccessFile randomAccessFile;
private FileLock fileLock;
public FileLockingMechanism(File lockFile) {
this.lockFile = lockFile;
}
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();
}
}

View file

@ -1,89 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import pgp.certificate_store.exception.BadNameException;
import java.io.File;
import java.util.regex.Pattern;
public class FilenameResolver {
private final File baseDirectory;
private final Pattern openPgpV4FingerprintPattern = Pattern.compile("^[a-f0-9]{40}$");
public FilenameResolver(File baseDirectory) {
this.baseDirectory = baseDirectory;
}
public File getBaseDirectory() {
return baseDirectory;
}
/**
* Calculate the file location for the certificate addressed by the given
* lowercase hexadecimal OpenPGP fingerprint.
*
* @param fingerprint fingerprint
* @return absolute certificate file location
*
* @throws BadNameException if the given fingerprint string is not a fingerprint
*/
public File getCertFileByFingerprint(String fingerprint) throws BadNameException {
if (!isFingerprint(fingerprint)) {
throw new BadNameException();
}
// is fingerprint
File subdirectory = new File(getBaseDirectory(), fingerprint.substring(0, 2));
File file = new File(subdirectory, fingerprint.substring(2));
return file;
}
/**
* Calculate the file location for the certification addressed using the given special name.
* For known special names, see {@link SpecialNames}.
*
* @param specialName special name (e.g. "trust-root")
* @return absolute certificate file location
*
* @throws BadNameException in case the given special name is not known
*/
public File getCertFileBySpecialName(String specialName)
throws BadNameException {
if (!isSpecialName(specialName)) {
throw new BadNameException(String.format("%s is not a known special name", specialName));
}
return new File(getBaseDirectory(), specialName);
}
/**
* Calculate the file location for the key addressed using the given special name.
* For known special names, see {@link SpecialNames}.
*
* @param specialName special name (e.g. "trust-root")
* @return absolute key file location
*
* @throws BadNameException in case the given special name is not known
*/
public File getKeyFileBySpecialName(String specialName)
throws BadNameException {
if (!isSpecialName(specialName)) {
throw new BadNameException(String.format("%s is not a known special name", specialName));
}
return new File(getBaseDirectory(), specialName + ".key");
}
private boolean isFingerprint(String fingerprint) {
return openPgpV4FingerprintPattern.matcher(fingerprint).matches();
}
private boolean isSpecialName(String specialName) {
return SpecialNames.lookupSpecialName(specialName) != null;
}
}

View file

@ -1,37 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.io.IOException;
public interface LockingMechanism {
/**
* Lock the store for writes.
* Readers can continue to use the store and will always see consistent certs.
*
* @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;
}

View file

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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);
}
}

View file

@ -0,0 +1,352 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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.Set;
/**
* Implementation of the Shared PGP Certificate Directory.
*
* @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/">Shared PGP Certificate Directory Specification</a>
*/
public class PGPCertificateDirectory
implements ReadOnlyPGPCertificateDirectory, WritingPGPCertificateDirectory, SubkeyLookup {
final Backend backend;
final SubkeyLookup subkeyLookup;
/**
* 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 {
return backend.readByFingerprint(fingerprint);
}
@Override
public Certificate getByFingerprintIfChanged(String fingerprint, long tag)
throws IOException, BadNameException, BadDataException {
if (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();
}
return null;
}
@Override
public Certificate getBySpecialNameIfChanged(String specialName, long tag)
throws IOException, BadNameException, BadDataException {
if (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<Certificate> items() {
return backend.readItems();
}
@Override
public Iterator<String> fingerprints() {
Iterator<Certificate> certs = items();
return new Iterator<String>() {
@Override
public boolean hasNext() {
return certs.hasNext();
}
@Override
public String next() {
return certs.next().getFingerprint();
}
};
}
@Override
public KeyMaterial getTrustRoot() throws IOException, BadDataException {
try {
return backend.readBySpecialName(SpecialNames.TRUST_ROOT);
} catch (BadNameException e) {
throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is implementation MUST");
}
}
@Override
public KeyMaterial insertTrustRoot(InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException, InterruptedException {
backend.getLock().lockDirectory();
KeyMaterial inserted = backend.doInsertTrustRoot(data, merge);
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<String> getCertificateFingerprintsForSubkeyId(long subkeyId) throws IOException {
return subkeyLookup.getCertificateFingerprintsForSubkeyId(subkeyId);
}
@Override
public void storeCertificateSubkeyIds(String certificate, List<Long> 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<Certificate> 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;
}
}

View file

@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<Certificate> getCertificatesBySubkeyId(long subkeyId)
throws IOException, BadDataException {
Set<String> fingerprints = directory.getCertificateFingerprintsForSubkeyId(subkeyId);
Set<Certificate> 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<Certificate> getCertificates() {
return directory.items();
}
@Override
public Iterator<String> getFingerprints() {
return directory.fingerprints();
}
}

View file

@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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;
/**
* 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
* <pre>trust-root</pre>.
* If no such certificate is found, <pre>null</pre> is returned.
*
* @return trust-root certificate
*
* @throws IOException in case of an IO error
* @throws BadDataException if the certificate contains bad data
*/
Certificate getTrustRootCertificate()
throws IOException, BadDataException;
/**
* Get the trust-root certificate if it has changed.
* This method uses the <pre>tag</pre> to calculate if the certificate might have changed.
* If the computed tag equals the given tag, the certificate has not changed, so <pre>null</pre> is returned.
* Otherwise. the changed certificate is returned.
*
* @param tag tag
* @return changed certificate, or null if the certificate is unchanged or not found.
*
* @throws IOException in case of an IO error
* @throws BadDataException if the certificate contains bad data
*/
Certificate getTrustRootCertificateIfChanged(long tag)
throws IOException, BadDataException;
/**
* Get the certificate identified by the given fingerprint.
* If no such certificate is found, return <pre>null</pre>.
*
* @param fingerprint lower-case fingerprint of the certificate
* @return certificate or null if no such certificate has been found
*
* @throws IOException in case of an IO error
* @throws BadNameException if the fingerprint is malformed
* @throws BadDataException if the certificate contains bad data
*/
Certificate getByFingerprint(String fingerprint)
throws IOException, BadNameException, BadDataException;
/**
* Get the certificate identified by the given fingerprint if it has changed.
* This method uses the <pre>tag</pre> to calculate, if the certificate might have changed.
* If the computed tag equals the given tag, the certificate has not changed, so <pre>null</pre> 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 or has not been found
*
* @throws IOException in case of an IO error
* @throws BadNameException if the fingerprint is malformed
* @throws BadDataException if the certificate contains bad data
*/
Certificate getByFingerprintIfChanged(String fingerprint, long tag)
throws IOException, BadNameException, BadDataException;
/**
* Get the certificate identified by the given special name.
* If no such certificate is found, <pre>null</pre> is returned.
*
* @param specialName special name
* @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
*/
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 <pre>tag</pre> to calculate, if the certificate might have changed.
* If the computed tag equals the given tag, the certificate has not changed, so <pre>null</pre> 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
*/
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<Certificate> items();
/**
* Get the fingerprints of all certificates in the directory, except for certificates which are stored by
* special name.
*
* @return iterator of fingerprints
*/
Iterator<String> fingerprints();
}

View file

@ -1,61 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import pgp.certificate_store.Key;
import pgp.certificate_store.KeyMerger;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.CertificateMerger;
public interface SharedPGPCertificateDirectory {
LockingMechanism getLock();
Certificate getByFingerprint(String fingerprint)
throws IOException, BadNameException, BadDataException;
Certificate getBySpecialName(String specialName)
throws IOException, BadNameException, BadDataException;
Key getTrustRoot()
throws IOException, BadDataException;
Key getTrustRootIfChanged(String tag)
throws IOException, BadDataException;
Key insertTrustRoot(InputStream data, KeyMerger merge)
throws IOException, BadDataException, InterruptedException;
Key tryInsertTrustRoot(InputStream data, KeyMerger merge)
throws IOException, BadDataException;
Certificate getByFingerprintIfChanged(String fingerprint, String tag)
throws IOException, BadNameException, BadDataException;
Certificate getBySpecialNameIfChanged(String specialName, String tag)
throws IOException, BadNameException, BadDataException;
Certificate insert(InputStream data, CertificateMerger merge)
throws IOException, BadDataException, InterruptedException;
Certificate tryInsert(InputStream data, CertificateMerger merge)
throws IOException, BadDataException;
Certificate insertWithSpecialName(String specialName, InputStream data, CertificateMerger merge)
throws IOException, BadDataException, BadNameException, InterruptedException;
Certificate tryInsertWithSpecialName(String specialName, InputStream data, CertificateMerger merge)
throws IOException, BadDataException, BadNameException;
Iterator<Certificate> items();
Iterator<String> fingerprints();
}

View file

@ -1,405 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import pgp.certificate_store.Key;
import pgp.certificate_store.KeyMaterial;
import pgp.certificate_store.KeyMerger;
import pgp.certificate_store.KeyReaderBackend;
import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException;
import pgp.certificate_store.exception.NotAStoreException;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.CertificateMerger;
public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDirectory {
private final FilenameResolver resolver;
private final LockingMechanism writeLock;
private final KeyReaderBackend keyReaderBackend;
public SharedPGPCertificateDirectoryImpl(BackendProvider backendProvider)
throws NotAStoreException {
this(backendProvider.provideKeyReaderBackend());
}
public SharedPGPCertificateDirectoryImpl(KeyReaderBackend keyReaderBackend)
throws NotAStoreException {
this(
BaseDirectoryProvider.getDefaultBaseDir(),
keyReaderBackend);
}
public SharedPGPCertificateDirectoryImpl(File baseDirectory,
KeyReaderBackend keyReaderBackend)
throws NotAStoreException {
this(
keyReaderBackend,
new FilenameResolver(baseDirectory),
FileLockingMechanism.defaultDirectoryFileLock(baseDirectory));
}
public SharedPGPCertificateDirectoryImpl(
KeyReaderBackend keyReaderBackend,
FilenameResolver filenameResolver,
LockingMechanism writeLock)
throws NotAStoreException {
this.keyReaderBackend = keyReaderBackend;
this.resolver = filenameResolver;
this.writeLock = writeLock;
File baseDirectory = resolver.getBaseDirectory();
if (!baseDirectory.exists()) {
if (!baseDirectory.mkdirs()) {
throw new NotAStoreException("Cannot create base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "'");
}
} else {
if (baseDirectory.isFile()) {
throw new NotAStoreException("Base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "' appears to be a file.");
}
}
}
@Override
public LockingMechanism getLock() {
return writeLock;
}
@Override
public Certificate getByFingerprint(String fingerprint)
throws IOException, BadNameException, BadDataException {
File certFile = resolver.getCertFileByFingerprint(fingerprint);
if (!certFile.exists()) {
return null;
}
FileInputStream fileIn = new FileInputStream(certFile);
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
Certificate certificate = readCertificate(bufferedIn);
if (!certificate.getFingerprint().equals(fingerprint)) {
// TODO: Figure out more suitable exception
throw new BadDataException();
}
return certificate;
}
private Certificate readCertificate(InputStream inputStream) throws BadDataException, IOException {
KeyMaterial record = keyReaderBackend.read(inputStream);
Certificate certificate = null;
if (record instanceof Certificate) {
certificate = (Certificate) record;
} else if (record instanceof Key) {
Key key = (Key) record;
certificate = key.getCertificate();
} else {
throw new BadDataException();
}
return certificate;
}
private Key readKey(InputStream inputStream) throws BadDataException, IOException {
KeyMaterial record = keyReaderBackend.read(inputStream);
if (record instanceof Key) {
return (Key) record;
}
throw new BadDataException();
}
@Override
public Certificate getBySpecialName(String specialName)
throws IOException, BadNameException, BadDataException {
File certFile = resolver.getCertFileBySpecialName(specialName);
if (!certFile.exists()) {
return null;
}
FileInputStream fileIn = new FileInputStream(certFile);
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
Certificate certificate = readCertificate(bufferedIn);
return certificate;
}
@Override
public Certificate getByFingerprintIfChanged(String fingerprint, String tag)
throws IOException, BadNameException, BadDataException {
Certificate certificate = getByFingerprint(fingerprint);
if (certificate.getTag().equals(tag)) {
return null;
}
return certificate;
}
@Override
public Certificate getBySpecialNameIfChanged(String specialName, String tag)
throws IOException, BadNameException, BadDataException {
Certificate certificate = getBySpecialName(specialName);
if (certificate.getTag().equals(tag)) {
return null;
}
return certificate;
}
@Override
public Key getTrustRoot() throws IOException, BadDataException {
File keyFile;
try {
keyFile = resolver.getKeyFileBySpecialName(SpecialNames.TRUST_ROOT);
} catch (BadNameException e) {
throw new RuntimeException("trust-root MUST be a known special name", e);
}
if (!keyFile.exists()) {
return null;
}
FileInputStream fileIn = new FileInputStream(keyFile);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileIn);
Key key = readKey(bufferedInputStream);
return key;
}
@Override
public Key getTrustRootIfChanged(String tag) throws IOException, BadDataException {
// TODO: The tag is likely intended for performance improvements,
// so really we should look it up somewhere without the need to parse the whole key.
Key key = getTrustRoot();
if (key.getTag().equals(tag)) {
return null;
}
return key;
}
@Override
public Certificate insert(InputStream data, CertificateMerger merge)
throws IOException, BadDataException, InterruptedException {
writeLock.lockDirectory();
Certificate certificate = _insert(data, merge);
writeLock.releaseDirectory();
return certificate;
}
@Override
public Certificate tryInsert(InputStream data, CertificateMerger merge)
throws IOException, BadDataException {
if (!writeLock.tryLockDirectory()) {
return null;
}
Certificate certificate = _insert(data, merge);
writeLock.releaseDirectory();
return certificate;
}
private Certificate _insert(InputStream data, CertificateMerger merge)
throws IOException, BadDataException {
Certificate newCertificate = readCertificate(data);
Certificate existingCertificate;
File certFile;
try {
existingCertificate = getByFingerprint(newCertificate.getFingerprint());
certFile = resolver.getCertFileByFingerprint(newCertificate.getFingerprint());
} catch (BadNameException e) {
throw new BadDataException();
}
if (existingCertificate != null && !existingCertificate.getTag().equals(newCertificate.getTag())) {
newCertificate = merge.merge(newCertificate, existingCertificate);
}
writeToFile(newCertificate.getInputStream(), certFile);
return newCertificate;
}
private Key _insertTrustRoot(InputStream data, KeyMerger merge)
throws IOException, BadDataException {
Key newKey = readKey(data);
Key existingKey;
File keyFile;
try {
existingKey = getTrustRoot();
keyFile = resolver.getKeyFileBySpecialName(SpecialNames.TRUST_ROOT);
} catch (BadNameException e) {
throw new RuntimeException(String.format("trust-root MUST be known special name.", e));
}
if (existingKey != null && !existingKey.getTag().equals(newKey.getTag())) {
newKey = merge.merge(newKey, existingKey);
}
writeToFile(newKey.getInputStream(), keyFile);
return newKey;
}
@Override
public Key insertTrustRoot(InputStream data, KeyMerger merge) throws IOException, BadDataException, InterruptedException {
writeLock.lockDirectory();
Key key = _insertTrustRoot(data, merge);
writeLock.releaseDirectory();
return key;
}
@Override
public Key tryInsertTrustRoot(InputStream data, KeyMerger merge) throws IOException, BadDataException {
return null;
}
private void writeToFile(InputStream inputStream, File certFile)
throws IOException {
certFile.getParentFile().mkdirs();
if (!certFile.exists() && !certFile.createNewFile()) {
throw new IOException("Could not create cert file " + certFile.getAbsolutePath());
}
FileOutputStream fileOut = new FileOutputStream(certFile);
byte[] buffer = new byte[4096];
int read;
while ((read = inputStream.read(buffer)) != -1) {
fileOut.write(buffer, 0, read);
}
inputStream.close();
fileOut.close();
}
@Override
public Certificate insertWithSpecialName(String specialName, InputStream data, CertificateMerger merge)
throws IOException, BadNameException, BadDataException, InterruptedException {
writeLock.lockDirectory();
Certificate certificate = _insertSpecial(specialName, data, merge);
writeLock.releaseDirectory();
return certificate;
}
@Override
public Certificate tryInsertWithSpecialName(String specialName, InputStream data, CertificateMerger merge)
throws IOException, BadNameException, BadDataException {
if (!writeLock.tryLockDirectory()) {
return null;
}
Certificate certificate = _insertSpecial(specialName, data, merge);
writeLock.releaseDirectory();
return certificate;
}
private Certificate _insertSpecial(String specialName, InputStream data, CertificateMerger merge)
throws IOException, BadNameException, BadDataException {
Certificate newCertificate = readCertificate(data);
Certificate existingCertificate = getBySpecialName(specialName);
File certFile = resolver.getCertFileBySpecialName(specialName);
if (existingCertificate != null && !existingCertificate.getTag().equals(newCertificate.getTag())) {
newCertificate = merge.merge(newCertificate, existingCertificate);
}
writeToFile(newCertificate.getInputStream(), certFile);
return newCertificate;
}
@Override
public Iterator<Certificate> items() {
return new Iterator<Certificate>() {
private final List<Lazy<Certificate>> 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<Certificate>() {
@Override
Certificate get() throws BadDataException {
try {
Certificate certificate = readCertificate(new FileInputStream(certFile));
if (!(subdirectory.getName() + certFile.getName()).equals(certificate.getFingerprint())) {
throw new BadDataException();
}
return certificate;
} catch (IOException e) {
throw new AssertionError("File got deleted.");
}
}
});
}
}
}
@Override
public boolean hasNext() {
return !certificateQueue.isEmpty();
}
@Override
public Certificate next() {
try {
return certificateQueue.remove(0).get();
} catch (BadDataException e) {
throw new AssertionError("Could not retrieve item: " + e.getMessage());
}
}
};
}
private abstract static class Lazy<E> {
abstract E get() throws BadDataException;
}
@Override
public Iterator<String> fingerprints() {
Iterator<Certificate> certificates = items();
return new Iterator<String>() {
@Override
public boolean hasNext() {
return certificates.hasNext();
}
@Override
public String next() {
return certificates.next().getFingerprint();
}
};
}
}

View file

@ -0,0 +1,126 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>trust-root</pre>.
*
* @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 <pre>trust-root</pre>.
* 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 <pre>trust-root</pre>.
* 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;
}

View file

@ -0,0 +1,445 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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.regex.Pattern;
public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirectory.Backend {
private abstract static class Lazy<E> {
abstract E get() throws BadDataException;
}
private static class FileLockingMechanism implements PGPCertificateDirectory.LockingMechanism {
private final File lockFile;
private RandomAccessFile randomAccessFile;
private FileLock fileLock;
FileLockingMechanism(File lockFile) {
this.lockFile = lockFile;
}
public static FileLockingMechanism defaultDirectoryFileLock(File baseDirectory) {
return new FileLockingMechanism(new File(baseDirectory, "writelock"));
}
@Override
public synchronized void lockDirectory() throws IOException, InterruptedException {
if (randomAccessFile != null) {
// we own the lock already. Let's wait...
this.wait();
}
try {
randomAccessFile = new RandomAccessFile(lockFile, "rw");
} catch (FileNotFoundException e) {
lockFile.createNewFile();
randomAccessFile = new RandomAccessFile(lockFile, "rw");
}
fileLock = randomAccessFile.getChannel().lock();
}
@Override
public synchronized boolean tryLockDirectory() throws IOException {
if (randomAccessFile != null) {
// We already locked the directory for another write operation.
// We fail, since we have not yet released the lock from the other operation.
return false;
}
try {
randomAccessFile = new RandomAccessFile(lockFile, "rw");
} catch (FileNotFoundException e) {
lockFile.createNewFile();
randomAccessFile = new RandomAccessFile(lockFile, "rw");
}
try {
fileLock = randomAccessFile.getChannel().tryLock();
if (fileLock == null) {
// try-lock failed, file is locked by another process.
randomAccessFile.close();
randomAccessFile = null;
return false;
}
} catch (OverlappingFileLockException e) {
// Some other object is holding the lock.
randomAccessFile.close();
randomAccessFile = null;
return false;
}
return true;
}
@Override
public boolean isLocked() {
return randomAccessFile != null;
}
@Override
public synchronized void releaseDirectory() throws IOException {
// unlock file
if (fileLock != null) {
fileLock.release();
fileLock = null;
}
// close file
if (randomAccessFile != null) {
randomAccessFile.close();
randomAccessFile = null;
}
// delete file
if (lockFile.exists()) {
lockFile.delete();
}
// notify waiters
this.notify();
}
}
private final File baseDirectory;
private final PGPCertificateDirectory.LockingMechanism lock;
private final FilenameResolver resolver;
private final KeyMaterialReaderBackend reader;
public FileBasedCertificateDirectoryBackend(File baseDirectory, KeyMaterialReaderBackend reader) throws NotAStoreException {
this.baseDirectory = baseDirectory;
this.resolver = new FilenameResolver(baseDirectory);
if (!baseDirectory.exists()) {
if (!baseDirectory.mkdirs()) {
throw new NotAStoreException("Cannot create base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "'");
}
} else {
if (baseDirectory.isFile()) {
throw new NotAStoreException("Base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "' appears to be a file.");
}
}
this.lock = FileLockingMechanism.defaultDirectoryFileLock(baseDirectory);
this.reader = reader;
}
@Override
public PGPCertificateDirectory.LockingMechanism getLock() {
return lock;
}
@Override
public Certificate readByFingerprint(String fingerprint) throws BadNameException, IOException, BadDataException {
File certFile = resolver.getCertFileByFingerprint(fingerprint);
if (!certFile.exists()) {
return null;
}
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<Certificate> readItems() {
return new Iterator<Certificate>() {
private final List<Lazy<Certificate>> certificateQueue = Collections.synchronizedList(new ArrayList<>());
// Constructor... wtf.
{
File[] subdirectories = baseDirectory.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory() && file.getName().matches("^[a-f0-9]{2}$");
}
});
for (File subdirectory : subdirectories) {
File[] files = subdirectory.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isFile() && file.getName().matches("^[a-f0-9]{38}$");
}
});
for (File certFile : files) {
certificateQueue.add(new Lazy<Certificate>() {
@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 IllegalArgumentException("File MUST exist.");
}
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);
}
public static class FilenameResolver {
private final File baseDirectory;
private final Pattern openPgpV4FingerprintPattern = Pattern.compile("^[a-f0-9]{40}$");
public FilenameResolver(File baseDirectory) {
this.baseDirectory = baseDirectory;
}
public File getBaseDirectory() {
return baseDirectory;
}
/**
* Calculate the file location for the certificate addressed by the given
* lowercase hexadecimal OpenPGP fingerprint.
*
* @param fingerprint fingerprint
* @return absolute certificate file location
*
* @throws BadNameException if the given fingerprint string is not a fingerprint
*/
public File getCertFileByFingerprint(String fingerprint) throws BadNameException {
if (!isFingerprint(fingerprint)) {
throw new BadNameException();
}
// is fingerprint
File subdirectory = new File(getBaseDirectory(), fingerprint.substring(0, 2));
File file = new File(subdirectory, fingerprint.substring(2));
return file;
}
/**
* Calculate the file location for the certification addressed using the given special name.
* For known special names, see {@link SpecialNames}.
*
* @param specialName special name (e.g. "trust-root")
* @return absolute certificate file location
*
* @throws BadNameException in case the given special name is not known
*/
public File getCertFileBySpecialName(String specialName)
throws BadNameException {
if (!isSpecialName(specialName)) {
throw new BadNameException(String.format("%s is not a known special name", specialName));
}
return new File(getBaseDirectory(), specialName);
}
/**
* Calculate the file location for the key addressed using the given special name.
* For known special names, see {@link SpecialNames}.
*
* @param specialName special name (e.g. "trust-root")
* @return absolute key file location
*
* @throws BadNameException in case the given special name is not known
*/
public File getKeyFileBySpecialName(String specialName)
throws BadNameException {
if (!isSpecialName(specialName)) {
throw new BadNameException(String.format("%s is not a known special name", specialName));
}
return new File(getBaseDirectory(), specialName + ".key");
}
private boolean isFingerprint(String fingerprint) {
return openPgpV4FingerprintPattern.matcher(fingerprint).matches();
}
private boolean isSpecialName(String specialName) {
return SpecialNames.lookupSpecialName(specialName) != null;
}
}
}

View file

@ -0,0 +1,160 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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;
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<String, Certificate> certificateFingerprintMap = new HashMap<>();
private final Map<String, KeyMaterial> keyMaterialSpecialNameMap = new HashMap<>();
private final PGPCertificateDirectory.LockingMechanism lock = new ObjectLockingMechanism();
private final KeyMaterialReaderBackend reader;
public InMemoryCertificateDirectoryBackend(KeyMaterialReaderBackend reader) {
this.reader = reader;
}
@Override
public PGPCertificateDirectory.LockingMechanism getLock() {
return lock;
}
@Override
public Certificate readByFingerprint(String fingerprint) {
return certificateFingerprintMap.get(fingerprint);
}
@Override
public KeyMaterial readBySpecialName(String specialName) throws BadNameException {
if (SpecialNames.lookupSpecialName(specialName) == null) {
throw new BadNameException("Invalid special name " + specialName);
}
return keyMaterialSpecialNameMap.get(specialName);
}
@Override
public Iterator<Certificate> 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, System.currentTimeMillis());
} else {
merged = new Certificate((Certificate) merged, System.currentTimeMillis());
}
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, System.currentTimeMillis());
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, System.currentTimeMillis());
} else {
merged = new Certificate((Certificate) merged, System.currentTimeMillis());
}
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();
}
}

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Storage Backends.
*/
package pgp.cert_d.backend;

View file

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package pgp.cert_d; package pgp.cert_d.subkey_lookup;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -11,8 +11,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import pgp.certificate_store.SubkeyLookup;
public class InMemorySubkeyLookup implements SubkeyLookup { public class InMemorySubkeyLookup implements SubkeyLookup {
private static final Map<Long, Set<String>> subkeyMap = new HashMap<>(); private static final Map<Long, Set<String>> subkeyMap = new HashMap<>();

View file

@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.subkey_lookup;
import java.io.File;
public class InMemorySubkeyLookupFactory implements SubkeyLookupFactory {
@Override
public SubkeyLookup createFileBasedInstance(File baseDirectory) {
return new InMemorySubkeyLookup();
}
}

View file

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store; package pgp.cert_d.subkey_lookup;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.subkey_lookup;
import java.io.File;
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);
}

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Subkey Lookup functionality.
*/
package pgp.cert_d.subkey_lookup;

View file

@ -6,6 +6,7 @@ package pgp.cert_d;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import pgp.cert_d.backend.FileBasedCertificateDirectoryBackend;
import pgp.certificate_store.exception.BadNameException; import pgp.certificate_store.exception.BadNameException;
import java.io.File; import java.io.File;
@ -18,13 +19,13 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
public class FilenameResolverTest { public class FilenameResolverTest {
private File baseDir; private File baseDir;
private FilenameResolver resolver; private FileBasedCertificateDirectoryBackend.FilenameResolver resolver;
@BeforeEach @BeforeEach
public void setup() throws IOException { public void setup() throws IOException {
baseDir = Files.createTempDirectory("filenameresolver").toFile(); baseDir = Files.createTempDirectory("filenameresolver").toFile();
baseDir.deleteOnExit(); baseDir.deleteOnExit();
resolver = new FilenameResolver(baseDir); resolver = new FileBasedCertificateDirectoryBackend.FilenameResolver(baseDir);
} }
@Test @Test

View file

@ -0,0 +1,345 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d;
import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
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.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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
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.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;
public class PGPCertificateDirectoryTest {
@SuppressWarnings("CharsetObjectCanBeUsed")
private static final Charset UTF8 = Charset.forName("UTF8");
private 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 <harry@potter.more>\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";
private static final String HARRY_FP = "23578fd17f207fdf62f7976c4e9d98917ad84522";
private 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 <ron@weasley.burrow>\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";
private static final String RON_FP = "b798af186bfe4c19902d49505647f00137ef4c41";
private 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 <cedric@diggo.ry>\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";
private static final String CEDRIC_FP = "5e75bf20646bc1a98d3b1bc2fe9cd472987c4021";
private static final KeyMaterialMerger merger = new TestKeyMaterialMerger();
private static Stream<PGPCertificateDirectory> 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(inMemory, fileBased);
}
@ParameterizedTest
@MethodSource("provideTestSubjects")
public void lockDirectoryAndInsertWillFail(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(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), merger);
assertNull(inserted);
directory.backend.getLock().releaseDirectory();
inserted = directory.tryInsert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), 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 {
assertNull(directory.getByFingerprint(CEDRIC_FP), "Empty directory MUST NOT contain certificate");
ByteArrayInputStream bytesIn = new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8));
Certificate certificate = directory.insert(bytesIn, 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 = CEDRIC_CERT.getBytes(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 {
assertNull(directory.getTrustRoot());
KeyMaterial trustRootMaterial = directory.insertTrustRoot(
new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8)), 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(new ByteArrayInputStream(RON_CERT.getBytes(UTF8)), merger);
directory.insert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), merger);
Set<String> expected = new HashSet<>(Arrays.asList(RON_FP, CEDRIC_FP));
Set<String> actual = new HashSet<>();
Iterator<String> 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(
new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8)), merger);
assertNotNull(trustRootMaterial.getTag());
Long tag = trustRootMaterial.getTag();
assertNull(directory.getTrustRootCertificateIfChanged(tag));
assertNotNull(directory.getTrustRootCertificateIfChanged(tag + 1));
Long oldTag = tag;
// "update" key
trustRootMaterial = directory.insertTrustRoot(
new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8)), 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,
new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8)), merger);
assertNotNull(specialName.getTag());
Long tag = specialName.getTag();
assertNull(directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag));
assertNotNull(directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag + 1));
Long oldTag = tag;
// "update" key
specialName = directory.insertWithSpecialName(SpecialNames.TRUST_ROOT,
new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8)), merger);
tag = specialName.getTag();
assertNotEquals(oldTag, tag);
assertNotNull(directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, oldTag));
}
@ParameterizedTest
@MethodSource("provideTestSubjects")
public void testGetByFingerprintIfChanged(PGPCertificateDirectory directory)
throws BadDataException, IOException, InterruptedException, BadNameException {
Certificate certificate = directory.insert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), merger);
Long tag = certificate.getTag();
assertNotNull(tag);
assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag));
assertNotNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag + 1));
Long oldTag = tag;
// "update" cert
certificate = directory.insert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), merger);
tag = certificate.getTag();
assertNotEquals(oldTag, tag);
assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag));
assertNotNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), oldTag));
}
@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(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), merger);
Long tag = certificate.getTag();
assertNotNull(tag);
assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag));
Long oldTag = tag;
// 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.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));
}
}

View file

@ -24,7 +24,8 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import pgp.cert_d.jdbc.sqlite.DatabaseSubkeyLookup; import pgp.cert_d.jdbc.sqlite.DatabaseSubkeyLookup;
import pgp.cert_d.jdbc.sqlite.SqliteSubkeyLookupDaoImpl; import pgp.cert_d.jdbc.sqlite.SqliteSubkeyLookupDaoImpl;
import pgp.certificate_store.SubkeyLookup; import pgp.cert_d.subkey_lookup.InMemorySubkeyLookup;
import pgp.cert_d.subkey_lookup.SubkeyLookup;
public class SubkeyLookupTest { public class SubkeyLookupTest {

View file

@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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;
}
}

View file

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<Long> subKeyIds = getSubkeyIds(cert);
return new Certificate(buffer.toByteArray(), fingerprint, subKeyIds, tag);
}
private PGPPublicKeyRing extractCert(PGPSecretKeyRing secretKeys) {
List<PGPPublicKey> publicKeyList = new ArrayList<>();
Iterator<PGPPublicKey> publicKeyIterator = secretKeys.getPublicKeys();
while (publicKeyIterator.hasNext()) {
publicKeyList.add(publicKeyIterator.next());
}
return new PGPPublicKeyRing(publicKeyList);
}
private static List<Long> getSubkeyIds(PGPKeyRing keyRing) {
List<Long> keyIds = new ArrayList<>();
Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
while (keys.hasNext()) {
keyIds.add(keys.next().getKeyID());
}
return keyIds;
}
}

View file

@ -1,39 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<Certificate> getCertificatesBySubkeyId(long subkeyId)
throws IOException {
Set<String> identifiers = getCertificateFingerprintsForSubkeyId(subkeyId);
if (identifiers.isEmpty()) {
return Collections.emptySet();
}
Set<Certificate> 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;
}
}

View file

@ -1,40 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
import java.io.IOException;
import java.io.InputStream;
import java.util.Set;
/**
* OpenPGP certificate (public key).
*/
public abstract class Certificate implements KeyMaterial {
/**
* Return an {@link InputStream} of the binary representation of the certificate.
*
* @return input stream
* @throws IOException in case of an IO error
*/
public abstract InputStream getInputStream() throws IOException;
/**
* Return a tag of the certificate.
* The tag is a checksum calculated over the binary representation of the certificate.
*
* @return tag
* @throws IOException in case of an IO error
*/
public abstract String getTag() throws IOException;
/**
* Return a {@link Set} containing key-ids of subkeys.
*
* @return subkeys
* @throws IOException in case of an IO error
*/
public abstract Set<Long> getSubkeyIds() throws IOException;
}

View file

@ -1,225 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 CertificateMerger} will be
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
* will be stored in the store and returned.
*
* This method will block until a write-lock on the store can be acquired. If you cannot afford blocking,
* consider to use {@link #tryInsertCertificate(InputStream, CertificateMerger)} instead.
*
* @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, CertificateMerger merge)
throws IOException, InterruptedException, BadDataException;
/**
* Insert a certificate into the store.
* If an instance of the certificate is already present in the store, the given {@link CertificateMerger} will be
* 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, CertificateMerger merge)
throws IOException, BadDataException;
/**
* Insert a certificate into the store.
* The certificate will be stored under the given special name instead of its fingerprint.
*
* If an instance of the certificate is already present under the special name in the store, the given {@link CertificateMerger} will be
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
* will be stored in the store and returned.
*
* This method will block until a write-lock on the store can be acquired. If you cannot afford blocking,
* consider to use {@link #tryInsertCertificateBySpecialName(String, InputStream, CertificateMerger)} instead.
*
* @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, CertificateMerger merge)
throws IOException, InterruptedException, BadDataException, BadNameException;
/**
* Insert a certificate into the store.
* The certificate will be stored under the given special name instead of its fingerprint.
*
* If an instance of the certificate is already present under the special name in the store, the given {@link CertificateMerger} will be
* 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, CertificateMerger 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<Certificate> 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<String> getFingerprints();
/**
* Return the current trust-root key.
* If no trust-root key is present, return null.
*
* @return trust-root key
*
* @throws IOException in case of an IO error
* @throws BadDataException if the key datum contains invalid data
*/
Key getTrustRoot()
throws IOException, BadDataException;
/**
* Return the current trust-root key, but only iff it changed since the last invocation of this method.
* To compare the key against its last returned result, the given tag is used.
* If the tag of the currently found key matches the given argument, return null.
*
* @param tag tag to compare freshness
* @return changed key or null
*
* @throws IOException in case of an IO error
* @throws BadDataException if the key datum contains invalid data
*/
Key getTrustRootIfChanged(String tag)
throws IOException, BadDataException;
/**
* Insert the given trust-root key into the store.
* If the key store already holds a trust-root key, the given {@link KeyMerger} callback will be used to merge
* the two instances into one {@link Key}. The result will be stored in the store and returned.
*
* This method will not block. Instead, if the store is already write-locked, this method will simply return null
* without writing anything.
* However, if the write-lock is available, this method will acquire the lock, write to the store, release the lock
* and return the written key.
*
* @param data input stream containing the new trust-root key
* @param keyMerger callback for merging with an existing key instance
* @return merged key
*
* @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 a valid OpenPGP key
*/
Key insertTrustRoot(InputStream data, KeyMerger keyMerger)
throws IOException, InterruptedException, BadDataException;
/**
* Insert the given trust-root key into the store.
* If the key store already holds a trust-root key, the given {@link KeyMerger} callback will be used to merge
* the two instances into one {@link Key}. The result will be stored in the store and returned.
*
* This method will block until a write-lock on the store can be acquired. If you cannot afford blocking,
* consider using {@link #tryInsertTrustRoot(InputStream, KeyMerger)} instead.
*
* @param data input stream containing the new trust-root key
* @param keyMerger callback for merging with an existing key instance
* @return merged key
*
* @throws IOException in case of an IO error
* @throws BadDataException if the data stream does not contain a valid OpenPGP key
*/
Key tryInsertTrustRoot(InputStream data, KeyMerger keyMerger)
throws IOException, BadDataException;
}

View file

@ -1,27 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
import java.io.IOException;
/**
* Merge a given certificate (update) with an existing certificate.
*/
public interface CertificateMerger {
/**
* Merge the given certificate data with the existing certificate and return the result.
*
* If no existing certificate is found (i.e. existing is null), this method returns the unmodified data.
*
* @param data certificate
* @param existing optional already existing copy of the certificate
* @return merged certificate
*
* @throws IOException in case of an IO error
*/
Certificate merge(Certificate data, Certificate existing) throws IOException;
}

View file

@ -1,9 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
public interface CertificateStore extends CertificateDirectory, SubkeyLookup {
}

View file

@ -1,32 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
import java.io.IOException;
import java.io.InputStream;
/**
* OpenPGP key (secret key).
*/
public abstract class Key implements KeyMaterial {
/**
* Return the certificate part of this OpenPGP key.
*
* @return OpenPGP certificate
*/
public abstract Certificate getCertificate();
/**
* Return an {@link InputStream} of the binary representation of the secret key.
*
* @return input stream
* @throws IOException in case of an IO error
*/
public abstract InputStream getInputStream() throws IOException;
public abstract String getTag() throws IOException;
}

View file

@ -1,17 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
public interface KeyMaterial {
/**
* Return the fingerprint of the certificate as 40 lowercase hex characters.
* TODO: Allow OpenPGP V5 fingerprints
*
* @return fingerprint
*/
String getFingerprint();
}

View file

@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store;
import java.io.IOException;
/**
* Merge a given {@link Key} (update) with an existing {@link Key}.
*/
public interface KeyMerger {
/**
* Merge the given key data with the existing {@link Key} and return the result.
* If no existing {@link Key} is found (i.e. if existing is null), this method returns the unmodified data.
*
* @param data key
* @param existing optional already existing copy of the key
* @return merged key
*
* @throws IOException in case of an IO error
*/
Key merge(Key data, Key existing) throws IOException;
}

View file

@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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;
/**
* Interface for an OpenPGP certificate (public key) store.
*/
public interface PGPCertificateStore {
/**
* 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 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
*/
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<Certificate> 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<Certificate> 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<String> getFingerprints();
}

View file

@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<Long> 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<Long> 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<Long> getSubkeyIds() {
return subkeyIds;
}
}

View file

@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<Long> getSubkeyIds() {
return certificate.getSubkeyIds();
}
}

View file

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<Long> getSubkeyIds();
}

View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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;
}

View file

@ -2,23 +2,24 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store; package pgp.certificate_store.certificate;
import pgp.certificate_store.exception.BadDataException; import pgp.certificate_store.exception.BadDataException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
public interface KeyReaderBackend { public interface KeyMaterialReaderBackend {
/** /**
* Read a {@link KeyMaterial} (either {@link Key} or {@link Certificate}) from the given {@link InputStream}. * 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 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 * @return key or certificate object
* *
* @throws IOException in case of an IO error * @throws IOException in case of an IO error
* @throws BadDataException in case that the data stream does not contain a valid OpenPGP key/certificate * @throws BadDataException in case that the data stream does not contain a valid OpenPGP key/certificate
*/ */
KeyMaterial read(InputStream data) throws IOException, BadDataException; KeyMaterial read(InputStream data, Long tag) throws IOException, BadDataException;
} }

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* General OpenPGP Certificate Storage related classes.
*/
package pgp.certificate_store.certificate;

View file

@ -3,8 +3,6 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
/** /**
* Exceptions defined by the Shared PGP Certificate Directory. * Exceptions.
*
* @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/#name-failure-modes">Failure Modes</a>
*/ */
package pgp.certificate_store.exception; package pgp.certificate_store.exception;

View file

@ -4,7 +4,7 @@
rootProject.name = 'cert-d-java' rootProject.name = 'cert-d-java'
include 'pgp-cert-d-java', include 'pgp-certificate-store',
'pgp-cert-d-java-jdbc-sqlite-lookup', 'pgp-cert-d-java',
'pgp-certificate-store' 'pgp-cert-d-java-jdbc-sqlite-lookup'

View file

@ -6,8 +6,9 @@ allprojects {
ext { ext {
shortVersion = '0.1.2' shortVersion = '0.1.2'
isSnapshot = true isSnapshot = true
minAndroidSdk = 10 minAndroidSdk = 26
javaSourceCompatibility = 1.8 javaSourceCompatibility = 1.8
bouncycastleVersion = '1.71'
slf4jVersion = '1.7.36' slf4jVersion = '1.7.36'
logbackVersion = '1.2.11' logbackVersion = '1.2.11'
junitVersion = '5.8.2' junitVersion = '5.8.2'