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