1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2024-11-16 01:12:05 +01:00

First working prototype

This commit is contained in:
Paul Schaub 2022-01-24 16:47:52 +01:00
parent 7703cc263d
commit d086332677
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
23 changed files with 707 additions and 219 deletions

View file

@ -0,0 +1,92 @@
// 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;
}
@Override
public synchronized void lockDirectory() throws IOException, InterruptedException {
if (randomAccessFile != null) {
// we own the lock already. Let's wait...
this.wait();
}
try {
randomAccessFile = new RandomAccessFile(lockFile, "rw");
} catch (FileNotFoundException e) {
lockFile.createNewFile();
randomAccessFile = new RandomAccessFile(lockFile, "rw");
}
fileLock = randomAccessFile.getChannel().lock();
}
@Override
public synchronized boolean tryLockDirectory() throws IOException {
if (randomAccessFile != null) {
// We already locked the directory for another write operation.
// We fail, since we have not yet released the lock from the other operation.
return false;
}
try {
randomAccessFile = new RandomAccessFile(lockFile, "rw");
} catch (FileNotFoundException e) {
lockFile.createNewFile();
randomAccessFile = new RandomAccessFile(lockFile, "rw");
}
try {
fileLock = randomAccessFile.getChannel().tryLock();
if (fileLock == null) {
// try-lock failed, file is locked by another process.
randomAccessFile.close();
randomAccessFile = null;
return false;
}
} catch (OverlappingFileLockException e) {
// Some other object is holding the lock.
randomAccessFile.close();
randomAccessFile = null;
return false;
}
return true;
}
@Override
public synchronized void releaseDirectory() throws IOException {
// unlock file
if (fileLock != null) {
fileLock.release();
fileLock = null;
}
// close file
if (randomAccessFile != null) {
randomAccessFile.close();
randomAccessFile = null;
}
// delete file
if (lockFile.exists()) {
lockFile.delete();
}
// notify waiters
this.notify();
}
}

View file

@ -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;
}

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d; package pgp.cert_d;
import java.io.File; import java.io.File;

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d; package pgp.cert_d;
import java.io.IOException; import java.io.IOException;
@ -6,24 +10,28 @@ import java.util.Iterator;
import pgp.cert_d.exception.BadDataException; import pgp.cert_d.exception.BadDataException;
import pgp.cert_d.exception.BadNameException; import pgp.cert_d.exception.BadNameException;
import pgp.certificate_store.Item; import pgp.certificate_store.Certificate;
import pgp.certificate_store.MergeCallback; import pgp.certificate_store.MergeCallback;
public interface SharedPGPCertificateDirectory { public interface SharedPGPCertificateDirectory {
Item get(String identifier) throws IOException, BadNameException; Certificate get(String fingerprint) throws IOException, BadNameException;
Item getIfChanged(String identifier, String tag) throws IOException, BadNameException; Certificate get(SpecialName specialName) throws IOException, BadNameException;
Item insert(InputStream data, MergeCallback merge) throws IOException, BadDataException; Certificate getIfChanged(String fingerprint, String tag) throws IOException, BadNameException;
Item tryInsert(InputStream data, MergeCallback merge) throws IOException, BadDataException; Certificate getIfChanged(SpecialName specialName, String tag) throws IOException, BadNameException;
Item insertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException, BadDataException, BadNameException; Certificate insert(InputStream data, MergeCallback merge) throws IOException, BadDataException, InterruptedException;
Item tryInsertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException, BadDataException, BadNameException; Certificate tryInsert(InputStream data, MergeCallback merge) throws IOException, BadDataException;
Iterator<Item> items(); Certificate insertSpecial(SpecialName specialName, InputStream data, MergeCallback merge) throws IOException, BadDataException, BadNameException, InterruptedException;
Certificate tryInsertSpecial(SpecialName specialName, InputStream data, MergeCallback merge) throws IOException, BadDataException, BadNameException;
Iterator<Certificate> items();
Iterator<String> fingerprints(); Iterator<String> fingerprints();
} }

View file

@ -1,33 +1,44 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d; package pgp.cert_d;
import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;
import java.util.Iterator; import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.SynchronousQueue;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import pgp.cert_d.exception.BadDataException; import pgp.cert_d.exception.BadDataException;
import pgp.cert_d.exception.BadNameException; import pgp.cert_d.exception.BadNameException;
import pgp.cert_d.exception.NotAStoreException; import pgp.cert_d.exception.NotAStoreException;
import pgp.certificate_store.Item; import pgp.certificate_store.Certificate;
import pgp.certificate_store.MergeCallback; import pgp.certificate_store.MergeCallback;
import pgp.certificate_store.ParserBackend;
public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDirectory { public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDirectory {
private final File baseDirectory; private final File baseDirectory;
private final Pattern openPgpV4FingerprintPattern = Pattern.compile("^[a-f0-9]{40}$"); private final Pattern openPgpV4FingerprintPattern = Pattern.compile("^[a-f0-9]{40}$");
private final WriteLock writeLock; private final LockingMechanism writeLock;
private final ParserBackend parserBackend;
public SharedPGPCertificateDirectoryImpl() throws NotAStoreException { public SharedPGPCertificateDirectoryImpl(ParserBackend parserBackend)
this(OSUtil.getDefaultBaseDir()); throws NotAStoreException {
this(OSUtil.getDefaultBaseDir(), parserBackend);
} }
public SharedPGPCertificateDirectoryImpl(File baseDirectory) throws NotAStoreException { public SharedPGPCertificateDirectoryImpl(File baseDirectory, ParserBackend parserBackend)
throws NotAStoreException {
this.parserBackend = parserBackend;
this.baseDirectory = baseDirectory; this.baseDirectory = baseDirectory;
if (!baseDirectory.exists()) { if (!baseDirectory.exists()) {
if (!baseDirectory.mkdirs()) { if (!baseDirectory.mkdirs()) {
@ -38,167 +49,271 @@ public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDi
throw new NotAStoreException("Base directory '" + getBaseDirectory().getAbsolutePath() + "' appears to be a file."); throw new NotAStoreException("Base directory '" + getBaseDirectory().getAbsolutePath() + "' appears to be a file.");
} }
} }
writeLock = new WriteLock(new File(getBaseDirectory(), "writelock")); writeLock = new FileLockingMechanism(new File(getBaseDirectory(), "writelock"));
} }
public File getBaseDirectory() { public File getBaseDirectory() {
return baseDirectory; return baseDirectory;
} }
private File getCertFile(String identifier) throws BadNameException { private File getCertFile(String fingerprint) throws BadNameException {
SpecialName specialName = SpecialName.fromString(identifier); if (!isFingerprint(fingerprint)) {
if (specialName != null) { throw new BadNameException();
// is special name }
return new File(getBaseDirectory(), specialName.getValue());
} else { // is fingerprint
if (!isFingerprint(identifier)) { File subdirectory = new File(getBaseDirectory(), fingerprint.substring(0, 2));
throw new BadNameException(); File file = new File(subdirectory, fingerprint.substring(2));
return file;
}
private File getCertFile(SpecialName specialName) {
return new File(getBaseDirectory(), specialName.getValue());
}
private boolean isFingerprint(String fingerprint) {
return openPgpV4FingerprintPattern.matcher(fingerprint).matches();
}
@Override
public Certificate get(String fingerprint) throws IOException, BadNameException {
File certFile = getCertFile(fingerprint);
if (!certFile.exists()) {
return null;
}
FileInputStream fileIn = new FileInputStream(certFile);
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
Certificate certificate = parserBackend.readCertificate(bufferedIn);
if (!certificate.getFingerprint().equals(fingerprint)) {
// TODO: Figure out more suitable exception
throw new BadNameException();
}
return certificate;
}
@Override
public Certificate get(SpecialName specialName) throws IOException {
File certFile = getCertFile(specialName);
if (!certFile.exists()) {
return null;
}
FileInputStream fileIn = new FileInputStream(certFile);
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
Certificate certificate = parserBackend.readCertificate(bufferedIn);
return certificate;
}
@Override
public Certificate getIfChanged(String fingerprint, String tag) throws IOException, BadNameException {
Certificate certificate = get(fingerprint);
if (certificate.getTag().equals(tag)) {
return null;
}
return certificate;
}
@Override
public Certificate getIfChanged(SpecialName specialName, String tag) throws IOException {
Certificate certificate = get(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 = parserBackend.readCertificate(data);
Certificate existingCertificate;
File certFile;
try {
existingCertificate = get(newCertificate.getFingerprint());
certFile = getCertFile(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 insertSpecial(SpecialName 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 tryInsertSpecial(SpecialName 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(SpecialName specialName, InputStream data, MergeCallback merge) throws IOException, BadNameException, BadDataException {
Certificate newCertificate = parserBackend.readCertificate(data);
Certificate existingCertificate = get(specialName);
File certFile = getCertFile(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 Queue<Lazy<Certificate>> certificateQueue = new SynchronousQueue<>();
// Constructor... wtf.
{
for (SpecialName specialName : SpecialName.values()) {
File certFile = getCertFile(specialName);
if (certFile.exists()) {
certificateQueue.add(
new Lazy<Certificate>() {
@Override
Certificate get() {
try {
return parserBackend.readCertificate(new FileInputStream(certFile));
} catch (IOException e) {
throw new AssertionError("File got deleted.");
}
}
});
}
}
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 = parserBackend.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.");
}
}
});
}
}
} }
// is fingerprint @Override
File subdirectory = new File(getBaseDirectory(), identifier.substring(0, 2)); public boolean hasNext() {
File file = new File(subdirectory, identifier.substring(2)); return !certificateQueue.isEmpty();
return file; }
}
@Override
public Certificate next() {
try {
return certificateQueue.poll().get();
} catch (BadDataException e) {
throw new AssertionError("Could not retrieve item: " + e.getMessage());
}
}
};
} }
private boolean isFingerprint(String identifier) { private abstract static class Lazy<E> {
return openPgpV4FingerprintPattern.matcher(identifier).matches(); abstract E get() throws BadDataException;
}
@Override
public Item get(String identifier) throws IOException, BadNameException {
File certFile = getCertFile(identifier);
if (certFile.exists()) {
return new Item(identifier, "TAG", new FileInputStream(certFile));
}
return null;
}
@Override
public Item getIfChanged(String identifier, String tag) throws IOException, BadNameException {
return null;
}
@Override
public Item insert(InputStream data, MergeCallback merge) throws IOException, BadDataException {
writeLock.lock();
Item item = _insert(data, merge);
writeLock.release();
return item;
}
@Override
public Item tryInsert(InputStream data, MergeCallback merge) throws IOException, BadDataException {
if (!writeLock.tryLock()) {
return null;
}
Item item = _insert(data, merge);
writeLock.release();
return item;
}
private Item _insert(InputStream data, MergeCallback merge) throws IOException, BadDataException {
return null;
}
@Override
public Item insertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException, BadNameException, BadDataException {
writeLock.lock();
Item item = _insertSpecial(specialName, data, merge);
writeLock.release();
return item;
}
@Override
public Item tryInsertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException, BadNameException, BadDataException {
if (!writeLock.tryLock()) {
return null;
}
Item item = _insertSpecial(specialName, data, merge);
writeLock.release();
return item;
}
private Item _insertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException, BadNameException, BadDataException {
return null;
}
@Override
public Iterator<Item> items() {
return null;
} }
@Override @Override
public Iterator<String> fingerprints() { public Iterator<String> fingerprints() {
return null; Iterator<Certificate> certificates = items();
} return new Iterator<String>() {
@Override
public static class WriteLock { public boolean hasNext() {
private final File lockFile; return certificates.hasNext();
private RandomAccessFile randomAccessFile;
private FileLock fileLock;
public WriteLock(File lockFile) {
this.lockFile = lockFile;
}
public synchronized void lock() throws IOException {
if (randomAccessFile != null) {
throw new IllegalStateException("File already locked.");
} }
try { @Override
randomAccessFile = new RandomAccessFile(lockFile, "rw"); public String next() {
} catch (FileNotFoundException e) { return certificates.next().getFingerprint();
lockFile.createNewFile();
randomAccessFile = new RandomAccessFile(lockFile, "rw");
} }
};
fileLock = randomAccessFile.getChannel().lock();
}
public synchronized boolean tryLock() throws IOException {
if (randomAccessFile != null) {
return false;
}
try {
randomAccessFile = new RandomAccessFile(lockFile, "rw");
} catch (FileNotFoundException e) {
lockFile.createNewFile();
randomAccessFile = new RandomAccessFile(lockFile, "rw");
}
fileLock = randomAccessFile.getChannel().tryLock();
if (fileLock == null) {
randomAccessFile.close();
randomAccessFile = null;
return false;
}
return true;
}
public synchronized void release() throws IOException {
if (lockFile.exists()) {
lockFile.delete();
}
if (fileLock != null) {
fileLock.release();
fileLock = null;
}
if (randomAccessFile != null) {
randomAccessFile.close();
randomAccessFile = null;
}
}
} }
} }

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d; package pgp.cert_d;
import java.util.HashMap; import java.util.HashMap;

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.exception; package pgp.cert_d.exception;
/** /**

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.exception; package pgp.cert_d.exception;
/** /**

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.cert_d.exception; package pgp.cert_d.exception;
/** /**

View file

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* 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.cert_d.exception;

View file

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* An implementation of the Shared PGP Certificate Directory for java.
*
* @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/">Shared PGP Certificate Directory</a>
*/
package pgp.cert_d;

View file

@ -0,0 +1,33 @@
// 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;
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;
}

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package pgp.certificate_store; package pgp.certificate_store;
import java.io.IOException; import java.io.IOException;
@ -6,19 +10,19 @@ import java.util.Iterator;
public interface CertificateStore { public interface CertificateStore {
Item get(String identifier) throws IOException; Certificate get(String identifier) throws IOException;
Item getIfChanged(String identifier, String tag) throws IOException; Certificate getIfChanged(String identifier, String tag) throws IOException;
Item insert(InputStream data, MergeCallback merge) throws IOException; Certificate insert(InputStream data, MergeCallback merge) throws IOException;
Item tryInsert(InputStream data, MergeCallback merge) throws IOException; Certificate tryInsert(InputStream data, MergeCallback merge) throws IOException;
Item insertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException; Certificate insertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException;
Item tryInsertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException; Certificate tryInsertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException;
Iterator<Item> items(); Iterator<Certificate> items();
Iterator<String> fingerprints(); Iterator<String> fingerprints();
} }

View file

@ -1,43 +0,0 @@
package pgp.certificate_store;
import java.io.InputStream;
public class Item {
private final String fingerprint;
private final String tag;
private final InputStream data;
public Item(String fingerprint, String tag, InputStream data) {
this.fingerprint = fingerprint;
this.tag = tag;
this.data = data;
}
/**
* Return the fingerprint of the certificate.
*
* @return certificate fingerprint
*/
public String getFingerprint() {
return fingerprint;
}
/**
* Return a tag used to check if the certificate was changed between retrievals.
*
* @return tag
*/
public String getTag() {
return tag;
}
/**
* Return an {@link InputStream} containing the certificate data.
*
* @return data
*/
public InputStream getData() {
return data;
}
}

View file

@ -1,7 +1,8 @@
package pgp.certificate_store; // SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
import java.io.InputStream; package pgp.certificate_store;
import java.io.OutputStream;
/** /**
* Merge a given certificate (update) with an existing certificate. * Merge a given certificate (update) with an existing certificate.
@ -11,12 +12,12 @@ public interface MergeCallback {
/** /**
* Merge the given certificate data with the existing certificate and return the result. * 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 binary representation of data. * If no existing certificate is found (i.e. existing is null), this method returns the unmodified data.
* *
* @param data input stream containing the certificate * @param data certificate
* @param existing optional input stream containing an already existing copy of the certificate * @param existing optional already existing copy of the certificate
* @return output stream containing the binary representation of the merged certificate * @return merged certificate
*/ */
OutputStream merge(InputStream data, InputStream existing); Certificate merge(Certificate data, Certificate existing);
} }

View file

@ -0,0 +1,14 @@
// 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;
public interface ParserBackend {
Certificate readCertificate(InputStream inputStream) throws IOException;
}

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Abstract definitions of an OpenPGP certificate store.
*/
package pgp.certificate_store;

View file

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
plugins {
id 'java-library'
}
group 'org.pgpainless'
repositories {
mavenCentral()
}
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
// Logging
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
api project(":pgpainless-core")
api project(":pgp-cert-d-java")
}
test {
useJUnitPlatform()
}

View file

@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.cert_d;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
import org.pgpainless.PGPainless;
import org.pgpainless.certificate_store.CertificateParser;
import org.pgpainless.key.OpenPgpFingerprint;
import pgp.cert_d.FileLockingMechanism;
import pgp.cert_d.LockingMechanism;
import pgp.cert_d.SharedPGPCertificateDirectory;
import pgp.cert_d.SharedPGPCertificateDirectoryImpl;
import pgp.cert_d.exception.BadDataException;
import pgp.cert_d.exception.BadNameException;
import pgp.cert_d.exception.NotAStoreException;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.MergeCallback;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
public class SharedPGPCertificateDirectoryTest {
Logger logger = LoggerFactory.getLogger(SharedPGPCertificateDirectoryTest.class);
SharedPGPCertificateDirectory directory;
private static MergeCallback dummyMerge = new MergeCallback() {
@Override
public Certificate merge(Certificate data, Certificate existing) {
return data;
}
};
@BeforeEach
public void beforeEach() throws IOException, NotAStoreException {
File tempDir = Files.createTempDirectory("pgp.cert.d-").toFile();
tempDir.deleteOnExit();
directory = new SharedPGPCertificateDirectoryImpl(tempDir, new CertificateParser());
}
@Test
public void simpleInsertGet() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException, BadDataException, InterruptedException, BadNameException {
logger.info(() -> "simpleInsertGet: " + ((SharedPGPCertificateDirectoryImpl) directory).getBaseDirectory().getAbsolutePath());
PGPSecretKeyRing key = PGPainless.generateKeyRing().modernKeyRing("Alice", null);
PGPPublicKeyRing cert = PGPainless.extractCertificate(key);
OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(cert);
ByteArrayInputStream certIn = new ByteArrayInputStream(cert.getEncoded());
// standard case: get() is null
assertNull(directory.get(fingerprint.toString().toLowerCase()));
// insert and check returned certs fingerprint
Certificate certificate = directory.insert(certIn, dummyMerge);
assertEquals(fingerprint.toString().toLowerCase(), certificate.getFingerprint());
// getIfChanged
assertNull(directory.getIfChanged(certificate.getFingerprint(), certificate.getTag()));
assertNotNull(directory.getIfChanged(certificate.getFingerprint(), "invalidTag"));
// tryInsert
certIn = new ByteArrayInputStream(cert.getEncoded());
assertNotNull(directory.tryInsert(certIn, dummyMerge));
}
@Test
public void tryInsertFailsWithLockedStore() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException, BadDataException, InterruptedException {
SharedPGPCertificateDirectoryImpl fileDirectory = (SharedPGPCertificateDirectoryImpl) directory;
logger.info(() -> "tryInsertFailsWithLockedStore: " + fileDirectory.getBaseDirectory().getAbsolutePath());
PGPSecretKeyRing key = PGPainless.generateKeyRing().modernKeyRing("Alice", null);
PGPPublicKeyRing cert = PGPainless.extractCertificate(key);
ByteArrayInputStream certIn = new ByteArrayInputStream(cert.getEncoded());
File lockFile = new File(fileDirectory.getBaseDirectory(), "writelock");
LockingMechanism lock = new FileLockingMechanism(lockFile);
lock.lockDirectory();
assertNull(directory.tryInsert(certIn, dummyMerge));
lock.releaseDirectory();
assertNotNull(directory.tryInsert(certIn, dummyMerge));
}
}

