mirror of
https://codeberg.org/PGPainless/cert-d-java.git
synced 2024-11-16 04:52:04 +01:00
Compare commits
No commits in common. "main" and "0.1.0" have entirely different histories.
63 changed files with 1076 additions and 2800 deletions
|
@ -14,7 +14,3 @@ Files: gradle*
|
||||||
Copyright: 2015 the original author or authors.
|
Copyright: 2015 the original author or authors.
|
||||||
License: Apache-2.0
|
License: Apache-2.0
|
||||||
|
|
||||||
# Woodpecker build files
|
|
||||||
Files: .woodpecker/*
|
|
||||||
Copyright: 2022 the original author or authors.
|
|
||||||
License: Apache-2.0
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
pipeline:
|
|
||||||
run:
|
|
||||||
image: gradle:7.5-jdk8
|
|
||||||
commands:
|
|
||||||
- git checkout $CI_COMMIT_BRANCH
|
|
||||||
# Code works
|
|
||||||
- gradle test
|
|
||||||
# Code is clean
|
|
||||||
- gradle check javadocAll
|
|
||||||
# Code has coverage
|
|
||||||
- gradle jacocoRootReport coveralls
|
|
||||||
secrets: [COVERALLS_REPO_TOKEN]
|
|
|
@ -1,7 +0,0 @@
|
||||||
# Code is licensed properly
|
|
||||||
# See https://reuse.software/
|
|
||||||
pipeline:
|
|
||||||
reuse:
|
|
||||||
image: fsfe/reuse:latest
|
|
||||||
commands:
|
|
||||||
- reuse lint
|
|
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -5,31 +5,5 @@ SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
# Cert-D-Java Changelog
|
# Cert-D-Java Changelog
|
||||||
|
|
||||||
## 0.2.2
|
# 0.1.0
|
||||||
- Bump Bouncy Castle to `1.75`
|
- Initial Release
|
||||||
- Bump `sqlite-jdbc` to `3.42.0.0`
|
|
||||||
|
|
||||||
## 0.2.1
|
|
||||||
- Throw `NoSuchElementException` when querying non-existent certificates
|
|
||||||
|
|
||||||
## 0.2.0
|
|
||||||
- `pgp-certificate-store`:
|
|
||||||
- Rework `Certificate`, `Key` to inherit from `KeyMaterial`
|
|
||||||
- Rename `CertificateReaderBackend` to `KeyMaterialReaderBackend`
|
|
||||||
- Rename `CertificateMerger` to `KeyMaterialMerger`
|
|
||||||
- Rework `PGPCertificateStore` class
|
|
||||||
- `pgp-cert-d-java`:
|
|
||||||
- Increase minimum Android API level to 26
|
|
||||||
- Add `PGPCertificateDirectories` factory class
|
|
||||||
- Rework `PGPCertificateDirectory` class by separating out backend logic
|
|
||||||
- Split interface into `ReadOnlyPGPCertificateDirectory` and `WritingPGPCertificateDirectory`
|
|
||||||
- `FileBasedCertificateDirectoryBackend`: Calculate tag based on file attributes (inode)
|
|
||||||
- `pgp-cert-d-java-jdbc-sqlite-lookup`:
|
|
||||||
- Add `DatabaseSubkeyLookupFactory`
|
|
||||||
|
|
||||||
## 0.1.1
|
|
||||||
- Bump `slf4j` to `1.7.36`
|
|
||||||
- Bump `logback` to `1.2.11`
|
|
||||||
|
|
||||||
## 0.1.0
|
|
||||||
- Initial Release
|
|
|
@ -5,13 +5,10 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Shared PGP Certificate Directory for Java
|
# Shared PGP Certificate Directory for Java
|
||||||
[![status-badge](https://ci.codeberg.org/api/badges/PGPainless/cert-d-java/status.svg?branch=main)](https://ci.codeberg.org/PGPainless/cert-d-java)
|
|
||||||
[![Coverage Status](https://coveralls.io/repos/github/pgpainless/cert-d-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/cert-d-java?branch=main)
|
|
||||||
[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/cert-d-java)](https://api.reuse.software/info/github.com/pgpainless/cert-d-java)
|
|
||||||
|
|
||||||
This repository contains a number of modules defining OpenPGP certificate storage for Java and Android applications.
|
This repository contains a number of modules defining OpenPGP certificate storage for Java and Android applications.
|
||||||
|
|
||||||
The module [pgp-certificate-store](pgp-certificate-store) defines generalized
|
The module [pgp-certificate-store](pgp-certificate-store] defines generalized
|
||||||
interfaces for OpenPGP Certificate storage.
|
interfaces for OpenPGP Certificate storage.
|
||||||
It can be used by applications and libraries such as
|
It can be used by applications and libraries such as
|
||||||
[PGPainless](https://pgpainless.org/) for certificate management.
|
[PGPainless](https://pgpainless.org/) for certificate management.
|
||||||
|
|
|
@ -59,6 +59,9 @@ allprojects {
|
||||||
}
|
}
|
||||||
|
|
||||||
project.ext {
|
project.ext {
|
||||||
|
slf4jVersion = '1.7.32'
|
||||||
|
logbackVersion = '1.2.9'
|
||||||
|
junitVersion = '5.8.2'
|
||||||
rootConfigDir = new File(rootDir, 'config')
|
rootConfigDir = new File(rootDir, 'config')
|
||||||
gitCommit = getGitCommit()
|
gitCommit = getGitCommit()
|
||||||
isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI'))
|
isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI'))
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: 2022 Paul Schaub <info@pgpainless.org>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: Apache-2.0
|
|
||||||
-->
|
|
||||||
|
|
||||||
# SQLite backed Subkey-ID Lookup
|
|
||||||
|
|
||||||
[![javadoc](https://javadoc.io/badge2/org.pgpainless/pgp-cert-d-java-jdbc-sqlite-lookup/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgp-cert-d-java-jdbc-sqlite-lookup)
|
|
||||||
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgp-cert-d-java-jdbc-sqlite-lookup)](https://search.maven.org/artifact/org.pgpainless/pgp-cert-d-java-jdbc-sqlite-lookup)
|
|
||||||
|
|
||||||
Implementation of `pgp-certificate-store`'s `SubkeyLookup` class using an SQLite database accessed through `sqlite-jdbc`.
|
|
|
@ -13,7 +13,6 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// JUnit for testing
|
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
|
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
|
||||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||||
|
@ -21,11 +20,8 @@ dependencies {
|
||||||
// Logging
|
// Logging
|
||||||
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
|
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
|
||||||
|
|
||||||
// pgp.cert.d cert store
|
|
||||||
implementation project(":pgp-cert-d-java")
|
implementation project(":pgp-cert-d-java")
|
||||||
|
api 'org.xerial:sqlite-jdbc:3.36.0.3'
|
||||||
// SQLite
|
|
||||||
api "org.xerial:sqlite-jdbc:$sqliteJdbcVersion"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
|
|
@ -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.cert_d.subkey_lookup.SubkeyLookup;
|
import pgp.certificate_store.SubkeyLookup;
|
||||||
|
|
||||||
public class DatabaseSubkeyLookup implements SubkeyLookup {
|
public class DatabaseSubkeyLookup implements SubkeyLookup {
|
||||||
|
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,6 +3,6 @@
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of a {@link pgp.cert_d.subkey_lookup.SubkeyLookup} mechanism using an SQLite Database.
|
* Implementation of a {@link pgp.certificate_store.SubkeyLookup} mechanism using an SQLite Database.
|
||||||
*/
|
*/
|
||||||
package pgp.cert_d.jdbc.sqlite;
|
package pgp.cert_d.jdbc.sqlite;
|
||||||
|
|
|
@ -22,15 +22,15 @@ import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
public class SqliteSubkeyLookupTest {
|
public class SqliteSubkeyLookupTest {
|
||||||
|
|
||||||
private File tempDir;
|
private File databaseFile;
|
||||||
private DatabaseSubkeyLookup lookup;
|
private DatabaseSubkeyLookup lookup;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void setupLookup() throws IOException {
|
public void setupLookup() throws IOException, SQLException {
|
||||||
tempDir = Files.createTempDirectory("pgp.cert.d").toFile();
|
databaseFile = Files.createTempFile("pgp.cert.d-", "lookup.db").toFile();
|
||||||
tempDir.deleteOnExit();
|
databaseFile.createNewFile();
|
||||||
lookup = (DatabaseSubkeyLookup) new DatabaseSubkeyLookupFactory()
|
databaseFile.deleteOnExit();
|
||||||
.createFileBasedInstance(tempDir);
|
lookup = new DatabaseSubkeyLookup(SqliteSubkeyLookupDaoImpl.forDatabaseFile(databaseFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 = (DatabaseSubkeyLookup) new DatabaseSubkeyLookupFactory().createFileBasedInstance(tempDir);
|
DatabaseSubkeyLookup secondInstance = new DatabaseSubkeyLookup(SqliteSubkeyLookupDaoImpl.forDatabaseFile(databaseFile));
|
||||||
assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), secondInstance.getCertificateFingerprintsForSubkeyId(1337));
|
assertEquals(Collections.singleton("eb85bb5fa33a75e15e944e63f231550c4f47e38e"), secondInstance.getCertificateFingerprintsForSubkeyId(1337));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,18 +6,11 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
# Shared PGP Certificate Directory for Java
|
# Shared PGP Certificate Directory for Java
|
||||||
|
|
||||||
[![javadoc](https://javadoc.io/badge2/org.pgpainless/pgp-cert-d-java/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgp-cert-d-java)
|
|
||||||
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgp-cert-d-java)](https://search.maven.org/artifact/org.pgpainless/pgp-cert-d-java)
|
|
||||||
|
|
||||||
Backend-agnostic implementation of the [Shared PGP Certificate Directory Specification](https://sequoia-pgp.gitlab.io/pgp-cert-d/).
|
Backend-agnostic implementation of the [Shared PGP Certificate Directory Specification](https://sequoia-pgp.gitlab.io/pgp-cert-d/).
|
||||||
This module implements the non-OpenPGP parts of the spec, e.g. locating the directory, resolving certificate file paths,
|
This module implements the non-OpenPGP parts of the spec, e.g. locating the directory, resolving certificate file paths,
|
||||||
locking the directory for writes etc.
|
locking the directory for writes etc.
|
||||||
|
|
||||||
This library can be used on Android API level 26 and up.
|
|
||||||
|
|
||||||
To get a useful implementation, a backend implementation such as `pgpainless-cert-d` is required, which needs to provide
|
To get a useful implementation, a backend implementation such as `pgpainless-cert-d` is required, which needs to provide
|
||||||
support for reading and merging certificates.
|
support for reading and merging certificates.
|
||||||
|
|
||||||
`pgp-cert-d-java` can be used as an implementation of `pgp-certificate-store` using the `PGPCertificateStoreAdapter` class.
|
`pgp-cert-d-java` can be used as an implementation of `pgp-certificate-store`.
|
||||||
|
|
||||||
Note: This is a library module. For a command line interface, see [pgpainless-cert-d-cli](https://github.com/pgpainless/cert-d-pgpainless/tree/main/pgpainless-cert-d-cli).
|
|
|
@ -15,10 +15,9 @@ repositories {
|
||||||
apply plugin: 'ru.vyarus.animalsniffer'
|
apply plugin: 'ru.vyarus.animalsniffer'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// animal sniffer for ensuring Android API compatibility
|
// animal sniffer
|
||||||
signature "net.sf.androidscents.signature:android-api-level-${animalsnifferSignatureVersion}@signature"
|
signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:2.3.3_r2@signature"
|
||||||
|
|
||||||
// JUnit
|
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
|
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
|
||||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||||
|
@ -26,13 +25,9 @@ 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
|
|
||||||
testImplementation project(":pgp-cert-d-java-jdbc-sqlite-lookup")
|
testImplementation project(":pgp-cert-d-java-jdbc-sqlite-lookup")
|
||||||
|
|
||||||
testImplementation "org.bouncycastle:bcprov-jdk15to18:$bouncycastleVersion"
|
api project(":pgp-certificate-store")
|
||||||
testImplementation "org.bouncycastle:bcpg-jdk15to18:$bouncyPgVersion"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
animalsniffer {
|
animalsniffer {
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import pgp.certificate_store.CertificateReaderBackend;
|
||||||
|
import pgp.certificate_store.MergeCallback;
|
||||||
|
|
||||||
|
public abstract class BackendProvider {
|
||||||
|
|
||||||
|
public abstract CertificateReaderBackend provideCertificateReaderBackend();
|
||||||
|
|
||||||
|
public abstract MergeCallback provideDefaultMergeCallback();
|
||||||
|
|
||||||
|
}
|
|
@ -6,16 +6,6 @@ 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() {
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import pgp.certificate_store.exception.BadDataException;
|
||||||
|
import pgp.certificate_store.exception.BadNameException;
|
||||||
|
import pgp.certificate_store.Certificate;
|
||||||
|
import pgp.certificate_store.MergeCallback;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caching wrapper for {@link SharedPGPCertificateDirectory} implementations.
|
||||||
|
*/
|
||||||
|
public class CachingSharedPGPCertificateDirectoryWrapper
|
||||||
|
implements SharedPGPCertificateDirectory {
|
||||||
|
|
||||||
|
private static final Map<String, String> tagMap = new HashMap<>();
|
||||||
|
private static final Map<String, Certificate> certificateMap = new HashMap<>();
|
||||||
|
private final SharedPGPCertificateDirectory underlyingCertificateDirectory;
|
||||||
|
|
||||||
|
public CachingSharedPGPCertificateDirectoryWrapper(SharedPGPCertificateDirectory wrapped) {
|
||||||
|
this.underlyingCertificateDirectory = wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the given certificate under the given identifier into the cache.
|
||||||
|
*
|
||||||
|
* @param identifier fingerprint or special name
|
||||||
|
* @param certificate certificate
|
||||||
|
*/
|
||||||
|
private void remember(String identifier, Certificate certificate) {
|
||||||
|
certificateMap.put(identifier, certificate);
|
||||||
|
try {
|
||||||
|
tagMap.put(identifier, certificate.getTag());
|
||||||
|
} catch (IOException e) {
|
||||||
|
tagMap.put(identifier, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true, if the cached tag differs from the provided tag.
|
||||||
|
*
|
||||||
|
* @param identifier fingerprint or special name
|
||||||
|
* @param tag tag
|
||||||
|
* @return true if cached tag differs, false otherwise
|
||||||
|
*/
|
||||||
|
private boolean tagChanged(String identifier, String tag) {
|
||||||
|
String tack = tagMap.get(identifier);
|
||||||
|
return !tagEquals(tag, tack);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true, if tag and tack are equal, false otherwise.
|
||||||
|
* @param tag tag
|
||||||
|
* @param tack other tag
|
||||||
|
* @return true if equal
|
||||||
|
*/
|
||||||
|
private static boolean tagEquals(String tag, String tack) {
|
||||||
|
return (tag == null && tack == null)
|
||||||
|
|| tag != null && tag.equals(tack);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the cache.
|
||||||
|
*/
|
||||||
|
public void invalidate() {
|
||||||
|
certificateMap.clear();
|
||||||
|
tagMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LockingMechanism getLock() {
|
||||||
|
return underlyingCertificateDirectory.getLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getByFingerprint(String fingerprint)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
Certificate certificate = certificateMap.get(fingerprint);
|
||||||
|
if (certificate == null) {
|
||||||
|
certificate = underlyingCertificateDirectory.getByFingerprint(fingerprint);
|
||||||
|
if (certificate != null) {
|
||||||
|
remember(fingerprint, certificate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getBySpecialName(String specialName)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
Certificate certificate = certificateMap.get(specialName);
|
||||||
|
if (certificate == null) {
|
||||||
|
certificate = underlyingCertificateDirectory.getBySpecialName(specialName);
|
||||||
|
if (certificate != null) {
|
||||||
|
remember(specialName, certificate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getByFingerprintIfChanged(String fingerprint, String tag)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
if (tagChanged(fingerprint, tag)) {
|
||||||
|
return getByFingerprint(fingerprint);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getBySpecialNameIfChanged(String specialName, String tag)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
if (tagChanged(specialName, tag)) {
|
||||||
|
return getBySpecialName(specialName);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate insert(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, InterruptedException {
|
||||||
|
Certificate certificate = underlyingCertificateDirectory.insert(data, merge);
|
||||||
|
remember(certificate.getFingerprint(), certificate);
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate tryInsert(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException {
|
||||||
|
Certificate certificate = underlyingCertificateDirectory.tryInsert(data, merge);
|
||||||
|
if (certificate != null) {
|
||||||
|
remember(certificate.getFingerprint(), certificate);
|
||||||
|
}
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate insertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, BadNameException, InterruptedException {
|
||||||
|
Certificate certificate = underlyingCertificateDirectory.insertWithSpecialName(specialName, data, merge);
|
||||||
|
remember(specialName, certificate);
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate tryInsertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, BadNameException {
|
||||||
|
Certificate certificate = underlyingCertificateDirectory.tryInsertWithSpecialName(specialName, data, merge);
|
||||||
|
if (certificate != null) {
|
||||||
|
remember(specialName, certificate);
|
||||||
|
}
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
// 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
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getCertFileBySpecialName(String specialName) throws BadNameException {
|
||||||
|
if (!isSpecialName(specialName)) {
|
||||||
|
throw new BadNameException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new File(getBaseDirectory(), specialName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isFingerprint(String fingerprint) {
|
||||||
|
return openPgpV4FingerprintPattern.matcher(fingerprint).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSpecialName(String specialName) {
|
||||||
|
return SpecialNames.lookupSpecialName(specialName) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package pgp.cert_d.subkey_lookup;
|
package pgp.cert_d;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -11,6 +11,8 @@ 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<>();
|
||||||
|
@ -28,7 +30,6 @@ public class InMemorySubkeyLookup implements SubkeyLookup {
|
||||||
public void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) {
|
public void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) {
|
||||||
for (long subkeyId : subkeyIds) {
|
for (long subkeyId : subkeyIds) {
|
||||||
Set<String> certificates = subkeyMap.get(subkeyId);
|
Set<String> certificates = subkeyMap.get(subkeyId);
|
||||||
// noinspection Java8MapApi
|
|
||||||
if (certificates == null) {
|
if (certificates == null) {
|
||||||
certificates = new HashSet<>();
|
certificates = new HashSet<>();
|
||||||
subkeyMap.put(subkeyId, certificates);
|
subkeyMap.put(subkeyId, certificates);
|
|
@ -0,0 +1,30 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.cert_d;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface LockingMechanism {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock the store for writes.
|
||||||
|
* Readers can continue to use the store and will always see consistent certs.
|
||||||
|
*/
|
||||||
|
void lockDirectory() throws IOException, InterruptedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try top lock the store for writes.
|
||||||
|
* Return false without locking the store in case the store was already locked.
|
||||||
|
*
|
||||||
|
* @return true if locking succeeded, false otherwise
|
||||||
|
*/
|
||||||
|
boolean tryLockDirectory() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release the directory write-lock acquired via {@link #lockDirectory()}.
|
||||||
|
*/
|
||||||
|
void releaseDirectory() throws IOException;
|
||||||
|
|
||||||
|
}
|
|
@ -1,46 +0,0 @@
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,367 +0,0 @@
|
||||||
// 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.NoSuchElementException;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
private final Pattern openPgpV4FingerprintPattern = Pattern.compile("^[a-f0-9]{40}$");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for a PGP certificate directory.
|
|
||||||
*
|
|
||||||
* @param backend storage backend
|
|
||||||
* @param subkeyLookup subkey lookup mechanism to map subkey-ids to certificates
|
|
||||||
*/
|
|
||||||
public PGPCertificateDirectory(Backend backend, SubkeyLookup subkeyLookup) {
|
|
||||||
this.backend = backend;
|
|
||||||
this.subkeyLookup = subkeyLookup;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Certificate getByFingerprint(String fingerprint) throws BadDataException, BadNameException, IOException {
|
|
||||||
if (!openPgpV4FingerprintPattern.matcher(fingerprint).matches()) {
|
|
||||||
throw new BadNameException();
|
|
||||||
}
|
|
||||||
Certificate certificate = backend.readByFingerprint(fingerprint);
|
|
||||||
if (certificate == null) {
|
|
||||||
throw new NoSuchElementException();
|
|
||||||
}
|
|
||||||
return certificate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Certificate getByFingerprintIfChanged(String fingerprint, long tag)
|
|
||||||
throws IOException, BadNameException, BadDataException {
|
|
||||||
if (!Objects.equals(tag, backend.getTagForFingerprint(fingerprint))) {
|
|
||||||
return getByFingerprint(fingerprint);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Certificate getBySpecialName(String specialName)
|
|
||||||
throws BadNameException, BadDataException, IOException {
|
|
||||||
KeyMaterial keyMaterial = backend.readBySpecialName(specialName);
|
|
||||||
if (keyMaterial != null) {
|
|
||||||
return keyMaterial.asCertificate();
|
|
||||||
}
|
|
||||||
throw new NoSuchElementException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Certificate getBySpecialNameIfChanged(String specialName, long tag)
|
|
||||||
throws IOException, BadNameException, BadDataException {
|
|
||||||
if (!Objects.equals(tag, backend.getTagForSpecialName(specialName))) {
|
|
||||||
return getBySpecialName(specialName);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Certificate getTrustRootCertificate()
|
|
||||||
throws IOException, BadDataException {
|
|
||||||
try {
|
|
||||||
return getBySpecialName(SpecialNames.TRUST_ROOT);
|
|
||||||
} catch (BadNameException e) {
|
|
||||||
throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is an implementation MUST");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Certificate getTrustRootCertificateIfChanged(long tag) throws IOException, BadDataException {
|
|
||||||
try {
|
|
||||||
return getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag);
|
|
||||||
} catch (BadNameException e) {
|
|
||||||
throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is an implementation MUST");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterator<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 {
|
|
||||||
KeyMaterial keyMaterial = backend.readBySpecialName(SpecialNames.TRUST_ROOT);
|
|
||||||
if (keyMaterial == null) {
|
|
||||||
throw new NoSuchElementException();
|
|
||||||
}
|
|
||||||
return keyMaterial;
|
|
||||||
} catch (BadNameException e) {
|
|
||||||
throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is implementation MUST");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeyMaterial insertTrustRoot(InputStream data, KeyMaterialMerger merge)
|
|
||||||
throws IOException, BadDataException, InterruptedException {
|
|
||||||
backend.getLock().lockDirectory();
|
|
||||||
KeyMaterial inserted = backend.doInsertTrustRoot(data, merge);
|
|
||||||
subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
|
|
||||||
backend.getLock().releaseDirectory();
|
|
||||||
return inserted;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeyMaterial tryInsertTrustRoot(InputStream data, KeyMaterialMerger merge)
|
|
||||||
throws IOException, BadDataException {
|
|
||||||
if (!backend.getLock().tryLockDirectory()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
KeyMaterial inserted = backend.doInsertTrustRoot(data, merge);
|
|
||||||
subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
|
|
||||||
backend.getLock().releaseDirectory();
|
|
||||||
return inserted;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Certificate insert(InputStream data, KeyMaterialMerger merge)
|
|
||||||
throws IOException, BadDataException, InterruptedException {
|
|
||||||
backend.getLock().lockDirectory();
|
|
||||||
Certificate inserted = backend.doInsert(data, merge);
|
|
||||||
subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
|
|
||||||
backend.getLock().releaseDirectory();
|
|
||||||
return inserted;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Certificate tryInsert(InputStream data, KeyMaterialMerger merge)
|
|
||||||
throws IOException, BadDataException {
|
|
||||||
if (!backend.getLock().tryLockDirectory()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Certificate inserted = backend.doInsert(data, merge);
|
|
||||||
subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
|
|
||||||
backend.getLock().releaseDirectory();
|
|
||||||
return inserted;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Certificate insertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
|
|
||||||
throws IOException, BadDataException, BadNameException, InterruptedException {
|
|
||||||
backend.getLock().lockDirectory();
|
|
||||||
Certificate inserted = backend.doInsertWithSpecialName(specialName, data, merge);
|
|
||||||
subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
|
|
||||||
backend.getLock().releaseDirectory();
|
|
||||||
return inserted;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Certificate tryInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
|
|
||||||
throws IOException, BadDataException, BadNameException {
|
|
||||||
if (!backend.getLock().tryLockDirectory()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Certificate inserted = backend.doInsertWithSpecialName(specialName, data, merge);
|
|
||||||
subkeyLookup.storeCertificateSubkeyIds(inserted.getFingerprint(), inserted.getSubkeyIds());
|
|
||||||
backend.getLock().releaseDirectory();
|
|
||||||
return inserted;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<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;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,127 +0,0 @@
|
||||||
// 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;
|
|
||||||
import java.util.NoSuchElementException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for a read-only OpenPGP certificate directory.
|
|
||||||
*/
|
|
||||||
public interface ReadOnlyPGPCertificateDirectory {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the trust-root certificate. This is a certificate which is stored under the special name
|
|
||||||
* <pre>trust-root</pre>.
|
|
||||||
*
|
|
||||||
* @return trust-root certificate
|
|
||||||
*
|
|
||||||
* @throws IOException in case of an IO error
|
|
||||||
* @throws BadDataException if the certificate contains bad data
|
|
||||||
* @throws NoSuchElementException if no such certificate is found
|
|
||||||
*/
|
|
||||||
Certificate getTrustRootCertificate()
|
|
||||||
throws IOException, BadDataException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the trust-root certificate if it has changed.
|
|
||||||
* This method uses the <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.
|
|
||||||
*
|
|
||||||
* @throws IOException in case of an IO error
|
|
||||||
* @throws BadDataException if the certificate contains bad data
|
|
||||||
* @throws NoSuchElementException if no such certificate is found
|
|
||||||
*/
|
|
||||||
Certificate getTrustRootCertificateIfChanged(long tag)
|
|
||||||
throws IOException, BadDataException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the certificate identified by the given fingerprint.
|
|
||||||
*
|
|
||||||
* @param fingerprint lower-case fingerprint of the certificate
|
|
||||||
* @return certificate
|
|
||||||
*
|
|
||||||
* @throws IOException in case of an IO error
|
|
||||||
* @throws BadNameException if the fingerprint is malformed
|
|
||||||
* @throws BadDataException if the certificate contains bad data
|
|
||||||
* @throws NoSuchElementException if no such certificate is found
|
|
||||||
*/
|
|
||||||
Certificate getByFingerprint(String fingerprint)
|
|
||||||
throws IOException, BadNameException, BadDataException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the certificate identified by the given fingerprint if it has changed.
|
|
||||||
* This method uses the <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
|
|
||||||
*
|
|
||||||
* @throws IOException in case of an IO error
|
|
||||||
* @throws BadNameException if the fingerprint is malformed
|
|
||||||
* @throws BadDataException if the certificate contains bad data
|
|
||||||
* @throws NoSuchElementException if no such certificate is found
|
|
||||||
*/
|
|
||||||
Certificate getByFingerprintIfChanged(String fingerprint, long tag)
|
|
||||||
throws IOException, BadNameException, BadDataException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the certificate identified by the given special name.
|
|
||||||
*
|
|
||||||
* @param specialName special name
|
|
||||||
* @return certificate
|
|
||||||
*
|
|
||||||
* @throws IOException in case of an IO error
|
|
||||||
* @throws BadNameException if the special name is not known
|
|
||||||
* @throws BadDataException if the certificate contains bad data
|
|
||||||
* @throws NoSuchElementException if no such certificate is found
|
|
||||||
*/
|
|
||||||
Certificate getBySpecialName(String specialName)
|
|
||||||
throws IOException, BadNameException, BadDataException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the certificate identified by the given special name or null, if it has not been changed.
|
|
||||||
* This method uses the <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
|
|
||||||
* @throws NoSuchElementException if no such certificate is found
|
|
||||||
*/
|
|
||||||
Certificate getBySpecialNameIfChanged(String specialName, long tag)
|
|
||||||
throws IOException, BadNameException, BadDataException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all certificates in the directory, except for certificates which are stored by special name.
|
|
||||||
*
|
|
||||||
* @return iterator of certificates
|
|
||||||
*/
|
|
||||||
Iterator<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();
|
|
||||||
}
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
// 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.exception.BadDataException;
|
||||||
|
import pgp.certificate_store.exception.BadNameException;
|
||||||
|
import pgp.certificate_store.Certificate;
|
||||||
|
import pgp.certificate_store.MergeCallback;
|
||||||
|
|
||||||
|
public interface SharedPGPCertificateDirectory {
|
||||||
|
|
||||||
|
LockingMechanism getLock();
|
||||||
|
|
||||||
|
Certificate getByFingerprint(String fingerprint)
|
||||||
|
throws IOException, BadNameException, BadDataException;
|
||||||
|
|
||||||
|
Certificate getBySpecialName(String specialName)
|
||||||
|
throws IOException, BadNameException, BadDataException;
|
||||||
|
|
||||||
|
Certificate getByFingerprintIfChanged(String fingerprint, String tag)
|
||||||
|
throws IOException, BadNameException, BadDataException;
|
||||||
|
|
||||||
|
Certificate getBySpecialNameIfChanged(String specialName, String tag)
|
||||||
|
throws IOException, BadNameException, BadDataException;
|
||||||
|
|
||||||
|
Certificate insert(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, InterruptedException;
|
||||||
|
|
||||||
|
Certificate tryInsert(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException;
|
||||||
|
|
||||||
|
Certificate insertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, BadNameException, InterruptedException;
|
||||||
|
|
||||||
|
Certificate tryInsertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, BadNameException;
|
||||||
|
|
||||||
|
Iterator<Certificate> items();
|
||||||
|
|
||||||
|
Iterator<String> fingerprints();
|
||||||
|
}
|
|
@ -0,0 +1,314 @@
|
||||||
|
// 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.exception.BadDataException;
|
||||||
|
import pgp.certificate_store.exception.BadNameException;
|
||||||
|
import pgp.certificate_store.exception.NotAStoreException;
|
||||||
|
import pgp.certificate_store.Certificate;
|
||||||
|
import pgp.certificate_store.CertificateReaderBackend;
|
||||||
|
import pgp.certificate_store.MergeCallback;
|
||||||
|
|
||||||
|
public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDirectory {
|
||||||
|
|
||||||
|
private final FilenameResolver resolver;
|
||||||
|
private final LockingMechanism writeLock;
|
||||||
|
private final CertificateReaderBackend certificateReaderBackend;
|
||||||
|
|
||||||
|
public SharedPGPCertificateDirectoryImpl(BackendProvider backendProvider)
|
||||||
|
throws NotAStoreException {
|
||||||
|
this(backendProvider.provideCertificateReaderBackend());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SharedPGPCertificateDirectoryImpl(CertificateReaderBackend certificateReaderBackend)
|
||||||
|
throws NotAStoreException {
|
||||||
|
this(
|
||||||
|
BaseDirectoryProvider.getDefaultBaseDir(),
|
||||||
|
certificateReaderBackend);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SharedPGPCertificateDirectoryImpl(File baseDirectory, CertificateReaderBackend certificateReaderBackend)
|
||||||
|
throws NotAStoreException {
|
||||||
|
this(
|
||||||
|
certificateReaderBackend,
|
||||||
|
new FilenameResolver(baseDirectory),
|
||||||
|
FileLockingMechanism.defaultDirectoryFileLock(baseDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SharedPGPCertificateDirectoryImpl(
|
||||||
|
CertificateReaderBackend certificateReaderBackend,
|
||||||
|
FilenameResolver filenameResolver,
|
||||||
|
LockingMechanism writeLock)
|
||||||
|
throws NotAStoreException {
|
||||||
|
this.certificateReaderBackend = certificateReaderBackend;
|
||||||
|
this.resolver = filenameResolver;
|
||||||
|
this.writeLock = writeLock;
|
||||||
|
|
||||||
|
File baseDirectory = resolver.getBaseDirectory();
|
||||||
|
if (!baseDirectory.exists()) {
|
||||||
|
if (!baseDirectory.mkdirs()) {
|
||||||
|
throw new NotAStoreException("Cannot create base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "'");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (baseDirectory.isFile()) {
|
||||||
|
throw new NotAStoreException("Base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "' appears to be a file.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LockingMechanism getLock() {
|
||||||
|
return writeLock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getByFingerprint(String fingerprint)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
File certFile = resolver.getCertFileByFingerprint(fingerprint);
|
||||||
|
if (!certFile.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInputStream fileIn = new FileInputStream(certFile);
|
||||||
|
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
|
||||||
|
Certificate certificate = certificateReaderBackend.readCertificate(bufferedIn);
|
||||||
|
|
||||||
|
if (!certificate.getFingerprint().equals(fingerprint)) {
|
||||||
|
// TODO: Figure out more suitable exception
|
||||||
|
throw new BadDataException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getBySpecialName(String specialName)
|
||||||
|
throws IOException, BadNameException {
|
||||||
|
File certFile = resolver.getCertFileBySpecialName(specialName);
|
||||||
|
if (!certFile.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInputStream fileIn = new FileInputStream(certFile);
|
||||||
|
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
|
||||||
|
Certificate certificate = certificateReaderBackend.readCertificate(bufferedIn);
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getByFingerprintIfChanged(String fingerprint, String tag)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
Certificate certificate = getByFingerprint(fingerprint);
|
||||||
|
if (certificate.getTag().equals(tag)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate getBySpecialNameIfChanged(String specialName, String tag)
|
||||||
|
throws IOException, BadNameException {
|
||||||
|
Certificate certificate = getBySpecialName(specialName);
|
||||||
|
if (certificate.getTag().equals(tag)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate insert(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, InterruptedException {
|
||||||
|
writeLock.lockDirectory();
|
||||||
|
|
||||||
|
Certificate certificate = _insert(data, merge);
|
||||||
|
|
||||||
|
writeLock.releaseDirectory();
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate tryInsert(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException {
|
||||||
|
if (!writeLock.tryLockDirectory()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Certificate certificate = _insert(data, merge);
|
||||||
|
|
||||||
|
writeLock.releaseDirectory();
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Certificate _insert(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException {
|
||||||
|
Certificate newCertificate = certificateReaderBackend.readCertificate(data);
|
||||||
|
Certificate existingCertificate;
|
||||||
|
File certFile;
|
||||||
|
try {
|
||||||
|
existingCertificate = getByFingerprint(newCertificate.getFingerprint());
|
||||||
|
certFile = resolver.getCertFileByFingerprint(newCertificate.getFingerprint());
|
||||||
|
} catch (BadNameException e) {
|
||||||
|
throw new BadDataException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingCertificate != null && !existingCertificate.getTag().equals(newCertificate.getTag())) {
|
||||||
|
newCertificate = merge.merge(newCertificate, existingCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeCertificate(newCertificate, certFile);
|
||||||
|
|
||||||
|
return newCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeCertificate(Certificate certificate, File certFile)
|
||||||
|
throws IOException {
|
||||||
|
certFile.getParentFile().mkdirs();
|
||||||
|
if (!certFile.exists() && !certFile.createNewFile()) {
|
||||||
|
throw new IOException("Could not create cert file " + certFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream certIn = certificate.getInputStream();
|
||||||
|
FileOutputStream fileOut = new FileOutputStream(certFile);
|
||||||
|
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int read;
|
||||||
|
while ((read = certIn.read(buffer)) != -1) {
|
||||||
|
fileOut.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
certIn.close();
|
||||||
|
fileOut.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate insertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadNameException, BadDataException, InterruptedException {
|
||||||
|
writeLock.lockDirectory();
|
||||||
|
|
||||||
|
Certificate certificate = _insertSpecial(specialName, data, merge);
|
||||||
|
|
||||||
|
writeLock.releaseDirectory();
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate tryInsertWithSpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
if (!writeLock.tryLockDirectory()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Certificate certificate = _insertSpecial(specialName, data, merge);
|
||||||
|
|
||||||
|
writeLock.releaseDirectory();
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Certificate _insertSpecial(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadNameException, BadDataException {
|
||||||
|
Certificate newCertificate = certificateReaderBackend.readCertificate(data);
|
||||||
|
Certificate existingCertificate = getBySpecialName(specialName);
|
||||||
|
File certFile = resolver.getCertFileBySpecialName(specialName);
|
||||||
|
|
||||||
|
if (existingCertificate != null && !existingCertificate.getTag().equals(newCertificate.getTag())) {
|
||||||
|
newCertificate = merge.merge(newCertificate, existingCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeCertificate(newCertificate, certFile);
|
||||||
|
|
||||||
|
return newCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<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 = certificateReaderBackend.readCertificate(new FileInputStream(certFile));
|
||||||
|
if (!(subdirectory.getName() + certFile.getName()).equals(certificate.getFingerprint())) {
|
||||||
|
throw new BadDataException();
|
||||||
|
}
|
||||||
|
return certificate;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError("File got deleted.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return !certificateQueue.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Certificate next() {
|
||||||
|
try {
|
||||||
|
return certificateQueue.remove(0).get();
|
||||||
|
} catch (BadDataException e) {
|
||||||
|
throw new AssertionError("Could not retrieve item: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract static class Lazy<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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,14 +9,11 @@ import java.util.Map;
|
||||||
|
|
||||||
public class SpecialNames {
|
public class SpecialNames {
|
||||||
|
|
||||||
public static final String TRUST_ROOT = "trust-root";
|
|
||||||
|
|
||||||
// Map to allow for potentially upper- and lowercase variants of the same special name
|
|
||||||
private static final Map<String, String> SPECIAL_NAMES = new HashMap<>();
|
private static final Map<String, String> SPECIAL_NAMES = new HashMap<>();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
SPECIAL_NAMES.put("TRUST-ROOT", TRUST_ROOT); // TODO: Remove
|
SPECIAL_NAMES.put("TRUST-ROOT", "trust-root"); // TODO: Remove
|
||||||
SPECIAL_NAMES.put(TRUST_ROOT, TRUST_ROOT);
|
SPECIAL_NAMES.put("trust-root", "trust-root");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String lookupSpecialName(String specialName) {
|
public static String lookupSpecialName(String specialName) {
|
||||||
|
|
|
@ -1,126 +0,0 @@
|
||||||
// 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;
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,448 +0,0 @@
|
||||||
// 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.NoSuchElementException;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of {@link PGPCertificateDirectory.Backend} which stores certificates in a directory structure.
|
|
||||||
*
|
|
||||||
* @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/#name-implementation">Shared PGP Certificate Directory</a>
|
|
||||||
*/
|
|
||||||
public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirectory.Backend {
|
|
||||||
|
|
||||||
private abstract static class Lazy<E> {
|
|
||||||
abstract E get() throws BadDataException;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Locking mechanism which uses a lock file to synchronize write-access to the store.
|
|
||||||
*/
|
|
||||||
private static class FileLockingMechanism implements PGPCertificateDirectory.LockingMechanism {
|
|
||||||
|
|
||||||
private final File lockFile;
|
|
||||||
private RandomAccessFile randomAccessFile;
|
|
||||||
private FileLock fileLock;
|
|
||||||
|
|
||||||
FileLockingMechanism(File lockFile) {
|
|
||||||
this.lockFile = lockFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static FileLockingMechanism defaultDirectoryFileLock(File baseDirectory) {
|
|
||||||
return new FileLockingMechanism(new File(baseDirectory, "writelock"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void lockDirectory() throws IOException, InterruptedException {
|
|
||||||
if (randomAccessFile != null) {
|
|
||||||
// we own the lock already. Let's wait...
|
|
||||||
this.wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
randomAccessFile = new RandomAccessFile(lockFile, "rw");
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
lockFile.createNewFile();
|
|
||||||
randomAccessFile = new RandomAccessFile(lockFile, "rw");
|
|
||||||
}
|
|
||||||
|
|
||||||
fileLock = randomAccessFile.getChannel().lock();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized boolean tryLockDirectory() throws IOException {
|
|
||||||
if (randomAccessFile != null) {
|
|
||||||
// We already locked the directory for another write operation.
|
|
||||||
// We fail, since we have not yet released the lock from the other operation.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
randomAccessFile = new RandomAccessFile(lockFile, "rw");
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
lockFile.createNewFile();
|
|
||||||
randomAccessFile = new RandomAccessFile(lockFile, "rw");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
fileLock = randomAccessFile.getChannel().tryLock();
|
|
||||||
if (fileLock == null) {
|
|
||||||
// try-lock failed, file is locked by another process.
|
|
||||||
randomAccessFile.close();
|
|
||||||
randomAccessFile = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (OverlappingFileLockException e) {
|
|
||||||
// Some other object is holding the lock.
|
|
||||||
randomAccessFile.close();
|
|
||||||
randomAccessFile = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLocked() {
|
|
||||||
return randomAccessFile != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void releaseDirectory() throws IOException {
|
|
||||||
// unlock file
|
|
||||||
if (fileLock != null) {
|
|
||||||
fileLock.release();
|
|
||||||
fileLock = null;
|
|
||||||
}
|
|
||||||
// close file
|
|
||||||
if (randomAccessFile != null) {
|
|
||||||
randomAccessFile.close();
|
|
||||||
randomAccessFile = null;
|
|
||||||
}
|
|
||||||
// delete file
|
|
||||||
if (lockFile.exists()) {
|
|
||||||
lockFile.delete();
|
|
||||||
}
|
|
||||||
// notify waiters
|
|
||||||
this.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final File baseDirectory;
|
|
||||||
private final PGPCertificateDirectory.LockingMechanism lock;
|
|
||||||
private final FilenameResolver resolver;
|
|
||||||
private final KeyMaterialReaderBackend reader;
|
|
||||||
|
|
||||||
public FileBasedCertificateDirectoryBackend(File baseDirectory, KeyMaterialReaderBackend reader) throws NotAStoreException {
|
|
||||||
this.baseDirectory = baseDirectory;
|
|
||||||
this.resolver = new FilenameResolver(baseDirectory);
|
|
||||||
|
|
||||||
if (!baseDirectory.exists()) {
|
|
||||||
if (!baseDirectory.mkdirs()) {
|
|
||||||
throw new NotAStoreException("Cannot create base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "'");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (baseDirectory.isFile()) {
|
|
||||||
throw new NotAStoreException("Base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "' appears to be a file.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.lock = FileLockingMechanism.defaultDirectoryFileLock(baseDirectory);
|
|
||||||
this.reader = reader;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PGPCertificateDirectory.LockingMechanism getLock() {
|
|
||||||
return lock;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Certificate readByFingerprint(String fingerprint) throws BadNameException, IOException, BadDataException {
|
|
||||||
File certFile = resolver.getCertFileByFingerprint(fingerprint);
|
|
||||||
if (!certFile.exists()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
long tag = getTagForFingerprint(fingerprint);
|
|
||||||
|
|
||||||
FileInputStream fileIn = new FileInputStream(certFile);
|
|
||||||
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
|
|
||||||
|
|
||||||
Certificate certificate = reader.read(bufferedIn, tag).asCertificate();
|
|
||||||
if (!certificate.getFingerprint().equals(fingerprint)) {
|
|
||||||
// TODO: Figure out more suitable exception
|
|
||||||
throw new BadDataException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return certificate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeyMaterial readBySpecialName(String specialName) throws BadNameException, IOException, BadDataException {
|
|
||||||
File certFile = resolver.getCertFileBySpecialName(specialName);
|
|
||||||
if (!certFile.exists()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
long tag = getTagForSpecialName(specialName);
|
|
||||||
|
|
||||||
FileInputStream fileIn = new FileInputStream(certFile);
|
|
||||||
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
|
|
||||||
KeyMaterial keyMaterial = reader.read(bufferedIn, tag);
|
|
||||||
|
|
||||||
return keyMaterial;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterator<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}$");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (subdirectories == null) {
|
|
||||||
subdirectories = new File[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (File subdirectory : subdirectories) {
|
|
||||||
File[] files = subdirectory.listFiles(new FileFilter() {
|
|
||||||
@Override
|
|
||||||
public boolean accept(File file) {
|
|
||||||
return file.isFile() && file.getName().matches("^[a-f0-9]{38}$");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (files == null) {
|
|
||||||
files = new File[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (File certFile : files) {
|
|
||||||
certificateQueue.add(new Lazy<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 NoSuchElementException();
|
|
||||||
}
|
|
||||||
Path path = file.toPath();
|
|
||||||
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
|
|
||||||
|
|
||||||
// On UNIX file systems, for example, fileKey() will return the device ID and inode
|
|
||||||
int fileId = attrs.fileKey().hashCode();
|
|
||||||
long lastMod = attrs.lastModifiedTime().toMillis();
|
|
||||||
|
|
||||||
return lastMod + (11L * fileId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private long writeToFile(InputStream inputStream, File certFile)
|
|
||||||
throws IOException {
|
|
||||||
certFile.getParentFile().mkdirs();
|
|
||||||
if (!certFile.exists() && !certFile.createNewFile()) {
|
|
||||||
throw new IOException("Could not create cert file " + certFile.getAbsolutePath());
|
|
||||||
}
|
|
||||||
|
|
||||||
FileOutputStream fileOut = new FileOutputStream(certFile);
|
|
||||||
|
|
||||||
byte[] buffer = new byte[4096];
|
|
||||||
int read;
|
|
||||||
while ((read = inputStream.read(buffer)) != -1) {
|
|
||||||
fileOut.write(buffer, 0, read);
|
|
||||||
}
|
|
||||||
|
|
||||||
inputStream.close();
|
|
||||||
fileOut.close();
|
|
||||||
return getTag(certFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class to resolve file names from certificate fingerprints / special names.
|
|
||||||
*/
|
|
||||||
public static class FilenameResolver {
|
|
||||||
|
|
||||||
private final File baseDirectory;
|
|
||||||
// matches v4 and v5 fingerprints (v4 = 40 hex chars, v5 = 64 hex chars)
|
|
||||||
private final Pattern openPgpFingerprint = Pattern.compile("^[a-f0-9]{40}([a-f0-9]{24})?$");
|
|
||||||
|
|
||||||
public FilenameResolver(File baseDirectory) {
|
|
||||||
this.baseDirectory = baseDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public File getBaseDirectory() {
|
|
||||||
return baseDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the file location for the certificate addressed by the given
|
|
||||||
* lowercase hexadecimal OpenPGP fingerprint.
|
|
||||||
*
|
|
||||||
* @param fingerprint fingerprint
|
|
||||||
* @return absolute certificate file location
|
|
||||||
*
|
|
||||||
* @throws BadNameException if the given fingerprint string is not a fingerprint
|
|
||||||
*/
|
|
||||||
public File getCertFileByFingerprint(String fingerprint) throws BadNameException {
|
|
||||||
if (!isFingerprint(fingerprint)) {
|
|
||||||
throw new BadNameException();
|
|
||||||
}
|
|
||||||
|
|
||||||
// is fingerprint
|
|
||||||
File subdirectory = new File(getBaseDirectory(), fingerprint.substring(0, 2));
|
|
||||||
File file = new File(subdirectory, fingerprint.substring(2));
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the file location for the certification addressed using the given special name.
|
|
||||||
* For known special names, see {@link SpecialNames}.
|
|
||||||
*
|
|
||||||
* @param specialName special name (e.g. "trust-root")
|
|
||||||
* @return absolute certificate file location
|
|
||||||
*
|
|
||||||
* @throws BadNameException in case the given special name is not known
|
|
||||||
*/
|
|
||||||
public File getCertFileBySpecialName(String specialName)
|
|
||||||
throws BadNameException {
|
|
||||||
if (!isSpecialName(specialName)) {
|
|
||||||
throw new BadNameException(String.format("%s is not a known special name", specialName));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new File(getBaseDirectory(), specialName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isFingerprint(String fingerprint) {
|
|
||||||
return openPgpFingerprint.matcher(fingerprint).matches();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isSpecialName(String specialName) {
|
|
||||||
return SpecialNames.lookupSpecialName(specialName) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,170 +0,0 @@
|
||||||
// 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;
|
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link PGPCertificateDirectory.Backend} which stores key material in-memory.
|
|
||||||
* It uses object locking with {@link #wait()} and {@link #notify()} to synchronize write-access.
|
|
||||||
*/
|
|
||||||
public class InMemoryCertificateDirectoryBackend implements PGPCertificateDirectory.Backend {
|
|
||||||
|
|
||||||
protected static class ObjectLockingMechanism implements PGPCertificateDirectory.LockingMechanism {
|
|
||||||
|
|
||||||
private boolean locked = false;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void lockDirectory() throws InterruptedException {
|
|
||||||
if (isLocked()) {
|
|
||||||
wait();
|
|
||||||
}
|
|
||||||
locked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized boolean tryLockDirectory() {
|
|
||||||
if (isLocked()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
locked = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized boolean isLocked() {
|
|
||||||
return locked;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void releaseDirectory() {
|
|
||||||
locked = false;
|
|
||||||
notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private final Map<String, Certificate> certificateFingerprintMap = new HashMap<>();
|
|
||||||
private final Map<String, KeyMaterial> keyMaterialSpecialNameMap = new HashMap<>();
|
|
||||||
private final PGPCertificateDirectory.LockingMechanism lock = new ObjectLockingMechanism();
|
|
||||||
private final KeyMaterialReaderBackend reader;
|
|
||||||
private final AtomicLong nonce = new AtomicLong(1);
|
|
||||||
|
|
||||||
public InMemoryCertificateDirectoryBackend(KeyMaterialReaderBackend reader) {
|
|
||||||
this.reader = reader;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PGPCertificateDirectory.LockingMechanism getLock() {
|
|
||||||
return lock;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Certificate readByFingerprint(String fingerprint) {
|
|
||||||
return certificateFingerprintMap.get(fingerprint);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeyMaterial readBySpecialName(String specialName) throws BadNameException {
|
|
||||||
if (SpecialNames.lookupSpecialName(specialName) == null) {
|
|
||||||
throw new BadNameException("Invalid special name " + specialName);
|
|
||||||
}
|
|
||||||
return keyMaterialSpecialNameMap.get(specialName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterator<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, newTag());
|
|
||||||
} else {
|
|
||||||
merged = new Certificate((Certificate) merged, newTag());
|
|
||||||
}
|
|
||||||
keyMaterialSpecialNameMap.put(SpecialNames.TRUST_ROOT, merged);
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Certificate doInsert(InputStream data, KeyMaterialMerger merge)
|
|
||||||
throws IOException, BadDataException {
|
|
||||||
KeyMaterial update = reader.read(data, null);
|
|
||||||
Certificate existing = readByFingerprint(update.getFingerprint());
|
|
||||||
Certificate merged = merge.merge(update, existing).asCertificate();
|
|
||||||
merged = new Certificate(merged, newTag());
|
|
||||||
certificateFingerprintMap.put(update.getFingerprint(), merged);
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
|
|
||||||
throws IOException, BadDataException, BadNameException {
|
|
||||||
KeyMaterial keyMaterial = reader.read(data, null);
|
|
||||||
KeyMaterial existing = readBySpecialName(specialName);
|
|
||||||
KeyMaterial merged = merge.merge(keyMaterial, existing);
|
|
||||||
if (merged instanceof Key) {
|
|
||||||
merged = new Key((Key) merged, newTag());
|
|
||||||
} else {
|
|
||||||
merged = new Certificate((Certificate) merged, newTag());
|
|
||||||
}
|
|
||||||
keyMaterialSpecialNameMap.put(specialName, merged);
|
|
||||||
return merged.asCertificate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Long getTagForFingerprint(String fingerprint) throws BadNameException, IOException {
|
|
||||||
Certificate certificate = certificateFingerprintMap.get(fingerprint);
|
|
||||||
if (certificate == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return certificate.getTag();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Long getTagForSpecialName(String specialName) throws BadNameException, IOException {
|
|
||||||
if (SpecialNames.lookupSpecialName(specialName) == null) {
|
|
||||||
throw new BadNameException("Invalid special name " + specialName);
|
|
||||||
}
|
|
||||||
KeyMaterial tagged = keyMaterialSpecialNameMap.get(specialName);
|
|
||||||
if (tagged == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return tagged.getTag();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long newTag() {
|
|
||||||
return System.currentTimeMillis() + nonce.incrementAndGet();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Storage Backends.
|
|
||||||
*/
|
|
||||||
package pgp.cert_d.backend;
|
|
|
@ -1,17 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package pgp.cert_d.subkey_lookup;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory class to instantiate {@link InMemorySubkeyLookup} objects.
|
|
||||||
*/
|
|
||||||
public class InMemorySubkeyLookupFactory implements SubkeyLookupFactory {
|
|
||||||
@Override
|
|
||||||
public SubkeyLookup createFileBasedInstance(File baseDirectory) {
|
|
||||||
return new InMemorySubkeyLookup();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package pgp.cert_d.subkey_lookup;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory class to instantiate different {@link SubkeyLookup} implementations.
|
|
||||||
*/
|
|
||||||
public interface SubkeyLookupFactory {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new {@link SubkeyLookup} instance that lives in the given baseDirectory.
|
|
||||||
*
|
|
||||||
* @param baseDirectory base directory
|
|
||||||
* @return subkey lookup
|
|
||||||
*/
|
|
||||||
SubkeyLookup createFileBasedInstance(File baseDirectory);
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subkey Lookup functionality.
|
|
||||||
*/
|
|
||||||
package pgp.cert_d.subkey_lookup;
|
|
|
@ -18,7 +18,7 @@ public class BaseDirectoryProviderTest {
|
||||||
public void testGetDefaultBaseDir_Linux() {
|
public void testGetDefaultBaseDir_Linux() {
|
||||||
assumeTrue(System.getProperty("os.name").equalsIgnoreCase("linux"));
|
assumeTrue(System.getProperty("os.name").equalsIgnoreCase("linux"));
|
||||||
File baseDir = BaseDirectoryProvider.getDefaultBaseDirForOS("linux");
|
File baseDir = BaseDirectoryProvider.getDefaultBaseDirForOS("linux");
|
||||||
assertTrue(baseDir.getAbsolutePath().endsWith("pgp.cert.d"));
|
assertTrue(baseDir.getAbsolutePath().endsWith("/.local/share/pgp.cert.d"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -1,131 +0,0 @@
|
||||||
// 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 pgp.cert_d.backend.FileBasedCertificateDirectoryBackend;
|
|
||||||
import pgp.cert_d.dummy.TestKeyMaterialMerger;
|
|
||||||
import pgp.cert_d.dummy.TestKeyMaterialReaderBackend;
|
|
||||||
import pgp.cert_d.subkey_lookup.InMemorySubkeyLookup;
|
|
||||||
import pgp.cert_d.subkey_lookup.SubkeyLookup;
|
|
||||||
import pgp.certificate_store.certificate.Certificate;
|
|
||||||
import pgp.certificate_store.certificate.KeyMaterialMerger;
|
|
||||||
import pgp.certificate_store.certificate.KeyMaterialReaderBackend;
|
|
||||||
import pgp.certificate_store.exception.BadDataException;
|
|
||||||
import pgp.certificate_store.exception.BadNameException;
|
|
||||||
import pgp.certificate_store.exception.NotAStoreException;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.junit.jupiter.api.Assumptions.assumeFalse;
|
|
||||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
|
||||||
|
|
||||||
public class FileBasedPGPCertificateDirectoryTest {
|
|
||||||
|
|
||||||
private static final KeyMaterialMerger merger = new TestKeyMaterialMerger();
|
|
||||||
@Test
|
|
||||||
public void testFileBasedCertificateDirectoryTagChangesWhenFileChanges()
|
|
||||||
throws IOException, NotAStoreException, BadDataException, InterruptedException, BadNameException {
|
|
||||||
File tempDir = Files.createTempDirectory("file-based-changes").toFile();
|
|
||||||
tempDir.deleteOnExit();
|
|
||||||
PGPCertificateDirectory directory = PGPCertificateDirectories.fileBasedCertificateDirectory(
|
|
||||||
new TestKeyMaterialReaderBackend(),
|
|
||||||
tempDir,
|
|
||||||
new InMemorySubkeyLookup());
|
|
||||||
FileBasedCertificateDirectoryBackend.FilenameResolver resolver =
|
|
||||||
new FileBasedCertificateDirectoryBackend.FilenameResolver(tempDir);
|
|
||||||
|
|
||||||
// Insert certificate
|
|
||||||
Certificate certificate = directory.insert(TestKeys.getCedricCert(), merger);
|
|
||||||
Long tag = certificate.getTag();
|
|
||||||
assertNotNull(tag);
|
|
||||||
assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag));
|
|
||||||
|
|
||||||
Long oldTag = tag;
|
|
||||||
|
|
||||||
Thread.sleep(10);
|
|
||||||
// Change the file on disk directly, this invalidates the tag due to changed modification date
|
|
||||||
File certFile = resolver.getCertFileByFingerprint(certificate.getFingerprint());
|
|
||||||
FileOutputStream fileOut = new FileOutputStream(certFile);
|
|
||||||
Streams.pipeAll(certificate.getInputStream(), fileOut);
|
|
||||||
fileOut.write("\n".getBytes());
|
|
||||||
fileOut.close();
|
|
||||||
|
|
||||||
// Old invalidated tag indicates a change, so the modified certificate is returned
|
|
||||||
certificate = directory.getByFingerprintIfChanged(certificate.getFingerprint(), oldTag);
|
|
||||||
assertNotNull(certificate);
|
|
||||||
|
|
||||||
// new tag is valid
|
|
||||||
tag = certificate.getTag();
|
|
||||||
assertNotEquals(oldTag, tag);
|
|
||||||
assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void fileBasedStoreInWriteProtectedAreaThrows() {
|
|
||||||
File root = new File("/");
|
|
||||||
assumeTrue(root.exists(), "This test only runs on unix-like systems");
|
|
||||||
File baseDirectory = new File(root, "pgp.cert.d");
|
|
||||||
assumeFalse(baseDirectory.mkdirs(), "This test assumes that we cannot create dirs in /");
|
|
||||||
|
|
||||||
KeyMaterialReaderBackend reader = new TestKeyMaterialReaderBackend();
|
|
||||||
SubkeyLookup lookup = new InMemorySubkeyLookup();
|
|
||||||
assertThrows(NotAStoreException.class, () -> PGPCertificateDirectories.fileBasedCertificateDirectory(
|
|
||||||
reader, baseDirectory, lookup));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void fileBasedStoreOnFileThrows()
|
|
||||||
throws IOException {
|
|
||||||
File tempDir = Files.createTempDirectory("containsAFile").toFile();
|
|
||||||
tempDir.deleteOnExit();
|
|
||||||
File baseDir = new File(tempDir, "pgp.cert.d");
|
|
||||||
baseDir.createNewFile(); // this is a file, not a dir
|
|
||||||
|
|
||||||
KeyMaterialReaderBackend reader = new TestKeyMaterialReaderBackend();
|
|
||||||
SubkeyLookup lookup = new InMemorySubkeyLookup();
|
|
||||||
assertThrows(NotAStoreException.class, () -> PGPCertificateDirectories.fileBasedCertificateDirectory(
|
|
||||||
reader, baseDir, lookup));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCertificateStoredUnderWrongFingerprintThrowsBadData()
|
|
||||||
throws IOException, NotAStoreException, BadDataException, InterruptedException, BadNameException {
|
|
||||||
File tempDir = Files.createTempDirectory("wrong-fingerprint").toFile();
|
|
||||||
tempDir.deleteOnExit();
|
|
||||||
PGPCertificateDirectory directory = PGPCertificateDirectories.fileBasedCertificateDirectory(
|
|
||||||
new TestKeyMaterialReaderBackend(),
|
|
||||||
tempDir,
|
|
||||||
new InMemorySubkeyLookup());
|
|
||||||
FileBasedCertificateDirectoryBackend.FilenameResolver resolver =
|
|
||||||
new FileBasedCertificateDirectoryBackend.FilenameResolver(tempDir);
|
|
||||||
|
|
||||||
// Insert Rons certificate
|
|
||||||
directory.insert(TestKeys.getRonCert(), merger);
|
|
||||||
|
|
||||||
// Copy Rons cert to Cedrics cert file
|
|
||||||
File ronCert = resolver.getCertFileByFingerprint(TestKeys.RON_FP);
|
|
||||||
FileInputStream inputStream = new FileInputStream(ronCert);
|
|
||||||
File cedricCert = resolver.getCertFileByFingerprint(TestKeys.CEDRIC_FP);
|
|
||||||
cedricCert.getParentFile().mkdirs();
|
|
||||||
cedricCert.createNewFile();
|
|
||||||
FileOutputStream outputStream = new FileOutputStream(cedricCert);
|
|
||||||
Streams.pipeAll(inputStream, outputStream);
|
|
||||||
inputStream.close();
|
|
||||||
outputStream.close();
|
|
||||||
|
|
||||||
// Reading cedrics cert will fail, as it has Rons fingerprint
|
|
||||||
assertThrows(BadDataException.class, () -> directory.getByFingerprint(TestKeys.CEDRIC_FP));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,7 +6,6 @@ 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;
|
||||||
|
@ -19,13 +18,13 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
public class FilenameResolverTest {
|
public class FilenameResolverTest {
|
||||||
|
|
||||||
private File baseDir;
|
private File baseDir;
|
||||||
private FileBasedCertificateDirectoryBackend.FilenameResolver resolver;
|
private 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 FileBasedCertificateDirectoryBackend.FilenameResolver(baseDir);
|
resolver = new FilenameResolver(baseDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -1,311 +0,0 @@
|
||||||
// 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.Named;
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
|
||||||
import pgp.cert_d.dummy.TestKeyMaterialMerger;
|
|
||||||
import pgp.cert_d.dummy.TestKeyMaterialReaderBackend;
|
|
||||||
import pgp.cert_d.subkey_lookup.InMemorySubkeyLookup;
|
|
||||||
import pgp.certificate_store.certificate.Certificate;
|
|
||||||
import pgp.certificate_store.certificate.Key;
|
|
||||||
import pgp.certificate_store.certificate.KeyMaterial;
|
|
||||||
import pgp.certificate_store.certificate.KeyMaterialMerger;
|
|
||||||
import pgp.certificate_store.exception.BadDataException;
|
|
||||||
import pgp.certificate_store.exception.BadNameException;
|
|
||||||
import pgp.certificate_store.exception.NotAStoreException;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.NoSuchElementException;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static pgp.cert_d.TestKeys.CEDRIC_FP;
|
|
||||||
import static pgp.cert_d.TestKeys.HARRY_FP;
|
|
||||||
import static pgp.cert_d.TestKeys.RON_FP;
|
|
||||||
|
|
||||||
public class PGPCertificateDirectoryTest {
|
|
||||||
|
|
||||||
|
|
||||||
private static final KeyMaterialMerger merger = new TestKeyMaterialMerger();
|
|
||||||
|
|
||||||
private static Stream<Arguments> provideTestSubjects()
|
|
||||||
throws IOException, NotAStoreException {
|
|
||||||
PGPCertificateDirectory inMemory = PGPCertificateDirectories.inMemoryCertificateDirectory(
|
|
||||||
new TestKeyMaterialReaderBackend());
|
|
||||||
|
|
||||||
File tempDir = Files.createTempDirectory("pgp-cert-d-test").toFile();
|
|
||||||
tempDir.deleteOnExit();
|
|
||||||
PGPCertificateDirectory fileBased = PGPCertificateDirectories.fileBasedCertificateDirectory(
|
|
||||||
new TestKeyMaterialReaderBackend(),
|
|
||||||
tempDir,
|
|
||||||
new InMemorySubkeyLookup());
|
|
||||||
|
|
||||||
return Stream.of(
|
|
||||||
Arguments.of(Named.of("InMemoryCertificateDirectory", inMemory)),
|
|
||||||
Arguments.of(Named.of("FileBasedCertificateDirectory", fileBased)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void getNonExistentCertByFingerprintThrowsNoSuchElementException(PGPCertificateDirectory directory) {
|
|
||||||
assertThrows(NoSuchElementException.class, () ->
|
|
||||||
directory.getByFingerprint("0000000000000000000000000000000000000000"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void getNonExistentCertByFingerprintIfChangedThrowsNoSuchElementException(PGPCertificateDirectory directory) {
|
|
||||||
assertThrows(NoSuchElementException.class, () ->
|
|
||||||
directory.getByFingerprintIfChanged("0000000000000000000000000000000000000000", 12));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void getNonExistentCertBySpecialNameThrowsNoSuchElementException(PGPCertificateDirectory directory) {
|
|
||||||
assertThrows(NoSuchElementException.class, () ->
|
|
||||||
directory.getBySpecialName(SpecialNames.TRUST_ROOT));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void getNonExistentCertBySpecialNameIfChangedThrowsNoSuchElementException(PGPCertificateDirectory directory) {
|
|
||||||
assertThrows(NoSuchElementException.class, () ->
|
|
||||||
directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, 12));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void getNonExistentTrustRootThrowsNoSuchElementException(PGPCertificateDirectory directory) {
|
|
||||||
assertThrows(NoSuchElementException.class, () ->
|
|
||||||
directory.getTrustRoot());
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void getNonExistentTrustRootIfChangedThrowsNoSuchElementException(PGPCertificateDirectory directory) {
|
|
||||||
assertThrows(NoSuchElementException.class, () ->
|
|
||||||
directory.getTrustRootCertificateIfChanged(12));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void getNonExistentTrustRootCertificateThrowsNoSuchElementException(PGPCertificateDirectory directory) {
|
|
||||||
assertThrows(NoSuchElementException.class, () ->
|
|
||||||
directory.getTrustRootCertificate());
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void lockDirectoryAndTryInsertWillFail(PGPCertificateDirectory directory)
|
|
||||||
throws IOException, InterruptedException, BadDataException {
|
|
||||||
// Manually lock the dir
|
|
||||||
assertFalse(directory.backend.getLock().isLocked());
|
|
||||||
directory.backend.getLock().lockDirectory();
|
|
||||||
assertTrue(directory.backend.getLock().isLocked());
|
|
||||||
assertFalse(directory.backend.getLock().tryLockDirectory());
|
|
||||||
|
|
||||||
Certificate inserted = directory.tryInsert(TestKeys.getCedricCert(), merger);
|
|
||||||
assertNull(inserted);
|
|
||||||
|
|
||||||
directory.backend.getLock().releaseDirectory();
|
|
||||||
inserted = directory.tryInsert(TestKeys.getCedricCert(), merger);
|
|
||||||
assertNotNull(inserted);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void lockDirectoryAndTryInsertTrustRootWillFail(PGPCertificateDirectory directory)
|
|
||||||
throws IOException, InterruptedException, BadDataException {
|
|
||||||
// Manually lock the dir
|
|
||||||
assertFalse(directory.backend.getLock().isLocked());
|
|
||||||
directory.backend.getLock().lockDirectory();
|
|
||||||
assertTrue(directory.backend.getLock().isLocked());
|
|
||||||
|
|
||||||
KeyMaterial inserted = directory.tryInsertTrustRoot(TestKeys.getHarryKey(), merger);
|
|
||||||
assertNull(inserted);
|
|
||||||
|
|
||||||
directory.backend.getLock().releaseDirectory();
|
|
||||||
inserted = directory.tryInsertTrustRoot(TestKeys.getHarryKey(), merger);
|
|
||||||
assertNotNull(inserted);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void lockDirectoryAndTryInsertWithSpecialNameWillFail(PGPCertificateDirectory directory)
|
|
||||||
throws IOException, InterruptedException, BadDataException, BadNameException {
|
|
||||||
// Manually lock the dir
|
|
||||||
assertFalse(directory.backend.getLock().isLocked());
|
|
||||||
directory.backend.getLock().lockDirectory();
|
|
||||||
assertTrue(directory.backend.getLock().isLocked());
|
|
||||||
|
|
||||||
Certificate inserted = directory.tryInsertWithSpecialName(SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger);
|
|
||||||
assertNull(inserted);
|
|
||||||
|
|
||||||
directory.backend.getLock().releaseDirectory();
|
|
||||||
inserted = directory.tryInsertWithSpecialName(SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger);
|
|
||||||
assertNotNull(inserted);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void getByInvalidNameFails(PGPCertificateDirectory directory) {
|
|
||||||
assertThrows(BadNameException.class, () -> directory.getBySpecialName("invalid"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void testInsertAndGetSingleCert(PGPCertificateDirectory directory)
|
|
||||||
throws BadDataException, IOException, InterruptedException, BadNameException {
|
|
||||||
assertThrows(NoSuchElementException.class, () -> directory.getByFingerprint(CEDRIC_FP), "Empty directory MUST NOT contain certificate");
|
|
||||||
|
|
||||||
Certificate certificate = directory.insert(TestKeys.getCedricCert(), merger);
|
|
||||||
assertEquals(CEDRIC_FP, certificate.getFingerprint(), "Fingerprint of inserted cert MUST match");
|
|
||||||
|
|
||||||
Certificate get = directory.getByFingerprint(CEDRIC_FP);
|
|
||||||
assertEquals(CEDRIC_FP, get.getFingerprint(), "Fingerprint of retrieved cert MUST match");
|
|
||||||
|
|
||||||
byte[] expected = TestKeys.CEDRIC_CERT.getBytes(Charset.forName("UTF8"));
|
|
||||||
ByteArrayOutputStream actual = new ByteArrayOutputStream();
|
|
||||||
Streams.pipeAll(get.getInputStream(), actual);
|
|
||||||
assertArrayEquals(expected, actual.toByteArray(), "InputStream of cert MUST match what we gave in");
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void testInsertAndGetTrustRootAndCert(PGPCertificateDirectory directory)
|
|
||||||
throws BadDataException, IOException, InterruptedException {
|
|
||||||
assertThrows(NoSuchElementException.class, () -> directory.getTrustRoot());
|
|
||||||
|
|
||||||
KeyMaterial trustRootMaterial = directory.insertTrustRoot(
|
|
||||||
TestKeys.getHarryKey(), merger);
|
|
||||||
assertNotNull(trustRootMaterial);
|
|
||||||
assertTrue(trustRootMaterial instanceof Key);
|
|
||||||
assertEquals(HARRY_FP, trustRootMaterial.getFingerprint());
|
|
||||||
|
|
||||||
Key trustRoot = (Key) directory.getTrustRoot();
|
|
||||||
assertEquals(HARRY_FP, trustRoot.getFingerprint());
|
|
||||||
Certificate trustRootCert = directory.getTrustRootCertificate();
|
|
||||||
assertEquals(HARRY_FP, trustRootCert.getFingerprint());
|
|
||||||
|
|
||||||
directory.tryInsert(TestKeys.getRonCert(), merger);
|
|
||||||
directory.insert(TestKeys.getCedricCert(), merger);
|
|
||||||
|
|
||||||
Set<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(
|
|
||||||
TestKeys.getHarryKey(), merger);
|
|
||||||
|
|
||||||
assertNotNull(trustRootMaterial.getTag());
|
|
||||||
Long tag = trustRootMaterial.getTag();
|
|
||||||
assertNull(directory.getTrustRootCertificateIfChanged(tag));
|
|
||||||
assertNotNull(directory.getTrustRootCertificateIfChanged(tag + 1));
|
|
||||||
|
|
||||||
Long oldTag = tag;
|
|
||||||
Thread.sleep(10);
|
|
||||||
// "update" key
|
|
||||||
trustRootMaterial = directory.insertTrustRoot(
|
|
||||||
TestKeys.getHarryKey(), merger);
|
|
||||||
tag = trustRootMaterial.getTag();
|
|
||||||
|
|
||||||
assertNotEquals(oldTag, tag);
|
|
||||||
assertNotNull(directory.getTrustRootCertificateIfChanged(oldTag));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void testGetBySpecialNameIfChanged(PGPCertificateDirectory directory)
|
|
||||||
throws BadDataException, IOException, InterruptedException, BadNameException {
|
|
||||||
KeyMaterial specialName = directory.insertWithSpecialName(SpecialNames.TRUST_ROOT,
|
|
||||||
TestKeys.getHarryKey(), merger);
|
|
||||||
|
|
||||||
assertNotNull(specialName.getTag());
|
|
||||||
Long tag = specialName.getTag();
|
|
||||||
assertNull(directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag));
|
|
||||||
assertNotNull(directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void testGetByFingerprintIfChanged(PGPCertificateDirectory directory)
|
|
||||||
throws BadDataException, IOException, InterruptedException, BadNameException {
|
|
||||||
Certificate certificate = directory.insert(TestKeys.getCedricCert(), merger);
|
|
||||||
Long tag = certificate.getTag();
|
|
||||||
assertNotNull(tag);
|
|
||||||
|
|
||||||
assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag));
|
|
||||||
assertNotNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void testOverwriteTrustRoot(PGPCertificateDirectory directory)
|
|
||||||
throws BadDataException, IOException, InterruptedException {
|
|
||||||
directory.insertTrustRoot(TestKeys.getHarryKey(), merger);
|
|
||||||
assertEquals(HARRY_FP, directory.getTrustRoot().getFingerprint());
|
|
||||||
assertTrue(directory.getTrustRoot() instanceof Key);
|
|
||||||
|
|
||||||
directory.insertTrustRoot(TestKeys.getCedricCert(), merger);
|
|
||||||
assertEquals(CEDRIC_FP, directory.getTrustRoot().getFingerprint());
|
|
||||||
assertTrue(directory.getTrustRoot() instanceof Certificate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void testOverwriteSpecialName(PGPCertificateDirectory directory)
|
|
||||||
throws BadDataException, IOException, InterruptedException, BadNameException {
|
|
||||||
directory.insertWithSpecialName(SpecialNames.TRUST_ROOT, TestKeys.getRonCert(), merger);
|
|
||||||
KeyMaterial bySpecialName = directory.getBySpecialName(SpecialNames.TRUST_ROOT);
|
|
||||||
assertEquals(RON_FP, bySpecialName.getFingerprint());
|
|
||||||
|
|
||||||
directory.insertWithSpecialName(SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger);
|
|
||||||
assertEquals(HARRY_FP, directory.getBySpecialName(SpecialNames.TRUST_ROOT).getFingerprint());
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@MethodSource("provideTestSubjects")
|
|
||||||
public void testOverwriteByFingerprint(PGPCertificateDirectory directory)
|
|
||||||
throws BadDataException, IOException, InterruptedException, BadNameException {
|
|
||||||
directory.insert(TestKeys.getRonCert(), merger);
|
|
||||||
Certificate extracted = directory.getByFingerprint(RON_FP);
|
|
||||||
assertEquals(RON_FP, extracted.getFingerprint());
|
|
||||||
|
|
||||||
directory.insert(TestKeys.getRonCert(), merger);
|
|
||||||
extracted = directory.getByFingerprint(RON_FP);
|
|
||||||
assertEquals(RON_FP, extracted.getFingerprint());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package pgp.cert_d;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import pgp.cert_d.backend.InMemoryCertificateDirectoryBackend;
|
|
||||||
import pgp.cert_d.dummy.TestKeyMaterialMerger;
|
|
||||||
import pgp.cert_d.dummy.TestKeyMaterialReaderBackend;
|
|
||||||
import pgp.cert_d.subkey_lookup.InMemorySubkeyLookupFactory;
|
|
||||||
import pgp.certificate_store.certificate.Certificate;
|
|
||||||
import pgp.certificate_store.exception.BadDataException;
|
|
||||||
import pgp.certificate_store.exception.BadNameException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.NoSuchElementException;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
|
|
||||||
public class PGPCertificateStoreAdapterTest {
|
|
||||||
|
|
||||||
private PGPCertificateDirectory directory;
|
|
||||||
private PGPCertificateStoreAdapter adapter;
|
|
||||||
|
|
||||||
private static final TestKeyMaterialMerger merger = new TestKeyMaterialMerger();
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
public void setup() {
|
|
||||||
directory = new PGPCertificateDirectory(
|
|
||||||
new InMemoryCertificateDirectoryBackend(new TestKeyMaterialReaderBackend()),
|
|
||||||
new InMemorySubkeyLookupFactory().createFileBasedInstance(null));
|
|
||||||
adapter = new PGPCertificateStoreAdapter(directory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testBadFPWithInvalidCharsYieldsBadNameException() {
|
|
||||||
assertThrows(BadNameException.class, () -> adapter.getCertificate("XYZ78fd17f207fdf62f7976c4e9d98917ad84522"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testBadFPWithTooFewCharsYieldsBadNameException() {
|
|
||||||
assertThrows(BadNameException.class, () -> adapter.getCertificate("23578fd17f207fdf62f7976c4e9d98917ad"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testInsertGetCertificate()
|
|
||||||
throws BadDataException, IOException, InterruptedException, BadNameException {
|
|
||||||
assertThrows(NoSuchElementException.class, () -> adapter.getCertificate(TestKeys.CEDRIC_FP));
|
|
||||||
assertFalse(adapter.getCertificates().hasNext());
|
|
||||||
|
|
||||||
Certificate certificate = adapter.insertCertificate(TestKeys.getCedricCert(), merger);
|
|
||||||
assertNotNull(certificate);
|
|
||||||
assertEquals(TestKeys.CEDRIC_FP, certificate.getFingerprint());
|
|
||||||
|
|
||||||
certificate = adapter.getCertificate(TestKeys.CEDRIC_FP.toUpperCase());
|
|
||||||
assertEquals(TestKeys.CEDRIC_FP, certificate.getFingerprint(), "We can also fetch with uppercase fps");
|
|
||||||
|
|
||||||
Iterator<String> fingerprints = adapter.getFingerprints();
|
|
||||||
assertEquals(TestKeys.CEDRIC_FP, fingerprints.next());
|
|
||||||
assertFalse(fingerprints.hasNext());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testInsertGetTrustRoot()
|
|
||||||
throws BadDataException, BadNameException, IOException, InterruptedException {
|
|
||||||
assertThrows(NoSuchElementException.class, () -> adapter.getCertificate(SpecialNames.TRUST_ROOT));
|
|
||||||
|
|
||||||
Certificate certificate = adapter.insertCertificateBySpecialName(
|
|
||||||
SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger);
|
|
||||||
assertNotNull(certificate);
|
|
||||||
assertEquals(TestKeys.HARRY_FP, certificate.getFingerprint());
|
|
||||||
|
|
||||||
assertFalse(adapter.getCertificates().hasNext(), "Special-named certs are not returned by getCertificates()");
|
|
||||||
assertFalse(adapter.getFingerprints().hasNext());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetCertificateIfChanged()
|
|
||||||
throws BadDataException, IOException, InterruptedException, BadNameException {
|
|
||||||
Certificate certificate = adapter.insertCertificate(TestKeys.getRonCert(), merger);
|
|
||||||
Long tag = certificate.getTag();
|
|
||||||
|
|
||||||
assertNull(adapter.getCertificateIfChanged(TestKeys.RON_FP, tag), "Cert has not changed, tag is still valid");
|
|
||||||
assertNotNull(adapter.getCertificateIfChanged(TestKeys.RON_FP, tag + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetTrustRootIfChanged()
|
|
||||||
throws BadDataException, BadNameException, IOException, InterruptedException {
|
|
||||||
Certificate certificate = adapter.insertCertificateBySpecialName(SpecialNames.TRUST_ROOT, TestKeys.getHarryKey(), merger);
|
|
||||||
Long tag = certificate.getTag();
|
|
||||||
|
|
||||||
assertNull(adapter.getCertificateIfChanged(SpecialNames.TRUST_ROOT, tag));
|
|
||||||
assertNotNull(adapter.getCertificateIfChanged(SpecialNames.TRUST_ROOT, tag * 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetCertificateBySubkeyId()
|
|
||||||
throws BadDataException, IOException, InterruptedException {
|
|
||||||
// Insert some certs
|
|
||||||
adapter.insertCertificate(TestKeys.getCedricCert(), merger);
|
|
||||||
adapter.insertCertificate(TestKeys.getHarryKey(), merger);
|
|
||||||
// Now insert Ron
|
|
||||||
Certificate certificate = adapter.insertCertificate(TestKeys.getRonCert(), merger);
|
|
||||||
List<Long> subkeyIds = certificate.getSubkeyIds();
|
|
||||||
|
|
||||||
assertFalse(adapter.getCertificatesBySubkeyId(0).hasNext());
|
|
||||||
|
|
||||||
for (Long subkeyId : subkeyIds) {
|
|
||||||
Iterator<Certificate> certsWithSubkey = adapter.getCertificatesBySubkeyId(subkeyId);
|
|
||||||
Certificate certWithSubkey = certsWithSubkey.next();
|
|
||||||
assertFalse(certsWithSubkey.hasNext());
|
|
||||||
|
|
||||||
assertEquals(TestKeys.RON_FP, certWithSubkey.getFingerprint());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -24,8 +24,7 @@ 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.cert_d.subkey_lookup.InMemorySubkeyLookup;
|
import pgp.certificate_store.SubkeyLookup;
|
||||||
import pgp.cert_d.subkey_lookup.SubkeyLookup;
|
|
||||||
|
|
||||||
public class SubkeyLookupTest {
|
public class SubkeyLookupTest {
|
||||||
|
|
||||||
|
|
|
@ -1,130 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package pgp.cert_d;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
|
|
||||||
public class TestKeys {
|
|
||||||
@SuppressWarnings("CharsetObjectCanBeUsed")
|
|
||||||
private static final Charset UTF8 = Charset.forName("UTF8");
|
|
||||||
|
|
||||||
public static final String HARRY_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
|
|
||||||
"Comment: 2357 8FD1 7F20 7FDF 62F7 976C 4E9D 9891 7AD8 4522\n" +
|
|
||||||
"Comment: Harry Potter <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";
|
|
||||||
public static final String HARRY_FP = "23578fd17f207fdf62f7976c4e9d98917ad84522";
|
|
||||||
|
|
||||||
public static final String RON_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
|
|
||||||
"Comment: B798 AF18 6BFE 4C19 902D 4950 5647 F001 37EF 4C41\n" +
|
|
||||||
"Comment: Ron Weasley <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";
|
|
||||||
public static final String RON_FP = "b798af186bfe4c19902d49505647f00137ef4c41";
|
|
||||||
|
|
||||||
public static final String CEDRIC_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" +
|
|
||||||
"Comment: 5E75 BF20 646B C1A9 8D3B 1BC2 FE9C D472 987C 4021\n" +
|
|
||||||
"Comment: Cedric Diggory <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";
|
|
||||||
public static final String CEDRIC_FP = "5e75bf20646bc1a98d3b1bc2fe9cd472987c4021";
|
|
||||||
|
|
||||||
public static InputStream getHarryKey() {
|
|
||||||
return new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static InputStream getRonCert() {
|
|
||||||
return new ByteArrayInputStream(RON_CERT.getBytes(UTF8));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static InputStream getCedricCert() {
|
|
||||||
return new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,8 +6,5 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
# PGP Certificate Store Definitions
|
# PGP Certificate Store Definitions
|
||||||
|
|
||||||
[![javadoc](https://javadoc.io/badge2/org.pgpainless/pgp-certificate-store/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgp-certificate-store)
|
|
||||||
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgp-certificate-store)](https://search.maven.org/artifact/org.pgpainless/pgp-certificate-store)
|
|
||||||
|
|
||||||
This module contains API definitions for an OpenPGP certificate store.
|
This module contains API definitions for an OpenPGP certificate store.
|
||||||
A certificate store is used to store public key certificates only.
|
A certificate store is used to store public key certificates only.
|
||||||
|
|
|
@ -15,10 +15,9 @@ repositories {
|
||||||
apply plugin: 'ru.vyarus.animalsniffer'
|
apply plugin: 'ru.vyarus.animalsniffer'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// animal sniffer for ensuring Android API compatibility
|
// animal sniffer
|
||||||
signature "net.sf.androidscents.signature:android-api-level-${animalsnifferSignatureVersion}@signature"
|
signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:2.3.3_r2@signature"
|
||||||
|
|
||||||
// JUnit for testing
|
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
public abstract class Certificate {
|
||||||
|
/**
|
||||||
|
* Return the fingerprint of the certificate as 40 lowercase hex characters.
|
||||||
|
* TODO: Allow OpenPGP V5 fingerprints
|
||||||
|
*
|
||||||
|
* @return fingerprint
|
||||||
|
*/
|
||||||
|
public abstract String getFingerprint();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an {@link InputStream} of the binary representation of the certificate.
|
||||||
|
*
|
||||||
|
* @return input stream
|
||||||
|
*/
|
||||||
|
public abstract InputStream getInputStream() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a tag of the certificate.
|
||||||
|
* The tag is a checksum calculated over the binary representation of the certificate.
|
||||||
|
*
|
||||||
|
* @return tag
|
||||||
|
*/
|
||||||
|
public abstract String getTag() throws IOException;
|
||||||
|
|
||||||
|
public abstract Set<Long> getSubkeyIds() throws IOException;
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
// 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
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
Certificate getCertificateIfChanged(String identifier, String tag)
|
||||||
|
throws IOException, BadNameException, BadDataException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a certificate into the store.
|
||||||
|
* If an instance of the certificate is already present in the store, the given {@link MergeCallback} will be
|
||||||
|
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
|
||||||
|
* will be stored in the store and returned.
|
||||||
|
*
|
||||||
|
* This method will block until a write-lock on the store can be acquired. If you cannot afford blocking,
|
||||||
|
* consider to use {@link #tryInsertCertificate(InputStream, MergeCallback)} instead.
|
||||||
|
*
|
||||||
|
* @param data input stream containing the new certificate instance
|
||||||
|
* @param merge callback for merging with an existing certificate instance
|
||||||
|
* @return merged certificate
|
||||||
|
*
|
||||||
|
* @throws IOException in case of an IO-error
|
||||||
|
* @throws InterruptedException in case the inserting thread gets interrupted
|
||||||
|
*/
|
||||||
|
Certificate insertCertificate(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, InterruptedException, BadDataException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a certificate into the store.
|
||||||
|
* If an instance of the certificate is already present in the store, the given {@link MergeCallback} will be
|
||||||
|
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
|
||||||
|
* will be stored in the store and returned.
|
||||||
|
*
|
||||||
|
* This method will not block. Instead, if the store is already write-locked, this method will simply return null
|
||||||
|
* without any writing.
|
||||||
|
* However, if the write-lock is available, this method will acquire the lock, write to the store, release the lock
|
||||||
|
* and return the written certificate.
|
||||||
|
*
|
||||||
|
* @param data input stream containing the new certificate instance
|
||||||
|
* @param merge callback for merging with an existing certificate instance
|
||||||
|
* @return merged certificate or null if the store cannot be locked
|
||||||
|
*
|
||||||
|
* @throws IOException in case of an IO-error
|
||||||
|
*/
|
||||||
|
Certificate tryInsertCertificate(InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a certificate into the store.
|
||||||
|
* The certificate will be stored under the given special name instead of its fingerprint.
|
||||||
|
*
|
||||||
|
* If an instance of the certificate is already present under the special name in the store, the given {@link MergeCallback} will be
|
||||||
|
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
|
||||||
|
* will be stored in the store and returned.
|
||||||
|
*
|
||||||
|
* This method will block until a write-lock on the store can be acquired. If you cannot afford blocking,
|
||||||
|
* consider to use {@link #tryInsertCertificateBySpecialName(String, InputStream, MergeCallback)} instead.
|
||||||
|
*
|
||||||
|
* @param 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
|
||||||
|
*/
|
||||||
|
Certificate insertCertificateBySpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, InterruptedException, BadDataException, BadNameException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a certificate into the store.
|
||||||
|
* The certificate will be stored under the given special name instead of its fingerprint.
|
||||||
|
*
|
||||||
|
* If an instance of the certificate is already present under the special name in the store, the given {@link MergeCallback} will be
|
||||||
|
* used to merge both the existing and the new instance of the {@link Certificate}. The resulting merged certificate
|
||||||
|
* will be stored in the store and returned.
|
||||||
|
*
|
||||||
|
* This method will not block. Instead, if the store is already write-locked, this method will simply return null
|
||||||
|
* without any writing.
|
||||||
|
* However, if the write-lock is available, this method will acquire the lock, write to the store, release the lock
|
||||||
|
* and return the written certificate.
|
||||||
|
*
|
||||||
|
* @param 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
|
||||||
|
*/
|
||||||
|
Certificate tryInsertCertificateBySpecialName(String specialName, InputStream data, MergeCallback merge)
|
||||||
|
throws IOException, BadDataException, BadNameException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an {@link Iterator} containing all certificates in the store.
|
||||||
|
* The iterator will contain both certificates addressed by special names and by fingerprints.
|
||||||
|
*
|
||||||
|
* @return certificates
|
||||||
|
*/
|
||||||
|
Iterator<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();
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface definition for a class that can read {@link Certificate Certificates} from binary
|
||||||
|
* {@link InputStream InputStreams}.
|
||||||
|
*/
|
||||||
|
public interface CertificateReaderBackend {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a {@link Certificate} from the given {@link InputStream}.
|
||||||
|
*
|
||||||
|
* @param inputStream input stream containing the binary representation of the certificate.
|
||||||
|
* @return certificate object
|
||||||
|
*
|
||||||
|
* @throws IOException in case of an IO error
|
||||||
|
*/
|
||||||
|
Certificate readCertificate(InputStream inputStream) throws IOException;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.certificate_store;
|
||||||
|
|
||||||
|
public interface CertificateStore extends CertificateDirectory, SubkeyLookup {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
// 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 MergeCallback {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge the given certificate data with the existing certificate and return the result.
|
||||||
|
*
|
||||||
|
* If no existing certificate is found (i.e. existing is null), this method returns the unmodified data.
|
||||||
|
*
|
||||||
|
* @param data certificate
|
||||||
|
* @param existing optional already existing copy of the certificate
|
||||||
|
* @return merged certificate
|
||||||
|
*/
|
||||||
|
Certificate merge(Certificate data, Certificate existing) throws IOException;
|
||||||
|
|
||||||
|
}
|
|
@ -1,123 +0,0 @@
|
||||||
// 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;
|
|
||||||
import java.util.NoSuchElementException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for an OpenPGP certificate (public key) store.
|
|
||||||
*/
|
|
||||||
public interface PGPCertificateStore {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the certificate that matches the given identifier.
|
|
||||||
*
|
|
||||||
* @param identifier identifier for a certificate.
|
|
||||||
* @return certificate or null
|
|
||||||
*
|
|
||||||
* @throws IOException in case of an IO-error
|
|
||||||
* @throws BadNameException if the identifier is invalid
|
|
||||||
* @throws BadDataException if the certificate file contains invalid data
|
|
||||||
* @throws NoSuchElementException if no such certificate is found
|
|
||||||
*/
|
|
||||||
Certificate getCertificate(String identifier)
|
|
||||||
throws IOException, BadNameException, BadDataException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the certificate that matches the given identifier, but only if it has been changed.
|
|
||||||
* Whether it has been changed is determined by calculating the tag in the directory
|
|
||||||
* (e.g. by looking at the inode and last modification date) and comparing the result with the tag provided by
|
|
||||||
* the caller.
|
|
||||||
*
|
|
||||||
* @param identifier certificate identifier
|
|
||||||
* @param tag tag by the caller
|
|
||||||
* @return certificate if it has been changed, null otherwise
|
|
||||||
*
|
|
||||||
* @throws IOException in case of an IO-error
|
|
||||||
* @throws BadNameException if the identifier is invalid
|
|
||||||
* @throws BadDataException if the certificate file contains invalid data
|
|
||||||
* @throws NoSuchElementException if no such certificate is found
|
|
||||||
*/
|
|
||||||
Certificate getCertificateIfChanged(String identifier, Long tag)
|
|
||||||
throws IOException, BadNameException, BadDataException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an {@link Iterator} over all certificates in the store that contain a subkey with the given
|
|
||||||
* subkey id.
|
|
||||||
* @param subkeyId id of the subkey
|
|
||||||
* @return iterator
|
|
||||||
*
|
|
||||||
* @throws IOException in case of an IO error
|
|
||||||
* @throws BadDataException if any of the certificate files contains invalid data
|
|
||||||
*/
|
|
||||||
Iterator<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();
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package pgp.cert_d.subkey_lookup;
|
package pgp.certificate_store;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -16,8 +16,6 @@ public interface SubkeyLookup {
|
||||||
*
|
*
|
||||||
* @param subkeyId subkey id
|
* @param subkeyId subkey id
|
||||||
* @return fingerprint of the certificate
|
* @return fingerprint of the certificate
|
||||||
*
|
|
||||||
* @throws IOException in case of an IO error
|
|
||||||
*/
|
*/
|
||||||
Set<String> getCertificateFingerprintsForSubkeyId(long subkeyId) throws IOException;
|
Set<String> getCertificateFingerprintsForSubkeyId(long subkeyId) throws IOException;
|
||||||
|
|
||||||
|
@ -27,7 +25,6 @@ public interface SubkeyLookup {
|
||||||
*
|
*
|
||||||
* @param certificate certificate fingerprint
|
* @param certificate certificate fingerprint
|
||||||
* @param subkeyIds subkey ids
|
* @param subkeyIds subkey ids
|
||||||
*
|
|
||||||
* @throws IOException in case of an IO error
|
* @throws IOException in case of an IO error
|
||||||
*/
|
*/
|
||||||
void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) throws IOException;
|
void storeCertificateSubkeyIds(String certificate, List<Long> subkeyIds) throws IOException;
|
|
@ -1,70 +0,0 @@
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
// 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();
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
// 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;
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package pgp.certificate_store.certificate;
|
|
||||||
|
|
||||||
import pgp.certificate_store.exception.BadDataException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public interface KeyMaterialReaderBackend {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read a {@link KeyMaterial} (either {@link Key} or {@link Certificate}) from the given {@link InputStream}.
|
|
||||||
*
|
|
||||||
* @param data input stream containing the binary representation of the key.
|
|
||||||
* @param tag tag for the key material. Might be null.
|
|
||||||
* @return key or certificate object
|
|
||||||
*
|
|
||||||
* @throws IOException in case of an IO error
|
|
||||||
* @throws BadDataException in case that the data stream does not contain a valid OpenPGP key/certificate
|
|
||||||
*/
|
|
||||||
KeyMaterial read(InputStream data, Long tag) throws IOException, BadDataException;
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
// 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;
|
|
|
@ -3,6 +3,8 @@
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exceptions.
|
* Exceptions defined by the Shared PGP Certificate Directory.
|
||||||
|
*
|
||||||
|
* @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/#name-failure-modes">Failure Modes</a>
|
||||||
*/
|
*/
|
||||||
package pgp.certificate_store.exception;
|
package pgp.certificate_store.exception;
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
rootProject.name = 'cert-d-java'
|
rootProject.name = 'cert-d-java'
|
||||||
|
|
||||||
include 'pgp-certificate-store',
|
include 'pgp-cert-d-java',
|
||||||
'pgp-cert-d-java',
|
'pgp-cert-d-java-jdbc-sqlite-lookup',
|
||||||
'pgp-cert-d-java-jdbc-sqlite-lookup'
|
'pgp-certificate-store'
|
||||||
|
|
||||||
|
|
|
@ -4,16 +4,9 @@
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
ext {
|
ext {
|
||||||
shortVersion = '0.2.3'
|
shortVersion = '0.1.0'
|
||||||
isSnapshot = true
|
isSnapshot = false
|
||||||
minAndroidSdk = 26
|
minAndroidSdk = 10
|
||||||
animalsnifferSignatureVersion = "$minAndroidSdk:8.0.0_r2"
|
|
||||||
javaSourceCompatibility = 1.8
|
javaSourceCompatibility = 1.8
|
||||||
bouncycastleVersion = '1.75'
|
|
||||||
bouncyPgVersion = "$bouncycastleVersion"
|
|
||||||
slf4jVersion = '1.7.36'
|
|
||||||
logbackVersion = '1.2.11'
|
|
||||||
junitVersion = '5.8.2'
|
|
||||||
sqliteJdbcVersion = '3.42.0.0'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue