Add back support for getXIfChanged(Y, tag)

This commit is contained in:
Paul Schaub 2022-08-24 13:04:28 +02:00
parent d050cb5516
commit 27f4598437
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
15 changed files with 423 additions and 101 deletions

View File

@ -16,7 +16,7 @@ apply plugin: 'ru.vyarus.animalsniffer'
dependencies { dependencies {
// animal sniffer for ensuring Android API compatibility // 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 // JUnit
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"

View File

@ -33,6 +33,16 @@ public class PGPCertificateDirectory
return backend.readByFingerprint(fingerprint); 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 @Override
public Certificate getBySpecialName(String specialName) public Certificate getBySpecialName(String specialName)
throws BadNameException, BadDataException, IOException { throws BadNameException, BadDataException, IOException {
@ -43,6 +53,15 @@ public class PGPCertificateDirectory
return null; 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 @Override
public Certificate getTrustRootCertificate() public Certificate getTrustRootCertificate()
throws IOException, BadDataException { 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 @Override
public Iterator<Certificate> items() { public Iterator<Certificate> items() {
return backend.readItems(); return backend.readItems();
@ -179,6 +207,10 @@ public class PGPCertificateDirectory
Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException, BadNameException; throws IOException, BadDataException, BadNameException;
Long getTagForFingerprint(String fingerprint) throws BadNameException, IOException;
Long getTagForSpecialName(String specialName) throws BadNameException, IOException;
} }
public interface LockingMechanism { public interface LockingMechanism {

View File

@ -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 @Override
public Iterator<Certificate> getCertificatesBySubkeyId(long subkeyId) public Iterator<Certificate> getCertificatesBySubkeyId(long subkeyId)
throws IOException, BadDataException { throws IOException, BadDataException {

View File

@ -16,12 +16,21 @@ public interface ReadOnlyPGPCertificateDirectory {
Certificate getTrustRootCertificate() Certificate getTrustRootCertificate()
throws IOException, BadDataException; throws IOException, BadDataException;
Certificate getTrustRootCertificateIfChanged(long tag)
throws IOException, BadDataException;
Certificate getByFingerprint(String fingerprint) Certificate getByFingerprint(String fingerprint)
throws IOException, BadNameException, BadDataException; throws IOException, BadNameException, BadDataException;
Certificate getByFingerprintIfChanged(String fingerprint, long tag)
throws IOException, BadNameException, BadDataException;
Certificate getBySpecialName(String specialName) Certificate getBySpecialName(String specialName)
throws IOException, BadNameException, BadDataException; throws IOException, BadNameException, BadDataException;
Certificate getBySpecialNameIfChanged(String specialName, long tag)
throws IOException, BadNameException, BadDataException;
Iterator<Certificate> items(); Iterator<Certificate> items();
Iterator<String> fingerprints(); Iterator<String> fingerprints();

View File

@ -7,6 +7,7 @@ package pgp.cert_d.backend;
import pgp.cert_d.PGPCertificateDirectory; import pgp.cert_d.PGPCertificateDirectory;
import pgp.cert_d.SpecialNames; import pgp.cert_d.SpecialNames;
import pgp.certificate_store.certificate.Certificate; import pgp.certificate_store.certificate.Certificate;
import pgp.certificate_store.certificate.Key;
import pgp.certificate_store.certificate.KeyMaterial; import pgp.certificate_store.certificate.KeyMaterial;
import pgp.certificate_store.certificate.KeyMaterialMerger; import pgp.certificate_store.certificate.KeyMaterialMerger;
import pgp.certificate_store.certificate.KeyMaterialReaderBackend; import pgp.certificate_store.certificate.KeyMaterialReaderBackend;
@ -25,6 +26,9 @@ import java.io.InputStream;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.nio.channels.FileLock; import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException; 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.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
@ -160,10 +164,12 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec
return null; return null;
} }
long tag = getTagForFingerprint(fingerprint);
FileInputStream fileIn = new FileInputStream(certFile); FileInputStream fileIn = new FileInputStream(certFile);
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn); BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
Certificate certificate = reader.read(bufferedIn).asCertificate(); Certificate certificate = reader.read(bufferedIn, tag).asCertificate();
if (!certificate.getFingerprint().equals(fingerprint)) { if (!certificate.getFingerprint().equals(fingerprint)) {
// TODO: Figure out more suitable exception // TODO: Figure out more suitable exception
throw new BadDataException(); throw new BadDataException();
@ -179,9 +185,11 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec
return null; return null;
} }
long tag = getTagForSpecialName(specialName);
FileInputStream fileIn = new FileInputStream(certFile); FileInputStream fileIn = new FileInputStream(certFile);
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn); BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
KeyMaterial keyMaterial = reader.read(bufferedIn); KeyMaterial keyMaterial = reader.read(bufferedIn, tag);
return keyMaterial; return keyMaterial;
} }
@ -214,7 +222,8 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec
@Override @Override
Certificate get() throws BadDataException { Certificate get() throws BadDataException {
try { 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())) { if (!(subdirectory.getName() + certFile.getName()).equals(certificate.getFingerprint())) {
throw new BadDataException(); throw new BadDataException();
} }
@ -246,7 +255,7 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec
@Override @Override
public KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge) throws BadDataException, IOException { public KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge) throws BadDataException, IOException {
KeyMaterial newCertificate = reader.read(data); KeyMaterial newCertificate = reader.read(data, null);
KeyMaterial existingCertificate; KeyMaterial existingCertificate;
File certFile; File certFile;
try { try {
@ -256,18 +265,22 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec
throw new BadDataException(); throw new BadDataException();
} }
if (existingCertificate != null && !newCertificate.getTag().equals(existingCertificate.getTag())) { if (existingCertificate != null) {
newCertificate = merge.merge(newCertificate, existingCertificate); 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; return newCertificate;
} }
@Override @Override
public Certificate doInsert(InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException { public Certificate doInsert(InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException {
KeyMaterial newCertificate = reader.read(data); KeyMaterial newCertificate = reader.read(data, null);
Certificate existingCertificate; Certificate existingCertificate;
File certFile; File certFile;
try { try {
@ -277,18 +290,17 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec
throw new BadDataException(); throw new BadDataException();
} }
if (existingCertificate != null && !newCertificate.getTag().equals(existingCertificate.getTag())) { if (existingCertificate != null) {
newCertificate = merge.merge(newCertificate, existingCertificate); newCertificate = merge.merge(newCertificate, existingCertificate);
} }
writeToFile(newCertificate.getInputStream(), certFile); long tag = writeToFile(newCertificate.getInputStream(), certFile);
return new Certificate(newCertificate.asCertificate(), tag);
return newCertificate.asCertificate();
} }
@Override @Override
public Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException, BadNameException { 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; KeyMaterial existingCertificate;
File certFile; File certFile;
try { try {
@ -298,16 +310,41 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec
throw new BadDataException(); throw new BadDataException();
} }
if (existingCertificate != null && !newCertificate.getTag().equals(existingCertificate.getTag())) { if (existingCertificate != null) {
newCertificate = merge.merge(newCertificate, existingCertificate); newCertificate = merge.merge(newCertificate, existingCertificate);
} }
writeToFile(newCertificate.getInputStream(), certFile); long tag = writeToFile(newCertificate.getInputStream(), certFile);
return new Certificate(newCertificate.asCertificate(), tag);
return newCertificate.asCertificate();
} }
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 { throws IOException {
certFile.getParentFile().mkdirs(); certFile.getParentFile().mkdirs();
if (!certFile.exists() && !certFile.createNewFile()) { if (!certFile.exists() && !certFile.createNewFile()) {
@ -324,6 +361,7 @@ public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirec
inputStream.close(); inputStream.close();
fileOut.close(); fileOut.close();
return getTag(certFile);
} }
public static class FilenameResolver { public static class FilenameResolver {

View File

@ -7,6 +7,7 @@ package pgp.cert_d.backend;
import pgp.cert_d.PGPCertificateDirectory; import pgp.cert_d.PGPCertificateDirectory;
import pgp.cert_d.SpecialNames; import pgp.cert_d.SpecialNames;
import pgp.certificate_store.certificate.Certificate; import pgp.certificate_store.certificate.Certificate;
import pgp.certificate_store.certificate.Key;
import pgp.certificate_store.certificate.KeyMaterial; import pgp.certificate_store.certificate.KeyMaterial;
import pgp.certificate_store.certificate.KeyMaterialMerger; import pgp.certificate_store.certificate.KeyMaterialMerger;
import pgp.certificate_store.certificate.KeyMaterialReaderBackend; import pgp.certificate_store.certificate.KeyMaterialReaderBackend;
@ -91,7 +92,7 @@ public class InMemoryCertificateDirectoryBackend implements PGPCertificateDirect
@Override @Override
public KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge) public KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge)
throws BadDataException, IOException { throws BadDataException, IOException {
KeyMaterial update = reader.read(data); KeyMaterial update = reader.read(data, null);
KeyMaterial existing = null; KeyMaterial existing = null;
try { try {
existing = readBySpecialName(SpecialNames.TRUST_ROOT); existing = readBySpecialName(SpecialNames.TRUST_ROOT);
@ -100,6 +101,11 @@ public class InMemoryCertificateDirectoryBackend implements PGPCertificateDirect
throw new RuntimeException(e); throw new RuntimeException(e);
} }
KeyMaterial merged = merge.merge(update, existing); 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); keyMaterialSpecialNameMap.put(SpecialNames.TRUST_ROOT, merged);
return merged; return merged;
} }
@ -108,9 +114,10 @@ public class InMemoryCertificateDirectoryBackend implements PGPCertificateDirect
@Override @Override
public Certificate doInsert(InputStream data, KeyMaterialMerger merge) public Certificate doInsert(InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException { throws IOException, BadDataException {
KeyMaterial update = reader.read(data); KeyMaterial update = reader.read(data, null);
Certificate existing = readByFingerprint(update.getFingerprint()); Certificate existing = readByFingerprint(update.getFingerprint());
Certificate merged = merge.merge(update, existing).asCertificate(); Certificate merged = merge.merge(update, existing).asCertificate();
merged = new Certificate(merged, System.currentTimeMillis());
certificateFingerprintMap.put(update.getFingerprint(), merged); certificateFingerprintMap.put(update.getFingerprint(), merged);
return merged; return merged;
} }
@ -118,10 +125,36 @@ public class InMemoryCertificateDirectoryBackend implements PGPCertificateDirect
@Override @Override
public Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) public Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge)
throws IOException, BadDataException, BadNameException { throws IOException, BadDataException, BadNameException {
KeyMaterial keyMaterial = reader.read(data); KeyMaterial keyMaterial = reader.read(data, null);
KeyMaterial existing = readBySpecialName(specialName); KeyMaterial existing = readBySpecialName(specialName);
KeyMaterial merged = merge.merge(keyMaterial, existing); 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); keyMaterialSpecialNameMap.put(specialName, merged);
return merged.asCertificate(); 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();
}
} }

View File

@ -5,12 +5,17 @@
package pgp.cert_d; package pgp.cert_d;
import org.bouncycastle.util.io.Streams; import org.bouncycastle.util.io.Streams;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource; 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.cert_d.subkey_lookup.InMemorySubkeyLookup;
import pgp.certificate_store.certificate.Certificate; import pgp.certificate_store.certificate.Certificate;
import pgp.certificate_store.certificate.Key; import pgp.certificate_store.certificate.Key;
import pgp.certificate_store.certificate.KeyMaterial; import pgp.certificate_store.certificate.KeyMaterial;
import pgp.certificate_store.certificate.KeyMaterialMerger;
import pgp.certificate_store.exception.BadDataException; import pgp.certificate_store.exception.BadDataException;
import pgp.certificate_store.exception.BadNameException; import pgp.certificate_store.exception.BadNameException;
import pgp.certificate_store.exception.NotAStoreException; import pgp.certificate_store.exception.NotAStoreException;
@ -18,6 +23,7 @@ import pgp.certificate_store.exception.NotAStoreException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.file.Files; 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.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; 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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
@ -37,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class PGPCertificateDirectoryTest { public class PGPCertificateDirectoryTest {
@SuppressWarnings("CharsetObjectCanBeUsed")
private static final Charset UTF8 = Charset.forName("UTF8"); private static final Charset UTF8 = Charset.forName("UTF8");
private static final String HARRY_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + 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"; "-----END PGP PUBLIC KEY BLOCK-----\n";
private static final String CEDRIC_FP = "5e75bf20646bc1a98d3b1bc2fe9cd472987c4021"; private static final String CEDRIC_FP = "5e75bf20646bc1a98d3b1bc2fe9cd472987c4021";
private static Stream<PGPCertificateDirectory> provideTestSubjects() throws IOException, NotAStoreException { private static final KeyMaterialMerger merger = new TestKeyMaterialMerger();
private static Stream<PGPCertificateDirectory> provideTestSubjects()
throws IOException, NotAStoreException {
PGPCertificateDirectory inMemory = PGPCertificateDirectories.inMemoryCertificateDirectory( PGPCertificateDirectory inMemory = PGPCertificateDirectories.inMemoryCertificateDirectory(
new TestKeyMaterialReaderBackend()); new TestKeyMaterialReaderBackend());
@ -159,18 +170,19 @@ public class PGPCertificateDirectoryTest {
@ParameterizedTest @ParameterizedTest
@MethodSource("provideTestSubjects") @MethodSource("provideTestSubjects")
public void lockDirectoryAndInsertWillFail(PGPCertificateDirectory directory) throws IOException, InterruptedException, BadDataException { public void lockDirectoryAndInsertWillFail(PGPCertificateDirectory directory)
throws IOException, InterruptedException, BadDataException {
// Manually lock the dir // Manually lock the dir
assertFalse(directory.backend.getLock().isLocked()); assertFalse(directory.backend.getLock().isLocked());
directory.backend.getLock().lockDirectory(); directory.backend.getLock().lockDirectory();
assertTrue(directory.backend.getLock().isLocked()); assertTrue(directory.backend.getLock().isLocked());
assertFalse(directory.backend.getLock().tryLockDirectory()); 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); assertNull(inserted);
directory.backend.getLock().releaseDirectory(); 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); assertNotNull(inserted);
} }
@ -188,7 +200,7 @@ public class PGPCertificateDirectoryTest {
ByteArrayInputStream bytesIn = new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)); 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"); assertEquals(CEDRIC_FP, certificate.getFingerprint(), "Fingerprint of inserted cert MUST match");
Certificate get = directory.getByFingerprint(CEDRIC_FP); Certificate get = directory.getByFingerprint(CEDRIC_FP);
@ -207,7 +219,7 @@ public class PGPCertificateDirectoryTest {
assertNull(directory.getTrustRoot()); assertNull(directory.getTrustRoot());
KeyMaterial trustRootMaterial = directory.insertTrustRoot( KeyMaterial trustRootMaterial = directory.insertTrustRoot(
new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8)), new TestKeyMaterialMerger()); new ByteArrayInputStream(HARRY_KEY.getBytes(UTF8)), merger);
assertNotNull(trustRootMaterial); assertNotNull(trustRootMaterial);
assertTrue(trustRootMaterial instanceof Key); assertTrue(trustRootMaterial instanceof Key);
assertEquals(HARRY_FP, trustRootMaterial.getFingerprint()); assertEquals(HARRY_FP, trustRootMaterial.getFingerprint());
@ -217,8 +229,8 @@ public class PGPCertificateDirectoryTest {
Certificate trustRootCert = directory.getTrustRootCertificate(); Certificate trustRootCert = directory.getTrustRootCertificate();
assertEquals(HARRY_FP, trustRootCert.getFingerprint()); assertEquals(HARRY_FP, trustRootCert.getFingerprint());
directory.tryInsert(new ByteArrayInputStream(RON_CERT.getBytes(UTF8)), new TestKeyMaterialMerger()); directory.tryInsert(new ByteArrayInputStream(RON_CERT.getBytes(UTF8)), merger);
directory.insert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), new TestKeyMaterialMerger()); directory.insert(new ByteArrayInputStream(CEDRIC_CERT.getBytes(UTF8)), merger);
Set<String> expected = new HashSet<>(Arrays.asList(RON_FP, CEDRIC_FP)); Set<String> expected = new HashSet<>(Arrays.asList(RON_FP, CEDRIC_FP));
@ -230,4 +242,104 @@ public class PGPCertificateDirectoryTest {
assertEquals(expected, actual); 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));
}
} }