View file

@ -17,6 +17,7 @@ dependencies {
api "org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion" api "org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion"
api "org.bouncycastle:bcpg-jdk15on:$bouncyCastleVersion" api "org.bouncycastle:bcpg-jdk15on:$bouncyCastleVersion"
api project(":pgp-certificate-store")
// https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305
implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'

View file

@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.certificate_store;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.util.encoders.Base64;
import org.pgpainless.PGPainless;
import org.pgpainless.key.OpenPgpFingerprint;
import pgp.certificate_store.Certificate;
import pgp.certificate_store.ParserBackend;
public class CertificateParser implements ParserBackend {
@Override
public Certificate readCertificate(InputStream inputStream) throws IOException {
final PGPPublicKeyRing certificate = PGPainless.readKeyRing().publicKeyRing(inputStream);
return new Certificate() {
@Override
public String getFingerprint() {
return OpenPgpFingerprint.of(certificate).toString().toLowerCase();
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(certificate.getEncoded());
}
@Override
public String getTag() throws IOException {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("No MessageDigest for SHA-256 instantiated, although BC is on the classpath: " + e.getMessage());
}
digest.update(certificate.getEncoded());
return Base64.toBase64String(digest.digest());
}
};
}
}

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Implementations of the module pgp-certificate-store using pgpainless-core.
*/
package org.pgpainless.certificate_store;

View file

@ -7,6 +7,7 @@ rootProject.name = 'PGPainless'
include 'pgpainless-core', include 'pgpainless-core',
'pgpainless-sop', 'pgpainless-sop',
'pgpainless-cli', 'pgpainless-cli',
'pgpainless-cert-d',
'pgp-certificate-store', 'pgp-certificate-store',
'pgp-cert-d-java' 'pgp-cert-d-java'