diff --git a/pgp-cert-d-java/build.gradle b/pgp-cert-d-java/build.gradle new file mode 100644 index 00000000..87c70185 --- /dev/null +++ b/pgp-cert-d-java/build.gradle @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// 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(":pgp-certificate-store") +} + +test { + useJUnitPlatform() +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/storage/CertDStore.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/OSUtil.java similarity index 57% rename from pgpainless-core/src/main/java/org/pgpainless/key/storage/CertDStore.java rename to pgp-cert-d-java/src/main/java/pgp/cert_d/OSUtil.java index 64780c52..d2459697 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/storage/CertDStore.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/OSUtil.java @@ -1,41 +1,10 @@ -package org.pgpainless.key.storage; +package pgp.cert_d; import java.io.File; -public class CertDStore { +public class OSUtil { - private final File baseDirectory; - private static final String STORE_NAME = "pgp.cert.d"; - - public CertDStore() { - this(getDefaultBaseDir()); - } - - public CertDStore(File baseDirectory) { - this.baseDirectory = baseDirectory; - } - - public File fingerprintToPrefixDir(String fingerprint) { - String dirName = fingerprint.toLowerCase().substring(0, 2); - return new File(baseDirectory, dirName); - } - - public String fingerprintToCertFileName(String fingerprint) { - String certFileName = fingerprint.toLowerCase().substring(2); - return certFileName; - } - - public File fingerprintToCertFile(String fingerprint) { - File dir = fingerprintToPrefixDir(fingerprint); - File certFile = new File(dir, fingerprintToCertFileName(fingerprint)); - return certFile; - } - - public File getBaseDirectory() { - return baseDirectory; - } - - private static File getDefaultBaseDir() { + public static File getDefaultBaseDir() { // Check for environment variable String baseDirFromEnv = System.getenv("PGP_CERT_D"); if (baseDirFromEnv != null) { @@ -49,6 +18,7 @@ public class CertDStore { } public static File getDefaultBaseDirForOS(String osName, String separator) { + String STORE_NAME = "pgp.cert.d"; if (osName.contains("win")) { String appData = System.getenv("APPDATA"); String roaming = appData + separator + "Roaming"; diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectory.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectory.java new file mode 100644 index 00000000..7e5cfe71 --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectory.java @@ -0,0 +1,29 @@ +package pgp.cert_d; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; + +import pgp.cert_d.exception.BadDataException; +import pgp.cert_d.exception.BadNameException; +import pgp.certificate_store.Item; +import pgp.certificate_store.MergeCallback; + +public interface SharedPGPCertificateDirectory { + + Item get(String identifier) throws IOException, BadNameException; + + Item getIfChanged(String identifier, String tag) throws IOException, BadNameException; + + Item insert(InputStream data, MergeCallback merge) throws IOException, BadDataException; + + Item tryInsert(InputStream data, MergeCallback merge) throws IOException, BadDataException; + + Item insertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException, BadDataException, BadNameException; + + Item tryInsertSpecial(String specialName, InputStream data, MergeCallback merge) throws IOException, BadDataException, BadNameException; + + Iterator items(); + + Iterator fingerprints(); +} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectoryImpl.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectoryImpl.java new file mode 100644 index 00000000..1b5041f4 --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/SharedPGPCertificateDirectoryImpl.java @@ -0,0 +1,204 @@ +package pgp.cert_d; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.nio.channels.FileLock; +import java.util.Iterator; +import java.util.regex.Pattern; + +import pgp.cert_d.exception.BadDataException; +import pgp.cert_d.exception.BadNameException; +import pgp.cert_d.exception.NotAStoreException; +import pgp.certificate_store.Item; +import pgp.certificate_store.MergeCallback; + +public class SharedPGPCertificateDirectoryImpl implements SharedPGPCertificateDirectory { + + private final File baseDirectory; + private final Pattern openPgpV4FingerprintPattern = Pattern.compile("^[a-f0-9]{40}$"); + + private final WriteLock writeLock; + + public SharedPGPCertificateDirectoryImpl() throws NotAStoreException { + this(OSUtil.getDefaultBaseDir()); + } + + public SharedPGPCertificateDirectoryImpl(File baseDirectory) throws NotAStoreException { + this.baseDirectory = baseDirectory; + if (!baseDirectory.exists()) { + if (!baseDirectory.mkdirs()) { + throw new NotAStoreException("Cannot create base directory '" + getBaseDirectory().getAbsolutePath() + "'"); + } + } else { + if (baseDirectory.isFile()) { + throw new NotAStoreException("Base directory '" + getBaseDirectory().getAbsolutePath() + "' appears to be a file."); + } + } + writeLock = new WriteLock(new File(getBaseDirectory(), "writelock")); + } + + public File getBaseDirectory() { + return baseDirectory; + } + + private File getCertFile(String identifier) throws BadNameException { + SpecialName specialName = SpecialName.fromString(identifier); + if (specialName != null) { + // is special name + return new File(getBaseDirectory(), specialName.getValue()); + } else { + if (!isFingerprint(identifier)) { + throw new BadNameException(); + } + + // is fingerprint + File subdirectory = new File(getBaseDirectory(), identifier.substring(0, 2)); + File file = new File(subdirectory, identifier.substring(2)); + return file; + } + } + + private boolean isFingerprint(String identifier) { + return openPgpV4FingerprintPattern.matcher(identifier).matches(); + } + + @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 items() { + return null; + } + + @Override + public Iterator fingerprints() { + return null; + } + + public static class WriteLock { + private final File lockFile; + 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 { + randomAccessFile = new RandomAccessFile(lockFile, "rw"); + } catch (FileNotFoundException e) { + 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; + } + } + } +} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/SpecialName.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/SpecialName.java new file mode 100644 index 00000000..7b08269d --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/SpecialName.java @@ -0,0 +1,38 @@ +package pgp.cert_d; + +import java.util.HashMap; +import java.util.Map; + +/** + * Enum of known special names. + */ +public enum SpecialName { + /** + * Certificate acting as trust root. + * This certificate is used to delegate other trustworthy certificates and to bind pet names to certificates. + */ + TRUST_ROOT("trust-root"), + ; + + static Map MAP = new HashMap<>(); + + static { + for (SpecialName specialName : values()) { + MAP.put(specialName.getValue(), specialName); + } + } + + final String value; + + SpecialName(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static SpecialName fromString(String value) { + return MAP.get(value); + } +} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/BadDataException.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/BadDataException.java new file mode 100644 index 00000000..56a1d2bd --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/BadDataException.java @@ -0,0 +1,8 @@ +package pgp.cert_d.exception; + +/** + * The data was not a valid OpenPGP cert or key in binary format. + */ +public class BadDataException extends Exception { + +} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/BadNameException.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/BadNameException.java new file mode 100644 index 00000000..5d398c36 --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/BadNameException.java @@ -0,0 +1,8 @@ +package pgp.cert_d.exception; + +/** + * Provided name was neither a valid fingerprint, nor a known special name. + */ +public class BadNameException extends Exception { + +} diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/NotAStoreException.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/NotAStoreException.java new file mode 100644 index 00000000..e03561ea --- /dev/null +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/exception/NotAStoreException.java @@ -0,0 +1,15 @@ +package pgp.cert_d.exception; + +/** + * The base dir cannot possibly contain a store. + */ +public class NotAStoreException extends Exception { + + public NotAStoreException() { + super(); + } + + public NotAStoreException(String message) { + super(message); + } +} diff --git a/pgp-certificate-store/build.gradle b/pgp-certificate-store/build.gradle new file mode 100644 index 00000000..8b5dfc18 --- /dev/null +++ b/pgp-certificate-store/build.gradle @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// 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" +} + +test { + useJUnitPlatform() +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/storage/CertificateStore.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateStore.java similarity index 94% rename from pgpainless-core/src/main/java/org/pgpainless/key/storage/CertificateStore.java rename to pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateStore.java index f3c2b86d..459a6f18 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/storage/CertificateStore.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/CertificateStore.java @@ -1,4 +1,4 @@ -package org.pgpainless.key.storage; +package pgp.certificate_store; import java.io.IOException; import java.io.InputStream; @@ -9,7 +9,7 @@ public interface CertificateStore { Item get(String identifier) throws IOException; Item getIfChanged(String identifier, String tag) throws IOException; -< + Item insert(InputStream data, MergeCallback merge) throws IOException; Item tryInsert(InputStream data, MergeCallback merge) throws IOException; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/storage/Item.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/Item.java similarity index 57% rename from pgpainless-core/src/main/java/org/pgpainless/key/storage/Item.java rename to pgp-certificate-store/src/main/java/pgp/certificate_store/Item.java index 15113ba5..28df9613 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/storage/Item.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/Item.java @@ -1,4 +1,4 @@ -package org.pgpainless.key.storage; +package pgp.certificate_store; import java.io.InputStream; @@ -14,14 +14,29 @@ public class Item { 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; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/storage/MergeCallback.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/MergeCallback.java similarity index 82% rename from pgpainless-core/src/main/java/org/pgpainless/key/storage/MergeCallback.java rename to pgp-certificate-store/src/main/java/pgp/certificate_store/MergeCallback.java index f9cb7e1b..468d2e5c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/storage/MergeCallback.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/MergeCallback.java @@ -1,6 +1,5 @@ -package org.pgpainless.key.storage; +package pgp.certificate_store; -import javax.annotation.Nullable; import java.io.InputStream; import java.io.OutputStream; @@ -18,6 +17,6 @@ public interface MergeCallback { * @param existing optional input stream containing an already existing copy of the certificate * @return output stream containing the binary representation of the merged certificate */ - OutputStream merge(InputStream data, @Nullable InputStream existing); + OutputStream merge(InputStream data, InputStream existing); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/storage/CertDStoreTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/storage/CertDStoreTest.java deleted file mode 100644 index 71285152..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/key/storage/CertDStoreTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.pgpainless.key.storage; - -import org.junit.jupiter.api.Test; - -import java.io.File; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class CertDStoreTest { - - @Test - public void testGetDefaultBaseDir() { - CertDStore store = new CertDStore(); - File baseDir = store.getBaseDirectory(); - assertEquals("pgp.cert.d", baseDir.getName()); - } -} diff --git a/settings.gradle b/settings.gradle index aea19392..2a244920 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,5 +6,7 @@ rootProject.name = 'PGPainless' include 'pgpainless-core', 'pgpainless-sop', - 'pgpainless-cli' + 'pgpainless-cli', + 'pgp-certificate-store', + 'pgp-cert-d-java'