View File

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: Apache-2.0 // 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.KeyMaterial;
import pgp.certificate_store.certificate.KeyMaterialMerger; import pgp.certificate_store.certificate.KeyMaterialMerger;

View File

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package pgp.cert_d; package pgp.cert_d.dummy;
import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPKeyRing;
@ -33,24 +33,22 @@ public class TestKeyMaterialReaderBackend implements KeyMaterialReaderBackend {
KeyFingerPrintCalculator fpCalc = new BcKeyFingerprintCalculator(); KeyFingerPrintCalculator fpCalc = new BcKeyFingerprintCalculator();
@Override @Override
public KeyMaterial read(InputStream data) throws IOException, BadDataException { public KeyMaterial read(InputStream data, Long tag) throws IOException, BadDataException {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
Streams.pipeAll(data, out); Streams.pipeAll(data, out);
try { try {
Key key = readKey(new ByteArrayInputStream(out.toByteArray())); return readKey(new ByteArrayInputStream(out.toByteArray()), tag);
return key;
} catch (IOException | PGPException e) { } catch (IOException | PGPException e) {
try { try {
Certificate certificate = readCertificate(new ByteArrayInputStream(out.toByteArray())); return readCertificate(new ByteArrayInputStream(out.toByteArray()), tag);
return certificate;
} catch (IOException e1) { } catch (IOException e1) {
throw new BadDataException(); 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(); ByteArrayOutputStream buffer = new ByteArrayOutputStream();
Streams.pipeAll(inputStream, buffer); Streams.pipeAll(inputStream, buffer);
inputStream.close(); inputStream.close();
@ -60,64 +58,21 @@ public class TestKeyMaterialReaderBackend implements KeyMaterialReaderBackend {
PGPSecretKeyRing secretKeys = new PGPSecretKeyRing(decoderStream, fpCalc); PGPSecretKeyRing secretKeys = new PGPSecretKeyRing(decoderStream, fpCalc);
PGPPublicKeyRing cert = extractCert(secretKeys); PGPPublicKeyRing cert = extractCert(secretKeys);
ByteArrayInputStream encoded = new ByteArrayInputStream(cert.getEncoded()); ByteArrayInputStream encoded = new ByteArrayInputStream(cert.getEncoded());
Certificate certificate = readCertificate(encoded); Certificate certificate = readCertificate(encoded, tag);
return new Key() { return new Key(buffer.toByteArray(), certificate, tag);
@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<Long> getSubkeyIds() throws IOException {
return certificate.getSubkeyIds();
}
};
} }
private Certificate readCertificate(InputStream inputStream) throws IOException { private Certificate readCertificate(InputStream inputStream, Long tag) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream(); ByteArrayOutputStream buffer = new ByteArrayOutputStream();
Streams.pipeAll(inputStream, buffer); Streams.pipeAll(inputStream, buffer);
ByteArrayInputStream in = new ByteArrayInputStream(buffer.toByteArray()); ByteArrayInputStream in = new ByteArrayInputStream(buffer.toByteArray());
InputStream decoderStream = PGPUtil.getDecoderStream(in); InputStream decoderStream = PGPUtil.getDecoderStream(in);
PGPPublicKeyRing cert = new PGPPublicKeyRing(decoderStream, fpCalc); PGPPublicKeyRing cert = new PGPPublicKeyRing(decoderStream, fpCalc);
return new Certificate() { String fingerprint = Hex.toHexString(cert.getPublicKey().getFingerprint()).toLowerCase();
@Override List<Long> subKeyIds = getSubkeyIds(cert);
public String getFingerprint() { return new Certificate(buffer.toByteArray(), fingerprint, subKeyIds, tag);
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<Long> getSubkeyIds() throws IOException {
return TestKeyMaterialReaderBackend.getSubkeyIds(cert);
}
};
} }
private PGPPublicKeyRing extractCert(PGPSecretKeyRing secretKeys) { private PGPPublicKeyRing extractCert(PGPSecretKeyRing secretKeys) {
@ -126,8 +81,7 @@ public class TestKeyMaterialReaderBackend implements KeyMaterialReaderBackend {
while (publicKeyIterator.hasNext()) { while (publicKeyIterator.hasNext()) {
publicKeyList.add(publicKeyIterator.next()); publicKeyList.add(publicKeyIterator.next());
} }
PGPPublicKeyRing publicKeyRing = new PGPPublicKeyRing(publicKeyList); return new PGPPublicKeyRing(publicKeyList);
return publicKeyRing;
} }
private static List<Long> getSubkeyIds(PGPKeyRing keyRing) { private static List<Long> getSubkeyIds(PGPKeyRing keyRing) {

View File

@ -32,6 +32,23 @@ public interface PGPCertificateStore {
Certificate getCertificate(String identifier) Certificate getCertificate(String identifier)
throws IOException, BadNameException, BadDataException; 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 * Return an {@link Iterator} over all certificates in the store that contain a subkey with the given
* subkey id. * subkey id.
@ -42,7 +59,7 @@ public interface PGPCertificateStore {
* @throws BadDataException if any of the certificate files contains invalid data * @throws BadDataException if any of the certificate files contains invalid data
*/ */
Iterator<Certificate> getCertificatesBySubkeyId(long subkeyId) Iterator<Certificate> getCertificatesBySubkeyId(long subkeyId)
throws IOException, BadDataException; throws IOException, BadDataException;
/** /**
* Insert a certificate into the store. * Insert a certificate into the store.

View File

@ -4,13 +4,67 @@
package pgp.certificate_store.certificate; package pgp.certificate_store.certificate;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.List;
/** /**
* OpenPGP certificate (public key). * 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<Long> subkeyIds;
private final Long tag;
/**
* Certificate constructor.
*
* @param bytes encoding of the certificate
* @param fingerprint fingerprint (lowercase hex characters)
* @param subkeyIds list of subkey ids
* @param tag tag
*/
public Certificate(byte[] bytes, String fingerprint, List<Long> subkeyIds, Long tag) {
this.bytes = bytes;
this.fingerprint = fingerprint;
this.subkeyIds = subkeyIds;
this.tag = tag;
}
/**
* Copy constructor to assign a new tag to the {@link Certificate}.
*
* @param cert certificate
* @param tag tag
*/
public Certificate(Certificate cert, Long tag) {
this(cert.bytes, cert.fingerprint, cert.subkeyIds, tag);
}
@Override
public String getFingerprint() {
return fingerprint;
}
@Override @Override
public Certificate asCertificate() { public Certificate asCertificate() {
return this; return this;
} }
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(bytes);
}
@Override
public Long getTag() {
return tag;
}
@Override
public List<Long> getSubkeyIds() {
return subkeyIds;
}
} }

View File

@ -4,21 +4,74 @@
package pgp.certificate_store.certificate; package pgp.certificate_store.certificate;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.List;
/** /**
* OpenPGP key (secret key). * 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 the certificate part of this OpenPGP key.
* *
* @return OpenPGP certificate * @return OpenPGP certificate
*/ */
public abstract Certificate getCertificate(); public Certificate getCertificate() {
return new Certificate(certificate, getTag());
}
@Override
public String getFingerprint() {
return certificate.getFingerprint();
}
@Override @Override
public Certificate asCertificate() { public Certificate asCertificate() {
return getCertificate(); return getCertificate();
} }
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(bytes);
}
@Override
public Long getTag() {
return tag;
}
@Override
public List<Long> getSubkeyIds() {
return certificate.getSubkeyIds();
}
} }

View File

@ -4,7 +4,6 @@
package pgp.certificate_store.certificate; package pgp.certificate_store.certificate;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -19,23 +18,34 @@ public interface KeyMaterial {
*/ */
String getFingerprint(); 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(); Certificate asCertificate();
/** /**
* Return an {@link InputStream} of the binary representation of the secret key. * Return an {@link InputStream} of the binary representation of the secret key.
* *
* @return input stream * @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 a {@link Set} containing key-ids of subkeys.
* *
* @return subkeys * @return subkeys
* @throws IOException in case of an IO error
*/ */
List<Long> getSubkeyIds() throws IOException; List<Long> getSubkeyIds();
} }

View File

@ -20,5 +20,5 @@ public interface KeyMaterialReaderBackend {
* @throws IOException in case of an IO error * @throws IOException in case of an IO error
* @throws BadDataException in case that the data stream does not contain a valid OpenPGP key/certificate * @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;
} }

View File

@ -6,7 +6,7 @@ allprojects {
ext { ext {
shortVersion = '0.1.2' shortVersion = '0.1.2'
isSnapshot = true isSnapshot = true
minAndroidSdk = 10 minAndroidSdk = 26
javaSourceCompatibility = 1.8 javaSourceCompatibility = 1.8
bouncycastleVersion = '1.71' bouncycastleVersion = '1.71'
slf4jVersion = '1.7.36' slf4jVersion = '1.7.36'