diff --git a/pgp-cert-d-java/build.gradle b/pgp-cert-d-java/build.gradle index a4b97b3..35032ca 100644 --- a/pgp-cert-d-java/build.gradle +++ b/pgp-cert-d-java/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'ru.vyarus.animalsniffer' dependencies { // animal sniffer for ensuring Android API compatibility - signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:2.3.3_r2@signature" + signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:8.0.0_r2@signature" // JUnit testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateDirectory.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateDirectory.java index 2a0d8f5..668bd2f 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateDirectory.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateDirectory.java @@ -33,6 +33,16 @@ public class PGPCertificateDirectory return backend.readByFingerprint(fingerprint); } + @Override + public Certificate getByFingerprintIfChanged(String fingerprint, long tag) + throws IOException, BadNameException, BadDataException { + if (tag != backend.getTagForFingerprint(fingerprint)) { + return getByFingerprint(fingerprint); + } + return null; + } + + @Override public Certificate getBySpecialName(String specialName) throws BadNameException, BadDataException, IOException { @@ -43,6 +53,15 @@ public class PGPCertificateDirectory return null; } + @Override + public Certificate getBySpecialNameIfChanged(String specialName, long tag) + throws IOException, BadNameException, BadDataException { + if (tag != backend.getTagForSpecialName(specialName)) { + return getBySpecialName(specialName); + } + return null; + } + @Override public Certificate getTrustRootCertificate() throws IOException, BadDataException { @@ -53,6 +72,15 @@ public class PGPCertificateDirectory } } + @Override + public Certificate getTrustRootCertificateIfChanged(long tag) throws IOException, BadDataException { + try { + return getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag); + } catch (BadNameException e) { + throw new AssertionError("'" + SpecialNames.TRUST_ROOT + "' is an implementation MUST"); + } + } + @Override public Iterator items() { return backend.readItems(); @@ -179,6 +207,10 @@ public class PGPCertificateDirectory Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException, BadNameException; + + Long getTagForFingerprint(String fingerprint) throws BadNameException, IOException; + + Long getTagForSpecialName(String specialName) throws BadNameException, IOException; } public interface LockingMechanism { diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateStoreAdapter.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateStoreAdapter.java index 5d95e6f..972d3aa 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateStoreAdapter.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/PGPCertificateStoreAdapter.java @@ -37,6 +37,16 @@ public class PGPCertificateStoreAdapter implements PGPCertificateStore { } } + @Override + public Certificate getCertificateIfChanged(String identifier, Long tag) + throws IOException, BadNameException, BadDataException { + if (SpecialNames.lookupSpecialName(identifier) != null) { + return directory.getBySpecialNameIfChanged(identifier, tag); + } else { + return directory.getByFingerprintIfChanged(identifier.toLowerCase(), tag); + } + } + @Override public Iterator getCertificatesBySubkeyId(long subkeyId) throws IOException, BadDataException { diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/ReadOnlyPGPCertificateDirectory.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/ReadOnlyPGPCertificateDirectory.java index 4c9fabc..8c5efdc 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/ReadOnlyPGPCertificateDirectory.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/ReadOnlyPGPCertificateDirectory.java @@ -16,12 +16,21 @@ public interface ReadOnlyPGPCertificateDirectory { Certificate getTrustRootCertificate() throws IOException, BadDataException; + Certificate getTrustRootCertificateIfChanged(long tag) + throws IOException, BadDataException; + Certificate getByFingerprint(String fingerprint) throws IOException, BadNameException, BadDataException; + Certificate getByFingerprintIfChanged(String fingerprint, long tag) + throws IOException, BadNameException, BadDataException; + Certificate getBySpecialName(String specialName) throws IOException, BadNameException, BadDataException; + Certificate getBySpecialNameIfChanged(String specialName, long tag) + throws IOException, BadNameException, BadDataException; + Iterator items(); Iterator fingerprints(); diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/FileBasedCertificateDirectoryBackend.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/FileBasedCertificateDirectoryBackend.java index 5decae2..e5750e8 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/FileBasedCertificateDirectoryBackend.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/FileBasedCertificateDirectoryBackend.java @@ -7,6 +7,7 @@ package pgp.cert_d.backend; import pgp.cert_d.PGPCertificateDirectory; import pgp.cert_d.SpecialNames; import pgp.certificate_store.certificate.Certificate; +import pgp.certificate_store.certificate.Key; import pgp.certificate_store.certificate.KeyMaterial; import pgp.certificate_store.certificate.KeyMaterialMerger; import pgp.certificate_store.certificate.KeyMaterialReaderBackend; @@ -25,6 +26,9 @@ import java.io.InputStream; import java.io.RandomAccessFile; import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -160,10 +164,12 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec return null; } + long tag = getTagForFingerprint(fingerprint); + FileInputStream fileIn = new FileInputStream(certFile); BufferedInputStream bufferedIn = new BufferedInputStream(fileIn); - Certificate certificate = reader.read(bufferedIn).asCertificate(); + Certificate certificate = reader.read(bufferedIn, tag).asCertificate(); if (!certificate.getFingerprint().equals(fingerprint)) { // TODO: Figure out more suitable exception throw new BadDataException(); @@ -179,9 +185,11 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec return null; } + long tag = getTagForSpecialName(specialName); + FileInputStream fileIn = new FileInputStream(certFile); BufferedInputStream bufferedIn = new BufferedInputStream(fileIn); - KeyMaterial keyMaterial = reader.read(bufferedIn); + KeyMaterial keyMaterial = reader.read(bufferedIn, tag); return keyMaterial; } @@ -214,7 +222,8 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec @Override Certificate get() throws BadDataException { try { - Certificate certificate = reader.read(new FileInputStream(certFile)).asCertificate(); + long tag = getTag(certFile); + Certificate certificate = reader.read(new FileInputStream(certFile), tag).asCertificate(); if (!(subdirectory.getName() + certFile.getName()).equals(certificate.getFingerprint())) { throw new BadDataException(); } @@ -246,7 +255,7 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec @Override public KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge) throws BadDataException, IOException { - KeyMaterial newCertificate = reader.read(data); + KeyMaterial newCertificate = reader.read(data, null); KeyMaterial existingCertificate; File certFile; try { @@ -256,18 +265,22 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec throw new BadDataException(); } - if (existingCertificate != null && !newCertificate.getTag().equals(existingCertificate.getTag())) { + if (existingCertificate != null) { newCertificate = merge.merge(newCertificate, existingCertificate); } - writeToFile(newCertificate.getInputStream(), certFile); - + long tag = writeToFile(newCertificate.getInputStream(), certFile); + if (newCertificate instanceof Key) { + newCertificate = new Key((Key) newCertificate, tag); + } else { + newCertificate = new Certificate((Certificate) newCertificate, tag); + } return newCertificate; } @Override public Certificate doInsert(InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException { - KeyMaterial newCertificate = reader.read(data); + KeyMaterial newCertificate = reader.read(data, null); Certificate existingCertificate; File certFile; try { @@ -277,18 +290,17 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec throw new BadDataException(); } - if (existingCertificate != null && !newCertificate.getTag().equals(existingCertificate.getTag())) { + if (existingCertificate != null) { newCertificate = merge.merge(newCertificate, existingCertificate); } - writeToFile(newCertificate.getInputStream(), certFile); - - return newCertificate.asCertificate(); + long tag = writeToFile(newCertificate.getInputStream(), certFile); + return new Certificate(newCertificate.asCertificate(), tag); } @Override public Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException, BadNameException { - KeyMaterial newCertificate = reader.read(data); + KeyMaterial newCertificate = reader.read(data, null); KeyMaterial existingCertificate; File certFile; try { @@ -298,16 +310,41 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec throw new BadDataException(); } - if (existingCertificate != null && !newCertificate.getTag().equals(existingCertificate.getTag())) { + if (existingCertificate != null) { newCertificate = merge.merge(newCertificate, existingCertificate); } - writeToFile(newCertificate.getInputStream(), certFile); - - return newCertificate.asCertificate(); + long tag = writeToFile(newCertificate.getInputStream(), certFile); + return new Certificate(newCertificate.asCertificate(), tag); } - private void writeToFile(InputStream inputStream, File certFile) + @Override + public Long getTagForFingerprint(String fingerprint) throws BadNameException, IOException { + File file = resolver.getCertFileByFingerprint(fingerprint); + return getTag(file); + } + + @Override + public Long getTagForSpecialName(String specialName) throws BadNameException, IOException { + File file = resolver.getCertFileBySpecialName(specialName); + return getTag(file); + } + + private Long getTag(File file) throws IOException { + if (!file.exists()) { + throw new IllegalArgumentException("File MUST exist."); + } + Path path = file.toPath(); + BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); + + // On UNIX file systems, for example, fileKey() will return the device ID and inode + int fileId = attrs.fileKey().hashCode(); + long lastMod = attrs.lastModifiedTime().toMillis(); + + return lastMod + (11L * fileId); + } + + private long writeToFile(InputStream inputStream, File certFile) throws IOException { certFile.getParentFile().mkdirs(); if (!certFile.exists() && !certFile.createNewFile()) { @@ -324,6 +361,7 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec inputStream.close(); fileOut.close(); + return getTag(certFile); } public static class FilenameResolver { diff --git a/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/InMemoryCertificateDirectoryBackend.java b/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/InMemoryCertificateDirectoryBackend.java index 26c6e2b..60069c8 100644 --- a/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/InMemoryCertificateDirectoryBackend.java +++ b/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/InMemoryCertificateDirectoryBackend.java @@ -7,6 +7,7 @@ package pgp.cert_d.backend; import pgp.cert_d.PGPCertificateDirectory; import pgp.cert_d.SpecialNames; import pgp.certificate_store.certificate.Certificate; +import pgp.certificate_store.certificate.Key; import pgp.certificate_store.certificate.KeyMaterial; import pgp.certificate_store.certificate.KeyMaterialMerger; import pgp.certificate_store.certificate.KeyMaterialReaderBackend; @@ -91,7 +92,7 @@ public class InMemoryCertificateDirectoryBackend implements PGPCertificateDirect @Override public KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge) throws BadDataException, IOException { - KeyMaterial update = reader.read(data); + KeyMaterial update = reader.read(data, null); KeyMaterial existing = null; try { existing = readBySpecialName(SpecialNames.TRUST_ROOT); @@ -100,6 +101,11 @@ public class InMemoryCertificateDirectoryBackend implements PGPCertificateDirect throw new RuntimeException(e); } KeyMaterial merged = merge.merge(update, existing); + if (merged instanceof Key) { + merged = new Key((Key) merged, System.currentTimeMillis()); + } else { + merged = new Certificate((Certificate) merged, System.currentTimeMillis()); + } keyMaterialSpecialNameMap.put(SpecialNames.TRUST_ROOT, merged); return merged; } @@ -108,9 +114,10 @@ public class InMemoryCertificateDirectoryBackend implements PGPCertificateDirect @Override public Certificate doInsert(InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException { - KeyMaterial update = reader.read(data); + KeyMaterial update = reader.read(data, null); Certificate existing = readByFingerprint(update.getFingerprint()); Certificate merged = merge.merge(update, existing).asCertificate(); + merged = new Certificate(merged, System.currentTimeMillis()); certificateFingerprintMap.put(update.getFingerprint(), merged); return merged; } @@ -118,10 +125,36 @@ public class InMemoryCertificateDirectoryBackend implements PGPCertificateDirect @Override public Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException, BadNameException { - KeyMaterial keyMaterial = reader.read(data); + KeyMaterial keyMaterial = reader.read(data, null); KeyMaterial existing = readBySpecialName(specialName); KeyMaterial merged = merge.merge(keyMaterial, existing); + if (merged instanceof Key) { + merged = new Key((Key) merged, System.currentTimeMillis()); + } else { + merged = new Certificate((Certificate) merged, System.currentTimeMillis()); + } keyMaterialSpecialNameMap.put(specialName, merged); return merged.asCertificate(); } + + @Override + public Long getTagForFingerprint(String fingerprint) throws BadNameException, IOException { + Certificate certificate = certificateFingerprintMap.get(fingerprint); + if (certificate == null) { + return null; + } + return certificate.getTag(); + } + + @Override + public Long getTagForSpecialName(String specialName) throws BadNameException, IOException { + if (SpecialNames.lookupSpecialName(specialName) == null) { + throw new BadNameException("Invalid special name " + specialName); + } + KeyMaterial tagged = keyMaterialSpecialNameMap.get(specialName); + if (tagged == null) { + return null; + } + return tagged.getTag(); + } } diff --git a/pgp-cert-d-java/src/test/java/pgp/cert_d/PGPCertificateDirectoryTest.java b/pgp-cert-d-java/src/test/java/pgp/cert_d/PGPCertificateDirectoryTest.java index 65cd558..a3803c2 100644 --- a/pgp-cert-d-java/src/test/java/pgp/cert_d/PGPCertificateDirectoryTest.java +++ b/pgp-cert-d-java/src/test/java/pgp/cert_d/PGPCertificateDirectoryTest.java @@ -5,12 +5,17 @@ package pgp.cert_d; import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import pgp.cert_d.backend.FileBasedCertificateDirectoryBackend; +import pgp.cert_d.dummy.TestKeyMaterialMerger; +import pgp.cert_d.dummy.TestKeyMaterialReaderBackend; import pgp.cert_d.subkey_lookup.InMemorySubkeyLookup; import pgp.certificate_store.certificate.Certificate; import pgp.certificate_store.certificate.Key; import pgp.certificate_store.certificate.KeyMaterial; +import pgp.certificate_store.certificate.KeyMaterialMerger; import pgp.certificate_store.exception.BadDataException; import pgp.certificate_store.exception.BadNameException; import pgp.certificate_store.exception.NotAStoreException; @@ -18,6 +23,7 @@ import pgp.certificate_store.exception.NotAStoreException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; @@ -30,6 +36,7 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -37,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class PGPCertificateDirectoryTest { + @SuppressWarnings("CharsetObjectCanBeUsed") private static final Charset UTF8 = Charset.forName("UTF8"); private static final String HARRY_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -143,7 +151,10 @@ public class PGPCertificateDirectoryTest { "-----END PGP PUBLIC KEY BLOCK-----\n"; private static final String CEDRIC_FP = "5e75bf20646bc1a98d3b1bc2fe9cd472987c4021"; - private static Stream provideTestSubjects() throws IOException, NotAStoreException { + private static final KeyMaterialMerger merger = new TestKeyMaterialMerger(); + + private static Stream provideTestSubjects() + throws IOException, NotAStoreException { PGPCertificateDirectory inMemory = PGPCertificateDirectories.inMemoryCertificateDirectory( new TestKeyMaterialReaderBackend()); @@ -159,18 +170,19 @@ public class PGPCertificateDirectoryTest { @ParameterizedTest @MethodSource("provideTestSubjects") - public void lockDirectoryAndInsertWillFail(PGPCertificateDirectory directory) throws IOException, InterruptedException, BadDataException { + public void lockDirectoryAndInsertWillFail(PGPCertificateDirectory directory) + throws IOException, InterruptedException, BadDataException { // Manually lock the dir assertFalse(directory.backend.getLock().isLocked()); directory.backend.getLock().lockDirectory(); assertTrue(directory.backend.getLock().isLocked()); assertFalse(directory.backend.getLock().tryLockDirectory()); - Certificate inserted = directory.tryInsert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), new TestKeyMaterialMerger()); + Certificate inserted = directory.tryInsert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), merger); assertNull(inserted); directory.backend.getLock().releaseDirectory(); - inserted = directory.tryInsert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), new TestKeyMaterialMerger()); + inserted = directory.tryInsert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), merger); assertNotNull(inserted); } @@ -188,7 +200,7 @@ public class PGPCertificateDirectoryTest { ByteArrayInputStream bytesIn = new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)); - Certificate certificate = directory.insert(bytesIn, new TestKeyMaterialMerger()); + Certificate certificate = directory.insert(bytesIn, merger); assertEquals(CEDRIC_FP, certificate.getFingerprint(), "Fingerprint of inserted cert MUST match"); Certificate get = directory.getByFingerprint(CEDRIC_FP); @@ -207,7 +219,7 @@ public class PGPCertificateDirectoryTest { assertNull(directory.getTrustRoot()); KeyMaterial trustRootMaterial = directory.insertTrustRoot( - new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8)), new TestKeyMaterialMerger()); + new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8)), merger); assertNotNull(trustRootMaterial); assertTrue(trustRootMaterial instanceof Key); assertEquals(HARRY_FP, trustRootMaterial.getFingerprint()); @@ -217,8 +229,8 @@ public class PGPCertificateDirectoryTest { Certificate trustRootCert = directory.getTrustRootCertificate(); assertEquals(HARRY_FP, trustRootCert.getFingerprint()); - directory.tryInsert(new ByteArrayInputStream(RON_CERT.getBytes(UTF8)), new TestKeyMaterialMerger()); - directory.insert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), new TestKeyMaterialMerger()); + directory.tryInsert(new ByteArrayInputStream(RON_CERT.getBytes(UTF8)), merger); + directory.insert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), merger); Set expected = new HashSet<>(Arrays.asList(RON_FP, CEDRIC_FP)); @@ -230,4 +242,104 @@ public class PGPCertificateDirectoryTest { assertEquals(expected, actual); } + + @ParameterizedTest + @MethodSource("provideTestSubjects") + public void testGetTrustRootIfChanged(PGPCertificateDirectory directory) + throws BadDataException, IOException, InterruptedException { + KeyMaterial trustRootMaterial = directory.insertTrustRoot( + new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8)), merger); + + assertNotNull(trustRootMaterial.getTag()); + Long tag = trustRootMaterial.getTag(); + assertNull(directory.getTrustRootCertificateIfChanged(tag)); + assertNotNull(directory.getTrustRootCertificateIfChanged(tag + 1)); + + Long oldTag = tag; + // "update" key + trustRootMaterial = directory.insertTrustRoot( + new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8)), merger); + tag = trustRootMaterial.getTag(); + + assertNotEquals(oldTag, tag); + assertNotNull(directory.getTrustRootCertificateIfChanged(oldTag)); + } + + @ParameterizedTest + @MethodSource("provideTestSubjects") + public void testGetBySpecialNameIfChanged(PGPCertificateDirectory directory) + throws BadDataException, IOException, InterruptedException, BadNameException { + KeyMaterial specialName = directory.insertWithSpecialName(SpecialNames.TRUST_ROOT, + new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8)), merger); + + assertNotNull(specialName.getTag()); + Long tag = specialName.getTag(); + assertNull(directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag)); + assertNotNull(directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, tag + 1)); + + Long oldTag = tag; + // "update" key + specialName = directory.insertWithSpecialName(SpecialNames.TRUST_ROOT, + new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8)), merger); + tag = specialName.getTag(); + + assertNotEquals(oldTag, tag); + assertNotNull(directory.getBySpecialNameIfChanged(SpecialNames.TRUST_ROOT, oldTag)); + } + + @ParameterizedTest + @MethodSource("provideTestSubjects") + public void testGetByFingerprintIfChanged(PGPCertificateDirectory directory) + throws BadDataException, IOException, InterruptedException, BadNameException { + Certificate certificate = directory.insert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), merger); + Long tag = certificate.getTag(); + assertNotNull(tag); + + assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag)); + assertNotNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag + 1)); + + Long oldTag = tag; + // "update" cert + certificate = directory.insert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), merger); + tag = certificate.getTag(); + + assertNotEquals(oldTag, tag); + assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag)); + assertNotNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), oldTag)); + } + + @Test + public void testFileBasedCertificateDirectoryTagChangesWhenFileChanges() throws IOException, NotAStoreException, BadDataException, InterruptedException, BadNameException { + File tempDir = Files.createTempDirectory("file-based-changes").toFile(); + tempDir.deleteOnExit(); + PGPCertificateDirectory directory = PGPCertificateDirectories.fileBasedCertificateDirectory( + new TestKeyMaterialReaderBackend(), + tempDir, + new InMemorySubkeyLookup()); + FileBasedCertificateDirectoryBackend.FilenameResolver resolver = + new FileBasedCertificateDirectoryBackend.FilenameResolver(tempDir); + + // Insert certificate + Certificate certificate = directory.insert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), merger); + Long tag = certificate.getTag(); + assertNotNull(tag); + assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag)); + + Long oldTag = tag; + + // Change the file on disk directly, this invalidates the tag due to changed modification date + File certFile = resolver.getCertFileByFingerprint(certificate.getFingerprint()); + FileOutputStream fileOut = new FileOutputStream(certFile); + Streams.pipeAll(certificate.getInputStream(), fileOut); + fileOut.close(); + + // Old invalidated tag indicates a change, so the modified certificate is returned + certificate = directory.getByFingerprintIfChanged(certificate.getFingerprint(), oldTag); + assertNotNull(certificate); + + // new tag is valid + tag = certificate.getTag(); + assertNotEquals(oldTag, tag); + assertNull(directory.getByFingerprintIfChanged(certificate.getFingerprint(), tag)); + } } diff --git a/pgp-cert-d-java/src/test/java/pgp/cert_d/TestKeyMaterialMerger.java b/pgp-cert-d-java/src/test/java/pgp/cert_d/dummy/TestKeyMaterialMerger.java similarity index 94% rename from pgp-cert-d-java/src/test/java/pgp/cert_d/TestKeyMaterialMerger.java rename to pgp-cert-d-java/src/test/java/pgp/cert_d/dummy/TestKeyMaterialMerger.java index 6810f0b..8ca24c3 100644 --- a/pgp-cert-d-java/src/test/java/pgp/cert_d/TestKeyMaterialMerger.java +++ b/pgp-cert-d-java/src/test/java/pgp/cert_d/dummy/TestKeyMaterialMerger.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package pgp.cert_d; +package pgp.cert_d.dummy; import pgp.certificate_store.certificate.KeyMaterial; import pgp.certificate_store.certificate.KeyMaterialMerger; diff --git a/pgp-cert-d-java/src/test/java/pgp/cert_d/TestKeyMaterialReaderBackend.java b/pgp-cert-d-java/src/test/java/pgp/cert_d/dummy/TestKeyMaterialReaderBackend.java similarity index 58% rename from pgp-cert-d-java/src/test/java/pgp/cert_d/TestKeyMaterialReaderBackend.java rename to pgp-cert-d-java/src/test/java/pgp/cert_d/dummy/TestKeyMaterialReaderBackend.java index d653837..2ef392d 100644 --- a/pgp-cert-d-java/src/test/java/pgp/cert_d/TestKeyMaterialReaderBackend.java +++ b/pgp-cert-d-java/src/test/java/pgp/cert_d/dummy/TestKeyMaterialReaderBackend.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package pgp.cert_d; +package pgp.cert_d.dummy; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyRing; @@ -33,24 +33,22 @@ public class TestKeyMaterialReaderBackend implements KeyMaterialReaderBackend { KeyFingerPrintCalculator fpCalc = new BcKeyFingerprintCalculator(); @Override - public KeyMaterial read(InputStream data) throws IOException, BadDataException { + public KeyMaterial read(InputStream data, Long tag) throws IOException, BadDataException { ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(data, out); try { - Key key = readKey(new ByteArrayInputStream(out.toByteArray())); - return key; + return readKey(new ByteArrayInputStream(out.toByteArray()), tag); } catch (IOException | PGPException e) { try { - Certificate certificate = readCertificate(new ByteArrayInputStream(out.toByteArray())); - return certificate; + return readCertificate(new ByteArrayInputStream(out.toByteArray()), tag); } catch (IOException e1) { throw new BadDataException(); } } } - private Key readKey(InputStream inputStream) throws IOException, PGPException { + private Key readKey(InputStream inputStream, Long tag) throws IOException, PGPException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); Streams.pipeAll(inputStream, buffer); inputStream.close(); @@ -60,64 +58,21 @@ public class TestKeyMaterialReaderBackend implements KeyMaterialReaderBackend { PGPSecretKeyRing secretKeys = new PGPSecretKeyRing(decoderStream, fpCalc); PGPPublicKeyRing cert = extractCert(secretKeys); ByteArrayInputStream encoded = new ByteArrayInputStream(cert.getEncoded()); - Certificate certificate = readCertificate(encoded); + Certificate certificate = readCertificate(encoded, tag); - return new Key() { - @Override - public Certificate getCertificate() { - return certificate; - } - - @Override - public String getFingerprint() { - return certificate.getFingerprint(); - } - - @Override - public InputStream getInputStream() throws IOException { - return new ByteArrayInputStream(buffer.toByteArray()); - } - - @Override - public String getTag() throws IOException { - return null; - } - - @Override - public List getSubkeyIds() throws IOException { - return certificate.getSubkeyIds(); - } - }; + return new Key(buffer.toByteArray(), certificate, tag); } - private Certificate readCertificate(InputStream inputStream) throws IOException { + private Certificate readCertificate(InputStream inputStream, Long tag) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); Streams.pipeAll(inputStream, buffer); ByteArrayInputStream in = new ByteArrayInputStream(buffer.toByteArray()); InputStream decoderStream = PGPUtil.getDecoderStream(in); PGPPublicKeyRing cert = new PGPPublicKeyRing(decoderStream, fpCalc); - return new Certificate() { - @Override - public String getFingerprint() { - return Hex.toHexString(cert.getPublicKey().getFingerprint()).toLowerCase(); - } - - @Override - public InputStream getInputStream() throws IOException { - return new ByteArrayInputStream(buffer.toByteArray()); - } - - @Override - public String getTag() throws IOException { - return null; - } - - @Override - public List getSubkeyIds() throws IOException { - return TestKeyMaterialReaderBackend.getSubkeyIds(cert); - } - }; + String fingerprint = Hex.toHexString(cert.getPublicKey().getFingerprint()).toLowerCase(); + List subKeyIds = getSubkeyIds(cert); + return new Certificate(buffer.toByteArray(), fingerprint, subKeyIds, tag); } private PGPPublicKeyRing extractCert(PGPSecretKeyRing secretKeys) { @@ -126,8 +81,7 @@ public class TestKeyMaterialReaderBackend implements KeyMaterialReaderBackend { while (publicKeyIterator.hasNext()) { publicKeyList.add(publicKeyIterator.next()); } - PGPPublicKeyRing publicKeyRing = new PGPPublicKeyRing(publicKeyList); - return publicKeyRing; + return new PGPPublicKeyRing(publicKeyList); } private static List getSubkeyIds(PGPKeyRing keyRing) { diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/PGPCertificateStore.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/PGPCertificateStore.java index acca57e..d0915b0 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/PGPCertificateStore.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/PGPCertificateStore.java @@ -32,6 +32,23 @@ public interface PGPCertificateStore { Certificate getCertificate(String identifier) throws IOException, BadNameException, BadDataException; + /** + * Return the certificate that matches the given identifier, but only if it has been changed. + * Whether it has been changed is determined by calculating the tag in the directory + * (e.g. by looking at the inode and last modification date) and comparing the result with the tag provided by + * the caller. + * + * @param identifier certificate identifier + * @param tag tag by the caller + * @return certificate if it has been changed, null otherwise + * + * @throws IOException in case of an IO-error + * @throws BadNameException if the identifier is invalid + * @throws BadDataException if the certificate file contains invalid data + */ + Certificate getCertificateIfChanged(String identifier, Long tag) + throws IOException, BadNameException, BadDataException; + /** * Return an {@link Iterator} over all certificates in the store that contain a subkey with the given * subkey id. @@ -42,7 +59,7 @@ public interface PGPCertificateStore { * @throws BadDataException if any of the certificate files contains invalid data */ Iterator getCertificatesBySubkeyId(long subkeyId) - throws IOException, BadDataException; + throws IOException, BadDataException; /** * Insert a certificate into the store. diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/Certificate.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/Certificate.java index d2b1d1b..175f739 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/Certificate.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/Certificate.java @@ -4,13 +4,67 @@ package pgp.certificate_store.certificate; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.List; + /** * OpenPGP certificate (public key). */ -public abstract class Certificate implements KeyMaterial { +public class Certificate implements KeyMaterial { + + private final byte[] bytes; + private final String fingerprint; + private final List subkeyIds; + private final Long tag; + + /** + * Certificate constructor. + * + * @param bytes encoding of the certificate + * @param fingerprint fingerprint (lowercase hex characters) + * @param subkeyIds list of subkey ids + * @param tag tag + */ + public Certificate(byte[] bytes, String fingerprint, List subkeyIds, Long tag) { + this.bytes = bytes; + this.fingerprint = fingerprint; + this.subkeyIds = subkeyIds; + this.tag = tag; + } + + /** + * Copy constructor to assign a new tag to the {@link Certificate}. + * + * @param cert certificate + * @param tag tag + */ + public Certificate(Certificate cert, Long tag) { + this(cert.bytes, cert.fingerprint, cert.subkeyIds, tag); + } + + @Override + public String getFingerprint() { + return fingerprint; + } @Override public Certificate asCertificate() { return this; } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(bytes); + } + + @Override + public Long getTag() { + return tag; + } + + @Override + public List getSubkeyIds() { + return subkeyIds; + } } diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/Key.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/Key.java index 5d6d713..a2a4aef 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/Key.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/Key.java @@ -4,21 +4,74 @@ package pgp.certificate_store.certificate; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.List; + /** * OpenPGP key (secret key). */ -public abstract class Key implements KeyMaterial { +public class Key implements KeyMaterial { + + private final byte[] bytes; + private final Certificate certificate; + private final Long tag; + + /** + * Key constructor. + * + * @param bytes encoding of the key + * @param certificate associated certificate + * @param tag tag + */ + public Key(byte[] bytes, Certificate certificate, Long tag) { + this.bytes = bytes; + this.certificate = certificate; + this.tag = tag; + } + + /** + * Copy constructor to change the tag of both the {@link Key} and its {@link Certificate}. + * + * @param key key + * @param tag tag + */ + public Key(Key key, Long tag) { + this(key.bytes, new Certificate(key.certificate, tag), tag); + } /** * Return the certificate part of this OpenPGP key. * * @return OpenPGP certificate */ - public abstract Certificate getCertificate(); + public Certificate getCertificate() { + return new Certificate(certificate, getTag()); + } + + @Override + public String getFingerprint() { + return certificate.getFingerprint(); + } @Override public Certificate asCertificate() { return getCertificate(); } + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(bytes); + } + + @Override + public Long getTag() { + return tag; + } + + @Override + public List getSubkeyIds() { + return certificate.getSubkeyIds(); + } + } diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterial.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterial.java index 5293edf..1438b47 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterial.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterial.java @@ -4,7 +4,6 @@ package pgp.certificate_store.certificate; -import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.Set; @@ -19,23 +18,34 @@ public interface KeyMaterial { */ String getFingerprint(); + /** + * Return the {@link Certificate} belonging to this key material. + * If this is already a {@link Certificate}, return this. + * If this is a {@link Key}, extract the {@link Certificate} and return it. + * + * @return certificate + */ Certificate asCertificate(); /** * Return an {@link InputStream} of the binary representation of the secret key. * * @return input stream - * @throws IOException in case of an IO error */ - InputStream getInputStream() throws IOException; + InputStream getInputStream(); - String getTag() throws IOException; + /** + * Return the tag belonging to this key material. + * The tag can be used to keep an application cache in sync with what is in the directory. + * + * @return tag + */ + Long getTag(); /** * Return a {@link Set} containing key-ids of subkeys. * * @return subkeys - * @throws IOException in case of an IO error */ - List getSubkeyIds() throws IOException; + List getSubkeyIds(); } diff --git a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterialReaderBackend.java b/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterialReaderBackend.java index 42b2bb4..42b3e2c 100644 --- a/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterialReaderBackend.java +++ b/pgp-certificate-store/src/main/java/pgp/certificate_store/certificate/KeyMaterialReaderBackend.java @@ -20,5 +20,5 @@ public interface KeyMaterialReaderBackend { * @throws IOException in case of an IO error * @throws BadDataException in case that the data stream does not contain a valid OpenPGP key/certificate */ - KeyMaterial read(InputStream data) throws IOException, BadDataException; + KeyMaterial read(InputStream data, Long tag) throws IOException, BadDataException; } diff --git a/version.gradle b/version.gradle index 0287d07..1951785 100644 --- a/version.gradle +++ b/version.gradle @@ -6,7 +6,7 @@ allprojects { ext { shortVersion = '0.1.2' isSnapshot = true - minAndroidSdk = 10 + minAndroidSdk = 26 javaSourceCompatibility = 1.8 bouncycastleVersion = '1.71' slf4jVersion = '1.7.36'