From e177dafd60382cd3e2695dab3b3fcff2e0cefcc4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 28 Dec 2021 14:07:27 +0100 Subject: [PATCH 0001/1204] PGPainless-1.0.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index cf20b587..0f1f88dc 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.0.0' - isSnapshot = false + shortVersion = '1.0.1' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From b58bdf8ff15d50d26bb552afdd65e54d3f260114 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 15 Jan 2022 00:59:01 +0100 Subject: [PATCH 0002/1204] Fix KeyAccessor.ViaKeyId sourcing primary user-id signature --- .../java/org/pgpainless/key/info/KeyAccessor.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java index bcdac8b6..6c299580 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java @@ -110,18 +110,13 @@ public abstract class KeyAccessor { @Override public @Nonnull PGPSignature getSignatureWithPreferences() { - PGPSignature signature; - if (key.getPrimaryKeyId() != key.getSubkeyId()) { - signature = info.getCurrentSubkeyBindingSignature(key.getSubkeyId()); - } else { + String primaryUserId = info.getPrimaryUserId(); + // If the key is located by Key ID, the algorithm of the primary User ID of the key provides the + // preferred symmetric algorithm. + PGPSignature signature = info.getLatestUserIdCertification(primaryUserId); + if (signature == null) { signature = info.getLatestDirectKeySelfSignature(); } - - if (signature != null) { - return signature; - } - - signature = info.getLatestUserIdCertification(info.getPrimaryUserId()); if (signature == null) { throw new IllegalStateException("No valid signature found."); } From d264982388803f4ccf35a5610b73d629e47c6f62 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 15 Jan 2022 01:02:59 +0100 Subject: [PATCH 0003/1204] PGPainless 1.0.1 --- CHANGELOG.md | 3 +++ README.md | 2 +- version.gradle | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 386b1a20..43aa5ddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.0.1 +- Fix sourcing of preferred algorithms by primary user-id when key is located via key-id + ## 1.0.0 - Introduce `DateUtil.toSecondsPrecision()` - Clean JUnit tests, fix code style issues and fix typos in documentation diff --git a/README.md b/README.md index 2a59ffc2..d2c01e3f 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.0.0' + implementation 'org.pgpainless:pgpainless-core:1.0.1' } ``` diff --git a/version.gradle b/version.gradle index 0f1f88dc..480322d8 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.0.1' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From 1cbeafb3adf9b910ee613658c6100e698f4d57a1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 15 Jan 2022 01:06:18 +0100 Subject: [PATCH 0004/1204] PGPainless-1.0.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 480322d8..7c12e305 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.0.1' - isSnapshot = false + shortVersion = '1.0.2' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From 9de196d6c5672ce20bff852005c1a34a992fbf90 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 15 Jan 2022 02:29:39 +0100 Subject: [PATCH 0005/1204] Fix test for algorithm preference extraction --- ...ymmetricAlgorithmDuringEncryptionTest.java | 121 ++++++++---------- 1 file changed, 53 insertions(+), 68 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/RespectPreferredSymmetricAlgorithmDuringEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/RespectPreferredSymmetricAlgorithmDuringEncryptionTest.java index be80c4e2..55564d84 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/RespectPreferredSymmetricAlgorithmDuringEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/RespectPreferredSymmetricAlgorithmDuringEncryptionTest.java @@ -19,80 +19,41 @@ public class RespectPreferredSymmetricAlgorithmDuringEncryptionTest { @Test public void algorithmPreferencesAreRespectedDependingOnEncryptionTarget() throws IOException, PGPException { - // Key has [AES128] as preferred symm. algo on latest user-id cert - String key = "-----BEGIN PGP ARMORED FILE-----\n" + - "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + + // Key has AES256, AES192, AES128 as primary user-ids sym algo prefs, + // and AES128 as secondary user-id prefs + String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 7E13 2E9C EAE8 7E7B AD6C 5329 94CE B847 EEFB 044B\n" + + "Comment: Bob Babbage \n" + "\n" + - "xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + - "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + - "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + - "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + - "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + - "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + - "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + - "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + - "vLIwa3T4CyshfT0AEQEAAc0hQm9iIEJhYmJhZ2UgPGJvYkBvcGVucGdwLmV4YW1w\n" + - "bGU+wsFTBBMBCgCHBYJgq9xiAgsHCRD7/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5v\n" + - "dGF0aW9ucy5zZXF1b2lhLXBncC5vcmdNgMRYEX46LCBpUimr3zIek/oZSVT+EcdR\n" + - "Y4Rno2QSzQYVCgkICwIEFgIDAQIXgAIbAwIeARYhBNGmbhojsYLJmA94jPv8yCoB\n" + - "XnMwAADsbAv/bpWiiT47IuGxe11aReA2ThLy8jwafKEOrHxiUvyJdG/s7Bn0QtqM\n" + - "9G/16QDOWbSiXMD2vJYB7ml7oYlSxDS6oVd1bfGRsRbRr6N/wCTMXBaB4TsYqbcl\n" + - "NOznt+RSRIWYKCHJDDEdBvuJmf+Mmi09NVHOupjOt51WiVWmm5GpVUl5789yBvN8\n" + - "iei7I85KB/bXV0CfUgw9jx8BwAANPri+l4Br5fKMoheguHBm8BLPzWCfvCxZORq5\n" + - "Nd9wLhEe+/7M2Y8AGzfn88XgGUXNOh7y8ZSD9AjK14UQilUg8IrYm7oJik29bVyh\n" + - "UyY7sAJB5B7TxjE374krsOkl+lXe6bWDguJhrjIR0S0OWXmFpt06uDIOuI+f6ach\n" + - "m0kbUELUiQOQ+4i17mph11WiQczT2iS7preLpI5cjQd1cIQczOjxDaRvNPvtxYne\n" + - "ijUCkQzPwGAAcuXRe94wW3VtimwswLM5wmhzCgjv7uZMvEg6lHpVRWrJA6oXj6f1\n" + - "MnufQ5Li2/zMwsEOBBMBCgA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE\n" + - "0aZuGiOxgsmYD3iM+/zIKgFeczAFAl2lnvoACgkQ+/zIKgFeczBvbAv/VNk90a6h\n" + - "G8Od9xTzXxH5YRFUSGfIA1yjPIVOnKqhMwps2U+sWE3urL+MvjyQRlyRV8oY9IOh\n" + - "Q5Esm6DOZYrTnE7qVETm1ajIAP2OFChEc55uH88x/anpPOXOJY7S8jbn3naC9qad\n" + - "75BrZ+3g9EBUWiy5p8TykP05WSnSxNRt7vFKLfEB4nGkehpwHXOVF0CRNwYle42b\n" + - "g8lpmdXFDcCZCi+qEbafmTQzkAqyzS3nCh3IAqq6Y0kBuaKLm2tSNUOlZbD+OHYQ\n" + - "NZ5Jix7cZUzs6Xh4+I55NRWl5smrLq66yOQoFPy9jot/Qxikx/wP3MsAzeGaZSEP\n" + - "c0fHp5G16rlGbxQ3vl8/usUV7W+TMEMljgwd5x8POR6HC8EaCDfVnUBCPi/Gv+eg\n" + - "LjsIbPJZZEroiE40e6/UoCiQtlpQB5exPJYSd1Q1txCwueih99PHepsDhmUQKiAC\n" + - "szNU+RRozAYau2VdHqnRJ7QYdxHDiH49jPK4NTMyb/tJh2TiIwcmsIpGzsDNBF2l\n" + - "nPIBDADWML9cbGMrp12CtF9b2P6z9TTT74S8iyBOzaSvdGDQY/sUtZXRg21HWamX\n" + - "nn9sSXvIDEINOQ6A9QxdxoqWdCHrOuW3ofneYXoG+zeKc4dC86wa1TR2q9vW+RMX\n" + - "SO4uImA+Uzula/6k1DogDf28qhCxMwG/i/m9g1c/0aApuDyKdQ1PXsHHNlgd/Dn6\n" + - "rrd5y2AObaifV7wIhEJnvqgFXDN2RXGjLeCOHV4Q2WTYPg/S4k1nMXVDwZXrvIsA\n" + - "0YwIMgIT86Rafp1qKlgPNbiIlC1g9RY/iFaGN2b4Ir6GDohBQSfZW2+LXoPZuVE/\n" + - "wGlQ01rh827KVZW4lXvqsge+wtnWlszcselGATyzqOK9LdHPdZGzROZYI2e8c+pa\n" + - "LNDdVPL6vdRBUnkCaEkOtl1mr2JpQi5nTU+gTX4IeInC7E+1a9UDF/Y85ybUz8XV\n" + - "8rUnR76UqVC7KidNepdHbZjjXCt8/Zo+Tec9JNbYNQB/e9ExmDntmlHEsSEQzFwz\n" + - "j8sxH48AEQEAAcLA9gQYAQoAIBYhBNGmbhojsYLJmA94jPv8yCoBXnMwBQJdpZzy\n" + - "AhsMAAoJEPv8yCoBXnMw6f8L/26C34dkjBffTzMj5Bdzm8MtF67OYneJ4TQMw7+4\n" + - "1IL4rVcSKhIhk/3Ud5knaRtP2ef1+5F66h9/RPQOJ5+tvBwhBAcUWSupKnUrdVaZ\n" + - "QanYmtSxcVV2PL9+QEiNN3tzluhaWO//rACxJ+K/ZXQlIzwQVTpNhfGzAaMVV9zp\n" + - "f3u0k14itcv6alKY8+rLZvO1wIIeRZLmU0tZDD5HtWDvUV7rIFI1WuoLb+KZgbYn\n" + - "3OWjCPHVdTrdZ2CqnZbG3SXw6awH9bzRLV9EXkbhIMez0deCVdeo+wFFklh8/5VK\n" + - "2b0vk/+wqMJxfpa1lHvJLobzOP9fvrswsr92MA2+k901WeISR7qEzcI0Fdg8AyFA\n" + - "ExaEK6VyjP7SXGLwvfisw34OxuZr3qmx1Sufu4toH3XrB7QJN8XyqqbsGxUCBqWi\n" + - "f9RSK4xjzRTe56iPeiSJJOIciMP9i2ldI+KgLycyeDvGoBj0HCLO3gVaBe4ubVrj\n" + - "5KjhX2PVNEJd3XZRzaXZE2aAMQ==\n" + - "=d5ke\n" + - "-----END PGP ARMORED FILE-----\n"; + "mDMEYeIhnhYJKwYBBAHaRw8BAQdAfs9SkOSEyAQmvwLwwUPCp3Qiw2t4rm+e7n8t\n" + + "oVjAmle0IUJvYiBCYWJiYWdlIDxib2JAb3BlbnBncC5leGFtcGxlPoiPBBMWCgBB\n" + + "BQJh4iGeCZCUzrhH7vsESxahBH4TLpzq6H57rWxTKZTOuEfu+wRLAp4BApsBBZYC\n" + + "AwEABIsJCAcFlQoJCAsCmQEAAKK/AP4lCifuXpZIUR4PrenGBZFtoZpB5s1i/YrB\n" + + "cnCuodQX9wEAyENhlXNYopWdgBZ9g4E1Y0cJfpwCwWhx0DeATmrSzAO0H0JvYmJ5\n" + + "MTI4IDxib2JieUBhZXMxMjguZXhhbXBsZT6IigQTFgoAPAUCYeIhngmQlM64R+77\n" + + "BEsWoQR+Ey6c6uh+e61sUymUzrhH7vsESwKeAQKbAQWWAgMBAAKLBwWVCgkICwAA\n" + + "y0wBAIhAEpQgJRizHitPx3WUpIYbKq3R5jAO34NnlmTzNVj6AP9aWHPsW5r7HuQh\n" + + "xJz+8zdCOuAxKv6tvHthSWJ64VWDDrg4BGHiIZ4SCisGAQQBl1UBBQEBB0CEIv13\n" + + "/qTXR0wiUG5DVZCWh/KLKrF5TemUfYXA/kBTOAMBCAeIdQQYFgoAHQUCYeIhngKe\n" + + "AQKbDAWWAgMBAASLCQgHBZUKCQgLAAoJEJTOuEfu+wRLwC4A/0/VDPPDE6kT/8C3\n" + + "9d8ekZkQE38o2nC58E62AjM5O2x6AQDMd0gcoKIxPi9uRi3nVsNS233a3MxFEjpe\n" + + "qqgyBnqxBLgzBGHiIZ4WCSsGAQQB2kcPAQEHQP7IGdT9moutwtys4A/ndkWJVWn/\n" + + "zkoOn3cSad1bP8y8iNUEGBYKAH0FAmHiIZ4CngECmwIFlgIDAQAEiwkIBwWVCgkI\n" + + "C18gBBkWCgAGBQJh4iGeAAoJENcuZc0+RPVgrucBAI+IzpplBIpySOIyzHJdjeFt\n" + + "ikwTBOY3OTriY2Z62Ec6AQDhVxO7LZuH3mTCklj4HelfMrhlqUlnYr7qCIjzI5BY\n" + + "BwAKCRCUzrhH7vsES4snAP4qzlEbaHpN7ZPomCOHD7J2+CHlyTtsRP45XWVCqNH1\n" + + "jAEAmzz5Lu67k97AzArpoGHgYh492w5BfdApV8BCaTW4AgI=\n" + + "=XwJQ\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); - // Encrypt to the user-id - // PGPainless should extract algorithm preferences from the latest user-id sig in this case (AES-128) - ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign().onOutputStream(out) - .withOptions( - ProducerOptions.encrypt(new EncryptionOptions() - .addRecipient(publicKeys, "Bob Babbage ") - )); - - encryptionStream.close(); - assertEquals(SymmetricKeyAlgorithm.AES_128, encryptionStream.getResult().getEncryptionAlgorithm()); - // Encrypt without specifying user-id - // PGPainless should now inspect the subkey binding sig for algorithm preferences (AES256, AES192, AES128) - out = new ByteArrayOutputStream(); - encryptionStream = PGPainless.encryptAndOrSign().onOutputStream(out) + // PGPainless now inspects the primary user-ids signature to get sym alg prefs (AES256, AES192, AES128) + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign().onOutputStream(out) .withOptions( ProducerOptions.encrypt(new EncryptionOptions() .addRecipient(publicKeys) // no user-id passed @@ -100,5 +61,29 @@ public class RespectPreferredSymmetricAlgorithmDuringEncryptionTest { encryptionStream.close(); assertEquals(SymmetricKeyAlgorithm.AES_256, encryptionStream.getResult().getEncryptionAlgorithm()); + + // Encrypt to the primary user-id + // PGPainless should extract algorithm preferences from the latest user-id sig in this case (AES-256, AES-192, AES-128) + out = new ByteArrayOutputStream(); + encryptionStream = PGPainless.encryptAndOrSign().onOutputStream(out) + .withOptions( + ProducerOptions.encrypt(new EncryptionOptions() + .addRecipient(publicKeys, "Bob Babbage ") + )); + + encryptionStream.close(); + assertEquals(SymmetricKeyAlgorithm.AES_256, encryptionStream.getResult().getEncryptionAlgorithm()); + + // Encrypt to the secondary user-id + // PGPainless extracts algorithm preferences from secondary user-id sig, in this case AES-128 + out = new ByteArrayOutputStream(); + encryptionStream = PGPainless.encryptAndOrSign().onOutputStream(out) + .withOptions( + ProducerOptions.encrypt(new EncryptionOptions() + .addRecipient(publicKeys, "Bobby128 ") + )); + + encryptionStream.close(); + assertEquals(SymmetricKeyAlgorithm.AES_128, encryptionStream.getResult().getEncryptionAlgorithm()); } } From e7f583c1af3da277db2099191714de19bb27aedf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 15 Jan 2022 02:43:59 +0100 Subject: [PATCH 0006/1204] Fix KeyRingInfo.get*Algorithm(keyId) --- .../org/pgpainless/key/info/KeyAccessor.java | 22 +++++++++++++++++++ .../org/pgpainless/key/info/KeyRingInfo.java | 11 +++++----- .../pgpainless/key/info/KeyRingInfoTest.java | 12 +++++----- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java index 6c299580..5fa71d46 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java @@ -123,4 +123,26 @@ public abstract class KeyAccessor { return signature; } } + + public static class SubKey extends KeyAccessor { + + public SubKey(KeyRingInfo info, SubkeyIdentifier key) { + super(info, key); + } + + @Override + public @Nonnull PGPSignature getSignatureWithPreferences() { + PGPSignature signature; + if (key.getPrimaryKeyId() == key.getSubkeyId()) { + signature = info.getLatestDirectKeySelfSignature(); + if (signature == null) { + signature = info.getLatestUserIdCertification(info.getPrimaryUserId()); + } + } else { + signature = info.getCurrentSubkeyBindingSignature(key.getSubkeyId()); + } + + return signature; + } + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 56737b39..74b72e61 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -975,7 +975,8 @@ public class KeyRingInfo { } public Set getPreferredHashAlgorithms(long keyId) { - return getKeyAccessor(null, keyId).getPreferredHashAlgorithms(); + return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)) + .getPreferredHashAlgorithms(); } public Set getPreferredSymmetricKeyAlgorithms() { @@ -987,7 +988,7 @@ public class KeyRingInfo { } public Set getPreferredSymmetricKeyAlgorithms(long keyId) { - return getKeyAccessor(null, keyId).getPreferredSymmetricKeyAlgorithms(); + return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)).getPreferredSymmetricKeyAlgorithms(); } public Set getPreferredCompressionAlgorithms() { @@ -999,15 +1000,15 @@ public class KeyRingInfo { } public Set getPreferredCompressionAlgorithms(long keyId) { - return getKeyAccessor(null, keyId).getPreferredCompressionAlgorithms(); + return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)).getPreferredCompressionAlgorithms(); } private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) { if (getPublicKey(keyID) == null) { - throw new IllegalArgumentException("No subkey with key id " + Long.toHexString(keyID) + " found on this key."); + throw new NoSuchElementException("No subkey with key id " + Long.toHexString(keyID) + " found on this key."); } if (userId != null && !getUserIds().contains(userId)) { - throw new IllegalArgumentException("No user-id '" + userId + "' found on this key."); + throw new NoSuchElementException("No user-id '" + userId + "' found on this key."); } return userId == null ? new KeyAccessor.ViaKeyId(this, new SubkeyIdentifier(keys, keyID)) : new KeyAccessor.ViaUserId(this, new SubkeyIdentifier(keys, keyID), userId); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java index 16b338e7..0cb351b7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java @@ -598,9 +598,9 @@ public class KeyRingInfoTest { KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); // Bob is an invalid userId - assertThrows(IllegalArgumentException.class, () -> info.getPreferredSymmetricKeyAlgorithms("Bob")); + assertThrows(NoSuchElementException.class, () -> info.getPreferredSymmetricKeyAlgorithms("Bob")); // 123 is an invalid keyid - assertThrows(IllegalArgumentException.class, () -> info.getPreferredSymmetricKeyAlgorithms(123L)); + assertThrows(NoSuchElementException.class, () -> info.getPreferredSymmetricKeyAlgorithms(123L)); assertEquals(preferredHashAlgorithms, info.getPreferredHashAlgorithms("Alice")); assertEquals(preferredHashAlgorithms, info.getPreferredHashAlgorithms(pkid)); @@ -608,9 +608,9 @@ public class KeyRingInfoTest { assertEquals(preferredHashAlgorithms, info.getPreferredHashAlgorithms(skid2)); // Bob is an invalid userId - assertThrows(IllegalArgumentException.class, () -> info.getPreferredCompressionAlgorithms("Bob")); + assertThrows(NoSuchElementException.class, () -> info.getPreferredCompressionAlgorithms("Bob")); // 123 is an invalid keyid - assertThrows(IllegalArgumentException.class, () -> info.getPreferredCompressionAlgorithms(123L)); + assertThrows(NoSuchElementException.class, () -> info.getPreferredCompressionAlgorithms(123L)); assertEquals(preferredCompressionAlgorithms, info.getPreferredCompressionAlgorithms("Alice")); assertEquals(preferredCompressionAlgorithms, info.getPreferredCompressionAlgorithms(pkid)); @@ -618,9 +618,9 @@ public class KeyRingInfoTest { assertEquals(preferredCompressionAlgorithms, info.getPreferredCompressionAlgorithms(skid2)); // Bob is an invalid userId - assertThrows(IllegalArgumentException.class, () -> info.getPreferredSymmetricKeyAlgorithms("Bob")); + assertThrows(NoSuchElementException.class, () -> info.getPreferredSymmetricKeyAlgorithms("Bob")); // 123 is an invalid keyid - assertThrows(IllegalArgumentException.class, () -> info.getPreferredSymmetricKeyAlgorithms(123L)); + assertThrows(NoSuchElementException.class, () -> info.getPreferredSymmetricKeyAlgorithms(123L)); assertEquals(preferredSymmetricAlgorithms, info.getPreferredSymmetricKeyAlgorithms("Alice")); assertEquals(preferredSymmetricAlgorithms, info.getPreferredSymmetricKeyAlgorithms(pkid)); From d9e3c6ed91503f066c77e54d9c85cf531eec9a7e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 1 Jan 2022 18:11:00 +0100 Subject: [PATCH 0007/1204] Remove investigative test with expired key --- .../InvestigateThunderbirdDecryption.java | 257 ------------------ 1 file changed, 257 deletions(-) delete mode 100644 pgpainless-core/src/test/java/investigations/InvestigateThunderbirdDecryption.java diff --git a/pgpainless-core/src/test/java/investigations/InvestigateThunderbirdDecryption.java b/pgpainless-core/src/test/java/investigations/InvestigateThunderbirdDecryption.java deleted file mode 100644 index d96f7f48..00000000 --- a/pgpainless-core/src/test/java/investigations/InvestigateThunderbirdDecryption.java +++ /dev/null @@ -1,257 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package investigations; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Date; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.encryption_signing.EncryptionOptions; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.util.ArmorUtils; - -public class InvestigateThunderbirdDecryption { - - String OUR_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: PGPainless\n" + - "Comment: 47D2 3A5E 1455 1FD2 0599 C1FC B57B 5451 9E2D 8FE4\n" + - "Comment: Alice \n" + - "\n" + - "lFgEYP8FlBYJKwYBBAHaRw8BAQdAeJ7fL4TbpSLUJsxGUFnN5MzDZr3lKoKWEO+z\n" + - "hQEFPqcAAP0T8ED8kcch++7UpcN7qZMP4ihbE9Fu9kp/IKOCZDVwGhF+tBxBbGlj\n" + - "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iHgEExYKACAFAmD/BZQCGwEFFgIDAQAE\n" + - "CwkIBwUVCgkICwIeAQIZAQAKCRC1e1RRni2P5PYqAQC/r4R4RFfVIOPAc16PiffO\n" + - "GDMzRUYAjIyflvOBIEE//QEAsZGQzIstdIp8gY5CF27pbnnSAA/OGPXbDsNArzPN\n" + - "tQicXQRg/wWUEgorBgEEAZdVAQUBAQdAFHEP5NzgON0usvHOsTsROojwVTAqgayc\n" + - "fdPdb597u3UDAQgHAAD/ShtbTmAyZJDjcEDfUNblOogyWntCEgb18Cs5rRm1+agP\n" + - "mIh1BBgWCgAdBQJg/wWUAhsMBRYCAwEABAsJCAcFFQoJCAsCHgEACgkQtXtUUZ4t\n" + - "j+SWdwD/cCXm/ufcaIMMOqRw10Lwefc4euOrpFScWA0rUjnK6yEBAMOH1kGHlLbz\n" + - "mk6D7RbBDdC3aW4xGRjSYBkyhbuxevsDnFgEYP8FlBYJKwYBBAHaRw8BAQdAmuvN\n" + - "FF+pklSxw3+VVqVu2g2ulpJE7HldtU/Jud/jiEgAAP0RPh7QWqm2hhY6vBNr8fhz\n" + - "3GBAfZ4A9HxVymuu1M6qMxEdiNUEGBYKAH0FAmD/BZQCGwIFFgIDAQAECwkIBwUV\n" + - "CgkICwIeAV8gBBkWCgAGBQJg/wWUAAoJEIYvdZaRbR0mBesA/2dxyf9vfRnyrNcm\n" + - "dguMzYe9oLfD2SU2Sa0jXcURQ+A6AP9uYaehPZvEH0kwdeSi60uCOVznCePrY1mK\n" + - "M6UEDMPGBwAKCRC1e1RRni2P5J1FAQDhI3tN5C/klh2j8ptQ7ht0LPlbgVU/WmT8\n" + - "kqejd80WVgEA4dg7MZTk+uzwOWEGIHyxWXRzma9a5k1kM+uxX3RflQU=\n" + - "=IEzi\n" + - "-----END PGP PRIVATE KEY BLOCK-----"; - - String THEIR_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "Version: FlowCrypt [BUILD_REPLACEABLE_VERSION] Gmail Encryption\n" + - "Comment: Seamlessly send and receive encrypted email\n" + - "\n" + - "xjMEYL/CRRYJKwYBBAHaRw8BAQdAxaJrnD/gWRGqaAVtQ8R9PI0ZGu/YESJ4\n" + - "HsJeeCxUZOvNF0RlbiA8ZGVuQGZsb3djcnlwdC5jb20+wn4EExYKACYFAmC/\n" + - "wkUCGwMFFgIDAQAECwkIBwUVCgkICwIeAQIZAQWJAQ/XOQAKCRCGwF2G4DXc\n" + - "cttHAP9Axna+jmFhZEajILW7BZ8UJpgz7mCC48RMtRj/pre4nQD/bKJXB+sD\n" + - "zti+tRbi7KNncgkSQeau+Vy/ZnpBUUHBWwjOOARgv8JFEgorBgEEAZdVAQUB\n" + - "AQdA3dN8Hh18Pqd6OevXWl36y7cM58ZRmUVEEZukXRIholYDAQgHwnUEGBYK\n" + - "AB0FAmC/wkUCGwwFFgIDAQAECwkIBwUVCgkICwIeAQAKCRCGwF2G4DXcclpK\n" + - "AQC0uUHWUFNao1Fl85+4c8WecGKsGCihNU9H3q+I1gz22gEAtVo1dWnc0t1f\n" + - "h1MUYq5FmME+KeFCBZZ9lrMAxRhvigI=\n" + - "=+XVJ\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"; - - @Test - public void generateMessage() throws PGPException, IOException { - // CHECKSTYLE:OFF - System.out.println("Decryption Key"); - System.out.println(OUR_KEY); - // CHECKSTYLE:ON - - PGPSecretKeyRing ourKey = PGPainless.readKeyRing().secretKeyRing(OUR_KEY); - PGPPublicKeyRing ourCert = PGPainless.extractCertificate(ourKey); - PGPPublicKeyRing theirCert = PGPainless.readKeyRing().publicKeyRing(THEIR_CERT); - - // CHECKSTYLE:OFF - System.out.println("Certificate:"); - System.out.println(ArmorUtils.toAsciiArmoredString(ourCert)); - - System.out.println("Crypt-Only:"); - // CHECKSTYLE:ON - ProducerOptions producerOptions = ProducerOptions - .encrypt(new EncryptionOptions().addRecipient(ourCert).addRecipient(theirCert)) - .setFileName("msg.txt") - .setModificationDate(new Date()); - - generateMessage(producerOptions); - - // CHECKSTYLE:OFF - System.out.println("Sign-Crypt:"); - // CHECKSTYLE:ON - - producerOptions = ProducerOptions - .signAndEncrypt(new EncryptionOptions().addRecipient(ourCert).addRecipient(theirCert), - new SigningOptions().addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), ourKey, DocumentSignatureType.BINARY_DOCUMENT)) - .setFileName("msg.txt") - .setModificationDate(new Date()); - - generateMessage(producerOptions); - } - - private void generateMessage(ProducerOptions producerOptions) throws PGPException, IOException { - String data = "Hello World\n"; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() - .onOutputStream(out) - .withOptions(producerOptions); - - Streams.pipeAll(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), encryptionStream); - encryptionStream.close(); - - // CHECKSTYLE:OFF - System.out.println(out); - // CHECKSTYLE:ON - } - - @Test - public void testWithSuspiciousKey() throws PGPException, IOException { - String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Comment: 4409 A346 1359 96C9 4595 510E 5A18 53D1 5656 CB7A\n" + - "Comment: Alice (Created with pgpkeygen.com) Date: Tue, 4 Jan 2022 17:20:38 +0100 Subject: [PATCH 0008/1204] Hex decode data in OpenPgpV4Fingerprint constructor --- .../src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.java | 2 +- .../signature/subpackets/SignatureSubpacketsUtil.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.java index b5b3d4a7..2605eb0c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.java @@ -72,7 +72,7 @@ public class OpenPgpV4Fingerprint extends OpenPgpFingerprint { } public OpenPgpV4Fingerprint(@Nonnull byte[] bytes) { - super(bytes); + super(Hex.encode(bytes)); } public OpenPgpV4Fingerprint(@Nonnull PGPPublicKey key) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index b73ddde7..960d5256 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -89,7 +89,7 @@ public final class SignatureSubpacketsUtil { OpenPgpFingerprint fingerprint = null; if (subpacket.getKeyVersion() == 4) { - fingerprint = new OpenPgpV4Fingerprint(Hex.encode(subpacket.getFingerprint())); + fingerprint = new OpenPgpV4Fingerprint(subpacket.getFingerprint()); } return fingerprint; From 1447dfc6427ffcb842c7a0634a4f71b99d07f2ad Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 4 Jan 2022 17:21:16 +0100 Subject: [PATCH 0009/1204] Add SignatureUtils.wasIssuedBy --- .../org/pgpainless/signature/SignatureUtils.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java index 96429da1..420efc26 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java @@ -33,6 +33,7 @@ import org.pgpainless.algorithm.SignatureType; import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.util.OpenPgpKeyAttributeUtil; import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; @@ -310,4 +311,18 @@ public final class SignatureUtils { } return list; } + + public static boolean wasIssuedBy(byte[] fingerprint, PGPSignature signature) { + if (fingerprint.length != 20) { + // Unknown fingerprint length + return false; + } + OpenPgpV4Fingerprint fp = new OpenPgpV4Fingerprint(fingerprint); + OpenPgpFingerprint issuerFp = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature); + if (issuerFp == null) { + return fp.getKeyId() == signature.getKeyID(); + } + + return fp.equals(issuerFp); + } } From 5884c4afcd38c080097e7393921f5c0c78cf3c8a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 4 Jan 2022 17:22:02 +0100 Subject: [PATCH 0010/1204] ArmorUtils: Add method to print single public keys --- .../java/org/pgpainless/util/ArmorUtils.java | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index 38527997..9d73635e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -18,8 +18,10 @@ import java.util.regex.Pattern; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPUtil; @@ -43,13 +45,23 @@ public final class ArmorUtils { } + public static String toAsciiArmoredString(PGPSecretKey secretKey) throws IOException { + MultiMap header = keyToHeader(secretKey.getPublicKey()); + return toAsciiArmoredString(secretKey.getEncoded(), header); + } + + public static String toAsciiArmoredString(PGPPublicKey publicKey) throws IOException { + MultiMap header = keyToHeader(publicKey); + return toAsciiArmoredString(publicKey.getEncoded(), header); + } + public static String toAsciiArmoredString(PGPSecretKeyRing secretKeys) throws IOException { - MultiMap header = keyToHeader(secretKeys); + MultiMap header = keysToHeader(secretKeys); return toAsciiArmoredString(secretKeys.getEncoded(), header); } public static String toAsciiArmoredString(PGPPublicKeyRing publicKeys) throws IOException { - MultiMap header = keyToHeader(publicKeys); + MultiMap header = keysToHeader(publicKeys); return toAsciiArmoredString(publicKeys.getEncoded(), header); } @@ -66,7 +78,7 @@ public final class ArmorUtils { } public static ArmoredOutputStream toAsciiArmoredStream(PGPKeyRing keyRing, OutputStream outputStream) { - MultiMap header = keyToHeader(keyRing); + MultiMap header = keysToHeader(keyRing); return toAsciiArmoredStream(outputStream, header); } @@ -94,10 +106,10 @@ public final class ArmorUtils { return sb.toString(); } - private static MultiMap keyToHeader(PGPKeyRing keyRing) { + private static MultiMap keyToHeader(PGPPublicKey publicKey) { MultiMap header = new MultiMap<>(); - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(keyRing); - Iterator userIds = keyRing.getPublicKey().getUserIDs(); + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); + Iterator userIds = publicKey.getUserIDs(); header.put(HEADER_COMMENT, fingerprint.prettyPrint()); if (userIds.hasNext()) { @@ -106,6 +118,11 @@ public final class ArmorUtils { return header; } + private static MultiMap keysToHeader(PGPKeyRing keyRing) { + PGPPublicKey publicKey = keyRing.getPublicKey(); + return keyToHeader(publicKey); + } + public static String toAsciiArmoredString(byte[] bytes) throws IOException { return toAsciiArmoredString(bytes, null); } @@ -147,7 +164,7 @@ public final class ArmorUtils { public static ArmoredOutputStream createArmoredOutputStreamFor(PGPKeyRing keyRing, OutputStream outputStream) { ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(outputStream); - MultiMap headerMap = keyToHeader(keyRing); + MultiMap headerMap = keysToHeader(keyRing); for (String header : headerMap.keySet()) { for (String value : headerMap.get(header)) { armor.addHeader(header, value); From 88e3c61b206f325c31e1fbb54b0d204497e90c60 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 4 Jan 2022 17:22:45 +0100 Subject: [PATCH 0011/1204] RevocationSignatureBuilder: Allow for generation of external revocation signatures --- .../signature/builder/RevocationSignatureBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java index ebae151d..e2e9c0c0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java @@ -52,8 +52,8 @@ public class RevocationSignatureBuilder extends AbstractSignatureBuilder Date: Tue, 4 Jan 2022 17:23:13 +0100 Subject: [PATCH 0012/1204] Workaround for BC not correctly parsing RevocationKey packets --- .../subpackets/SignatureSubpacketsUtil.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index 960d5256..986fcb7c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -10,7 +10,6 @@ import java.util.Date; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; - import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -37,7 +36,6 @@ import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.bouncycastle.util.encoders.Hex; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.Feature; import org.pgpainless.algorithm.HashAlgorithm; @@ -581,7 +579,16 @@ public final class SignatureSubpacketsUtil { if (allPackets.length == 0) { return null; } - return (P) allPackets[allPackets.length - 1]; // return last + + org.bouncycastle.bcpg.SignatureSubpacket last = allPackets[allPackets.length - 1]; + + if (type == SignatureSubpacket.revocationKey) { + // RevocationKey subpackets are not castable for some reason + // We need to manually construct the new object + return (P) new RevocationKey(last.isCritical(), last.isLongLength(), last.getData()); + } + + return (P) last; } /** From 5e0ca369bfb905183cc5e2612a581d891f98f1ed Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 4 Jan 2022 17:53:37 +0100 Subject: [PATCH 0013/1204] Document workaround for https://github.com/bcgit/bc-java/pull/1085 --- .../signature/subpackets/SignatureSubpacketsUtil.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index 986fcb7c..396311c6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -584,7 +584,9 @@ public final class SignatureSubpacketsUtil { if (type == SignatureSubpacket.revocationKey) { // RevocationKey subpackets are not castable for some reason - // We need to manually construct the new object + // See https://github.com/bcgit/bc-java/pull/1085 for an upstreamed fix + // We need to manually construct the new object for now. + // TODO: Remove workaround when BC 1.71 is released (and has our fix) return (P) new RevocationKey(last.isCritical(), last.isLongLength(), last.getData()); } From 1cb49f4b121b9f411d3e5d80557c6c590cf574e6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 7 Jan 2022 14:28:36 +0100 Subject: [PATCH 0014/1204] Update SOP implementation to the latest spec version See https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03 --- .../cli/commands/SignVerifyTest.java | 17 +++- .../java/org/pgpainless/sop/DecryptImpl.java | 4 - .../java/org/pgpainless/sop/EncryptImpl.java | 4 +- .../org/pgpainless/sop/ExtractCertImpl.java | 33 +++++--- .../java/org/pgpainless/sop/SignImpl.java | 38 ++++++--- .../java/org/pgpainless/sop/VersionImpl.java | 17 ++++ .../sop/EncryptDecryptRoundTripTest.java | 14 +--- .../java/org/pgpainless/sop/SignTest.java | 22 ++--- .../java/org/pgpainless/sop/VersionTest.java | 33 +++++++- .../sop/cli/picocli/commands/EncryptCmd.java | 4 +- .../sop/cli/picocli/commands/SignCmd.java | 28 +++++-- .../cli/picocli/commands/EncryptCmdTest.java | 14 ++-- .../sop/cli/picocli/commands/SignCmdTest.java | 13 +-- sop-java/src/main/java/sop/MicAlg.java | 55 +++++++++++++ .../java/sop/exception/SOPGPException.java | 80 +++++++++++++++++-- .../src/main/java/sop/operation/Decrypt.java | 16 ++-- .../src/main/java/sop/operation/Encrypt.java | 4 +- .../main/java/sop/operation/ExtractCert.java | 12 +-- .../src/main/java/sop/operation/Sign.java | 15 ++-- .../src/main/java/sop/operation/Verify.java | 8 +- .../src/main/java/sop/operation/Version.java | 29 ++++++- 21 files changed, 348 insertions(+), 112 deletions(-) create mode 100644 sop-java/src/main/java/sop/MicAlg.java diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java index 4c31f7ca..525e3e1d 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java @@ -5,13 +5,16 @@ package org.pgpainless.cli.commands; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -77,6 +80,9 @@ public class SignVerifyTest { Streams.pipeAll(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), dataOut); dataOut.close(); + // Define micalg output file + File micalgOut = new File(tempDir, "micalg"); + // Sign test data FileInputStream dataIn = new FileInputStream(dataFile); System.setIn(dataIn); @@ -84,7 +90,7 @@ public class SignVerifyTest { assertTrue(sigFile.createNewFile()); FileOutputStream sigOut = new FileOutputStream(sigFile); System.setOut(new PrintStream(sigOut)); - PGPainlessCLI.execute("sign", "--armor", aliceKeyFile.getAbsolutePath()); + PGPainlessCLI.execute("sign", "--armor", "--micalg-out", micalgOut.getAbsolutePath(), aliceKeyFile.getAbsolutePath()); sigOut.close(); // verify test data signature @@ -105,6 +111,15 @@ public class SignVerifyTest { assertEquals(signingKeyFingerprint.toString(), split[1].trim()); assertEquals(primaryKeyFingerprint.toString(), split[2].trim()); + // Test micalg output + assertTrue(micalgOut.exists()); + FileReader fileReader = new FileReader(micalgOut); + BufferedReader bufferedReader = new BufferedReader(fileReader); + String line = bufferedReader.readLine(); + assertNull(bufferedReader.readLine()); + bufferedReader.close(); + assertEquals("pgp-sha512", line); + System.setIn(originalIn); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 606f8673..bef260c4 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -100,10 +100,6 @@ public class DecryptImpl implements Decrypt { PGPSecretKeyRingCollection secretKeys = PGPainless.readKeyRing() .secretKeyRingCollection(keyIn); - if (secretKeys.size() != 1) { - throw new SOPGPException.BadData(new AssertionError("Exactly one single secret key expected. Got " + secretKeys.size())); - } - for (PGPSecretKeyRing secretKey : secretKeys) { KeyRingInfo info = new KeyRingInfo(secretKey); if (!info.isFullyDecrypted()) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index b869d6ba..bb0af660 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -49,7 +49,7 @@ public class EncryptImpl implements Encrypt { } @Override - public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.CertCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { + public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { try { PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); if (keys.size() != 1) { @@ -62,7 +62,7 @@ public class EncryptImpl implements Encrypt { try { signingOptions.addInlineSignatures(SecretKeyRingProtector.unprotectedKeys(), keys, DocumentSignatureType.BINARY_DOCUMENT); } catch (IllegalArgumentException e) { - throw new SOPGPException.CertCannotSign(); + throw new SOPGPException.KeyCannotSign(); } catch (WrongPassphraseException e) { throw new SOPGPException.KeyIsProtected(); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java index d22c71c3..6c3c825f 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java @@ -7,16 +7,18 @@ package org.pgpainless.sop; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.pgpainless.PGPainless; -import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.util.ArmorUtils; -import sop.operation.ExtractCert; import sop.Ready; import sop.exception.SOPGPException; +import sop.operation.ExtractCert; public class ExtractCertImpl implements ExtractCert { @@ -30,21 +32,34 @@ public class ExtractCertImpl implements ExtractCert { @Override public Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData { - PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(keyInputStream); - if (key == null) { + PGPSecretKeyRingCollection keys; + try { + keys = PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream); + } catch (PGPException e) { + throw new IOException("Cannot read keys.", e); + } + + if (keys == null || keys.size() == 0) { throw new SOPGPException.BadData(new PGPException("No key data found.")); } - PGPPublicKeyRing cert = KeyRingUtils.publicKeyRingFrom(key); + List certs = new ArrayList<>(); + for (PGPSecretKeyRing key : keys) { + PGPPublicKeyRing cert = PGPainless.extractCertificate(key); + certs.add(cert); + } return new Ready() { @Override public void writeTo(OutputStream outputStream) throws IOException { - OutputStream out = armor ? ArmorUtils.createArmoredOutputStreamFor(cert, outputStream) : outputStream; - cert.encode(out); - if (armor) { - out.close(); + for (PGPPublicKeyRing cert : certs) { + OutputStream out = armor ? ArmorUtils.createArmoredOutputStreamFor(cert, outputStream) : outputStream; + cert.encode(out); + + if (armor) { + out.close(); + } } } }; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java index 22b6ed32..068903d7 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java @@ -26,7 +26,8 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmoredOutputStreamFactory; -import sop.Ready; +import sop.MicAlg; +import sop.ReadyWithResult; import sop.enums.SignAs; import sop.exception.SOPGPException; import sop.operation.Sign; @@ -53,16 +54,14 @@ public class SignImpl implements Sign { public Sign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { try { PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); - if (keys.size() != 1) { - throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size())); - } - PGPSecretKeyRing key = keys.iterator().next(); - KeyRingInfo info = new KeyRingInfo(key); - if (!info.isFullyDecrypted()) { - throw new SOPGPException.KeyIsProtected(); + for (PGPSecretKeyRing key : keys) { + KeyRingInfo info = new KeyRingInfo(key); + if (!info.isFullyDecrypted()) { + throw new SOPGPException.KeyIsProtected(); + } + signingOptions.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key, modeToSigType(mode)); } - signingOptions.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key, modeToSigType(mode)); } catch (PGPException e) { throw new SOPGPException.BadData(e); } @@ -70,7 +69,7 @@ public class SignImpl implements Sign { } @Override - public Ready data(InputStream data) throws IOException { + public ReadyWithResult data(InputStream data) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); try { EncryptionStream signingStream = PGPainless.encryptAndOrSign() @@ -78,9 +77,9 @@ public class SignImpl implements Sign { .withOptions(ProducerOptions.sign(signingOptions) .setAsciiArmor(armor)); - return new Ready() { + return new ReadyWithResult() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public MicAlg writeTo(OutputStream outputStream) throws IOException { if (signingStream.isClosed()) { throw new IllegalStateException("EncryptionStream is already closed."); @@ -106,6 +105,8 @@ public class SignImpl implements Sign { } out.close(); outputStream.close(); // armor out does not close underlying stream + + return micAlgFromSignatures(signatures); } }; @@ -115,6 +116,19 @@ public class SignImpl implements Sign { } + private MicAlg micAlgFromSignatures(Iterable signatures) { + int algorithmId = 0; + for (PGPSignature signature : signatures) { + int sigAlg = signature.getHashAlgorithm(); + if (algorithmId == 0 || algorithmId == sigAlg) { + algorithmId = sigAlg; + } else { + return MicAlg.empty(); + } + } + return algorithmId == 0 ? MicAlg.empty() : MicAlg.fromHashAlgorithmId(algorithmId); + } + private static DocumentSignatureType modeToSigType(SignAs mode) { return mode == SignAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index b08b2466..41fbd838 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Properties; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import sop.operation.Version; public class VersionImpl implements Version { @@ -33,4 +34,20 @@ public class VersionImpl implements Version { } return version; } + + @Override + public String getBackendVersion() { + double bcVersion = new BouncyCastleProvider().getVersion(); + return String.format("Bouncycastle %,.2f", bcVersion); + } + + @Override + public String getExtendedVersion() { + return getName() + " " + getVersion() + "\n" + + "Based on PGPainless " + getVersion() + "\n" + + "Using " + getBackendVersion() + "\n" + + "See https://pgpainless.org\n" + + "Implementing Stateless OpenPGP Protocol Version 3\n" + + "See https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03"; + } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java index 807c9dd3..d0699ae7 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -51,7 +51,7 @@ public class EncryptDecryptRoundTripTest { } @Test - public void basicRoundTripWithKey() throws IOException, SOPGPException.CertCannotSign { + public void basicRoundTripWithKey() throws IOException, SOPGPException.KeyCannotSign { byte[] encrypted = sop.encrypt() .signWith(aliceKey) .withCert(aliceCert) @@ -74,7 +74,7 @@ public class EncryptDecryptRoundTripTest { } @Test - public void basicRoundTripWithoutArmorUsingKey() throws IOException, SOPGPException.CertCannotSign { + public void basicRoundTripWithoutArmorUsingKey() throws IOException, SOPGPException.KeyCannotSign { byte[] aliceKeyNoArmor = sop.generateKey() .userId("Alice ") .noArmor() @@ -189,16 +189,6 @@ public class EncryptDecryptRoundTripTest { .toByteArrayAndResult()); } - @Test - public void decrypt_withKeyWithMultipleKeysFails() { - byte[] keys = new byte[aliceKey.length + bobKey.length]; - System.arraycopy(aliceKey, 0, keys, 0 , aliceKey.length); - System.arraycopy(bobKey, 0, keys, aliceKey.length, bobKey.length); - - assertThrows(SOPGPException.BadData.class, () -> sop.decrypt() - .withKey(keys)); - } - @Test public void decrypt_withKeyWithPasswordProtectionFails() { String passwordProtectedKey = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java index 3167618c..4736002f 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java @@ -13,13 +13,11 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import java.util.Arrays; import java.util.Date; import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -56,7 +54,7 @@ public class SignTest { byte[] signature = sop.sign() .key(key) .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); assertTrue(new String(signature).startsWith("-----BEGIN PGP SIGNATURE-----")); @@ -76,7 +74,7 @@ public class SignTest { .key(key) .noArmor() .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); assertFalse(new String(signature).startsWith("-----BEGIN PGP SIGNATURE-----")); @@ -95,7 +93,7 @@ public class SignTest { byte[] signature = sop.sign() .key(key) .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); assertThrows(SOPGPException.NoSignature.class, () -> sop.verify() .cert(cert) @@ -109,7 +107,7 @@ public class SignTest { byte[] signature = sop.sign() .key(key) .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); assertThrows(SOPGPException.NoSignature.class, () -> sop.verify() .cert(cert) @@ -124,22 +122,12 @@ public class SignTest { .mode(SignAs.Text) .key(key) .data(data) - .getBytes(); + .toByteArrayAndResult().getBytes(); PGPSignature sig = SignatureUtils.readSignatures(signature).get(0); assertEquals(SignatureType.CANONICAL_TEXT_DOCUMENT.getCode(), sig.getSignatureType()); } - @Test - public void rejectKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing key1 = PGPainless.generateKeyRing().modernKeyRing("Alice", null); - PGPSecretKeyRing key2 = PGPainless.generateKeyRing().modernKeyRing("Bob", null); - PGPSecretKeyRingCollection collection = new PGPSecretKeyRingCollection(Arrays.asList(key1, key2)); - byte[] keys = collection.getEncoded(); - - assertThrows(SOPGPException.BadData.class, () -> sop.sign().key(keys)); - } - @Test public void rejectEncryptedKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing key = PGPainless.generateKeyRing() diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java index 712df550..c9739471 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java @@ -5,19 +5,48 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import sop.SOP; public class VersionTest { + private static SOP sop; + + @BeforeAll + public static void setup() { + sop = new SOPImpl(); + } + @Test public void testGetVersion() { - assertNotNull(new SOPImpl().version().getVersion()); + String version = sop.version().getVersion(); + assertNotNull(version); + assertFalse(version.isEmpty()); } @Test public void assertNameEqualsPGPainless() { - assertEquals("PGPainless-SOP", new SOPImpl().version().getName()); + assertEquals("PGPainless-SOP", sop.version().getName()); + } + + @Test + public void testGetBackendVersion() { + String backendVersion = sop.version().getBackendVersion(); + assertNotNull(backendVersion); + assertFalse(backendVersion.isEmpty()); + } + + @Test + public void testGetExtendedVersion() { + String extendedVersion = sop.version().getExtendedVersion(); + assertNotNull(extendedVersion); + assertFalse(extendedVersion.isEmpty()); + + String firstLine = extendedVersion.split("\n")[0]; + assertEquals(sop.version().getName() + " " + sop.version().getVersion(), firstLine); } } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java index d1ee253c..2ccf0477 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java @@ -82,8 +82,8 @@ public class EncryptCmd implements Runnable { throw new SOPGPException.KeyIsProtected("Key from " + keyFile.getAbsolutePath() + " is password protected.", keyIsProtected); } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { throw new SOPGPException.UnsupportedAsymmetricAlgo("Key from " + keyFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo); - } catch (SOPGPException.CertCannotSign certCannotSign) { - throw new RuntimeException("Key from " + keyFile.getAbsolutePath() + " cannot sign.", certCannotSign); + } catch (SOPGPException.KeyCannotSign keyCannotSign) { + throw new SOPGPException.KeyCannotSign("Key from " + keyFile.getAbsolutePath() + " cannot sign.", keyCannotSign); } catch (SOPGPException.BadData badData) { throw new SOPGPException.BadData("Key file " + keyFile.getAbsolutePath() + " does not contain a valid OpenPGP private key.", badData); } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java index 961869ce..735e9664 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java @@ -7,12 +7,14 @@ package sop.cli.picocli.commands; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import picocli.CommandLine; -import sop.Ready; +import sop.MicAlg; +import sop.ReadyWithResult; import sop.cli.picocli.Print; import sop.cli.picocli.SopCLI; import sop.enums.SignAs; @@ -34,9 +36,13 @@ public class SignCmd implements Runnable { SignAs type; @CommandLine.Parameters(description = "Secret keys used for signing", - paramLabel = "KEY") + paramLabel = "KEYS") List secretKeyFile = new ArrayList<>(); + @CommandLine.Option(names = "--micalg-out", description = "Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156)", + paramLabel = "MICALG") + File micAlgOut; + @Override public void run() { Sign sign = SopCLI.getSop().sign(); @@ -51,8 +57,12 @@ public class SignCmd implements Runnable { } } + if (micAlgOut != null && micAlgOut.exists()) { + throw new SOPGPException.OutputExists(String.format("Target %s of option %s already exists.", micAlgOut.getAbsolutePath(), "--micalg-out")); + } + if (secretKeyFile.isEmpty()) { - Print.errln("Missing required parameter 'KEY'."); + Print.errln("Missing required parameter 'KEYS'."); System.exit(19); } @@ -83,8 +93,16 @@ public class SignCmd implements Runnable { } try { - Ready ready = sign.data(System.in); - ready.writeTo(System.out); + ReadyWithResult ready = sign.data(System.in); + MicAlg micAlg = ready.writeTo(System.out); + + if (micAlgOut != null) { + // Write micalg out + micAlgOut.createNewFile(); + FileOutputStream micAlgOutStream = new FileOutputStream(micAlgOut); + micAlg.writeTo(micAlgOutStream); + micAlgOutStream.close(); + } } catch (IOException e) { Print.errln("IO Error."); Print.trace(e); diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java index cfa8a3f6..91f0a1e7 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java @@ -91,7 +91,7 @@ public class EncryptCmdTest { } @Test - public void signWith_multipleTimesGetPassedDown() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData { + public void signWith_multipleTimesGetPassedDown() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { File keyFile1 = File.createTempFile("sign-with-1-", ".asc"); File keyFile2 = File.createTempFile("sign-with-2-", ".asc"); @@ -107,7 +107,7 @@ public class EncryptCmdTest { @Test @ExpectSystemExitWithStatus(67) - public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException { + public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); File keyFile = File.createTempFile("sign-with", ".asc"); SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", "starship"}); @@ -115,23 +115,23 @@ public class EncryptCmdTest { @Test @ExpectSystemExitWithStatus(13) - public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException { + public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); File keyFile = File.createTempFile("sign-with", ".asc"); SopCLI.main(new String[] {"encrypt", "--with-password", "123456", "--sign-with", keyFile.getAbsolutePath()}); } @Test - @ExpectSystemExitWithStatus(1) - public void signWith_certCannotSignCausesExit1() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData { - when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.CertCannotSign()); + @ExpectSystemExitWithStatus(79) + public void signWith_certCannotSignCausesExit1() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { + when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign()); File keyFile = File.createTempFile("sign-with", ".asc"); SopCLI.main(new String[] {"encrypt", "--with-password", "dragon", "--sign-with", keyFile.getAbsolutePath()}); } @Test @ExpectSystemExitWithStatus(41) - public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException { + public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File keyFile = File.createTempFile("sign-with", ".asc"); SopCLI.main(new String[] {"encrypt", "--with-password", "orange", "--sign-with", keyFile.getAbsolutePath()}); diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java index 8de61409..c5c6e201 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java @@ -19,7 +19,8 @@ import java.io.OutputStream; import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import sop.Ready; +import sop.MicAlg; +import sop.ReadyWithResult; import sop.SOP; import sop.cli.picocli.SopCLI; import sop.exception.SOPGPException; @@ -33,10 +34,10 @@ public class SignCmdTest { @BeforeEach public void mockComponents() throws IOException, SOPGPException.ExpectedText { sign = mock(Sign.class); - when(sign.data((InputStream) any())).thenReturn(new Ready() { + when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public void writeTo(OutputStream outputStream) { - + public MicAlg writeTo(OutputStream outputStream) { + return MicAlg.fromHashAlgorithmId(10); } }); @@ -109,9 +110,9 @@ public class SignCmdTest { @Test @ExpectSystemExitWithStatus(1) public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText { - when(sign.data((InputStream) any())).thenReturn(new Ready() { + when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public MicAlg writeTo(OutputStream outputStream) throws IOException { throw new IOException(); } }); diff --git a/sop-java/src/main/java/sop/MicAlg.java b/sop-java/src/main/java/sop/MicAlg.java new file mode 100644 index 00000000..5bee7875 --- /dev/null +++ b/sop-java/src/main/java/sop/MicAlg.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop; + +import java.io.OutputStream; +import java.io.PrintWriter; + +public class MicAlg { + + private final String micAlg; + + public MicAlg(String micAlg) { + if (micAlg == null) { + throw new IllegalArgumentException("MicAlg String cannot be null."); + } + this.micAlg = micAlg; + } + + public static MicAlg empty() { + return new MicAlg(""); + } + + public static MicAlg fromHashAlgorithmId(int id) { + switch (id) { + case 1: + return new MicAlg("pgp-md5"); + case 2: + return new MicAlg("pgp-sha1"); + case 3: + return new MicAlg("pgp-ripemd160"); + case 8: + return new MicAlg("pgp-sha256"); + case 9: + return new MicAlg("pgp-sha384"); + case 10: + return new MicAlg("pgp-sha512"); + case 11: + return new MicAlg("pgp-sha224"); + default: + throw new IllegalArgumentException("Unsupported hash algorithm ID: " + id); + } + } + + public String getMicAlg() { + return micAlg; + } + + public void writeTo(OutputStream outputStream) { + PrintWriter pw = new PrintWriter(outputStream); + pw.write(getMicAlg()); + pw.close(); + } +} diff --git a/sop-java/src/main/java/sop/exception/SOPGPException.java b/sop-java/src/main/java/sop/exception/SOPGPException.java index a8c98c9c..dfd09540 100644 --- a/sop-java/src/main/java/sop/exception/SOPGPException.java +++ b/sop-java/src/main/java/sop/exception/SOPGPException.java @@ -24,6 +24,9 @@ public abstract class SOPGPException extends RuntimeException { public abstract int getExitCode(); + /** + * No acceptable signatures found (sop verify). + */ public static class NoSignature extends SOPGPException { public static final int EXIT_CODE = 3; @@ -38,6 +41,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Asymmetric algorithm unsupported (sop encrypt). + */ public static class UnsupportedAsymmetricAlgo extends SOPGPException { public static final int EXIT_CODE = 13; @@ -56,6 +62,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Certificate not encryption capable (e,g, expired, revoked, unacceptable usage). + */ public static class CertCannotEncrypt extends SOPGPException { public static final int EXIT_CODE = 17; @@ -69,10 +78,9 @@ public abstract class SOPGPException extends RuntimeException { } } - public static class CertCannotSign extends Exception { - - } - + /** + * Missing required argument. + */ public static class MissingArg extends SOPGPException { public static final int EXIT_CODE = 19; @@ -87,6 +95,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Incomplete verification instructions (sop decrypt). + */ public static class IncompleteVerification extends SOPGPException { public static final int EXIT_CODE = 23; @@ -101,6 +112,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Unable to decrypt (sop decrypt). + */ public static class CannotDecrypt extends SOPGPException { public static final int EXIT_CODE = 29; @@ -111,6 +125,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Non-UTF-8 or otherwise unreliable password (sop encrypt). + */ public static class PasswordNotHumanReadable extends SOPGPException { public static final int EXIT_CODE = 31; @@ -121,6 +138,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Unsupported option. + */ public static class UnsupportedOption extends SOPGPException { public static final int EXIT_CODE = 37; @@ -139,6 +159,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Invalid data type (no secret key where KEYS expected, etc.). + */ public static class BadData extends SOPGPException { public static final int EXIT_CODE = 41; @@ -157,6 +180,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Non-Text input where text expected. + */ public static class ExpectedText extends SOPGPException { public static final int EXIT_CODE = 53; @@ -167,6 +193,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Output file already exists. + */ public static class OutputExists extends SOPGPException { public static final int EXIT_CODE = 59; @@ -181,6 +210,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Input file does not exist. + */ public static class MissingInput extends SOPGPException { public static final int EXIT_CODE = 61; @@ -195,6 +227,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * A KEYS input is protected (locked) with a password, and sop cannot unlock it. + */ public static class KeyIsProtected extends SOPGPException { public static final int EXIT_CODE = 67; @@ -213,6 +248,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * Unsupported subcommand. + */ public static class UnsupportedSubcommand extends SOPGPException { public static final int EXIT_CODE = 69; @@ -227,6 +265,9 @@ public abstract class SOPGPException extends RuntimeException { } } + /** + * An indirect parameter is a special designator (it starts with @), but sop does not know how to handle the prefix. + */ public static class UnsupportedSpecialPrefix extends SOPGPException { public static final int EXIT_CODE = 71; @@ -237,7 +278,10 @@ public abstract class SOPGPException extends RuntimeException { } } - + /** + * A indirect input parameter is a special designator (it starts with @), + * and a filename matching the designator is actually present. + */ public static class AmbiguousInput extends SOPGPException { public static final int EXIT_CODE = 73; @@ -251,4 +295,30 @@ public abstract class SOPGPException extends RuntimeException { return EXIT_CODE; } } + + /** + * Key not signature-capable (e.g. expired, revoked, unacceptable usage flags) + * (sop sign and sop encrypt with --sign-with). + */ + public static class KeyCannotSign extends SOPGPException { + + public static final int EXIT_CODE = 79; + + public KeyCannotSign() { + super(); + } + + public KeyCannotSign(String message) { + super(message); + } + + public KeyCannotSign(String s, KeyCannotSign keyCannotSign) { + super(s, keyCannotSign); + } + + @Override + public int getExitCode() { + return EXIT_CODE; + } + } } diff --git a/sop-java/src/main/java/sop/operation/Decrypt.java b/sop-java/src/main/java/sop/operation/Decrypt.java index 4cbd6f35..0811ac2d 100644 --- a/sop-java/src/main/java/sop/operation/Decrypt.java +++ b/sop-java/src/main/java/sop/operation/Decrypt.java @@ -35,9 +35,9 @@ public interface Decrypt { throws SOPGPException.UnsupportedOption; /** - * Adds the verification cert. + * Adds one or more verification cert. * - * @param cert input stream containing the cert + * @param cert input stream containing the cert(s) * @return builder instance */ Decrypt verifyWithCert(InputStream cert) @@ -45,9 +45,9 @@ public interface Decrypt { IOException; /** - * Adds the verification cert. + * Adds one or more verification cert. * - * @param cert byte array containing the cert + * @param cert byte array containing the cert(s) * @return builder instance */ default Decrypt verifyWithCert(byte[] cert) @@ -75,9 +75,9 @@ public interface Decrypt { SOPGPException.UnsupportedOption; /** - * Adds the decryption key. + * Adds one or more decryption key. * - * @param key input stream containing the key + * @param key input stream containing the key(s) * @return builder instance */ Decrypt withKey(InputStream key) @@ -86,9 +86,9 @@ public interface Decrypt { SOPGPException.UnsupportedAsymmetricAlgo; /** - * Adds the decryption key. + * Adds one or more decryption key. * - * @param key byte array containing the key + * @param key byte array containing the key(s) * @return builder instance */ default Decrypt withKey(byte[] key) diff --git a/sop-java/src/main/java/sop/operation/Encrypt.java b/sop-java/src/main/java/sop/operation/Encrypt.java index b5a92b25..784c07a0 100644 --- a/sop-java/src/main/java/sop/operation/Encrypt.java +++ b/sop-java/src/main/java/sop/operation/Encrypt.java @@ -38,7 +38,7 @@ public interface Encrypt { */ Encrypt signWith(InputStream key) throws SOPGPException.KeyIsProtected, - SOPGPException.CertCannotSign, + SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData; @@ -50,7 +50,7 @@ public interface Encrypt { */ default Encrypt signWith(byte[] key) throws SOPGPException.KeyIsProtected, - SOPGPException.CertCannotSign, + SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { return signWith(new ByteArrayInputStream(key)); diff --git a/sop-java/src/main/java/sop/operation/ExtractCert.java b/sop-java/src/main/java/sop/operation/ExtractCert.java index 7a0de5c6..32491111 100644 --- a/sop-java/src/main/java/sop/operation/ExtractCert.java +++ b/sop-java/src/main/java/sop/operation/ExtractCert.java @@ -21,18 +21,18 @@ public interface ExtractCert { ExtractCert noArmor(); /** - * Extract the cert from the provided key. + * Extract the cert(s) from the provided key(s). * - * @param keyInputStream input stream containing the encoding of an OpenPGP key - * @return result containing the encoding of the keys cert + * @param keyInputStream input stream containing the encoding of one or more OpenPGP keys + * @return result containing the encoding of the keys certs */ Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData; /** - * Extract the cert from the provided key. + * Extract the cert(s) from the provided key(s). * - * @param key byte array containing the encoding of an OpenPGP key - * @return result containing the encoding of the keys cert + * @param key byte array containing the encoding of one or more OpenPGP key + * @return result containing the encoding of the keys certs */ default Ready key(byte[] key) throws IOException, SOPGPException.BadData { return key(new ByteArrayInputStream(key)); diff --git a/sop-java/src/main/java/sop/operation/Sign.java b/sop-java/src/main/java/sop/operation/Sign.java index 9b9c3a6f..75f4e5a8 100644 --- a/sop-java/src/main/java/sop/operation/Sign.java +++ b/sop-java/src/main/java/sop/operation/Sign.java @@ -8,7 +8,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import sop.Ready; +import sop.MicAlg; +import sop.ReadyWithResult; import sop.enums.SignAs; import sop.exception.SOPGPException; @@ -31,17 +32,17 @@ public interface Sign { Sign mode(SignAs mode) throws SOPGPException.UnsupportedOption; /** - * Adds the signer key. + * Add one or more signing keys. * - * @param key input stream containing encoded key + * @param key input stream containing encoded keys * @return builder instance */ Sign key(InputStream key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException; /** - * Adds the signer key. + * Add one or more signing keys. * - * @param key byte array containing encoded key + * @param key byte array containing encoded keys * @return builder instance */ default Sign key(byte[] key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { @@ -54,7 +55,7 @@ public interface Sign { * @param data input stream containing data * @return ready */ - Ready data(InputStream data) throws IOException, SOPGPException.ExpectedText; + ReadyWithResult data(InputStream data) throws IOException, SOPGPException.ExpectedText; /** * Signs data. @@ -62,7 +63,7 @@ public interface Sign { * @param data byte array containing data * @return ready */ - default Ready data(byte[] data) throws IOException, SOPGPException.ExpectedText { + default ReadyWithResult data(byte[] data) throws IOException, SOPGPException.ExpectedText { return data(new ByteArrayInputStream(data)); } } diff --git a/sop-java/src/main/java/sop/operation/Verify.java b/sop-java/src/main/java/sop/operation/Verify.java index 30905de2..1bf9fe09 100644 --- a/sop-java/src/main/java/sop/operation/Verify.java +++ b/sop-java/src/main/java/sop/operation/Verify.java @@ -29,17 +29,17 @@ public interface Verify extends VerifySignatures { Verify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption; /** - * Adds the verification cert. + * Add one or more verification cert. * - * @param cert input stream containing the encoded cert + * @param cert input stream containing the encoded certs * @return builder instance */ Verify cert(InputStream cert) throws SOPGPException.BadData; /** - * Adds the verification cert. + * Add one or more verification cert. * - * @param cert byte array containing the encoded cert + * @param cert byte array containing the encoded certs * @return builder instance */ default Verify cert(byte[] cert) throws SOPGPException.BadData { diff --git a/sop-java/src/main/java/sop/operation/Version.java b/sop-java/src/main/java/sop/operation/Version.java index ab32099a..0b50993f 100644 --- a/sop-java/src/main/java/sop/operation/Version.java +++ b/sop-java/src/main/java/sop/operation/Version.java @@ -8,15 +8,42 @@ public interface Version { /** * Return the implementations name. + * e.g. "SOP", * * @return implementation name */ String getName(); /** - * Return the implementations version string. + * Return the implementations short version string. + * e.g. "1.0" * * @return version string */ String getVersion(); + + /** + * Return version information about the used OpenPGP backend. + * e.g. "Bouncycastle 1.70" + * + * @return backend version string + */ + String getBackendVersion(); + + /** + * Return an extended version string containing multiple lines of version information. + * The first line MUST match the information produced by {@link #getName()} and {@link #getVersion()}, but the rest of the text + * has no defined structure. + * Example: + *
+     *     "SOP 1.0
+     *     Awesome PGP!
+     *     Using Bouncycastle 1.70
+     *     LibFoo 1.2.2
+     *     See https://pgp.example.org/sop/ for more information"
+     * 
+ * + * @return extended version string + */ + String getExtendedVersion(); } From 987c328ad889b034ed13851fa57cdca869d4905c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 8 Jan 2022 01:07:45 +0100 Subject: [PATCH 0015/1204] Add MicAlgTest --- .../src/test/java/sop/util/MicAlgTest.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 sop-java/src/test/java/sop/util/MicAlgTest.java diff --git a/sop-java/src/test/java/sop/util/MicAlgTest.java b/sop-java/src/test/java/sop/util/MicAlgTest.java new file mode 100644 index 00000000..f720c85b --- /dev/null +++ b/sop-java/src/test/java/sop/util/MicAlgTest.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import sop.MicAlg; + +public class MicAlgTest { + + @Test + public void constructorNullArgThrows() { + assertThrows(IllegalArgumentException.class, () -> new MicAlg(null)); + } + + @Test + public void emptyMicAlgIsEmptyString() { + MicAlg empty = MicAlg.empty(); + assertNotNull(empty.getMicAlg()); + assertTrue(empty.getMicAlg().isEmpty()); + } + + @Test + public void fromInvalidAlgorithmIdThrows() { + assertThrows(IllegalArgumentException.class, () -> MicAlg.fromHashAlgorithmId(-1)); + } + + @Test + public void fromHashAlgorithmIdsKnownAlgsMatch() { + Map knownAlgorithmMicalgs = new HashMap<>(); + knownAlgorithmMicalgs.put(1, "pgp-md5"); + knownAlgorithmMicalgs.put(2, "pgp-sha1"); + knownAlgorithmMicalgs.put(3, "pgp-ripemd160"); + knownAlgorithmMicalgs.put(8, "pgp-sha256"); + knownAlgorithmMicalgs.put(9, "pgp-sha384"); + knownAlgorithmMicalgs.put(10, "pgp-sha512"); + knownAlgorithmMicalgs.put(11, "pgp-sha224"); + + for (Integer id : knownAlgorithmMicalgs.keySet()) { + MicAlg micAlg = MicAlg.fromHashAlgorithmId(id); + assertEquals(knownAlgorithmMicalgs.get(id), micAlg.getMicAlg()); + } + } +} From f2d88d8a86c7d94aaf80a2a26a48684ad5a4d508 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 8 Jan 2022 17:28:19 +0100 Subject: [PATCH 0016/1204] Introduce SigningResult class to allow for additional signing information to be returned --- .../java/org/pgpainless/sop/SignImpl.java | 11 ++-- .../sop/cli/picocli/commands/SignCmd.java | 6 ++- .../sop/cli/picocli/commands/SignCmdTest.java | 12 ++--- sop-java/src/main/java/sop/SigningResult.java | 50 +++++++++++++++++++ .../src/main/java/sop/operation/Sign.java | 6 +-- 5 files changed, 70 insertions(+), 15 deletions(-) create mode 100644 sop-java/src/main/java/sop/SigningResult.java diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java index 068903d7..b3d18043 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java @@ -28,6 +28,7 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmoredOutputStreamFactory; import sop.MicAlg; import sop.ReadyWithResult; +import sop.SigningResult; import sop.enums.SignAs; import sop.exception.SOPGPException; import sop.operation.Sign; @@ -69,7 +70,7 @@ public class SignImpl implements Sign { } @Override - public ReadyWithResult data(InputStream data) throws IOException { + public ReadyWithResult data(InputStream data) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); try { EncryptionStream signingStream = PGPainless.encryptAndOrSign() @@ -77,9 +78,9 @@ public class SignImpl implements Sign { .withOptions(ProducerOptions.sign(signingOptions) .setAsciiArmor(armor)); - return new ReadyWithResult() { + return new ReadyWithResult() { @Override - public MicAlg writeTo(OutputStream outputStream) throws IOException { + public SigningResult writeTo(OutputStream outputStream) throws IOException { if (signingStream.isClosed()) { throw new IllegalStateException("EncryptionStream is already closed."); @@ -106,7 +107,9 @@ public class SignImpl implements Sign { out.close(); outputStream.close(); // armor out does not close underlying stream - return micAlgFromSignatures(signatures); + return SigningResult.builder() + .setMicAlg(micAlgFromSignatures(signatures)) + .build(); } }; diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java index 735e9664..250679fd 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java @@ -15,6 +15,7 @@ import java.util.List; import picocli.CommandLine; import sop.MicAlg; import sop.ReadyWithResult; +import sop.SigningResult; import sop.cli.picocli.Print; import sop.cli.picocli.SopCLI; import sop.enums.SignAs; @@ -93,9 +94,10 @@ public class SignCmd implements Runnable { } try { - ReadyWithResult ready = sign.data(System.in); - MicAlg micAlg = ready.writeTo(System.out); + ReadyWithResult ready = sign.data(System.in); + SigningResult result = ready.writeTo(System.out); + MicAlg micAlg = result.getMicAlg(); if (micAlgOut != null) { // Write micalg out micAlgOut.createNewFile(); diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java index c5c6e201..ce0ce54a 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java @@ -19,9 +19,9 @@ import java.io.OutputStream; import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import sop.MicAlg; import sop.ReadyWithResult; import sop.SOP; +import sop.SigningResult; import sop.cli.picocli.SopCLI; import sop.exception.SOPGPException; import sop.operation.Sign; @@ -34,10 +34,10 @@ public class SignCmdTest { @BeforeEach public void mockComponents() throws IOException, SOPGPException.ExpectedText { sign = mock(Sign.class); - when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult() { + when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public MicAlg writeTo(OutputStream outputStream) { - return MicAlg.fromHashAlgorithmId(10); + public SigningResult writeTo(OutputStream outputStream) { + return SigningResult.builder().build(); } }); @@ -110,9 +110,9 @@ public class SignCmdTest { @Test @ExpectSystemExitWithStatus(1) public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText { - when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult() { + when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public MicAlg writeTo(OutputStream outputStream) throws IOException { + public SigningResult writeTo(OutputStream outputStream) throws IOException { throw new IOException(); } }); diff --git a/sop-java/src/main/java/sop/SigningResult.java b/sop-java/src/main/java/sop/SigningResult.java new file mode 100644 index 00000000..2cb142dc --- /dev/null +++ b/sop-java/src/main/java/sop/SigningResult.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop; + +/** + * This class contains various information about a signed message. + */ +public final class SigningResult { + + private final MicAlg micAlg; + + private SigningResult(MicAlg micAlg) { + this.micAlg = micAlg; + } + + /** + * Return a string identifying the digest mechanism used to create the signed message. + * This is useful for setting the micalg= parameter for the multipart/signed + * content type of a PGP/MIME object as described in section 5 of [RFC3156]. + * + * If more than one signature was generated and different digest mechanisms were used, + * the value of the micalg object is an empty string. + * + * @return micalg + */ + public MicAlg getMicAlg() { + return micAlg; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private MicAlg micAlg; + + public Builder setMicAlg(MicAlg micAlg) { + this.micAlg = micAlg; + return this; + } + + public SigningResult build() { + SigningResult signingResult = new SigningResult(micAlg); + return signingResult; + } + } +} diff --git a/sop-java/src/main/java/sop/operation/Sign.java b/sop-java/src/main/java/sop/operation/Sign.java index 75f4e5a8..be518cde 100644 --- a/sop-java/src/main/java/sop/operation/Sign.java +++ b/sop-java/src/main/java/sop/operation/Sign.java @@ -8,8 +8,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import sop.MicAlg; import sop.ReadyWithResult; +import sop.SigningResult; import sop.enums.SignAs; import sop.exception.SOPGPException; @@ -55,7 +55,7 @@ public interface Sign { * @param data input stream containing data * @return ready */ - ReadyWithResult data(InputStream data) throws IOException, SOPGPException.ExpectedText; + ReadyWithResult data(InputStream data) throws IOException, SOPGPException.ExpectedText; /** * Signs data. @@ -63,7 +63,7 @@ public interface Sign { * @param data byte array containing data * @return ready */ - default ReadyWithResult data(byte[] data) throws IOException, SOPGPException.ExpectedText { + default ReadyWithResult data(byte[] data) throws IOException, SOPGPException.ExpectedText { return data(new ByteArrayInputStream(data)); } } From 1b1a13e7d0eb74abc094465dea50e22911c3a97b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 9 Jan 2022 01:24:56 +0100 Subject: [PATCH 0017/1204] Fix spelling of Bouncy Castle --- .../src/main/java/org/pgpainless/sop/VersionImpl.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index 41fbd838..d1c15455 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -12,6 +12,10 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; import sop.operation.Version; public class VersionImpl implements Version { + + // draft version + private static final String SOP_VERSION = "3"; + @Override public String getName() { return "PGPainless-SOP"; @@ -38,7 +42,7 @@ public class VersionImpl implements Version { @Override public String getBackendVersion() { double bcVersion = new BouncyCastleProvider().getVersion(); - return String.format("Bouncycastle %,.2f", bcVersion); + return String.format("Bouncy Castle %,.2f", bcVersion); } @Override @@ -47,7 +51,7 @@ public class VersionImpl implements Version { "Based on PGPainless " + getVersion() + "\n" + "Using " + getBackendVersion() + "\n" + "See https://pgpainless.org\n" + - "Implementing Stateless OpenPGP Protocol Version 3\n" + + "Implementation of the Stateless OpenPGP Protocol Version " + SOP_VERSION + "\n" + "See https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03"; } } From 824b8de40412da3693eef5cb3c4a250523989aec Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 9 Jan 2022 01:25:18 +0100 Subject: [PATCH 0018/1204] Add sessionKey fromString test --- sop-java/src/test/java/sop/util/SessionKeyTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/sop-java/src/test/java/sop/util/SessionKeyTest.java b/sop-java/src/test/java/sop/util/SessionKeyTest.java index 2d03279d..2891d0d4 100644 --- a/sop-java/src/test/java/sop/util/SessionKeyTest.java +++ b/sop-java/src/test/java/sop/util/SessionKeyTest.java @@ -6,6 +6,7 @@ package sop.util; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; import sop.SessionKey; @@ -45,4 +46,16 @@ public class SessionKeyTest { assertNotEquals(s1, null); assertNotEquals(s1, "FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"); } + + @Test + public void fromString_missingAlgorithmIdThrows() { + String missingAlgorithId = "FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"; + assertThrows(IllegalArgumentException.class, () -> SessionKey.fromString(missingAlgorithId)); + } + + @Test + public void fromString_wrongDivider() { + String semicolonDivider = "9;FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"; + assertThrows(IllegalArgumentException.class, () -> SessionKey.fromString(semicolonDivider)); + } } From 1c2cbf0e75abd7d8f278bfbefffa7cac951bd2d1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 9 Jan 2022 02:02:04 +0100 Subject: [PATCH 0019/1204] Add SigningResultTest --- .../test/java/sop/util/SigningResultTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 sop-java/src/test/java/sop/util/SigningResultTest.java diff --git a/sop-java/src/test/java/sop/util/SigningResultTest.java b/sop-java/src/test/java/sop/util/SigningResultTest.java new file mode 100644 index 00000000..0d35cdc1 --- /dev/null +++ b/sop-java/src/test/java/sop/util/SigningResultTest.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import sop.MicAlg; +import sop.SigningResult; + +public class SigningResultTest { + + @Test + public void basicBuilderTest() { + SigningResult result = SigningResult.builder() + .setMicAlg(MicAlg.fromHashAlgorithmId(10)) + .build(); + + assertEquals("pgp-sha512", result.getMicAlg().getMicAlg()); + } +} From 1aca7112d2a72bb36be50859d2e21bb3b726e741 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 9 Jan 2022 02:04:12 +0100 Subject: [PATCH 0020/1204] SOP commands: Throw UnsupportedSubcommand error when sop.command() returns null --- .../sop/cli/picocli/commands/ArmorCmd.java | 4 + .../sop/cli/picocli/commands/DearmorCmd.java | 6 ++ .../sop/cli/picocli/commands/DecryptCmd.java | 2 +- .../DetachInbandSignatureAndMessageCmd.java | 6 +- .../sop/cli/picocli/commands/EncryptCmd.java | 4 + .../cli/picocli/commands/ExtractCertCmd.java | 4 + .../cli/picocli/commands/GenerateKeyCmd.java | 4 + .../sop/cli/picocli/commands/SignCmd.java | 3 + .../sop/cli/picocli/commands/VerifyCmd.java | 4 + .../sop/cli/picocli/commands/VersionCmd.java | 4 + .../test/java/sop/cli/picocli/SOPTest.java | 88 +++++++++++++++++++ .../java/sop/exception/SOPGPException.java | 8 -- 12 files changed, 127 insertions(+), 10 deletions(-) diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java index 139cfcdb..a015a688 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java @@ -25,6 +25,10 @@ public class ArmorCmd implements Runnable { @Override public void run() { Armor armor = SopCLI.getSop().armor(); + if (armor == null) { + throw new SOPGPException.UnsupportedSubcommand("Command 'armor' not implemented."); + } + if (label != null) { try { armor.label(label); diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java index f3c62908..343b1135 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java @@ -10,6 +10,7 @@ import picocli.CommandLine; import sop.cli.picocli.Print; import sop.cli.picocli.SopCLI; import sop.exception.SOPGPException; +import sop.operation.Dearmor; @CommandLine.Command(name = "dearmor", description = "Remove ASCII Armor from standard input", @@ -18,6 +19,11 @@ public class DearmorCmd implements Runnable { @Override public void run() { + Dearmor dearmor = SopCLI.getSop().dearmor(); + if (dearmor == null) { + throw new SOPGPException.UnsupportedSubcommand("Command 'dearmor' not implemented."); + } + try { SopCLI.getSop() .dearmor() diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java index 58f49db9..8fc4650b 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java @@ -94,7 +94,7 @@ public class DecryptCmd implements Runnable { Decrypt decrypt = SopCLI.getSop().decrypt(); if (decrypt == null) { - throw new SOPGPException.UnsupportedSubcommand("Subcommand 'decrypt' not implemented."); + throw new SOPGPException.UnsupportedSubcommand("Command 'decrypt' not implemented."); } setNotAfter(notAfter, decrypt); diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java index 77471681..f5c71a2a 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java @@ -32,11 +32,15 @@ public class DetachInbandSignatureAndMessageCmd implements Runnable { @Override public void run() { + DetachInbandSignatureAndMessage detach = SopCLI.getSop().detachInbandSignatureAndMessage(); + if (detach == null) { + throw new SOPGPException.UnsupportedSubcommand("Command 'detach-inband-signature-and-message' not implemented."); + } + if (signaturesOut == null) { throw new SOPGPException.MissingArg("--signatures-out is required."); } - DetachInbandSignatureAndMessage detach = SopCLI.getSop().detachInbandSignatureAndMessage(); if (!armor) { detach.noArmor(); } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java index 2ccf0477..0634240b 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java @@ -51,6 +51,10 @@ public class EncryptCmd implements Runnable { @Override public void run() { Encrypt encrypt = SopCLI.getSop().encrypt(); + if (encrypt == null) { + throw new SOPGPException.UnsupportedSubcommand("Command 'encrypt' not implemented."); + } + if (type != null) { try { encrypt.mode(type); diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java index 59656e65..f4559339 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java @@ -25,6 +25,10 @@ public class ExtractCertCmd implements Runnable { @Override public void run() { ExtractCert extractCert = SopCLI.getSop().extractCert(); + if (extractCert == null) { + throw new SOPGPException.UnsupportedSubcommand("Command 'extract-cert' not implemented."); + } + if (!armor) { extractCert.noArmor(); } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java index f97fcfa0..28bde279 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java @@ -31,6 +31,10 @@ public class GenerateKeyCmd implements Runnable { @Override public void run() { GenerateKey generateKey = SopCLI.getSop().generateKey(); + if (generateKey == null) { + throw new SOPGPException.UnsupportedSubcommand("Command 'generate-key' not implemented."); + } + for (String userId : userId) { generateKey.userId(userId); } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java index 250679fd..7574923e 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java @@ -47,6 +47,9 @@ public class SignCmd implements Runnable { @Override public void run() { Sign sign = SopCLI.getSop().sign(); + if (sign == null) { + throw new SOPGPException.UnsupportedSubcommand("Command 'sign' not implemented."); + } if (type != null) { try { diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java index d731b25a..2702b4b9 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java @@ -53,6 +53,10 @@ public class VerifyCmd implements Runnable { @Override public void run() { Verify verify = SopCLI.getSop().verify(); + if (verify == null) { + throw new SOPGPException.UnsupportedSubcommand("Command 'verify' not implemented."); + } + if (notAfter != null) { try { verify.notAfter(DateParser.parseNotAfter(notAfter)); diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java index 0d5da1a6..6926c5c9 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java @@ -7,6 +7,7 @@ package sop.cli.picocli.commands; import picocli.CommandLine; import sop.cli.picocli.Print; import sop.cli.picocli.SopCLI; +import sop.exception.SOPGPException; import sop.operation.Version; @CommandLine.Command(name = "version", description = "Display version information about the tool", @@ -16,6 +17,9 @@ public class VersionCmd implements Runnable { @Override public void run() { Version version = SopCLI.getSop().version(); + if (version == null) { + throw new SOPGPException.UnsupportedSubcommand("Command 'version' not implemented."); + } Print.outln(version.getName() + " " + version.getVersion()); } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java index fbf4cfa7..6360a779 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java @@ -4,11 +4,26 @@ package sop.cli.picocli; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.Test; import sop.SOP; +import sop.operation.Armor; +import sop.operation.Dearmor; +import sop.operation.Decrypt; +import sop.operation.DetachInbandSignatureAndMessage; +import sop.operation.Encrypt; +import sop.operation.ExtractCert; +import sop.operation.GenerateKey; +import sop.operation.Sign; +import sop.operation.Verify; +import sop.operation.Version; public class SOPTest { @@ -28,4 +43,77 @@ public class SOPTest { // At this point, no SOP backend is set, so an InvalidStateException triggers exit(1) SopCLI.main(new String[] {"armor"}); } + + @Test + public void UnsupportedSubcommandsTest() { + SOP nullCommandSOP = new SOP() { + @Override + public Version version() { + return null; + } + + @Override + public GenerateKey generateKey() { + return null; + } + + @Override + public ExtractCert extractCert() { + return null; + } + + @Override + public Sign sign() { + return null; + } + + @Override + public Verify verify() { + return null; + } + + @Override + public Encrypt encrypt() { + return null; + } + + @Override + public Decrypt decrypt() { + return null; + } + + @Override + public Armor armor() { + return null; + } + + @Override + public Dearmor dearmor() { + return null; + } + + @Override + public DetachInbandSignatureAndMessage detachInbandSignatureAndMessage() { + return null; + } + }; + SopCLI.setSopInstance(nullCommandSOP); + + List commands = new ArrayList<>(); + commands.add(new String[] {"armor"}); + commands.add(new String[] {"dearmor"}); + commands.add(new String[] {"decrypt"}); + commands.add(new String[] {"detach-inband-signature-and-message"}); + commands.add(new String[] {"encrypt"}); + commands.add(new String[] {"extract-cert"}); + commands.add(new String[] {"generate-key"}); + commands.add(new String[] {"sign"}); + commands.add(new String[] {"verify", "signature.asc", "cert.asc"}); + commands.add(new String[] {"version"}); + + for (String[] command : commands) { + int exit = SopCLI.execute(command); + assertEquals(69, exit, "Unexpected exit code for non-implemented command " + Arrays.toString(command) + ": " + exit); + } + } } diff --git a/sop-java/src/main/java/sop/exception/SOPGPException.java b/sop-java/src/main/java/sop/exception/SOPGPException.java index dfd09540..6b844f59 100644 --- a/sop-java/src/main/java/sop/exception/SOPGPException.java +++ b/sop-java/src/main/java/sop/exception/SOPGPException.java @@ -52,10 +52,6 @@ public abstract class SOPGPException extends RuntimeException { super(message, e); } - public UnsupportedAsymmetricAlgo(Throwable e) { - super(e); - } - @Override public int getExitCode() { return EXIT_CODE; @@ -308,10 +304,6 @@ public abstract class SOPGPException extends RuntimeException { super(); } - public KeyCannotSign(String message) { - super(message); - } - public KeyCannotSign(String s, KeyCannotSign keyCannotSign) { super(s, keyCannotSign); } From e374951ed00ccea3032bde77a2783a952642b646 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 9 Jan 2022 02:31:04 +0100 Subject: [PATCH 0021/1204] Remove ProofUtil. This does not belong here. --- .../signature/consumer/ProofUtil.java | 141 ------------------ .../signature/builder/ProofUtilTest.java | 77 ---------- 2 files changed, 218 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/consumer/ProofUtil.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/signature/builder/ProofUtilTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/ProofUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/ProofUtil.java deleted file mode 100644 index e22886ca..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/ProofUtil.java +++ /dev/null @@ -1,141 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.sig.NotationData; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.pgpainless.PGPainless; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.builder.DirectKeySignatureBuilder; -import org.pgpainless.signature.builder.SelfSignatureBuilder; - -public class ProofUtil { - - public PGPSecretKeyRing addProof(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, Proof proof) - throws PGPException { - return addProofs(secretKey, protector, Collections.singletonList(proof)); - } - - public PGPSecretKeyRing addProofs(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, List proofs) - throws PGPException { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); - return addProofs(secretKey, protector, info.getPrimaryUserId(), proofs); - } - - public PGPSecretKeyRing addProof(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, String userId, Proof proof) - throws PGPException { - return addProofs(secretKey, protector, userId, Collections.singletonList(proof)); - } - - public PGPSecretKeyRing addProofs(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, - @Nullable String userId, List proofs) - throws PGPException { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); - PGPSecretKey certificationKey = secretKey.getSecretKey(); - PGPPublicKey certificationPubKey = certificationKey.getPublicKey(); - PGPSignature certification = null; - - // null userid -> make direct key sig - if (userId == null) { - PGPSignature previousCertification = info.getLatestDirectKeySelfSignature(); - if (previousCertification == null) { - throw new NoSuchElementException("No previous valid direct key signature found."); - } - - DirectKeySignatureBuilder sigBuilder = new DirectKeySignatureBuilder(certificationKey, protector, previousCertification); - for (Proof proof : proofs) { - sigBuilder.getHashedSubpackets().addNotationData(false, proof.getNotationName(), proof.getNotationValue()); - } - certification = sigBuilder.build(certificationPubKey); - certificationPubKey = PGPPublicKey.addCertification(certificationPubKey, certification); - } else { - if (!info.isUserIdValid(userId)) { - throw new IllegalArgumentException("User ID " + userId + " seems to not be valid for this key."); - } - PGPSignature previousCertification = info.getLatestUserIdCertification(userId); - if (previousCertification == null) { - throw new NoSuchElementException("No previous valid user-id certification found."); - } - - SelfSignatureBuilder sigBuilder = new SelfSignatureBuilder(certificationKey, protector, previousCertification); - for (Proof proof : proofs) { - sigBuilder.getHashedSubpackets().addNotationData(false, proof.getNotationName(), proof.getNotationValue()); - } - certification = sigBuilder.build(certificationPubKey, userId); - certificationPubKey = PGPPublicKey.addCertification(certificationPubKey, userId, certification); - } - certificationKey = PGPSecretKey.replacePublicKey(certificationKey, certificationPubKey); - secretKey = PGPSecretKeyRing.insertSecretKey(secretKey, certificationKey); - - return secretKey; - } - - public static class Proof { - public static final String NOTATION_NAME = "proof@metacode.biz"; - private final String notationValue; - - public Proof(String notationValue) { - if (notationValue == null) { - throw new IllegalArgumentException("Notation value cannot be null."); - } - String trimmed = notationValue.trim(); - if (trimmed.isEmpty()) { - throw new IllegalArgumentException("Notation value cannot be empty."); - } - this.notationValue = trimmed; - } - - public String getNotationName() { - return NOTATION_NAME; - } - - public String getNotationValue() { - return notationValue; - } - - public static Proof fromMatrixPermalink(String username, String eventPermalink) { - Pattern pattern = Pattern.compile("^https:\\/\\/matrix\\.to\\/#\\/(![a-zA-Z]{18}:matrix\\.org)\\/(\\$[a-zA-Z0-9\\-_]{43})\\?via=.*$"); - Matcher matcher = pattern.matcher(eventPermalink); - if (!matcher.matches()) { - throw new IllegalArgumentException("Invalid matrix event permalink."); - } - String roomId = matcher.group(1); - String eventId = matcher.group(2); - return new Proof(String.format("matrix:u/%s?org.keyoxide.r=%s&org.keyoxide.e=%s", username, roomId, eventId)); - } - - @Override - public String toString() { - return getNotationName() + "=" + getNotationValue(); - } - } - - public static List getProofs(PGPSignature signature) { - PGPSignatureSubpacketVector hashedSubpackets = signature.getHashedSubPackets(); - NotationData[] notations = hashedSubpackets.getNotationDataOccurrences(); - - List proofs = new ArrayList<>(); - for (NotationData notation : notations) { - if (notation.getNotationName().equals(Proof.NOTATION_NAME)) { - proofs.add(new Proof(notation.getNotationValue())); - } - } - return proofs; - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ProofUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ProofUtilTest.java deleted file mode 100644 index 0b5d8f0c..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ProofUtilTest.java +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.consumer.ProofUtil; - -public class ProofUtilTest { - - @Test - public void testEmptyProofThrows() { - assertThrows(IllegalArgumentException.class, () -> new ProofUtil.Proof("")); - } - - @Test - public void testNullProofThrows() { - assertThrows(IllegalArgumentException.class, () -> new ProofUtil.Proof(null)); - } - - @Test - public void proofIsTrimmed() { - ProofUtil.Proof proof = new ProofUtil.Proof(" foo:bar "); - assertEquals("proof@metacode.biz=foo:bar", proof.toString()); - } - - @Test - public void testMatrixProof() { - String matrixUser = "@foo:matrix.org"; - String permalink = "https://matrix.to/#/!dBfQZxCoGVmSTujfiv:matrix.org/$3dVX1nv3lmwnKxc0mgto_Sf-REVr45Z6G7LWLWal10w?via=chat.matrix.org"; - ProofUtil.Proof proof = ProofUtil.Proof.fromMatrixPermalink(matrixUser, permalink); - - assertEquals("proof@metacode.biz=matrix:u/@foo:matrix.org?org.keyoxide.r=!dBfQZxCoGVmSTujfiv:matrix.org&org.keyoxide.e=$3dVX1nv3lmwnKxc0mgto_Sf-REVr45Z6G7LWLWal10w", - proof.toString()); - } - - @Test - public void testXmppBasicProof() { - String jid = "alice@pgpainless.org"; - ProofUtil.Proof proof = new ProofUtil.Proof("xmpp:" + jid); - - assertEquals("proof@metacode.biz=xmpp:alice@pgpainless.org", proof.toString()); - } - - @Test - public void testAddProof() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InterruptedException { - String userId = "Alice "; - PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing(userId, null); - Thread.sleep(1000L); - secretKey = new ProofUtil().addProof( - secretKey, - SecretKeyRingProtector.unprotectedKeys(), - new ProofUtil.Proof("xmpp:alice@pgpainless.org")); - - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); - PGPSignature signature = info.getLatestUserIdCertification(userId); - assertNotNull(signature); - assertFalse(ProofUtil.getProofs(signature).isEmpty()); - } -} From 01839728f0cca74bac981a3814f37aa72aa1c88d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 10 Jan 2022 14:38:58 +0100 Subject: [PATCH 0022/1204] Remove workaround for publicKey.getBitStrength() == -1 in BC see https://github.com/bcgit/bc-java/issues/972 --- .../secretkeyring/SecretKeyRingEditor.java | 3 +- .../consumer/SignatureValidator.java | 11 ++---- .../main/java/org/pgpainless/util/BCUtil.java | 38 ------------------- .../BrainpoolKeyGenerationTest.java | 11 +++--- 4 files changed, 10 insertions(+), 53 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index 66336c6f..7eb5a92f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -64,7 +64,6 @@ import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.BCUtil; import org.pgpainless.util.CollectionUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.selection.userid.SelectUserId; @@ -278,7 +277,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { // check key against public key algorithm policy PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.fromId(subkey.getPublicKey().getAlgorithm()); - int bitStrength = BCUtil.getBitStrength(subkey.getPublicKey()); + int bitStrength = subkey.getPublicKey().getBitStrength(); if (!PGPainless.getPolicy().getPublicKeyAlgorithmPolicy().isAcceptable(publicKeyAlgorithm, bitStrength)) { throw new IllegalArgumentException("Public key algorithm policy violation: " + publicKeyAlgorithm + " with bit strength " + bitStrength + " is not acceptable."); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java index 3bd059a2..d0de0d46 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java @@ -4,7 +4,6 @@ package org.pgpainless.signature.consumer; -import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Date; import java.util.Iterator; @@ -32,7 +31,6 @@ import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.BCUtil; import org.pgpainless.util.DateUtil; import org.pgpainless.util.NotationRegistry; @@ -171,15 +169,14 @@ public abstract class SignatureValidator { @Override public void verify(PGPSignature signature) throws SignatureValidationException { PublicKeyAlgorithm algorithm = PublicKeyAlgorithm.fromId(signingKey.getAlgorithm()); - try { - int bitStrength = BCUtil.getBitStrength(signingKey); + int bitStrength = signingKey.getBitStrength(); + if (bitStrength == -1) { + throw new SignatureValidationException("Cannot determine bit strength of signing key."); + } if (!policy.getPublicKeyAlgorithmPolicy().isAcceptable(algorithm, bitStrength)) { throw new SignatureValidationException("Signature was made using unacceptable key. " + algorithm + " (" + bitStrength + " bits) is not acceptable according to the public key algorithm policy."); } - } catch (NoSuchAlgorithmException e) { - throw new SignatureValidationException("Cannot determine bit strength of signing key.", e); - } } }; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java index 25bd72e9..78e604c8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java @@ -4,50 +4,12 @@ package org.pgpainless.util; -import java.security.NoSuchAlgorithmException; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.bcpg.ECPublicBCPGKey; -import org.bouncycastle.openpgp.PGPPublicKey; - public final class BCUtil { private BCUtil() { } - /** - * Utility method to get the bit strength of OpenPGP keys. - * Bouncycastle is lacking support for some keys (eg. EdDSA, X25519), so this method - * manually derives the bit strength from the keys curves OID. - * - * @param key key - * @return bit strength - */ - public static int getBitStrength(PGPPublicKey key) throws NoSuchAlgorithmException { - int bitStrength = key.getBitStrength(); - - if (bitStrength == -1) { - // BC's PGPPublicKey.getBitStrength() does fail for some keys (EdDSA, X25519) - // therefore we manually set the bit strength. - // see https://github.com/bcgit/bc-java/issues/972 - - ASN1ObjectIdentifier oid = ((ECPublicBCPGKey) key.getPublicKeyPacket().getKey()).getCurveOID(); - if (oid.getId().equals("1.3.6.1.4.1.11591.15.1")) { - // ed25519 is 256 bits - bitStrength = 256; - } else if (oid.getId().equals("1.3.6.1.4.1.3029.1.5.1")) { - // curvey25519 is 256 bits - bitStrength = 256; - } else { - throw new NoSuchAlgorithmException("Unknown curve: " + oid.getId()); - } - - } - return bitStrength; - } - - /** * A constant time equals comparison - does not terminate early if * test will fail. For best results always pass the expected value diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java index 8a8afe0e..969a0587 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java @@ -29,9 +29,8 @@ import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.info.KeyInfo; import org.pgpainless.key.util.UserId; -import org.pgpainless.util.BCUtil; -import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.TestAllImplementations; public class BrainpoolKeyGenerationTest { @@ -96,22 +95,22 @@ public class BrainpoolKeyGenerationTest { PGPSecretKey ecdsaPrim = iterator.next(); KeyInfo ecdsaInfo = new KeyInfo(ecdsaPrim); assertEquals(EllipticCurve._BRAINPOOLP384R1.getName(), ecdsaInfo.getCurveName()); - assertEquals(384, BCUtil.getBitStrength(ecdsaPrim.getPublicKey())); + assertEquals(384, ecdsaPrim.getPublicKey().getBitStrength()); PGPSecretKey eddsaSub = iterator.next(); KeyInfo eddsaInfo = new KeyInfo(eddsaSub); assertEquals(EdDSACurve._Ed25519.getName(), eddsaInfo.getCurveName()); - assertEquals(256, BCUtil.getBitStrength(eddsaSub.getPublicKey())); + assertEquals(256, eddsaSub.getPublicKey().getBitStrength()); PGPSecretKey xdhSub = iterator.next(); KeyInfo xdhInfo = new KeyInfo(xdhSub); assertEquals(XDHSpec._X25519.getCurveName(), xdhInfo.getCurveName()); - assertEquals(256, BCUtil.getBitStrength(xdhSub.getPublicKey())); + assertEquals(256, xdhSub.getPublicKey().getBitStrength()); PGPSecretKey rsaSub = iterator.next(); KeyInfo rsaInfo = new KeyInfo(rsaSub); assertThrows(IllegalArgumentException.class, rsaInfo::getCurveName, "RSA is not a curve-based encryption system"); - assertEquals(3072, BCUtil.getBitStrength(rsaSub.getPublicKey())); + assertEquals(3072, rsaSub.getPublicKey().getBitStrength()); } public PGPSecretKeyRing generateKey(KeySpec primaryKey, KeySpec subKey, String userId) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { From bbc42fd8e4eb9c6fba1da2e13e2e80c2e6969957 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 10 Jan 2022 14:51:17 +0100 Subject: [PATCH 0023/1204] Document workaround for BCs ECUtil.getCurveName() returning null for ed25519 keys See https://github.com/bcgit/bc-java/issues/1087 --- .../src/main/java/org/pgpainless/key/info/KeyInfo.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java index 1cc4b6c8..a37eda24 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java @@ -89,12 +89,19 @@ public class KeyInfo { public static String getCurveName(ECPublicBCPGKey key) { ASN1ObjectIdentifier identifier = key.getCurveOID(); + String curveName = ECUtil.getCurveName(identifier); + if (curveName != null) { + return curveName; + } + // Workaround for ECUtil not recognizing ed25519 + // see https://github.com/bcgit/bc-java/issues/1087 + // TODO: Remove once BC 1.71 gets released and contains a fix if (identifier.equals(GNUObjectIdentifiers.Ed25519)) { return EdDSACurve._Ed25519.getName(); } - return ECUtil.getCurveName(identifier); + return null; } /** From 2e7a21b5b70720def9987626e5f40c48b84e1f9e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 10 Jan 2022 16:16:49 +0100 Subject: [PATCH 0024/1204] Add javadoc description to DetachInbandSignatureAndMessage --- .../java/sop/operation/DetachInbandSignatureAndMessage.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sop-java/src/main/java/sop/operation/DetachInbandSignatureAndMessage.java b/sop-java/src/main/java/sop/operation/DetachInbandSignatureAndMessage.java index 46bd3f77..9e22258c 100644 --- a/sop-java/src/main/java/sop/operation/DetachInbandSignatureAndMessage.java +++ b/sop-java/src/main/java/sop/operation/DetachInbandSignatureAndMessage.java @@ -11,6 +11,9 @@ import java.io.InputStream; import sop.ReadyWithResult; import sop.Signatures; +/** + * Split cleartext signed messages up into data and signatures. + */ public interface DetachInbandSignatureAndMessage { /** From fc432901ed8cfcb9076374d6238b226e9eea4d06 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 10 Jan 2022 16:44:18 +0100 Subject: [PATCH 0025/1204] Add information to SOP READMEs --- pgpainless-sop/README.md | 42 ++++++++++++++++++++++++++++++++- sop-java/README.md | 51 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 15aa483c..067f85c8 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -6,7 +6,47 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless-SOP +[![Spec Revision: 3](https://img.shields.io/badge/Spec%20Revision-3-blue)](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03) +[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-sop)](https://search.maven.org/artifact/org.pgpainless/pgpainless-sop) +[![JavaDoc](https://badgen.net/badge/javadoc/yes/green)](https://pgpainless.org/releases/latest/javadoc/org/pgpainless/sop/package-summary.html) +[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/pgpainless)](https://api.reuse.software/info/github.com/pgpainless/pgpainless) + Implementation of the Stateless OpenPGP Protocol using PGPainless. This module implements `sop-java` using `pgpainless-core`. -If your code depends on `sop-java`, this module can be used as a realization of those interfaces. \ No newline at end of file +If your code depends on `sop-java`, this module can be used as a realization of those interfaces. + +## Usage Examples +```java +SOP sop = new SOPImpl(); + +// Generate an OpenPGP key +byte[] key = sop.generateKey() + .userId("Alice ") + .generate() + .getBytes(); + +// Extract the certificate (public key) +byte[] cert = sop.extractCert() + .key(key) + .getBytes(); + +// Encrypt a message +byte[] message = ... +byte[] encrypted = sop.encrypt() + .withCert(cert) + .signWith(key) + .plaintext(message) + .getBytes(); + +// Decrypt a message +ByteArrayAndResult messageAndVerifications = sop.decrypt() + .verifyWith(cert) + .withKey(key) + .ciphertext(encrypted) + .toByteArrayAndResult(); +byte[] decrypted = messageAndVerifications.getBytes(); +// Signature Verifications +DecryptionResult messageInfo = messageAndVerifications.getResult(); +List signatureVerifications = messageInfo.getVerifications(); +``` \ No newline at end of file diff --git a/sop-java/README.md b/sop-java/README.md index 86d02008..452576c6 100644 --- a/sop-java/README.md +++ b/sop-java/README.md @@ -6,13 +6,60 @@ SPDX-License-Identifier: Apache-2.0 # SOP-Java +[![Spec Revision: 3](https://img.shields.io/badge/Spec%20Revision-3-blue)](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03) +[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/sop-java)](https://search.maven.org/artifact/org.pgpainless/sop-java) +[![JavaDoc](https://badgen.net/badge/javadoc/yes/green)](https://pgpainless.org/releases/latest/javadoc/sop/SOP.html) +[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/pgpainless)](https://api.reuse.software/info/github.com/pgpainless/pgpainless) + Stateless OpenPGP Protocol for Java. This module contains interfaces that model the API described by the -[Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) specification. +[Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03) specification. This module is not a command line application! For that, see `sop-java-picocli`. +## Usage Examples + +The API defined by `sop-java` is super straight forward: +```java +SOP sop = ... // e.g. new org.pgpainless.sop.SOPImpl(); + +// Generate an OpenPGP key +byte[] key = sop.generateKey() + .userId("Alice ") + .generate() + .getBytes(); + +// Extract the certificate (public key) +byte[] cert = sop.extractCert() + .key(key) + .getBytes(); + +// Encrypt a message +byte[] message = ... +byte[] encrypted = sop.encrypt() + .withCert(cert) + .signWith(key) + .plaintext(message) + .getBytes(); + +// Decrypt a message +ByteArrayAndResult messageAndVerifications = sop.decrypt() + .verifyWith(cert) + .withKey(key) + .ciphertext(encrypted) + .toByteArrayAndResult(); +byte[] decrypted = messageAndVerifications.getBytes(); +// Signature Verifications +DecryptionResult messageInfo = messageAndVerifications.getResult(); +List signatureVerifications = messageInfo.getVerifications(); +``` + +Furthermore, the API is capable of signing messages and verifying unencrypted signed data, as well as adding and removing ASCII armor. + +### Limitations +As per the spec, sop-java does not (yet) deal with encrypted OpenPGP keys. + ## Why should I use this? If you need to use OpenPGP functionality like encrypting/decrypting messages, or creating/verifying @@ -30,4 +77,4 @@ by swapping out the dependency with minimal changes to your code. Did you create an [OpenPGP](https://datatracker.ietf.org/doc/html/rfc4880) implementation that can be used in the Java ecosystem? By implementing the `sop-java` interface, you can turn your library into a command line interface (see `sop-java-picocli`). This allows you to plug your library into the [OpenPGP interoperability test suite](https://tests.sequoia-pgp.org/) -of the [Sequoia-PGP](https://sequoia-pgp.org/) project. \ No newline at end of file +of the [Sequoia-PGP](https://sequoia-pgp.org/) project. From 19b6c8b1e3c918f940baed871c80d8b368bd6e2c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 10 Jan 2022 17:11:28 +0100 Subject: [PATCH 0026/1204] SOP-CLI: Implement additional version flags --- .../java/org/pgpainless/sop/VersionImpl.java | 18 ++++++++---- .../sop/cli/picocli/commands/VersionCmd.java | 28 ++++++++++++++++++- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index d1c15455..7675fcb9 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -6,6 +6,7 @@ package org.pgpainless.sop; import java.io.IOException; import java.io.InputStream; +import java.util.Locale; import java.util.Properties; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -14,7 +15,7 @@ import sop.operation.Version; public class VersionImpl implements Version { // draft version - private static final String SOP_VERSION = "3"; + private static final String SOP_VERSION = "03"; @Override public String getName() { @@ -42,16 +43,21 @@ public class VersionImpl implements Version { @Override public String getBackendVersion() { double bcVersion = new BouncyCastleProvider().getVersion(); - return String.format("Bouncy Castle %,.2f", bcVersion); + return String.format(Locale.US, "Bouncy Castle %.2f", bcVersion); } @Override public String getExtendedVersion() { return getName() + " " + getVersion() + "\n" + - "Based on PGPainless " + getVersion() + "\n" + - "Using " + getBackendVersion() + "\n" + - "See https://pgpainless.org\n" + + "https://codeberg.org/PGPainless/pgpainless/src/branch/master/pgpainless-sop\n" + + "\n" + "Implementation of the Stateless OpenPGP Protocol Version " + SOP_VERSION + "\n" + - "See https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03"; + "https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-" + SOP_VERSION + "\n" + + "\n" + + "Based on pgpainless-core " + getVersion() + "\n" + + "https://pgpainless.org\n" + + "\n" + + "Using " + getBackendVersion() + "\n" + + "https://www.bouncycastle.org/java.html"; } } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java index 6926c5c9..4a319192 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java @@ -14,6 +14,19 @@ import sop.operation.Version; exitCodeOnInvalidInput = 37) public class VersionCmd implements Runnable { + @CommandLine.ArgGroup() + Exclusive exclusive; + + static class Exclusive { + @CommandLine.Option(names = "--extended", description = "Print an extended version string.") + boolean extended; + + @CommandLine.Option(names = "--backend", description = "Print information about the cryptographic backend.") + boolean backend; + } + + + @Override public void run() { Version version = SopCLI.getSop().version(); @@ -21,6 +34,19 @@ public class VersionCmd implements Runnable { throw new SOPGPException.UnsupportedSubcommand("Command 'version' not implemented."); } - Print.outln(version.getName() + " " + version.getVersion()); + if (exclusive == null) { + Print.outln(version.getName() + " " + version.getVersion()); + return; + } + + if (exclusive.extended) { + Print.outln(version.getExtendedVersion()); + return; + } + + if (exclusive.backend) { + Print.outln(version.getBackendVersion()); + return; + } } } From 9800ca8bd4ed4db76ab84706f8ffb4316c1cd20a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 10 Jan 2022 17:20:15 +0100 Subject: [PATCH 0027/1204] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43aa5ddb..cf32f122 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.0.2-SNAPSHOT +- Update SOP implementation to specification revision 03 +- `OpenPGPV4Fingerprint`: Hex decode bytes in constructor +- Add `ArmorUtils.toAsciiArmoredString()` for single key +- Fix `ClassCastException` when retrieving `RevocationKey` subpackets from signatures + ## 1.0.1 - Fix sourcing of preferred algorithms by primary user-id when key is located via key-id From c6bc8f9774ed37e5575155f90922315b30ea8ead Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Jan 2022 15:18:34 +0100 Subject: [PATCH 0028/1204] Moved sop-java and sop-java-picocli to its own repositories See https://github.com/pgpainless/sop-java See https://codeberg.org/PGPainless/sop-java --- build.gradle | 7 + pgpainless-cli/build.gradle | 2 +- pgpainless-sop/build.gradle | 2 +- settings.gradle | 2 - sop-java-picocli/README.md | 35 +- sop-java-picocli/build.gradle | 39 -- .../main/java/sop/cli/picocli/DateParser.java | 33 -- .../main/java/sop/cli/picocli/FileUtil.java | 98 ----- .../src/main/java/sop/cli/picocli/Print.java | 26 -- .../picocli/SOPExceptionExitCodeMapper.java | 34 -- .../picocli/SOPExecutionExceptionHandler.java | 26 -- .../src/main/java/sop/cli/picocli/SopCLI.java | 68 ---- .../sop/cli/picocli/commands/ArmorCmd.java | 54 --- .../sop/cli/picocli/commands/DearmorCmd.java | 42 --- .../sop/cli/picocli/commands/DecryptCmd.java | 240 ------------ .../DetachInbandSignatureAndMessageCmd.java | 59 --- .../sop/cli/picocli/commands/EncryptCmd.java | 123 ------- .../cli/picocli/commands/ExtractCertCmd.java | 45 --- .../cli/picocli/commands/GenerateKeyCmd.java | 63 ---- .../sop/cli/picocli/commands/SignCmd.java | 121 ------ .../sop/cli/picocli/commands/VerifyCmd.java | 136 ------- .../sop/cli/picocli/commands/VersionCmd.java | 52 --- .../cli/picocli/commands/package-info.java | 8 - .../java/sop/cli/picocli/package-info.java | 8 - .../java/sop/cli/picocli/DateParserTest.java | 49 --- .../java/sop/cli/picocli/FileUtilTest.java | 123 ------- .../test/java/sop/cli/picocli/SOPTest.java | 119 ------ .../cli/picocli/commands/ArmorCmdTest.java | 101 ----- .../cli/picocli/commands/DearmorCmdTest.java | 61 ---- .../cli/picocli/commands/DecryptCmdTest.java | 344 ------------------ .../cli/picocli/commands/EncryptCmdTest.java | 194 ---------- .../picocli/commands/ExtractCertCmdTest.java | 76 ---- .../picocli/commands/GenerateKeyCmdTest.java | 98 ----- .../sop/cli/picocli/commands/SignCmdTest.java | 128 ------- .../cli/picocli/commands/VerifyCmdTest.java | 204 ----------- .../cli/picocli/commands/VersionCmdTest.java | 46 --- sop-java/README.md | 81 +---- sop-java/build.gradle | 22 -- .../src/main/java/sop/ByteArrayAndResult.java | 50 --- .../src/main/java/sop/DecryptionResult.java | 29 -- sop-java/src/main/java/sop/MicAlg.java | 55 --- sop-java/src/main/java/sop/Ready.java | 45 --- .../src/main/java/sop/ReadyWithResult.java | 41 --- sop-java/src/main/java/sop/SOP.java | 95 ----- sop-java/src/main/java/sop/SessionKey.java | 79 ---- sop-java/src/main/java/sop/Signatures.java | 21 -- sop-java/src/main/java/sop/SigningResult.java | 50 --- sop-java/src/main/java/sop/Verification.java | 58 --- .../src/main/java/sop/enums/ArmorLabel.java | 13 - .../src/main/java/sop/enums/EncryptAs.java | 11 - sop-java/src/main/java/sop/enums/SignAs.java | 10 - .../src/main/java/sop/enums/package-info.java | 9 - .../java/sop/exception/SOPGPException.java | 316 ---------------- .../main/java/sop/exception/package-info.java | 9 - .../src/main/java/sop/operation/Armor.java | 41 --- .../src/main/java/sop/operation/Dearmor.java | 33 -- .../src/main/java/sop/operation/Decrypt.java | 118 ------ .../DetachInbandSignatureAndMessage.java | 44 --- .../src/main/java/sop/operation/Encrypt.java | 109 ------ .../main/java/sop/operation/ExtractCert.java | 40 -- .../main/java/sop/operation/GenerateKey.java | 36 -- .../src/main/java/sop/operation/Sign.java | 69 ---- .../src/main/java/sop/operation/Verify.java | 67 ---- .../java/sop/operation/VerifySignatures.java | 40 -- .../src/main/java/sop/operation/Version.java | 49 --- .../main/java/sop/operation/package-info.java | 9 - sop-java/src/main/java/sop/package-info.java | 8 - sop-java/src/main/java/sop/util/HexUtil.java | 47 --- sop-java/src/main/java/sop/util/Optional.java | 50 --- .../main/java/sop/util/ProxyOutputStream.java | 80 ---- sop-java/src/main/java/sop/util/UTCUtil.java | 56 --- .../src/main/java/sop/util/package-info.java | 8 - .../java/sop/util/ByteArrayAndResultTest.java | 33 -- .../src/test/java/sop/util/HexUtilTest.java | 63 ---- .../src/test/java/sop/util/MicAlgTest.java | 53 --- .../src/test/java/sop/util/OptionalTest.java | 78 ---- .../java/sop/util/ProxyOutputStreamTest.java | 40 -- .../src/test/java/sop/util/ReadyTest.java | 30 -- .../java/sop/util/ReadyWithResultTest.java | 44 --- .../test/java/sop/util/SessionKeyTest.java | 61 ---- .../test/java/sop/util/SigningResultTest.java | 23 -- .../src/test/java/sop/util/UTCUtilTest.java | 48 --- 82 files changed, 11 insertions(+), 5226 deletions(-) delete mode 100644 sop-java-picocli/build.gradle delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/DateParser.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/Print.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java delete mode 100644 sop-java-picocli/src/test/java/sop/cli/picocli/DateParserTest.java delete mode 100644 sop-java-picocli/src/test/java/sop/cli/picocli/FileUtilTest.java delete mode 100644 sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java delete mode 100644 sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java delete mode 100644 sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java delete mode 100644 sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java delete mode 100644 sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java delete mode 100644 sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java delete mode 100644 sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java delete mode 100644 sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java delete mode 100644 sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java delete mode 100644 sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java delete mode 100644 sop-java/build.gradle delete mode 100644 sop-java/src/main/java/sop/ByteArrayAndResult.java delete mode 100644 sop-java/src/main/java/sop/DecryptionResult.java delete mode 100644 sop-java/src/main/java/sop/MicAlg.java delete mode 100644 sop-java/src/main/java/sop/Ready.java delete mode 100644 sop-java/src/main/java/sop/ReadyWithResult.java delete mode 100644 sop-java/src/main/java/sop/SOP.java delete mode 100644 sop-java/src/main/java/sop/SessionKey.java delete mode 100644 sop-java/src/main/java/sop/Signatures.java delete mode 100644 sop-java/src/main/java/sop/SigningResult.java delete mode 100644 sop-java/src/main/java/sop/Verification.java delete mode 100644 sop-java/src/main/java/sop/enums/ArmorLabel.java delete mode 100644 sop-java/src/main/java/sop/enums/EncryptAs.java delete mode 100644 sop-java/src/main/java/sop/enums/SignAs.java delete mode 100644 sop-java/src/main/java/sop/enums/package-info.java delete mode 100644 sop-java/src/main/java/sop/exception/SOPGPException.java delete mode 100644 sop-java/src/main/java/sop/exception/package-info.java delete mode 100644 sop-java/src/main/java/sop/operation/Armor.java delete mode 100644 sop-java/src/main/java/sop/operation/Dearmor.java delete mode 100644 sop-java/src/main/java/sop/operation/Decrypt.java delete mode 100644 sop-java/src/main/java/sop/operation/DetachInbandSignatureAndMessage.java delete mode 100644 sop-java/src/main/java/sop/operation/Encrypt.java delete mode 100644 sop-java/src/main/java/sop/operation/ExtractCert.java delete mode 100644 sop-java/src/main/java/sop/operation/GenerateKey.java delete mode 100644 sop-java/src/main/java/sop/operation/Sign.java delete mode 100644 sop-java/src/main/java/sop/operation/Verify.java delete mode 100644 sop-java/src/main/java/sop/operation/VerifySignatures.java delete mode 100644 sop-java/src/main/java/sop/operation/Version.java delete mode 100644 sop-java/src/main/java/sop/operation/package-info.java delete mode 100644 sop-java/src/main/java/sop/package-info.java delete mode 100644 sop-java/src/main/java/sop/util/HexUtil.java delete mode 100644 sop-java/src/main/java/sop/util/Optional.java delete mode 100644 sop-java/src/main/java/sop/util/ProxyOutputStream.java delete mode 100644 sop-java/src/main/java/sop/util/UTCUtil.java delete mode 100644 sop-java/src/main/java/sop/util/package-info.java delete mode 100644 sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java delete mode 100644 sop-java/src/test/java/sop/util/HexUtilTest.java delete mode 100644 sop-java/src/test/java/sop/util/MicAlgTest.java delete mode 100644 sop-java/src/test/java/sop/util/OptionalTest.java delete mode 100644 sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java delete mode 100644 sop-java/src/test/java/sop/util/ReadyTest.java delete mode 100644 sop-java/src/test/java/sop/util/ReadyWithResultTest.java delete mode 100644 sop-java/src/test/java/sop/util/SessionKeyTest.java delete mode 100644 sop-java/src/test/java/sop/util/SigningResultTest.java delete mode 100644 sop-java/src/test/java/sop/util/UTCUtilTest.java diff --git a/build.gradle b/build.gradle index 277f1a8b..af0936e1 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,12 @@ allprojects { apply plugin: 'jacoco' apply plugin: 'checkstyle' + // Only generate jar for submodules + // https://stackoverflow.com/a/25445035 + jar { + onlyIf { !sourceSets.main.allSource.files.isEmpty() } + } + // For non-sop modules, enable android api compatibility check if (it.name.equals('pgpainless-core') || it.name.equals('sop-java') || it.name.equals('pgpainless-sop')) { // animalsniffer @@ -68,6 +74,7 @@ allprojects { logbackVersion = '1.2.9' junitVersion = '5.8.2' picocliVersion = '4.6.2' + sopJavaVersion = '1.1.0' rootConfigDir = new File(rootDir, 'config') gitCommit = getGitCommit() isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI')) diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle index eeacf5fa..9db15a5b 100644 --- a/pgpainless-cli/build.gradle +++ b/pgpainless-cli/build.gradle @@ -40,7 +40,7 @@ dependencies { implementation "ch.qos.logback:logback-classic:$logbackVersion" implementation(project(":pgpainless-sop")) - implementation(project(":sop-java-picocli")) + implementation "org.pgpainless:sop-java-picocli:$sopJavaVersion" implementation "info.picocli:picocli:$picocliVersion" diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index 5ce6a7c6..87e893b2 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -20,7 +20,7 @@ dependencies { testImplementation "ch.qos.logback:logback-classic:$logbackVersion" implementation(project(":pgpainless-core")) - implementation(project(":sop-java")) + implementation "org.pgpainless:sop-java:$sopJavaVersion" } test { diff --git a/settings.gradle b/settings.gradle index 7ebc7ad4..aea19392 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,8 +5,6 @@ rootProject.name = 'PGPainless' include 'pgpainless-core', - 'sop-java', 'pgpainless-sop', - 'sop-java-picocli', 'pgpainless-cli' diff --git a/sop-java-picocli/README.md b/sop-java-picocli/README.md index f76c9295..3c9234af 100644 --- a/sop-java-picocli/README.md +++ b/sop-java-picocli/README.md @@ -1,34 +1 @@ - -# SOP-Java-Picocli - -Implementation of the [Stateless OpenPGP Command Line Interface](https://tools.ietf.org/html/draft-dkg-openpgp-stateless-cli-01) specification. -This terminal application allows generation of OpenPGP keys, extraction of public key certificates, -armoring and de-armoring of data, as well as - of course - encryption/decryption of messages and creation/verification of signatures. - -## Install a SOP backend - -This module comes without a SOP backend, so in order to function you need to extend it with an implementation of the interfaces defined in `sop-java`. -An implementation using PGPainless can be found in the module `pgpainless-sop`, but it is of course possible to provide your -own implementation. - -Just install your SOP backend by calling -```java -// static method call prior to execution of the main method -SopCLI.setSopInstance(yourSopImpl); -``` - -## Usage - -To get an overview of available commands of the application, execute -```shell -java -jar sop-java-picocli-XXX.jar help -``` - -If you just want to get started encrypting messages, see the module `pgpainless-cli` which initializes -`sop-java-picocli` with `pgpainless-sop`, so you can get started right away without the need to manually wire stuff up. - -Enjoy! \ No newline at end of file +# [MOVED](https://github.com/pgpainless/sop-java/tree/master/sop-java-picocli) \ No newline at end of file diff --git a/sop-java-picocli/build.gradle b/sop-java-picocli/build.gradle deleted file mode 100644 index 81183a09..00000000 --- a/sop-java-picocli/build.gradle +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -plugins { - id 'application' -} - -dependencies { - testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - - // https://todd.ginsberg.com/post/testing-system-exit/ - testImplementation 'com.ginsberg:junit5-system-exit:1.1.1' - testImplementation 'org.mockito:mockito-core:4.2.0' - - implementation(project(":sop-java")) - implementation "info.picocli:picocli:$picocliVersion" - - // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 - implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' -} - -mainClassName = 'sop.cli.picocli.SopCLI' - -jar { - manifest { - attributes 'Main-Class': "$mainClassName" - } - - from { - configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } { - exclude "META-INF/*.SF" - exclude "META-INF/*.DSA" - exclude "META-INF/*.RSA" - } -} - diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/DateParser.java b/sop-java-picocli/src/main/java/sop/cli/picocli/DateParser.java deleted file mode 100644 index d2e2188e..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/DateParser.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import java.util.Date; - -import sop.util.UTCUtil; - -public class DateParser { - - public static final Date BEGINNING_OF_TIME = new Date(0); - public static final Date END_OF_TIME = new Date(8640000000000000L); - - public static Date parseNotAfter(String notAfter) { - Date date = notAfter.equals("now") ? new Date() : notAfter.equals("-") ? END_OF_TIME : UTCUtil.parseUTCDate(notAfter); - if (date == null) { - Print.errln("Invalid date string supplied as value of --not-after."); - System.exit(1); - } - return date; - } - - public static Date parseNotBefore(String notBefore) { - Date date = notBefore.equals("now") ? new Date() : notBefore.equals("-") ? BEGINNING_OF_TIME : UTCUtil.parseUTCDate(notBefore); - if (date == null) { - Print.errln("Invalid date string supplied as value of --not-before."); - System.exit(1); - } - return date; - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java b/sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java deleted file mode 100644 index cd92e6db..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; - -import sop.exception.SOPGPException; - -public class FileUtil { - - private static final String ERROR_AMBIGUOUS = "File name '%s' is ambiguous. File with the same name exists on the filesystem."; - private static final String ERROR_ENV_FOUND = "Environment variable '%s' not set."; - private static final String ERROR_OUTPUT_EXISTS = "Output file '%s' already exists."; - private static final String ERROR_INPUT_NOT_EXIST = "File '%s' does not exist."; - private static final String ERROR_CANNOT_CREATE_FILE = "Output file '%s' cannot be created: %s"; - - public static final String PRFX_ENV = "@ENV:"; - public static final String PRFX_FD = "@FD:"; - - private static EnvironmentVariableResolver envResolver = System::getenv; - - public static void setEnvironmentVariableResolver(EnvironmentVariableResolver envResolver) { - if (envResolver == null) { - throw new NullPointerException("Variable envResolver cannot be null."); - } - FileUtil.envResolver = envResolver; - } - - public interface EnvironmentVariableResolver { - /** - * Resolve the value of the given environment variable. - * Return null if the variable is not present. - * - * @param name name of the variable - * @return variable value or null - */ - String resolveEnvironmentVariable(String name); - } - - public static File getFile(String fileName) { - if (fileName == null) { - throw new NullPointerException("File name cannot be null."); - } - - if (fileName.startsWith(PRFX_ENV)) { - - if (new File(fileName).exists()) { - throw new SOPGPException.AmbiguousInput(String.format(ERROR_AMBIGUOUS, fileName)); - } - - String envName = fileName.substring(PRFX_ENV.length()); - String envValue = envResolver.resolveEnvironmentVariable(envName); - if (envValue == null) { - throw new IllegalArgumentException(String.format(ERROR_ENV_FOUND, envName)); - } - return new File(envValue); - } else if (fileName.startsWith(PRFX_FD)) { - - if (new File(fileName).exists()) { - throw new SOPGPException.AmbiguousInput(String.format(ERROR_AMBIGUOUS, fileName)); - } - - throw new IllegalArgumentException("File descriptors not supported."); - } - - return new File(fileName); - } - - public static FileInputStream getFileInputStream(String fileName) { - File file = getFile(fileName); - try { - FileInputStream inputStream = new FileInputStream(file); - return inputStream; - } catch (FileNotFoundException e) { - throw new SOPGPException.MissingInput(String.format(ERROR_INPUT_NOT_EXIST, fileName), e); - } - } - - public static File createNewFileOrThrow(File file) throws IOException { - if (file == null) { - throw new NullPointerException("File cannot be null."); - } - - try { - if (!file.createNewFile()) { - throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_EXISTS, file.getAbsolutePath())); - } - } catch (IOException e) { - throw new IOException(String.format(ERROR_CANNOT_CREATE_FILE, file.getAbsolutePath(), e.getMessage())); - } - return file; - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java b/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java deleted file mode 100644 index d6474e1d..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -public class Print { - - public static void errln(String string) { - // CHECKSTYLE:OFF - System.err.println(string); - // CHECKSTYLE:ON - } - - public static void trace(Throwable e) { - // CHECKSTYLE:OFF - e.printStackTrace(); - // CHECKSTYLE:ON - } - - public static void outln(String string) { - // CHECKSTYLE:OFF - System.out.println(string); - // CHECKSTYLE:ON - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java deleted file mode 100644 index 8b38af32..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.CommandLine; -import sop.exception.SOPGPException; - -public class SOPExceptionExitCodeMapper implements CommandLine.IExitCodeExceptionMapper { - - @Override - public int getExitCode(Throwable exception) { - if (exception instanceof SOPGPException) { - return ((SOPGPException) exception).getExitCode(); - } - if (exception instanceof CommandLine.UnmatchedArgumentException) { - CommandLine.UnmatchedArgumentException ex = (CommandLine.UnmatchedArgumentException) exception; - // Unmatched option of subcommand (eg. `generate-key -k`) - if (ex.isUnknownOption()) { - return SOPGPException.UnsupportedOption.EXIT_CODE; - } - // Unmatched subcommand - return SOPGPException.UnsupportedSubcommand.EXIT_CODE; - } - // Invalid option (eg. `--label Invalid`) - if (exception instanceof CommandLine.ParameterException) { - return SOPGPException.UnsupportedOption.EXIT_CODE; - } - - // Others, like IOException etc. - return 1; - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java deleted file mode 100644 index bbd8b976..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.CommandLine; - -public class SOPExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler { - - @Override - public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) { - int exitCode = commandLine.getExitCodeExceptionMapper() != null ? - commandLine.getExitCodeExceptionMapper().getExitCode(ex) : - commandLine.getCommandSpec().exitCodeOnExecutionException(); - CommandLine.Help.ColorScheme colorScheme = commandLine.getColorScheme(); - // CHECKSTYLE:OFF - if (ex.getMessage() != null) { - commandLine.getErr().println(colorScheme.errorText(ex.getMessage())); - } - ex.printStackTrace(commandLine.getErr()); - // CHECKSTYLE:ON - - return exitCode; - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java deleted file mode 100644 index bc0ae3dd..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.CommandLine; -import sop.SOP; -import sop.cli.picocli.commands.ArmorCmd; -import sop.cli.picocli.commands.DearmorCmd; -import sop.cli.picocli.commands.DecryptCmd; -import sop.cli.picocli.commands.DetachInbandSignatureAndMessageCmd; -import sop.cli.picocli.commands.EncryptCmd; -import sop.cli.picocli.commands.ExtractCertCmd; -import sop.cli.picocli.commands.GenerateKeyCmd; -import sop.cli.picocli.commands.SignCmd; -import sop.cli.picocli.commands.VerifyCmd; -import sop.cli.picocli.commands.VersionCmd; - -@CommandLine.Command( - exitCodeOnInvalidInput = 69, - subcommands = { - CommandLine.HelpCommand.class, - ArmorCmd.class, - DearmorCmd.class, - DecryptCmd.class, - DetachInbandSignatureAndMessageCmd.class, - EncryptCmd.class, - ExtractCertCmd.class, - GenerateKeyCmd.class, - SignCmd.class, - VerifyCmd.class, - VersionCmd.class - } -) -public class SopCLI { - // Singleton - static SOP SOP_INSTANCE; - - public static String EXECUTABLE_NAME = "sop"; - - public static void main(String[] args) { - int exitCode = execute(args); - if (exitCode != 0) { - System.exit(exitCode); - } - } - - public static int execute(String[] args) { - return new CommandLine(SopCLI.class) - .setCommandName(EXECUTABLE_NAME) - .setExecutionExceptionHandler(new SOPExecutionExceptionHandler()) - .setExitCodeExceptionMapper(new SOPExceptionExitCodeMapper()) - .setCaseInsensitiveEnumValuesAllowed(true) - .execute(args); - } - - public static SOP getSop() { - if (SOP_INSTANCE == null) { - throw new IllegalStateException("No SOP backend set."); - } - return SOP_INSTANCE; - } - - public static void setSopInstance(SOP instance) { - SOP_INSTANCE = instance; - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java deleted file mode 100644 index a015a688..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.IOException; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; -import sop.operation.Armor; - -@CommandLine.Command(name = "armor", - description = "Add ASCII Armor to standard input", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ArmorCmd implements Runnable { - - @CommandLine.Option(names = {"--label"}, description = "Label to be used in the header and tail of the armoring.", paramLabel = "{auto|sig|key|cert|message}") - ArmorLabel label; - - @Override - public void run() { - Armor armor = SopCLI.getSop().armor(); - if (armor == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'armor' not implemented."); - } - - if (label != null) { - try { - armor.label(label); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Armor labels not supported."); - System.exit(unsupportedOption.getExitCode()); - } - } - - try { - Ready ready = armor.data(System.in); - ready.writeTo(System.out); - } catch (SOPGPException.BadData badData) { - Print.errln("Bad data."); - Print.trace(badData); - System.exit(badData.getExitCode()); - } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java deleted file mode 100644 index 343b1135..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.IOException; - -import picocli.CommandLine; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Dearmor; - -@CommandLine.Command(name = "dearmor", - description = "Remove ASCII Armor from standard input", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DearmorCmd implements Runnable { - - @Override - public void run() { - Dearmor dearmor = SopCLI.getSop().dearmor(); - if (dearmor == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'dearmor' not implemented."); - } - - try { - SopCLI.getSop() - .dearmor() - .data(System.in) - .writeTo(System.out); - } catch (SOPGPException.BadData e) { - Print.errln("Bad data."); - Print.trace(e); - System.exit(e.getExitCode()); - } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java deleted file mode 100644 index 8fc4650b..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java +++ /dev/null @@ -1,240 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.regex.Pattern; - -import picocli.CommandLine; -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.Verification; -import sop.cli.picocli.DateParser; -import sop.cli.picocli.FileUtil; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Decrypt; -import sop.util.HexUtil; - -@CommandLine.Command(name = "decrypt", - description = "Decrypt a message from standard input", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DecryptCmd implements Runnable { - - private static final String SESSION_KEY_OUT = "--session-key-out"; - private static final String VERIFY_OUT = "--verify-out"; - - private static final String ERROR_UNSUPPORTED_OPTION = "Option '%s' is not supported."; - private static final String ERROR_FILE_NOT_EXIST = "File '%s' does not exist."; - private static final String ERROR_OUTPUT_OF_OPTION_EXISTS = "Target %s of option %s already exists."; - - @CommandLine.Option( - names = {SESSION_KEY_OUT}, - description = "Can be used to learn the session key on successful decryption", - paramLabel = "SESSIONKEY") - File sessionKeyOut; - - @CommandLine.Option( - names = {"--with-session-key"}, - description = "Enables decryption of the \"CIPHERTEXT\" using the session key directly against the \"SEIPD\" packet", - paramLabel = "SESSIONKEY") - List withSessionKey = new ArrayList<>(); - - @CommandLine.Option( - names = {"--with-password"}, - description = "Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"", - paramLabel = "PASSWORD") - List withPassword = new ArrayList<>(); - - @CommandLine.Option(names = {VERIFY_OUT}, - description = "Produces signature verification status to the designated file", - paramLabel = "VERIFICATIONS") - File verifyOut; - - @CommandLine.Option(names = {"--verify-with"}, - description = "Certificates whose signatures would be acceptable for signatures over this message", - paramLabel = "CERT") - List certs = new ArrayList<>(); - - @CommandLine.Option(names = {"--not-before"}, - description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + - "Reject signatures with a creation date not in range.\n" + - "Defaults to beginning of time (\"-\").", - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {"--not-after"}, - description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + - "Reject signatures with a creation date not in range.\n" + - "Defaults to current system time (\"now\").\n" + - "Accepts special value \"-\" for end of time.", - paramLabel = "DATE") - String notAfter = "now"; - - @CommandLine.Parameters(index = "0..*", - description = "Secret keys to attempt decryption with", - paramLabel = "KEY") - List keys = new ArrayList<>(); - - @Override - public void run() { - throwIfOutputExists(verifyOut, VERIFY_OUT); - throwIfOutputExists(sessionKeyOut, SESSION_KEY_OUT); - - Decrypt decrypt = SopCLI.getSop().decrypt(); - if (decrypt == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'decrypt' not implemented."); - } - - setNotAfter(notAfter, decrypt); - setNotBefore(notBefore, decrypt); - setWithPasswords(withPassword, decrypt); - setWithSessionKeys(withSessionKey, decrypt); - setVerifyWith(certs, decrypt); - setDecryptWith(keys, decrypt); - - if (verifyOut != null && certs.isEmpty()) { - String errorMessage = "Option %s is requested, but no option %s was provided."; - throw new SOPGPException.IncompleteVerification(String.format(errorMessage, VERIFY_OUT, "--verify-with")); - } - - try { - ReadyWithResult ready = decrypt.ciphertext(System.in); - DecryptionResult result = ready.writeTo(System.out); - writeSessionKeyOut(result); - writeVerifyOut(result); - } catch (SOPGPException.BadData badData) { - throw new SOPGPException.BadData("No valid OpenPGP message found on Standard Input.", badData); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } - } - - private void throwIfOutputExists(File outputFile, String optionName) { - if (outputFile == null) { - return; - } - - if (outputFile.exists()) { - throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_OF_OPTION_EXISTS, outputFile.getAbsolutePath(), optionName)); - } - } - - private void writeVerifyOut(DecryptionResult result) throws IOException { - if (verifyOut != null) { - FileUtil.createNewFileOrThrow(verifyOut); - try (FileOutputStream outputStream = new FileOutputStream(verifyOut)) { - PrintWriter writer = new PrintWriter(outputStream); - for (Verification verification : result.getVerifications()) { - // CHECKSTYLE:OFF - writer.println(verification.toString()); - // CHECKSTYLE:ON - } - writer.flush(); - } - } - } - - private void writeSessionKeyOut(DecryptionResult result) throws IOException { - if (sessionKeyOut != null) { - FileUtil.createNewFileOrThrow(sessionKeyOut); - - try (FileOutputStream outputStream = new FileOutputStream(sessionKeyOut)) { - if (!result.getSessionKey().isPresent()) { - throw new SOPGPException.UnsupportedOption("Session key not extracted. Possibly the feature --session-key-out is not supported."); - } else { - SessionKey sessionKey = result.getSessionKey().get(); - outputStream.write(sessionKey.getAlgorithm()); - outputStream.write(sessionKey.getKey()); - } - } - } - } - - private void setDecryptWith(List keys, Decrypt decrypt) { - for (File key : keys) { - try (FileInputStream keyIn = new FileInputStream(key)) { - decrypt.withKey(keyIn); - } catch (SOPGPException.KeyIsProtected keyIsProtected) { - throw new SOPGPException.KeyIsProtected("Key in file " + key.getAbsolutePath() + " is password protected.", keyIsProtected); - } catch (SOPGPException.BadData badData) { - throw new SOPGPException.BadData("File " + key.getAbsolutePath() + " does not contain a private key.", badData); - } catch (FileNotFoundException e) { - throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, key.getAbsolutePath()), e); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - private void setVerifyWith(List certs, Decrypt decrypt) { - for (File cert : certs) { - try (FileInputStream certIn = new FileInputStream(cert)) { - decrypt.verifyWithCert(certIn); - } catch (FileNotFoundException e) { - throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, cert.getAbsolutePath()), e); - } catch (SOPGPException.BadData badData) { - throw new SOPGPException.BadData("File " + cert.getAbsolutePath() + " does not contain a valid certificate.", badData); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } - } - } - - private void setWithSessionKeys(List withSessionKey, Decrypt decrypt) { - Pattern sessionKeyPattern = Pattern.compile("^\\d+:[0-9A-F]+$"); - for (String sessionKey : withSessionKey) { - if (!sessionKeyPattern.matcher(sessionKey).matches()) { - throw new IllegalArgumentException("Session keys are expected in the format 'ALGONUM:HEXKEY'."); - } - String[] split = sessionKey.split(":"); - byte algorithm = (byte) Integer.parseInt(split[0]); - byte[] key = HexUtil.hexToBytes(split[1]); - - try { - decrypt.withSessionKey(new SessionKey(algorithm, key)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-session-key"), unsupportedOption); - } - } - } - - private void setWithPasswords(List withPassword, Decrypt decrypt) { - for (String password : withPassword) { - try { - decrypt.withPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-password"), unsupportedOption); - } - } - } - - private void setNotAfter(String notAfter, Decrypt decrypt) { - Date notAfterDate = DateParser.parseNotAfter(notAfter); - try { - decrypt.verifyNotAfter(notAfterDate); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-after"), unsupportedOption); - } - } - - private void setNotBefore(String notBefore, Decrypt decrypt) { - Date notBeforeDate = DateParser.parseNotBefore(notBefore); - try { - decrypt.verifyNotBefore(notBeforeDate); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-before"), unsupportedOption); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java deleted file mode 100644 index f5c71a2a..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DetachInbandSignatureAndMessageCmd.java +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; - -import picocli.CommandLine; -import sop.Signatures; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.DetachInbandSignatureAndMessage; - -@CommandLine.Command(name = "detach-inband-signature-and-message", - description = "Split a clearsigned message", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DetachInbandSignatureAndMessageCmd implements Runnable { - - @CommandLine.Option( - names = {"--signatures-out"}, - description = "Destination to which a detached signatures block will be written", - paramLabel = "SIGNATURES") - File signaturesOut; - - @CommandLine.Option(names = "--no-armor", - description = "ASCII armor the output", - negatable = true) - boolean armor = true; - - @Override - public void run() { - DetachInbandSignatureAndMessage detach = SopCLI.getSop().detachInbandSignatureAndMessage(); - if (detach == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'detach-inband-signature-and-message' not implemented."); - } - - if (signaturesOut == null) { - throw new SOPGPException.MissingArg("--signatures-out is required."); - } - - if (!armor) { - detach.noArmor(); - } - - try { - Signatures signatures = detach - .message(System.in).writeTo(System.out); - if (!signaturesOut.createNewFile()) { - throw new SOPGPException.OutputExists("Destination of --signatures-out already exists."); - } - signatures.writeTo(new FileOutputStream(signaturesOut)); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java deleted file mode 100644 index 0634240b..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.operation.Encrypt; - -@CommandLine.Command(name = "encrypt", - description = "Encrypt a message from standard input", - exitCodeOnInvalidInput = 37) -public class EncryptCmd implements Runnable { - - @CommandLine.Option(names = "--no-armor", - description = "ASCII armor the output", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = {"--as"}, - description = "Type of the input data. Defaults to 'binary'", - paramLabel = "{binary|text|mime}") - EncryptAs type; - - @CommandLine.Option(names = "--with-password", - description = "Encrypt the message with a password", - paramLabel = "PASSWORD") - List withPassword = new ArrayList<>(); - - @CommandLine.Option(names = "--sign-with", - description = "Sign the output with a private key", - paramLabel = "KEY") - List signWith = new ArrayList<>(); - - @CommandLine.Parameters(description = "Certificates the message gets encrypted to", - index = "0..*", - paramLabel = "CERTS") - List certs = new ArrayList<>(); - - @Override - public void run() { - Encrypt encrypt = SopCLI.getSop().encrypt(); - if (encrypt == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'encrypt' not implemented."); - } - - if (type != null) { - try { - encrypt.mode(type); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption("Unsupported option '--as'.", unsupportedOption); - } - } - - if (withPassword.isEmpty() && certs.isEmpty()) { - throw new SOPGPException.MissingArg("At least one password or cert file required for encryption."); - } - - for (String password : withPassword) { - try { - encrypt.withPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption("Unsupported option '--with-password'.", unsupportedOption); - } - } - - for (File keyFile : signWith) { - try (FileInputStream keyIn = new FileInputStream(keyFile)) { - encrypt.signWith(keyIn); - } catch (FileNotFoundException e) { - throw new SOPGPException.MissingInput("Key file " + keyFile.getAbsolutePath() + " not found.", e); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.KeyIsProtected keyIsProtected) { - throw new SOPGPException.KeyIsProtected("Key from " + keyFile.getAbsolutePath() + " is password protected.", keyIsProtected); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - throw new SOPGPException.UnsupportedAsymmetricAlgo("Key from " + keyFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo); - } catch (SOPGPException.KeyCannotSign keyCannotSign) { - throw new SOPGPException.KeyCannotSign("Key from " + keyFile.getAbsolutePath() + " cannot sign.", keyCannotSign); - } catch (SOPGPException.BadData badData) { - throw new SOPGPException.BadData("Key file " + keyFile.getAbsolutePath() + " does not contain a valid OpenPGP private key.", badData); - } - } - - for (File certFile : certs) { - try (FileInputStream certIn = new FileInputStream(certFile)) { - encrypt.withCert(certIn); - } catch (FileNotFoundException e) { - throw new SOPGPException.MissingInput("Certificate file " + certFile.getAbsolutePath() + " not found.", e); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - throw new SOPGPException.UnsupportedAsymmetricAlgo("Certificate from " + certFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo); - } catch (SOPGPException.CertCannotEncrypt certCannotEncrypt) { - throw new SOPGPException.CertCannotEncrypt("Certificate from " + certFile.getAbsolutePath() + " is not capable of encryption.", certCannotEncrypt); - } catch (SOPGPException.BadData badData) { - throw new SOPGPException.BadData("Certificate file " + certFile.getAbsolutePath() + " does not contain a valid OpenPGP certificate.", badData); - } - } - - if (!armor) { - encrypt.noArmor(); - } - - try { - Ready ready = encrypt.plaintext(System.in); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java deleted file mode 100644 index f4559339..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.IOException; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.ExtractCert; - -@CommandLine.Command(name = "extract-cert", - description = "Extract a public key certificate from a secret key from standard input", - exitCodeOnInvalidInput = 37) -public class ExtractCertCmd implements Runnable { - - @CommandLine.Option(names = "--no-armor", - description = "ASCII armor the output", - negatable = true) - boolean armor = true; - - @Override - public void run() { - ExtractCert extractCert = SopCLI.getSop().extractCert(); - if (extractCert == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'extract-cert' not implemented."); - } - - if (!armor) { - extractCert.noArmor(); - } - - try { - Ready ready = extractCert.key(System.in); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.BadData badData) { - throw new SOPGPException.BadData("Standard Input does not contain valid OpenPGP private key material.", badData); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java deleted file mode 100644 index 28bde279..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.GenerateKey; - -@CommandLine.Command(name = "generate-key", - description = "Generate a secret key", - exitCodeOnInvalidInput = 37) -public class GenerateKeyCmd implements Runnable { - - @CommandLine.Option(names = "--no-armor", - description = "ASCII armor the output", - negatable = true) - boolean armor = true; - - @CommandLine.Parameters(description = "User-ID, eg. \"Alice \"") - List userId = new ArrayList<>(); - - @Override - public void run() { - GenerateKey generateKey = SopCLI.getSop().generateKey(); - if (generateKey == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'generate-key' not implemented."); - } - - for (String userId : userId) { - generateKey.userId(userId); - } - - if (!armor) { - generateKey.noArmor(); - } - - try { - Ready ready = generateKey.generate(); - ready.writeTo(System.out); - } catch (SOPGPException.MissingArg missingArg) { - Print.errln("Missing argument."); - Print.trace(missingArg); - System.exit(missingArg.getExitCode()); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - Print.errln("Unsupported asymmetric algorithm."); - Print.trace(unsupportedAsymmetricAlgo); - System.exit(unsupportedAsymmetricAlgo.getExitCode()); - } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java deleted file mode 100644 index 7574923e..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import picocli.CommandLine; -import sop.MicAlg; -import sop.ReadyWithResult; -import sop.SigningResult; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.enums.SignAs; -import sop.exception.SOPGPException; -import sop.operation.Sign; - -@CommandLine.Command(name = "sign", - description = "Create a detached signature on the data from standard input", - exitCodeOnInvalidInput = 37) -public class SignCmd implements Runnable { - - @CommandLine.Option(names = "--no-armor", - description = "ASCII armor the output", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = "--as", description = "Defaults to 'binary'. If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53.", - paramLabel = "{binary|text}") - SignAs type; - - @CommandLine.Parameters(description = "Secret keys used for signing", - paramLabel = "KEYS") - List secretKeyFile = new ArrayList<>(); - - @CommandLine.Option(names = "--micalg-out", description = "Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156)", - paramLabel = "MICALG") - File micAlgOut; - - @Override - public void run() { - Sign sign = SopCLI.getSop().sign(); - if (sign == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'sign' not implemented."); - } - - if (type != null) { - try { - sign.mode(type); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Unsupported option '--as'"); - Print.trace(unsupportedOption); - System.exit(unsupportedOption.getExitCode()); - } - } - - if (micAlgOut != null && micAlgOut.exists()) { - throw new SOPGPException.OutputExists(String.format("Target %s of option %s already exists.", micAlgOut.getAbsolutePath(), "--micalg-out")); - } - - if (secretKeyFile.isEmpty()) { - Print.errln("Missing required parameter 'KEYS'."); - System.exit(19); - } - - for (File keyFile : secretKeyFile) { - try (FileInputStream keyIn = new FileInputStream(keyFile)) { - sign.key(keyIn); - } catch (FileNotFoundException e) { - Print.errln("File " + keyFile.getAbsolutePath() + " does not exist."); - Print.trace(e); - System.exit(1); - } catch (IOException e) { - Print.errln("Cannot access file " + keyFile.getAbsolutePath()); - Print.trace(e); - System.exit(1); - } catch (SOPGPException.KeyIsProtected e) { - Print.errln("Key " + keyFile.getName() + " is password protected."); - Print.trace(e); - System.exit(1); - } catch (SOPGPException.BadData badData) { - Print.errln("Bad data in key file " + keyFile.getAbsolutePath() + ":"); - Print.trace(badData); - System.exit(badData.getExitCode()); - } - } - - if (!armor) { - sign.noArmor(); - } - - try { - ReadyWithResult ready = sign.data(System.in); - SigningResult result = ready.writeTo(System.out); - - MicAlg micAlg = result.getMicAlg(); - if (micAlgOut != null) { - // Write micalg out - micAlgOut.createNewFile(); - FileOutputStream micAlgOutStream = new FileOutputStream(micAlgOut); - micAlg.writeTo(micAlgOutStream); - micAlgOutStream.close(); - } - } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); - } catch (SOPGPException.ExpectedText expectedText) { - Print.errln("Expected text input, but got binary data."); - Print.trace(expectedText); - System.exit(expectedText.getExitCode()); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java deleted file mode 100644 index 2702b4b9..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import picocli.CommandLine; -import sop.Verification; -import sop.cli.picocli.DateParser; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Verify; - -@CommandLine.Command(name = "verify", - description = "Verify a detached signature over the data from standard input", - exitCodeOnInvalidInput = 37) -public class VerifyCmd implements Runnable { - - @CommandLine.Parameters(index = "0", - description = "Detached signature", - paramLabel = "SIGNATURE") - File signature; - - @CommandLine.Parameters(index = "1..*", - arity = "1..*", - description = "Public key certificates", - paramLabel = "CERT") - List certificates = new ArrayList<>(); - - @CommandLine.Option(names = {"--not-before"}, - description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + - "Reject signatures with a creation date not in range.\n" + - "Defaults to beginning of time (\"-\").", - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {"--not-after"}, - description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + - "Reject signatures with a creation date not in range.\n" + - "Defaults to current system time (\"now\").\n" + - "Accepts special value \"-\" for end of time.", - paramLabel = "DATE") - String notAfter = "now"; - - @Override - public void run() { - Verify verify = SopCLI.getSop().verify(); - if (verify == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'verify' not implemented."); - } - - if (notAfter != null) { - try { - verify.notAfter(DateParser.parseNotAfter(notAfter)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Unsupported option '--not-after'."); - Print.trace(unsupportedOption); - System.exit(unsupportedOption.getExitCode()); - } - } - if (notBefore != null) { - try { - verify.notBefore(DateParser.parseNotBefore(notBefore)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Unsupported option '--not-before'."); - Print.trace(unsupportedOption); - System.exit(unsupportedOption.getExitCode()); - } - } - - for (File certFile : certificates) { - try (FileInputStream certIn = new FileInputStream(certFile)) { - verify.cert(certIn); - } catch (FileNotFoundException fileNotFoundException) { - Print.errln("Certificate file " + certFile.getAbsolutePath() + " not found."); - - Print.trace(fileNotFoundException); - System.exit(1); - } catch (IOException ioException) { - Print.errln("IO Error."); - Print.trace(ioException); - System.exit(1); - } catch (SOPGPException.BadData badData) { - Print.errln("Certificate file " + certFile.getAbsolutePath() + " appears to not contain a valid OpenPGP certificate."); - Print.trace(badData); - System.exit(badData.getExitCode()); - } - } - - if (signature != null) { - try (FileInputStream sigIn = new FileInputStream(signature)) { - verify.signatures(sigIn); - } catch (FileNotFoundException e) { - Print.errln("Signature file " + signature.getAbsolutePath() + " does not exist."); - Print.trace(e); - System.exit(1); - } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); - } catch (SOPGPException.BadData badData) { - Print.errln("File " + signature.getAbsolutePath() + " does not contain a valid OpenPGP signature."); - Print.trace(badData); - System.exit(badData.getExitCode()); - } - } - - List verifications = null; - try { - verifications = verify.data(System.in); - } catch (SOPGPException.NoSignature e) { - Print.errln("No verifiable signature found."); - Print.trace(e); - System.exit(e.getExitCode()); - } catch (IOException ioException) { - Print.errln("IO Error."); - Print.trace(ioException); - System.exit(1); - } catch (SOPGPException.BadData badData) { - Print.errln("Standard Input appears not to contain a valid OpenPGP message."); - Print.trace(badData); - System.exit(badData.getExitCode()); - } - for (Verification verification : verifications) { - Print.outln(verification.toString()); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java deleted file mode 100644 index 4a319192..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Version; - -@CommandLine.Command(name = "version", description = "Display version information about the tool", - exitCodeOnInvalidInput = 37) -public class VersionCmd implements Runnable { - - @CommandLine.ArgGroup() - Exclusive exclusive; - - static class Exclusive { - @CommandLine.Option(names = "--extended", description = "Print an extended version string.") - boolean extended; - - @CommandLine.Option(names = "--backend", description = "Print information about the cryptographic backend.") - boolean backend; - } - - - - @Override - public void run() { - Version version = SopCLI.getSop().version(); - if (version == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'version' not implemented."); - } - - if (exclusive == null) { - Print.outln(version.getName() + " " + version.getVersion()); - return; - } - - if (exclusive.extended) { - Print.outln(version.getExtendedVersion()); - return; - } - - if (exclusive.backend) { - Print.outln(version.getBackendVersion()); - return; - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java deleted file mode 100644 index fc6aefda..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Subcommands of the PGPainless SOP. - */ -package sop.cli.picocli.commands; diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java deleted file mode 100644 index 83f426d6..00000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Implementation of the Stateless OpenPGP Command Line Interface using Picocli. - */ -package sop.cli.picocli; diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/DateParserTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/DateParserTest.java deleted file mode 100644 index 5c7def50..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/DateParserTest.java +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.Date; - -import org.junit.jupiter.api.Test; -import sop.util.UTCUtil; - -public class DateParserTest { - - @Test - public void parseNotAfterDashReturnsEndOfTime() { - assertEquals(DateParser.END_OF_TIME, DateParser.parseNotAfter("-")); - } - - @Test - public void parseNotBeforeDashReturnsBeginningOfTime() { - assertEquals(DateParser.BEGINNING_OF_TIME, DateParser.parseNotBefore("-")); - } - - @Test - public void parseNotAfterNowReturnsNow() { - assertEquals(new Date().getTime(), DateParser.parseNotAfter("now").getTime(), 1000); - } - - @Test - public void parseNotBeforeNowReturnsNow() { - assertEquals(new Date().getTime(), DateParser.parseNotBefore("now").getTime(), 1000); - } - - @Test - public void parseNotAfterTimestamp() { - String timestamp = "2019-10-24T23:48:29Z"; - Date date = DateParser.parseNotAfter(timestamp); - assertEquals(timestamp, UTCUtil.formatUTCDate(date)); - } - - @Test - public void parseNotBeforeTimestamp() { - String timestamp = "2019-10-29T18:36:45Z"; - Date date = DateParser.parseNotBefore(timestamp); - assertEquals(timestamp, UTCUtil.formatUTCDate(date)); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/FileUtilTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/FileUtilTest.java deleted file mode 100644 index eeb4589d..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/FileUtilTest.java +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.file.Files; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import sop.exception.SOPGPException; - -public class FileUtilTest { - - @BeforeAll - public static void setup() { - FileUtil.setEnvironmentVariableResolver(new FileUtil.EnvironmentVariableResolver() { - @Override - public String resolveEnvironmentVariable(String name) { - if (name.equals("test123")) { - return "test321"; - } - return null; - } - }); - } - - @Test - public void getFile_ThrowsForNull() { - assertThrows(NullPointerException.class, () -> FileUtil.getFile(null)); - } - - @Test - public void getFile_prfxEnvAlreadyExists() throws IOException { - File tempFile = new File("@ENV:test"); - tempFile.createNewFile(); - tempFile.deleteOnExit(); - - assertThrows(SOPGPException.AmbiguousInput.class, () -> FileUtil.getFile("@ENV:test")); - } - - @Test - public void getFile_EnvironmentVariable() { - File file = FileUtil.getFile("@ENV:test123"); - assertEquals("test321", file.getName()); - } - - @Test - public void getFile_nonExistentEnvVariable() { - assertThrows(IllegalArgumentException.class, () -> FileUtil.getFile("@ENV:INVALID")); - } - - @Test - public void getFile_prfxFdAlreadyExists() throws IOException { - File tempFile = new File("@FD:1"); - tempFile.createNewFile(); - tempFile.deleteOnExit(); - - assertThrows(SOPGPException.AmbiguousInput.class, () -> FileUtil.getFile("@FD:1")); - } - - @Test - public void getFile_prfxFdNotSupported() { - assertThrows(IllegalArgumentException.class, () -> FileUtil.getFile("@FD:2")); - } - - @Test - public void createNewFileOrThrow_throwsForNull() { - assertThrows(NullPointerException.class, () -> FileUtil.createNewFileOrThrow(null)); - } - - @Test - public void createNewFileOrThrow_success() throws IOException { - File dir = Files.createTempDirectory("test").toFile(); - dir.deleteOnExit(); - File file = new File(dir, "file"); - - assertFalse(file.exists()); - FileUtil.createNewFileOrThrow(file); - assertTrue(file.exists()); - } - - @Test - public void createNewFileOrThrow_alreadyExists() throws IOException { - File dir = Files.createTempDirectory("test").toFile(); - dir.deleteOnExit(); - File file = new File(dir, "file"); - - FileUtil.createNewFileOrThrow(file); - assertTrue(file.exists()); - assertThrows(SOPGPException.OutputExists.class, () -> FileUtil.createNewFileOrThrow(file)); - } - - @Test - public void getFileInputStream_success() throws IOException { - File dir = Files.createTempDirectory("test").toFile(); - dir.deleteOnExit(); - File file = new File(dir, "file"); - - FileUtil.createNewFileOrThrow(file); - FileInputStream inputStream = FileUtil.getFileInputStream(file.getAbsolutePath()); - assertNotNull(inputStream); - } - - @Test - public void getFileInputStream_fileNotFound() throws IOException { - File dir = Files.createTempDirectory("test").toFile(); - dir.deleteOnExit(); - File file = new File(dir, "file"); - - assertThrows(SOPGPException.MissingInput.class, - () -> FileUtil.getFileInputStream(file.getAbsolutePath())); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java deleted file mode 100644 index 6360a779..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.Test; -import sop.SOP; -import sop.operation.Armor; -import sop.operation.Dearmor; -import sop.operation.Decrypt; -import sop.operation.DetachInbandSignatureAndMessage; -import sop.operation.Encrypt; -import sop.operation.ExtractCert; -import sop.operation.GenerateKey; -import sop.operation.Sign; -import sop.operation.Verify; -import sop.operation.Version; - -public class SOPTest { - - @Test - @ExpectSystemExitWithStatus(69) - public void assertExitOnInvalidSubcommand() { - SOP sop = mock(SOP.class); - SopCLI.setSopInstance(sop); - - SopCLI.main(new String[] {"invalid"}); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void assertThrowsIfNoSOPBackendSet() { - SopCLI.SOP_INSTANCE = null; - // At this point, no SOP backend is set, so an InvalidStateException triggers exit(1) - SopCLI.main(new String[] {"armor"}); - } - - @Test - public void UnsupportedSubcommandsTest() { - SOP nullCommandSOP = new SOP() { - @Override - public Version version() { - return null; - } - - @Override - public GenerateKey generateKey() { - return null; - } - - @Override - public ExtractCert extractCert() { - return null; - } - - @Override - public Sign sign() { - return null; - } - - @Override - public Verify verify() { - return null; - } - - @Override - public Encrypt encrypt() { - return null; - } - - @Override - public Decrypt decrypt() { - return null; - } - - @Override - public Armor armor() { - return null; - } - - @Override - public Dearmor dearmor() { - return null; - } - - @Override - public DetachInbandSignatureAndMessage detachInbandSignatureAndMessage() { - return null; - } - }; - SopCLI.setSopInstance(nullCommandSOP); - - List commands = new ArrayList<>(); - commands.add(new String[] {"armor"}); - commands.add(new String[] {"dearmor"}); - commands.add(new String[] {"decrypt"}); - commands.add(new String[] {"detach-inband-signature-and-message"}); - commands.add(new String[] {"encrypt"}); - commands.add(new String[] {"extract-cert"}); - commands.add(new String[] {"generate-key"}); - commands.add(new String[] {"sign"}); - commands.add(new String[] {"verify", "signature.asc", "cert.asc"}); - commands.add(new String[] {"version"}); - - for (String[] command : commands) { - int exit = SopCLI.execute(command); - assertEquals(69, exit, "Unexpected exit code for non-implemented command " + Arrays.toString(command) + ": " + exit); - } - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java deleted file mode 100644 index 01aaa9a5..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import com.ginsberg.junit.exit.FailOnSystemExit; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import sop.Ready; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; -import sop.operation.Armor; - -public class ArmorCmdTest { - - private Armor armor; - private SOP sop; - - @BeforeEach - public void mockComponents() throws SOPGPException.BadData { - armor = mock(Armor.class); - sop = mock(SOP.class); - when(sop.armor()).thenReturn(armor); - when(armor.data((InputStream) any())).thenReturn(nopReady()); - - SopCLI.setSopInstance(sop); - } - - @Test - public void assertLabelIsNotCalledByDefault() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"armor"}); - verify(armor, never()).label(any()); - } - - @Test - public void assertLabelIsCalledWhenFlaggedWithArgument() throws SOPGPException.UnsupportedOption { - for (ArmorLabel label : ArmorLabel.values()) { - SopCLI.main(new String[] {"armor", "--label", label.name()}); - verify(armor, times(1)).label(label); - } - } - - @Test - public void assertDataIsAlwaysCalled() throws SOPGPException.BadData { - SopCLI.main(new String[] {"armor"}); - verify(armor, times(1)).data((InputStream) any()); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void assertThrowsForInvalidLabel() { - SopCLI.main(new String[] {"armor", "--label", "Invalid"}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void ifLabelsUnsupportedExit37() throws SOPGPException.UnsupportedOption { - when(armor.label(any())).thenThrow(new SOPGPException.UnsupportedOption("Custom Armor labels are not supported.")); - - SopCLI.main(new String[] {"armor", "--label", "Sig"}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void ifBadDataExit41() throws SOPGPException.BadData { - when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - - SopCLI.main(new String[] {"armor"}); - } - - @Test - @FailOnSystemExit - public void ifNoErrorsNoExit() { - when(sop.armor()).thenReturn(armor); - - SopCLI.main(new String[] {"armor"}); - } - - private static Ready nopReady() { - return new Ready() { - @Override - public void writeTo(OutputStream outputStream) { - } - }; - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java deleted file mode 100644 index aaad201b..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import sop.Ready; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Dearmor; - -public class DearmorCmdTest { - - private SOP sop; - private Dearmor dearmor; - - @BeforeEach - public void mockComponents() throws IOException, SOPGPException.BadData { - sop = mock(SOP.class); - dearmor = mock(Dearmor.class); - when(dearmor.data((InputStream) any())).thenReturn(nopReady()); - when(sop.dearmor()).thenReturn(dearmor); - - SopCLI.setSopInstance(sop); - } - - private static Ready nopReady() { - return new Ready() { - @Override - public void writeTo(OutputStream outputStream) { - } - }; - } - - @Test - public void assertDataIsCalled() throws IOException, SOPGPException.BadData { - SopCLI.main(new String[] {"dearmor"}); - verify(dearmor, times(1)).data((InputStream) any()); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData { - when(dearmor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor"))); - SopCLI.main(new String[] {"dearmor"}); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java deleted file mode 100644 index 9e1c35ba..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java +++ /dev/null @@ -1,344 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.Date; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatcher; -import org.mockito.ArgumentMatchers; -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SOP; -import sop.SessionKey; -import sop.Verification; -import sop.cli.picocli.DateParser; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Decrypt; -import sop.util.HexUtil; -import sop.util.UTCUtil; - -public class DecryptCmdTest { - - private Decrypt decrypt; - - @BeforeEach - public void mockComponents() throws SOPGPException.UnsupportedOption, SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.PasswordNotHumanReadable, SOPGPException.CannotDecrypt { - SOP sop = mock(SOP.class); - decrypt = mock(Decrypt.class); - - when(decrypt.verifyNotAfter(any())).thenReturn(decrypt); - when(decrypt.verifyNotBefore(any())).thenReturn(decrypt); - when(decrypt.withPassword(any())).thenReturn(decrypt); - when(decrypt.withSessionKey(any())).thenReturn(decrypt); - when(decrypt.withKey((InputStream) any())).thenReturn(decrypt); - when(decrypt.ciphertext((InputStream) any())).thenReturn(nopReadyWithResult()); - - when(sop.decrypt()).thenReturn(decrypt); - - SopCLI.setSopInstance(sop); - } - - private static ReadyWithResult nopReadyWithResult() { - return new ReadyWithResult() { - @Override - public DecryptionResult writeTo(OutputStream outputStream) { - return new DecryptionResult(null, Collections.emptyList()); - } - }; - } - - @Test - @ExpectSystemExitWithStatus(19) - public void missingArgumentsExceptionCausesExit19() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt { - when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.MissingArg("Missing arguments.")); - SopCLI.main(new String[] {"decrypt"}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void badDataExceptionCausesExit41() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt { - when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"decrypt"}); - } - - @Test - @ExpectSystemExitWithStatus(31) - public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption { - when(decrypt.withPassword(any())).thenThrow(new SOPGPException.PasswordNotHumanReadable()); - SopCLI.main(new String[] {"decrypt", "--with-password", "pretendThisIsNotReadable"}); - } - - @Test - public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"decrypt", "--with-password", "orange"}); - verify(decrypt, times(1)).withPassword("orange"); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Decrypting with password not supported.")); - SopCLI.main(new String[] {"decrypt", "--with-password", "swordfish"}); - } - - @Test - public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption { - Date now = new Date(); - SopCLI.main(new String[] {"decrypt"}); - verify(decrypt, times(1)).verifyNotBefore(DateParser.BEGINNING_OF_TIME); - verify(decrypt, times(1)).verifyNotAfter( - ArgumentMatchers.argThat(argument -> { - // allow 1-second difference - return Math.abs(now.getTime() - argument.getTime()) <= 1000; - })); - } - - @Test - public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"decrypt", "--not-before", "-", "--not-after", "-"}); - verify(decrypt, times(1)).verifyNotBefore(DateParser.BEGINNING_OF_TIME); - verify(decrypt, times(1)).verifyNotAfter(DateParser.END_OF_TIME); - } - - @Test - public void assertVerifyNotAfterAndBeforeNowResultsInMinTimeRange() throws SOPGPException.UnsupportedOption { - Date now = new Date(); - ArgumentMatcher isMaxOneSecOff = argument -> { - // Allow less than 1-second difference - return Math.abs(now.getTime() - argument.getTime()) <= 1000; - }; - - SopCLI.main(new String[] {"decrypt", "--not-before", "now", "--not-after", "now"}); - verify(decrypt, times(1)).verifyNotAfter(ArgumentMatchers.argThat(isMaxOneSecOff)); - verify(decrypt, times(1)).verifyNotBefore(ArgumentMatchers.argThat(isMaxOneSecOff)); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void assertMalformedDateInNotBeforeCausesExit1() { - // ParserException causes exit(1) - SopCLI.main(new String[] {"decrypt", "--not-before", "invalid"}); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void assertMalformedDateInNotAfterCausesExit1() { - // ParserException causes exit(1) - SopCLI.main(new String[] {"decrypt", "--not-after", "invalid"}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption { - when(decrypt.verifyNotAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); - SopCLI.main(new String[] {"decrypt", "--not-after", "now"}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption { - when(decrypt.verifyNotBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); - SopCLI.main(new String[] {"decrypt", "--not-before", "now"}); - } - - @Test - @ExpectSystemExitWithStatus(59) - public void assertExistingSessionKeyOutFileCausesExit59() throws IOException { - File tempFile = File.createTempFile("existing-session-key-", ".tmp"); - tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void assertWhenSessionKeyCannotBeExtractedExit37() throws IOException { - Path tempDir = Files.createTempDirectory("session-key-out-dir"); - File tempFile = new File(tempDir.toFile(), "session-key"); - tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); - } - - @Test - public void assertSessionKeyIsProperlyWrittenToSessionKeyFile() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { - byte[] key = "C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137".getBytes(StandardCharsets.UTF_8); - when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult() { - @Override - public DecryptionResult writeTo(OutputStream outputStream) { - return new DecryptionResult( - new SessionKey((byte) 9, key), - Collections.emptyList() - ); - } - }); - Path tempDir = Files.createTempDirectory("session-key-out-dir"); - File tempFile = new File(tempDir.toFile(), "session-key"); - tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); - - ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream(); - try (FileInputStream fileIn = new FileInputStream(tempFile)) { - byte[] buf = new byte[32]; - int read = fileIn.read(buf); - while (read != -1) { - bytesInFile.write(buf, 0, read); - read = fileIn.read(buf); - } - } - - byte[] algAndKey = new byte[key.length + 1]; - algAndKey[0] = (byte) 9; - System.arraycopy(key, 0, algAndKey, 1, key.length); - assertArrayEquals(algAndKey, bytesInFile.toByteArray()); - } - - @Test - @ExpectSystemExitWithStatus(29) - public void assertUnableToDecryptExceptionResultsInExit29() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData { - when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.CannotDecrypt()); - SopCLI.main(new String[] {"decrypt"}); - } - - @Test - @ExpectSystemExitWithStatus(3) - public void assertNoSignatureExceptionCausesExit3() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData { - when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult() { - @Override - public DecryptionResult writeTo(OutputStream outputStream) throws SOPGPException.NoSignature { - throw new SOPGPException.NoSignature(); - } - }); - SopCLI.main(new String[] {"decrypt"}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void badDataInVerifyWithCausesExit41() throws IOException, SOPGPException.BadData { - when(decrypt.verifyWithCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - File tempFile = File.createTempFile("verify-with-", ".tmp"); - SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(61) - public void unexistentCertFileCausesExit61() { - SopCLI.main(new String[] {"decrypt", "--verify-with", "invalid"}); - } - - @Test - @ExpectSystemExitWithStatus(59) - public void existingVerifyOutCausesExit59() throws IOException { - File certFile = File.createTempFile("existing-verify-out-cert", ".asc"); - File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp"); - - SopCLI.main(new String[] {"decrypt", "--verify-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); - } - - @Test - public void verifyOutIsProperlyWritten() throws IOException, SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData { - File certFile = File.createTempFile("verify-out-cert", ".asc"); - File verifyOut = new File(certFile.getParent(), "verify-out.txt"); - if (verifyOut.exists()) { - verifyOut.delete(); - } - verifyOut.deleteOnExit(); - Date date = UTCUtil.parseUTCDate("2021-07-11T20:58:23Z"); - when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult() { - @Override - public DecryptionResult writeTo(OutputStream outputStream) { - return new DecryptionResult(null, Collections.singletonList( - new Verification( - date, - "1B66A707819A920925BC6777C3E0AFC0B2DFF862", - "C8CD564EBF8D7BBA90611D8D071773658BF6BF86")) - ); - } - }); - - SopCLI.main(new String[] {"decrypt", "--verify-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); - try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) { - String line = reader.readLine(); - assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line); - } - } - - @Test - public void assertWithSessionKeyIsPassedDown() throws SOPGPException.UnsupportedOption { - SessionKey key1 = new SessionKey((byte) 9, HexUtil.hexToBytes("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137")); - SessionKey key2 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); - SopCLI.main(new String[] {"decrypt", - "--with-session-key", "9:C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137", - "--with-session-key", "9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"}); - verify(decrypt).withSessionKey(key1); - verify(decrypt).withSessionKey(key2); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void assertMalformedSessionKeysResultInExit1() { - SopCLI.main(new String[] {"decrypt", - "--with-session-key", "C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void assertBadDataInKeysResultsInExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { - when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(61) - public void assertKeyFileNotFoundCausesExit61() { - SopCLI.main(new String[] {"decrypt", "nonexistent-key"}); - } - - @Test - @ExpectSystemExitWithStatus(67) - public void assertProtectedKeyCausesExit67() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { - when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); - File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(13) - public void assertUnsupportedAlgorithmExceptionCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { - when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new IOException())); - File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(23) - public void verifyOutWithoutVerifyWithCausesExit23() { - SopCLI.main(new String[] {"decrypt", "--verify-out", "out.file"}); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java deleted file mode 100644 index 91f0a1e7..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java +++ /dev/null @@ -1,194 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import sop.Ready; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.operation.Encrypt; - -public class EncryptCmdTest { - - Encrypt encrypt; - - @BeforeEach - public void mockComponents() throws IOException { - encrypt = mock(Encrypt.class); - when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() { - @Override - public void writeTo(OutputStream outputStream) { - - } - }); - - SOP sop = mock(SOP.class); - when(sop.encrypt()).thenReturn(encrypt); - - SopCLI.setSopInstance(sop); - } - - @Test - @ExpectSystemExitWithStatus(19) - public void missingBothPasswordAndCertFileCauseExit19() { - SopCLI.main(new String[] {"encrypt", "--no-armor"}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void as_unsupportedEncryptAsCausesExit37() throws SOPGPException.UnsupportedOption { - when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting encryption mode not supported.")); - - SopCLI.main(new String[] {"encrypt", "--as", "Binary"}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void as_invalidModeOptionCausesExit37() { - SopCLI.main(new String[] {"encrypt", "--as", "invalid"}); - } - - @Test - public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption { - for (EncryptAs mode : EncryptAs.values()) { - SopCLI.main(new String[] {"encrypt", "--as", mode.name(), "--with-password", "0rbit"}); - verify(encrypt, times(1)).mode(mode); - } - } - - @Test - @ExpectSystemExitWithStatus(31) - public void withPassword_notHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable()); - - SopCLI.main(new String[] {"encrypt", "--with-password", "pretendThisIsNotReadable"}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported.")); - - SopCLI.main(new String[] {"encrypt", "--with-password", "orange"}); - } - - @Test - public void signWith_multipleTimesGetPassedDown() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { - File keyFile1 = File.createTempFile("sign-with-1-", ".asc"); - File keyFile2 = File.createTempFile("sign-with-2-", ".asc"); - - SopCLI.main(new String[] {"encrypt", "--with-password", "password", "--sign-with", keyFile1.getAbsolutePath(), "--sign-with", keyFile2.getAbsolutePath()}); - verify(encrypt, times(2)).signWith((InputStream) any()); - } - - @Test - @ExpectSystemExitWithStatus(61) - public void signWith_nonExistentKeyFileCausesExit61() { - SopCLI.main(new String[] {"encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"}); - } - - @Test - @ExpectSystemExitWithStatus(67) - public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { - when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); - File keyFile = File.createTempFile("sign-with", ".asc"); - SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", "starship"}); - } - - @Test - @ExpectSystemExitWithStatus(13) - public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { - when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); - File keyFile = File.createTempFile("sign-with", ".asc"); - SopCLI.main(new String[] {"encrypt", "--with-password", "123456", "--sign-with", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(79) - public void signWith_certCannotSignCausesExit1() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { - when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign()); - File keyFile = File.createTempFile("sign-with", ".asc"); - SopCLI.main(new String[] {"encrypt", "--with-password", "dragon", "--sign-with", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { - when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - File keyFile = File.createTempFile("sign-with", ".asc"); - SopCLI.main(new String[] {"encrypt", "--with-password", "orange", "--sign-with", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(61) - public void cert_nonExistentCertFileCausesExit61() { - SopCLI.main(new String[] {"encrypt", "invalid.asc"}); - } - - @Test - @ExpectSystemExitWithStatus(13) - public void cert_unsupportedAsymmetricAlgorithmCausesExit13() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { - when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); - File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(17) - public void cert_certCannotEncryptCausesExit17() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { - when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt.", new Exception())); - File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void cert_badDataCausesExit41() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { - when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); - } - - @Test - public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"encrypt", "--with-password", "clownfish"}); - verify(encrypt, never()).noArmor(); - } - - @Test - public void noArmor_callGetsPassedDown() { - SopCLI.main(new String[] {"encrypt", "--with-password", "monkey", "--no-armor"}); - verify(encrypt, times(1)).noArmor(); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void writeTo_ioExceptionCausesExit1() throws IOException { - when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - throw new IOException(); - } - }); - - SopCLI.main(new String[] {"encrypt", "--with-password", "wildcat"}); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java deleted file mode 100644 index 382fe300..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import sop.Ready; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.ExtractCert; - -public class ExtractCertCmdTest { - - ExtractCert extractCert; - - @BeforeEach - public void mockComponents() throws IOException, SOPGPException.BadData { - extractCert = mock(ExtractCert.class); - when(extractCert.key((InputStream) any())).thenReturn(new Ready() { - @Override - public void writeTo(OutputStream outputStream) { - } - }); - - SOP sop = mock(SOP.class); - when(sop.extractCert()).thenReturn(extractCert); - - SopCLI.setSopInstance(sop); - } - - @Test - public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"extract-cert"}); - verify(extractCert, never()).noArmor(); - } - - @Test - public void noArmor_passedDown() { - SopCLI.main(new String[] {"extract-cert", "--no-armor"}); - verify(extractCert, times(1)).noArmor(); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void key_ioExceptionCausesExit1() throws IOException, SOPGPException.BadData { - when(extractCert.key((InputStream) any())).thenReturn(new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - throw new IOException(); - } - }); - SopCLI.main(new String[] {"extract-cert"}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void key_badDataCausesExit41() throws IOException, SOPGPException.BadData { - when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"extract-cert"}); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java deleted file mode 100644 index 643cf363..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.OutputStream; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InOrder; -import org.mockito.Mockito; -import sop.Ready; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.GenerateKey; - -public class GenerateKeyCmdTest { - - GenerateKey generateKey; - - @BeforeEach - public void mockComponents() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { - generateKey = mock(GenerateKey.class); - when(generateKey.generate()).thenReturn(new Ready() { - @Override - public void writeTo(OutputStream outputStream) { - - } - }); - - SOP sop = mock(SOP.class); - when(sop.generateKey()).thenReturn(generateKey); - - SopCLI.setSopInstance(sop); - } - - @Test - public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"generate-key", "Alice"}); - verify(generateKey, never()).noArmor(); - } - - @Test - public void noArmor_passedDown() { - SopCLI.main(new String[] {"generate-key", "--no-armor", "Alice"}); - verify(generateKey, times(1)).noArmor(); - } - - @Test - public void userId_multipleUserIdsPassedDownInProperOrder() { - SopCLI.main(new String[] {"generate-key", "Alice ", "Bob "}); - - InOrder inOrder = Mockito.inOrder(generateKey); - inOrder.verify(generateKey).userId("Alice "); - inOrder.verify(generateKey).userId("Bob "); - - verify(generateKey, times(2)).userId(any()); - } - - @Test - @ExpectSystemExitWithStatus(19) - public void missingArgumentCausesExit19() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { - // TODO: RFC4880-bis and the current Stateless OpenPGP CLI spec allow keys to have no user-ids, - // so we might want to change this test in the future. - when(generateKey.generate()).thenThrow(new SOPGPException.MissingArg("Missing user-id.")); - SopCLI.main(new String[] {"generate-key"}); - } - - @Test - @ExpectSystemExitWithStatus(13) - public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { - when(generateKey.generate()).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); - SopCLI.main(new String[] {"generate-key", "Alice"}); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void ioExceptionCausesExit1() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { - when(generateKey.generate()).thenReturn(new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - throw new IOException(); - } - }); - SopCLI.main(new String[] {"generate-key", "Alice"}); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java deleted file mode 100644 index ce0ce54a..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java +++ /dev/null @@ -1,128 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import sop.ReadyWithResult; -import sop.SOP; -import sop.SigningResult; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Sign; - -public class SignCmdTest { - - Sign sign; - File keyFile; - - @BeforeEach - public void mockComponents() throws IOException, SOPGPException.ExpectedText { - sign = mock(Sign.class); - when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult() { - @Override - public SigningResult writeTo(OutputStream outputStream) { - return SigningResult.builder().build(); - } - }); - - SOP sop = mock(SOP.class); - when(sop.sign()).thenReturn(sign); - - SopCLI.setSopInstance(sop); - - keyFile = File.createTempFile("sign-", ".asc"); - } - - @Test - public void as_optionsAreCaseInsensitive() { - SopCLI.main(new String[] {"sign", "--as", "Binary", keyFile.getAbsolutePath()}); - SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); - SopCLI.main(new String[] {"sign", "--as", "BINARY", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void as_invalidOptionCausesExit37() { - SopCLI.main(new String[] {"sign", "--as", "Invalid", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { - when(sign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting signing mode not supported.")); - SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void key_nonExistentKeyFileCausesExit1() { - SopCLI.main(new String[] {"sign", "invalid.asc"}); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void key_keyIsProtectedCausesExit1() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { - when(sign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { - when(sign.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(19) - public void key_missingKeyFileCausesExit19() { - SopCLI.main(new String[] {"sign"}); - } - - @Test - public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); - verify(sign, never()).noArmor(); - } - - @Test - public void noArmor_passedDown() { - SopCLI.main(new String[] {"sign", "--no-armor", keyFile.getAbsolutePath()}); - verify(sign, times(1)).noArmor(); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText { - when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult() { - @Override - public SigningResult writeTo(OutputStream outputStream) throws IOException { - throw new IOException(); - } - }); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(53) - public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText { - when(sign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText()); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java deleted file mode 100644 index 028d2451..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java +++ /dev/null @@ -1,204 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintStream; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatchers; -import sop.SOP; -import sop.Verification; -import sop.cli.picocli.DateParser; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Verify; -import sop.util.UTCUtil; - -public class VerifyCmdTest { - - Verify verify; - File signature; - File cert; - - PrintStream originalSout; - - @BeforeEach - public void prepare() throws SOPGPException.UnsupportedOption, SOPGPException.BadData, SOPGPException.NoSignature, IOException { - originalSout = System.out; - - verify = mock(Verify.class); - when(verify.notBefore(any())).thenReturn(verify); - when(verify.notAfter(any())).thenReturn(verify); - when(verify.cert((InputStream) any())).thenReturn(verify); - when(verify.signatures((InputStream) any())).thenReturn(verify); - when(verify.data((InputStream) any())).thenReturn( - Collections.singletonList( - new Verification( - UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"), - "EB85BB5FA33A75E15E944E63F231550C4F47E38E", - "EB85BB5FA33A75E15E944E63F231550C4F47E38E") - ) - ); - - SOP sop = mock(SOP.class); - when(sop.verify()).thenReturn(verify); - - SopCLI.setSopInstance(sop); - - signature = File.createTempFile("signature-", ".asc"); - cert = File.createTempFile("cert-", ".asc"); - } - - @AfterEach - public void restoreSout() { - System.setOut(originalSout); - } - - @Test - public void notAfter_passedDown() throws SOPGPException.UnsupportedOption { - Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"); - SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); - verify(verify, times(1)).notAfter(date); - } - - @Test - public void notAfter_now() throws SOPGPException.UnsupportedOption { - Date now = new Date(); - SopCLI.main(new String[] {"verify", "--not-after", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); - verify(verify, times(1)).notAfter(dateMatcher(now)); - } - - @Test - public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", "--not-after", "-", signature.getAbsolutePath(), cert.getAbsolutePath()}); - verify(verify, times(1)).notAfter(DateParser.END_OF_TIME); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { - when(verify.notAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); - SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); - } - - @Test - public void notBefore_passedDown() throws SOPGPException.UnsupportedOption { - Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"); - SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); - verify(verify, times(1)).notBefore(date); - } - - @Test - public void notBefore_now() throws SOPGPException.UnsupportedOption { - Date now = new Date(); - SopCLI.main(new String[] {"verify", "--not-before", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); - verify(verify, times(1)).notBefore(dateMatcher(now)); - } - - @Test - public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", "--not-before", "-", signature.getAbsolutePath(), cert.getAbsolutePath()}); - verify(verify, times(1)).notBefore(DateParser.BEGINNING_OF_TIME); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { - when(verify.notBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); - SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); - } - - @Test - public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); - verify(verify, times(1)).notAfter(dateMatcher(new Date())); - verify(verify, times(1)).notBefore(DateParser.BEGINNING_OF_TIME); - } - - private static Date dateMatcher(Date date) { - return ArgumentMatchers.argThat(argument -> Math.abs(argument.getTime() - date.getTime()) < 1000); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void cert_fileNotFoundCausesExit1() { - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), "invalid.asc"}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void cert_badDataCausesExit41() throws SOPGPException.BadData { - when(verify.cert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(1) - public void signature_fileNotFoundCausesExit1() { - SopCLI.main(new String[] {"verify", "invalid.sig", cert.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void signature_badDataCausesExit41() throws SOPGPException.BadData { - when(verify.signatures((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(3) - public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { - when(verify.data((InputStream) any())).thenThrow(new SOPGPException.NoSignature()); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); - } - - @Test - @ExpectSystemExitWithStatus(41) - public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { - when(verify.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); - } - - @Test - public void resultIsPrintedProperly() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { - when(verify.data((InputStream) any())).thenReturn(Arrays.asList( - new Verification(UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"), - "EB85BB5FA33A75E15E944E63F231550C4F47E38E", - "EB85BB5FA33A75E15E944E63F231550C4F47E38E"), - new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"), - "C90E6D36200A1B922A1509E77618196529AE5FF8", - "C4BC2DDB38CCE96485EBE9C2F20691179038E5C6") - )); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); - - System.setOut(originalSout); - - String expected = "2019-10-29T18:36:45Z EB85BB5FA33A75E15E944E63F231550C4F47E38E EB85BB5FA33A75E15E944E63F231550C4F47E38E\n" + - "2019-10-24T23:48:29Z C90E6D36200A1B922A1509E77618196529AE5FF8 C4BC2DDB38CCE96485EBE9C2F20691179038E5C6\n"; - - assertEquals(expected, out.toString()); - } -} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java deleted file mode 100644 index 98ea58e2..00000000 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.operation.Version; - -public class VersionCmdTest { - - private Version version; - - @BeforeEach - public void mockComponents() { - SOP sop = mock(SOP.class); - version = mock(Version.class); - when(version.getName()).thenReturn("MockSop"); - when(version.getVersion()).thenReturn("1.0"); - when(sop.version()).thenReturn(version); - - SopCLI.setSopInstance(sop); - } - - @Test - public void assertVersionCommandWorks() { - SopCLI.main(new String[] {"version"}); - verify(version, times(1)).getVersion(); - verify(version, times(1)).getName(); - } - - @Test - @ExpectSystemExitWithStatus(37) - public void assertInvalidOptionResultsInExit37() { - SopCLI.main(new String[] {"version", "--invalid"}); - } -} diff --git a/sop-java/README.md b/sop-java/README.md index 452576c6..e10261b6 100644 --- a/sop-java/README.md +++ b/sop-java/README.md @@ -1,80 +1 @@ - - -# SOP-Java - -[![Spec Revision: 3](https://img.shields.io/badge/Spec%20Revision-3-blue)](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03) -[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/sop-java)](https://search.maven.org/artifact/org.pgpainless/sop-java) -[![JavaDoc](https://badgen.net/badge/javadoc/yes/green)](https://pgpainless.org/releases/latest/javadoc/sop/SOP.html) -[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/pgpainless)](https://api.reuse.software/info/github.com/pgpainless/pgpainless) - -Stateless OpenPGP Protocol for Java. - -This module contains interfaces that model the API described by the -[Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03) specification. - -This module is not a command line application! For that, see `sop-java-picocli`. - -## Usage Examples - -The API defined by `sop-java` is super straight forward: -```java -SOP sop = ... // e.g. new org.pgpainless.sop.SOPImpl(); - -// Generate an OpenPGP key -byte[] key = sop.generateKey() - .userId("Alice ") - .generate() - .getBytes(); - -// Extract the certificate (public key) -byte[] cert = sop.extractCert() - .key(key) - .getBytes(); - -// Encrypt a message -byte[] message = ... -byte[] encrypted = sop.encrypt() - .withCert(cert) - .signWith(key) - .plaintext(message) - .getBytes(); - -// Decrypt a message -ByteArrayAndResult messageAndVerifications = sop.decrypt() - .verifyWith(cert) - .withKey(key) - .ciphertext(encrypted) - .toByteArrayAndResult(); -byte[] decrypted = messageAndVerifications.getBytes(); -// Signature Verifications -DecryptionResult messageInfo = messageAndVerifications.getResult(); -List signatureVerifications = messageInfo.getVerifications(); -``` - -Furthermore, the API is capable of signing messages and verifying unencrypted signed data, as well as adding and removing ASCII armor. - -### Limitations -As per the spec, sop-java does not (yet) deal with encrypted OpenPGP keys. - -## Why should I use this? - -If you need to use OpenPGP functionality like encrypting/decrypting messages, or creating/verifying -signatures inside your application, you probably don't want to start from scratch and instead reuse some library. - -Instead of locking yourselves in by depending hard on that one library, you can simply depend on the interfaces from -`sop-java` and plug in a library (such as `pgpainless-sop`) that implements said interfaces. - -That way you don't make yourself dependent from a single OpenPGP library and stay flexible. -Should another library emerge, that better suits your needs (and implements `sop-java`), you can easily switch -by swapping out the dependency with minimal changes to your code. - -## Why should I *implement* this? - -Did you create an [OpenPGP](https://datatracker.ietf.org/doc/html/rfc4880) implementation that can be used in the Java ecosystem? -By implementing the `sop-java` interface, you can turn your library into a command line interface (see `sop-java-picocli`). -This allows you to plug your library into the [OpenPGP interoperability test suite](https://tests.sequoia-pgp.org/) -of the [Sequoia-PGP](https://sequoia-pgp.org/) project. +# [MOVED](https://github.com/pgpainless/sop-java/tree/master/sop-java) \ No newline at end of file diff --git a/sop-java/build.gradle b/sop-java/build.gradle deleted file mode 100644 index c2e2f1fb..00000000 --- a/sop-java/build.gradle +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -plugins { - id 'java' -} - -group 'org.pgpainless' - -repositories { - mavenCentral() -} - -dependencies { - testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" -} - -test { - useJUnitPlatform() -} \ No newline at end of file diff --git a/sop-java/src/main/java/sop/ByteArrayAndResult.java b/sop-java/src/main/java/sop/ByteArrayAndResult.java deleted file mode 100644 index fd2b39a7..00000000 --- a/sop-java/src/main/java/sop/ByteArrayAndResult.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; - -/** - * Tuple of a byte array and associated result object. - * @param type of result - */ -public class ByteArrayAndResult { - - private final byte[] bytes; - private final T result; - - public ByteArrayAndResult(byte[] bytes, T result) { - this.bytes = bytes; - this.result = result; - } - - /** - * Return the byte array part. - * - * @return bytes - */ - public byte[] getBytes() { - return bytes; - } - - /** - * Return the result part. - * - * @return result - */ - public T getResult() { - return result; - } - - /** - * Return the byte array part as an {@link InputStream}. - * - * @return input stream - */ - public InputStream getInputStream() { - return new ByteArrayInputStream(getBytes()); - } -} diff --git a/sop-java/src/main/java/sop/DecryptionResult.java b/sop-java/src/main/java/sop/DecryptionResult.java deleted file mode 100644 index 4f0e1ab2..00000000 --- a/sop-java/src/main/java/sop/DecryptionResult.java +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import sop.util.Optional; - -public class DecryptionResult { - - private final Optional sessionKey; - private final List verifications; - - public DecryptionResult(SessionKey sessionKey, List verifications) { - this.sessionKey = Optional.ofNullable(sessionKey); - this.verifications = Collections.unmodifiableList(verifications); - } - - public Optional getSessionKey() { - return sessionKey; - } - - public List getVerifications() { - return new ArrayList<>(verifications); - } -} diff --git a/sop-java/src/main/java/sop/MicAlg.java b/sop-java/src/main/java/sop/MicAlg.java deleted file mode 100644 index 5bee7875..00000000 --- a/sop-java/src/main/java/sop/MicAlg.java +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.OutputStream; -import java.io.PrintWriter; - -public class MicAlg { - - private final String micAlg; - - public MicAlg(String micAlg) { - if (micAlg == null) { - throw new IllegalArgumentException("MicAlg String cannot be null."); - } - this.micAlg = micAlg; - } - - public static MicAlg empty() { - return new MicAlg(""); - } - - public static MicAlg fromHashAlgorithmId(int id) { - switch (id) { - case 1: - return new MicAlg("pgp-md5"); - case 2: - return new MicAlg("pgp-sha1"); - case 3: - return new MicAlg("pgp-ripemd160"); - case 8: - return new MicAlg("pgp-sha256"); - case 9: - return new MicAlg("pgp-sha384"); - case 10: - return new MicAlg("pgp-sha512"); - case 11: - return new MicAlg("pgp-sha224"); - default: - throw new IllegalArgumentException("Unsupported hash algorithm ID: " + id); - } - } - - public String getMicAlg() { - return micAlg; - } - - public void writeTo(OutputStream outputStream) { - PrintWriter pw = new PrintWriter(outputStream); - pw.write(getMicAlg()); - pw.close(); - } -} diff --git a/sop-java/src/main/java/sop/Ready.java b/sop-java/src/main/java/sop/Ready.java deleted file mode 100644 index 71ab26ec..00000000 --- a/sop-java/src/main/java/sop/Ready.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public abstract class Ready { - - /** - * Write the data to the provided output stream. - * - * @param outputStream output stream - * @throws IOException in case of an IO error - */ - public abstract void writeTo(OutputStream outputStream) throws IOException; - - /** - * Return the data as a byte array by writing it to a {@link ByteArrayOutputStream} first and then returning - * the array. - * - * @return data as byte array - * @throws IOException in case of an IO error - */ - public byte[] getBytes() throws IOException { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - writeTo(bytes); - return bytes.toByteArray(); - } - - /** - * Return an input stream containing the data. - * - * @return input stream - * @throws IOException in case of an IO error - */ - public InputStream getInputStream() throws IOException { - return new ByteArrayInputStream(getBytes()); - } -} diff --git a/sop-java/src/main/java/sop/ReadyWithResult.java b/sop-java/src/main/java/sop/ReadyWithResult.java deleted file mode 100644 index 9feeddae..00000000 --- a/sop-java/src/main/java/sop/ReadyWithResult.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -import sop.exception.SOPGPException; - -public abstract class ReadyWithResult { - - /** - * Write the data e.g. decrypted plaintext to the provided output stream and return the result of the - * processing operation. - * - * @param outputStream output stream - * @return result, eg. signatures - * - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature if there are no valid signatures found - */ - public abstract T writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature; - - /** - * Return the data as a {@link ByteArrayAndResult}. - * Calling {@link ByteArrayAndResult#getBytes()} will give you access to the data as byte array, while - * {@link ByteArrayAndResult#getResult()} will grant access to the appended result. - * - * @return byte array and result - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature if there are no valid signatures found - */ - public ByteArrayAndResult toByteArrayAndResult() throws IOException, SOPGPException.NoSignature { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - T result = writeTo(bytes); - return new ByteArrayAndResult<>(bytes.toByteArray(), result); - } -} diff --git a/sop-java/src/main/java/sop/SOP.java b/sop-java/src/main/java/sop/SOP.java deleted file mode 100644 index 2c2ccf16..00000000 --- a/sop-java/src/main/java/sop/SOP.java +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import sop.operation.Armor; -import sop.operation.Dearmor; -import sop.operation.Decrypt; -import sop.operation.DetachInbandSignatureAndMessage; -import sop.operation.Encrypt; -import sop.operation.ExtractCert; -import sop.operation.GenerateKey; -import sop.operation.Sign; -import sop.operation.Verify; -import sop.operation.Version; - -/** - * Stateless OpenPGP Interface. - */ -public interface SOP { - - /** - * Get information about the implementations name and version. - * - * @return version - */ - Version version(); - - /** - * Generate a secret key. - * Customize the operation using the builder {@link GenerateKey}. - * - * @return builder instance - */ - GenerateKey generateKey(); - - /** - * Extract a certificate (public key) from a secret key. - * Customize the operation using the builder {@link ExtractCert}. - * - * @return builder instance - */ - ExtractCert extractCert(); - - /** - * Create detached signatures. - * Customize the operation using the builder {@link Sign}. - * - * @return builder instance - */ - Sign sign(); - - /** - * Verify detached signatures. - * Customize the operation using the builder {@link Verify}. - * - * @return builder instance - */ - Verify verify(); - - /** - * Encrypt a message. - * Customize the operation using the builder {@link Encrypt}. - * - * @return builder instance - */ - Encrypt encrypt(); - - /** - * Decrypt a message. - * Customize the operation using the builder {@link Decrypt}. - * - * @return builder instance - */ - Decrypt decrypt(); - - /** - * Convert binary OpenPGP data to ASCII. - * Customize the operation using the builder {@link Armor}. - * - * @return builder instance - */ - Armor armor(); - - /** - * Converts ASCII armored OpenPGP data to binary. - * Customize the operation using the builder {@link Dearmor}. - * - * @return builder instance - */ - Dearmor dearmor(); - - DetachInbandSignatureAndMessage detachInbandSignatureAndMessage(); -} diff --git a/sop-java/src/main/java/sop/SessionKey.java b/sop-java/src/main/java/sop/SessionKey.java deleted file mode 100644 index 2adcec4d..00000000 --- a/sop-java/src/main/java/sop/SessionKey.java +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.util.Arrays; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import sop.util.HexUtil; - -public class SessionKey { - - private static final Pattern PATTERN = Pattern.compile("^(\\d):([0-9a-fA-F]+)$"); - - private final byte algorithm; - private final byte[] sessionKey; - - public SessionKey(byte algorithm, byte[] sessionKey) { - this.algorithm = algorithm; - this.sessionKey = sessionKey; - } - - /** - * Return the symmetric algorithm octet. - * - * @return algorithm id - */ - public byte getAlgorithm() { - return algorithm; - } - - /** - * Return the session key. - * - * @return session key - */ - public byte[] getKey() { - return sessionKey; - } - - @Override - public int hashCode() { - return getAlgorithm() * 17 + Arrays.hashCode(getKey()); - } - - @Override - public boolean equals(Object other) { - if (other == null) { - return false; - } - if (this == other) { - return true; - } - if (!(other instanceof SessionKey)) { - return false; - } - - SessionKey otherKey = (SessionKey) other; - return getAlgorithm() == otherKey.getAlgorithm() && Arrays.equals(getKey(), otherKey.getKey()); - } - - public static SessionKey fromString(String string) { - Matcher matcher = PATTERN.matcher(string); - if (!matcher.matches()) { - throw new IllegalArgumentException("Provided session key does not match expected format."); - } - byte algorithm = Byte.parseByte(matcher.group(1)); - String key = matcher.group(2); - - return new SessionKey(algorithm, HexUtil.hexToBytes(key)); - } - - @Override - public String toString() { - return "" + (int) getAlgorithm() + ':' + HexUtil.bytesToHex(sessionKey); - } -} diff --git a/sop-java/src/main/java/sop/Signatures.java b/sop-java/src/main/java/sop/Signatures.java deleted file mode 100644 index dd3f000d..00000000 --- a/sop-java/src/main/java/sop/Signatures.java +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.IOException; -import java.io.OutputStream; - -public abstract class Signatures extends Ready { - - /** - * Write OpenPGP signatures to the provided output stream. - * - * @param signatureOutputStream output stream - * @throws IOException in case of an IO error - */ - @Override - public abstract void writeTo(OutputStream signatureOutputStream) throws IOException; - -} diff --git a/sop-java/src/main/java/sop/SigningResult.java b/sop-java/src/main/java/sop/SigningResult.java deleted file mode 100644 index 2cb142dc..00000000 --- a/sop-java/src/main/java/sop/SigningResult.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -/** - * This class contains various information about a signed message. - */ -public final class SigningResult { - - private final MicAlg micAlg; - - private SigningResult(MicAlg micAlg) { - this.micAlg = micAlg; - } - - /** - * Return a string identifying the digest mechanism used to create the signed message. - * This is useful for setting the micalg= parameter for the multipart/signed - * content type of a PGP/MIME object as described in section 5 of [RFC3156]. - * - * If more than one signature was generated and different digest mechanisms were used, - * the value of the micalg object is an empty string. - * - * @return micalg - */ - public MicAlg getMicAlg() { - return micAlg; - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - - private MicAlg micAlg; - - public Builder setMicAlg(MicAlg micAlg) { - this.micAlg = micAlg; - return this; - } - - public SigningResult build() { - SigningResult signingResult = new SigningResult(micAlg); - return signingResult; - } - } -} diff --git a/sop-java/src/main/java/sop/Verification.java b/sop-java/src/main/java/sop/Verification.java deleted file mode 100644 index 2047c3d4..00000000 --- a/sop-java/src/main/java/sop/Verification.java +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.util.Date; - -import sop.util.UTCUtil; - -public class Verification { - - private final Date creationTime; - private final String signingKeyFingerprint; - private final String signingCertFingerprint; - - public Verification(Date creationTime, String signingKeyFingerprint, String signingCertFingerprint) { - this.creationTime = creationTime; - this.signingKeyFingerprint = signingKeyFingerprint; - this.signingCertFingerprint = signingCertFingerprint; - } - - /** - * Return the signatures' creation time. - * - * @return signature creation time - */ - public Date getCreationTime() { - return creationTime; - } - - /** - * Return the fingerprint of the signing (sub)key. - * - * @return signing key fingerprint - */ - public String getSigningKeyFingerprint() { - return signingKeyFingerprint; - } - - /** - * Return the fingerprint fo the signing certificate. - * - * @return signing certificate fingerprint - */ - public String getSigningCertFingerprint() { - return signingCertFingerprint; - } - - @Override - public String toString() { - return UTCUtil.formatUTCDate(getCreationTime()) + - ' ' + - getSigningKeyFingerprint() + - ' ' + - getSigningCertFingerprint(); - } -} diff --git a/sop-java/src/main/java/sop/enums/ArmorLabel.java b/sop-java/src/main/java/sop/enums/ArmorLabel.java deleted file mode 100644 index aeaa6f9b..00000000 --- a/sop-java/src/main/java/sop/enums/ArmorLabel.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum ArmorLabel { - Auto, - Sig, - Key, - Cert, - Message -} diff --git a/sop-java/src/main/java/sop/enums/EncryptAs.java b/sop-java/src/main/java/sop/enums/EncryptAs.java deleted file mode 100644 index 2de6792b..00000000 --- a/sop-java/src/main/java/sop/enums/EncryptAs.java +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum EncryptAs { - Binary, - Text, - MIME -} diff --git a/sop-java/src/main/java/sop/enums/SignAs.java b/sop-java/src/main/java/sop/enums/SignAs.java deleted file mode 100644 index fcd79f4d..00000000 --- a/sop-java/src/main/java/sop/enums/SignAs.java +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum SignAs { - Binary, - Text -} diff --git a/sop-java/src/main/java/sop/enums/package-info.java b/sop-java/src/main/java/sop/enums/package-info.java deleted file mode 100644 index 67148d3e..00000000 --- a/sop-java/src/main/java/sop/enums/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Enumerations. - */ -package sop.enums; diff --git a/sop-java/src/main/java/sop/exception/SOPGPException.java b/sop-java/src/main/java/sop/exception/SOPGPException.java deleted file mode 100644 index 6b844f59..00000000 --- a/sop-java/src/main/java/sop/exception/SOPGPException.java +++ /dev/null @@ -1,316 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.exception; - -public abstract class SOPGPException extends RuntimeException { - - public SOPGPException() { - super(); - } - - public SOPGPException(String message) { - super(message); - } - - public SOPGPException(Throwable e) { - super(e); - } - - public SOPGPException(String message, Throwable cause) { - super(message, cause); - } - - public abstract int getExitCode(); - - /** - * No acceptable signatures found (sop verify). - */ - public static class NoSignature extends SOPGPException { - - public static final int EXIT_CODE = 3; - - public NoSignature() { - super("No verifiable signature found."); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Asymmetric algorithm unsupported (sop encrypt). - */ - public static class UnsupportedAsymmetricAlgo extends SOPGPException { - - public static final int EXIT_CODE = 13; - - public UnsupportedAsymmetricAlgo(String message, Throwable e) { - super(message, e); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Certificate not encryption capable (e,g, expired, revoked, unacceptable usage). - */ - public static class CertCannotEncrypt extends SOPGPException { - public static final int EXIT_CODE = 17; - - public CertCannotEncrypt(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Missing required argument. - */ - public static class MissingArg extends SOPGPException { - - public static final int EXIT_CODE = 19; - - public MissingArg(String s) { - super(s); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Incomplete verification instructions (sop decrypt). - */ - public static class IncompleteVerification extends SOPGPException { - - public static final int EXIT_CODE = 23; - - public IncompleteVerification(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Unable to decrypt (sop decrypt). - */ - public static class CannotDecrypt extends SOPGPException { - - public static final int EXIT_CODE = 29; - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Non-UTF-8 or otherwise unreliable password (sop encrypt). - */ - public static class PasswordNotHumanReadable extends SOPGPException { - - public static final int EXIT_CODE = 31; - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Unsupported option. - */ - public static class UnsupportedOption extends SOPGPException { - - public static final int EXIT_CODE = 37; - - public UnsupportedOption(String message) { - super(message); - } - - public UnsupportedOption(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Invalid data type (no secret key where KEYS expected, etc.). - */ - public static class BadData extends SOPGPException { - - public static final int EXIT_CODE = 41; - - public BadData(Throwable e) { - super(e); - } - - public BadData(String message, BadData badData) { - super(message, badData); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Non-Text input where text expected. - */ - public static class ExpectedText extends SOPGPException { - - public static final int EXIT_CODE = 53; - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Output file already exists. - */ - public static class OutputExists extends SOPGPException { - - public static final int EXIT_CODE = 59; - - public OutputExists(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Input file does not exist. - */ - public static class MissingInput extends SOPGPException { - - public static final int EXIT_CODE = 61; - - public MissingInput(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * A KEYS input is protected (locked) with a password, and sop cannot unlock it. - */ - public static class KeyIsProtected extends SOPGPException { - - public static final int EXIT_CODE = 67; - - public KeyIsProtected() { - super(); - } - - public KeyIsProtected(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Unsupported subcommand. - */ - public static class UnsupportedSubcommand extends SOPGPException { - - public static final int EXIT_CODE = 69; - - public UnsupportedSubcommand(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * An indirect parameter is a special designator (it starts with @), but sop does not know how to handle the prefix. - */ - public static class UnsupportedSpecialPrefix extends SOPGPException { - - public static final int EXIT_CODE = 71; - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * A indirect input parameter is a special designator (it starts with @), - * and a filename matching the designator is actually present. - */ - public static class AmbiguousInput extends SOPGPException { - - public static final int EXIT_CODE = 73; - - public AmbiguousInput(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Key not signature-capable (e.g. expired, revoked, unacceptable usage flags) - * (sop sign and sop encrypt with --sign-with). - */ - public static class KeyCannotSign extends SOPGPException { - - public static final int EXIT_CODE = 79; - - public KeyCannotSign() { - super(); - } - - public KeyCannotSign(String s, KeyCannotSign keyCannotSign) { - super(s, keyCannotSign); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } -} diff --git a/sop-java/src/main/java/sop/exception/package-info.java b/sop-java/src/main/java/sop/exception/package-info.java deleted file mode 100644 index 4abc562b..00000000 --- a/sop-java/src/main/java/sop/exception/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Exception classes. - */ -package sop.exception; diff --git a/sop-java/src/main/java/sop/operation/Armor.java b/sop-java/src/main/java/sop/operation/Armor.java deleted file mode 100644 index dea3257a..00000000 --- a/sop-java/src/main/java/sop/operation/Armor.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; - -import sop.Ready; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; - -public interface Armor { - - /** - * Overrides automatic detection of label. - * - * @param label armor label - * @return builder instance - */ - Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption; - - /** - * Armor the provided data. - * - * @param data input stream of unarmored OpenPGP data - * @return armored data - */ - Ready data(InputStream data) throws SOPGPException.BadData; - - /** - * Armor the provided data. - * - * @param data unarmored OpenPGP data - * @return armored data - */ - default Ready data(byte[] data) throws SOPGPException.BadData { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/java/sop/operation/Dearmor.java b/sop-java/src/main/java/sop/operation/Dearmor.java deleted file mode 100644 index 35eceb56..00000000 --- a/sop-java/src/main/java/sop/operation/Dearmor.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.exception.SOPGPException; - -public interface Dearmor { - - /** - * Dearmor armored OpenPGP data. - * - * @param data armored OpenPGP data - * @return input stream of unarmored data - */ - Ready data(InputStream data) throws SOPGPException.BadData, IOException; - - /** - * Dearmor armored OpenPGP data. - * - * @param data armored OpenPGP data - * @return input stream of unarmored data - */ - default Ready data(byte[] data) throws SOPGPException.BadData, IOException { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/java/sop/operation/Decrypt.java b/sop-java/src/main/java/sop/operation/Decrypt.java deleted file mode 100644 index 0811ac2d..00000000 --- a/sop-java/src/main/java/sop/operation/Decrypt.java +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Date; - -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.exception.SOPGPException; - -public interface Decrypt { - - /** - * Makes the SOP consider signatures before this date invalid. - * - * @param timestamp timestamp - * @return builder instance - */ - Decrypt verifyNotBefore(Date timestamp) - throws SOPGPException.UnsupportedOption; - - /** - * Makes the SOP consider signatures after this date invalid. - * - * @param timestamp timestamp - * @return builder instance - */ - Decrypt verifyNotAfter(Date timestamp) - throws SOPGPException.UnsupportedOption; - - /** - * Adds one or more verification cert. - * - * @param cert input stream containing the cert(s) - * @return builder instance - */ - Decrypt verifyWithCert(InputStream cert) - throws SOPGPException.BadData, - IOException; - - /** - * Adds one or more verification cert. - * - * @param cert byte array containing the cert(s) - * @return builder instance - */ - default Decrypt verifyWithCert(byte[] cert) - throws SOPGPException.BadData, IOException { - return verifyWithCert(new ByteArrayInputStream(cert)); - } - - /** - * Tries to decrypt with the given session key. - * - * @param sessionKey session key - * @return builder instance - */ - Decrypt withSessionKey(SessionKey sessionKey) - throws SOPGPException.UnsupportedOption; - - /** - * Tries to decrypt with the given password. - * - * @param password password - * @return builder instance - */ - Decrypt withPassword(String password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption; - - /** - * Adds one or more decryption key. - * - * @param key input stream containing the key(s) - * @return builder instance - */ - Decrypt withKey(InputStream key) - throws SOPGPException.KeyIsProtected, - SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo; - - /** - * Adds one or more decryption key. - * - * @param key byte array containing the key(s) - * @return builder instance - */ - default Decrypt withKey(byte[] key) - throws SOPGPException.KeyIsProtected, - SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo { - return withKey(new ByteArrayInputStream(key)); - } - - /** - * Decrypts the given ciphertext, returning verification results and plaintext. - * @param ciphertext ciphertext - * @return ready with result - */ - ReadyWithResult ciphertext(InputStream ciphertext) - throws SOPGPException.BadData, SOPGPException.MissingArg, SOPGPException.CannotDecrypt; - - /** - * Decrypts the given ciphertext, returning verification results and plaintext. - * @param ciphertext ciphertext - * @return ready with result - */ - default ReadyWithResult ciphertext(byte[] ciphertext) - throws SOPGPException.BadData, SOPGPException.MissingArg, SOPGPException.CannotDecrypt { - return ciphertext(new ByteArrayInputStream(ciphertext)); - } -} diff --git a/sop-java/src/main/java/sop/operation/DetachInbandSignatureAndMessage.java b/sop-java/src/main/java/sop/operation/DetachInbandSignatureAndMessage.java deleted file mode 100644 index 9e22258c..00000000 --- a/sop-java/src/main/java/sop/operation/DetachInbandSignatureAndMessage.java +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.ReadyWithResult; -import sop.Signatures; - -/** - * Split cleartext signed messages up into data and signatures. - */ -public interface DetachInbandSignatureAndMessage { - - /** - * Do not wrap the signatures in ASCII armor. - * @return builder - */ - DetachInbandSignatureAndMessage noArmor(); - - /** - * Detach the provided cleartext signed message from its signatures. - * - * @param messageInputStream input stream containing the signed message - * @return result containing the detached message - * @throws IOException in case of an IO error - */ - ReadyWithResult message(InputStream messageInputStream) throws IOException; - - /** - * Detach the provided cleartext signed message from its signatures. - * - * @param message byte array containing the signed message - * @return result containing the detached message - * @throws IOException in case of an IO error - */ - default ReadyWithResult message(byte[] message) throws IOException { - return message(new ByteArrayInputStream(message)); - } -} diff --git a/sop-java/src/main/java/sop/operation/Encrypt.java b/sop-java/src/main/java/sop/operation/Encrypt.java deleted file mode 100644 index 784c07a0..00000000 --- a/sop-java/src/main/java/sop/operation/Encrypt.java +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; - -public interface Encrypt { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - Encrypt noArmor(); - - /** - * Sets encryption mode. - * - * @param mode mode - * @return builder instance - */ - Encrypt mode(EncryptAs mode) - throws SOPGPException.UnsupportedOption; - - /** - * Adds the signer key. - * - * @param key input stream containing the encoded signer key - * @return builder instance - */ - Encrypt signWith(InputStream key) - throws SOPGPException.KeyIsProtected, - SOPGPException.KeyCannotSign, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData; - - /** - * Adds the signer key. - * - * @param key byte array containing the encoded signer key - * @return builder instance - */ - default Encrypt signWith(byte[] key) - throws SOPGPException.KeyIsProtected, - SOPGPException.KeyCannotSign, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData { - return signWith(new ByteArrayInputStream(key)); - } - - /** - * Encrypt with the given password. - * - * @param password password - * @return builder instance - */ - Encrypt withPassword(String password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption; - - /** - * Encrypt with the given cert. - * - * @param cert input stream containing the encoded cert. - * @return builder instance - */ - Encrypt withCert(InputStream cert) - throws SOPGPException.CertCannotEncrypt, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData; - - /** - * Encrypt with the given cert. - * - * @param cert byte array containing the encoded cert. - * @return builder instance - */ - default Encrypt withCert(byte[] cert) - throws SOPGPException.CertCannotEncrypt, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData { - return withCert(new ByteArrayInputStream(cert)); - } - - /** - * Encrypt the given data yielding the ciphertext. - * @param plaintext plaintext - * @return input stream containing the ciphertext - */ - Ready plaintext(InputStream plaintext) - throws IOException; - - /** - * Encrypt the given data yielding the ciphertext. - * @param plaintext plaintext - * @return input stream containing the ciphertext - */ - default Ready plaintext(byte[] plaintext) throws IOException { - return plaintext(new ByteArrayInputStream(plaintext)); - } -} diff --git a/sop-java/src/main/java/sop/operation/ExtractCert.java b/sop-java/src/main/java/sop/operation/ExtractCert.java deleted file mode 100644 index 32491111..00000000 --- a/sop-java/src/main/java/sop/operation/ExtractCert.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.exception.SOPGPException; - -public interface ExtractCert { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - ExtractCert noArmor(); - - /** - * Extract the cert(s) from the provided key(s). - * - * @param keyInputStream input stream containing the encoding of one or more OpenPGP keys - * @return result containing the encoding of the keys certs - */ - Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData; - - /** - * Extract the cert(s) from the provided key(s). - * - * @param key byte array containing the encoding of one or more OpenPGP key - * @return result containing the encoding of the keys certs - */ - default Ready key(byte[] key) throws IOException, SOPGPException.BadData { - return key(new ByteArrayInputStream(key)); - } -} diff --git a/sop-java/src/main/java/sop/operation/GenerateKey.java b/sop-java/src/main/java/sop/operation/GenerateKey.java deleted file mode 100644 index c652e84a..00000000 --- a/sop-java/src/main/java/sop/operation/GenerateKey.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.exception.SOPGPException; - -public interface GenerateKey { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - GenerateKey noArmor(); - - /** - * Adds a user-id. - * - * @param userId user-id - * @return builder instance - */ - GenerateKey userId(String userId); - - /** - * Generate the OpenPGP key and return it encoded as an {@link InputStream}. - * - * @return key - */ - Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo, IOException; -} diff --git a/sop-java/src/main/java/sop/operation/Sign.java b/sop-java/src/main/java/sop/operation/Sign.java deleted file mode 100644 index be518cde..00000000 --- a/sop-java/src/main/java/sop/operation/Sign.java +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.ReadyWithResult; -import sop.SigningResult; -import sop.enums.SignAs; -import sop.exception.SOPGPException; - -public interface Sign { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - Sign noArmor(); - - /** - * Sets the signature mode. - * Note: This method has to be called before {@link #key(InputStream)} is called. - * - * @param mode signature mode - * @return builder instance - */ - Sign mode(SignAs mode) throws SOPGPException.UnsupportedOption; - - /** - * Add one or more signing keys. - * - * @param key input stream containing encoded keys - * @return builder instance - */ - Sign key(InputStream key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException; - - /** - * Add one or more signing keys. - * - * @param key byte array containing encoded keys - * @return builder instance - */ - default Sign key(byte[] key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { - return key(new ByteArrayInputStream(key)); - } - - /** - * Signs data. - * - * @param data input stream containing data - * @return ready - */ - ReadyWithResult data(InputStream data) throws IOException, SOPGPException.ExpectedText; - - /** - * Signs data. - * - * @param data byte array containing data - * @return ready - */ - default ReadyWithResult data(byte[] data) throws IOException, SOPGPException.ExpectedText { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/java/sop/operation/Verify.java b/sop-java/src/main/java/sop/operation/Verify.java deleted file mode 100644 index 1bf9fe09..00000000 --- a/sop-java/src/main/java/sop/operation/Verify.java +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.Date; - -import sop.exception.SOPGPException; - -public interface Verify extends VerifySignatures { - - /** - * Makes the SOP implementation consider signatures before this date invalid. - * - * @param timestamp timestamp - * @return builder instance - */ - Verify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption; - - /** - * Makes the SOP implementation consider signatures after this date invalid. - * - * @param timestamp timestamp - * @return builder instance - */ - Verify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption; - - /** - * Add one or more verification cert. - * - * @param cert input stream containing the encoded certs - * @return builder instance - */ - Verify cert(InputStream cert) throws SOPGPException.BadData; - - /** - * Add one or more verification cert. - * - * @param cert byte array containing the encoded certs - * @return builder instance - */ - default Verify cert(byte[] cert) throws SOPGPException.BadData { - return cert(new ByteArrayInputStream(cert)); - } - - /** - * Provides the signatures. - * @param signatures input stream containing encoded, detached signatures. - * - * @return builder instance - */ - VerifySignatures signatures(InputStream signatures) throws SOPGPException.BadData; - - /** - * Provides the signatures. - * @param signatures byte array containing encoded, detached signatures. - * - * @return builder instance - */ - default VerifySignatures signatures(byte[] signatures) throws SOPGPException.BadData { - return signatures(new ByteArrayInputStream(signatures)); - } - -} diff --git a/sop-java/src/main/java/sop/operation/VerifySignatures.java b/sop-java/src/main/java/sop/operation/VerifySignatures.java deleted file mode 100644 index d41a8edd..00000000 --- a/sop-java/src/main/java/sop/operation/VerifySignatures.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -import sop.Verification; -import sop.exception.SOPGPException; - -public interface VerifySignatures { - - /** - * Provide the signed data (without signatures). - * - * @param data signed data - * @return list of signature verifications - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - List data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData; - - /** - * Provide the signed data (without signatures). - * - * @param data signed data - * @return list of signature verifications - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - default List data(byte[] data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/java/sop/operation/Version.java b/sop-java/src/main/java/sop/operation/Version.java deleted file mode 100644 index 0b50993f..00000000 --- a/sop-java/src/main/java/sop/operation/Version.java +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -public interface Version { - - /** - * Return the implementations name. - * e.g. "SOP", - * - * @return implementation name - */ - String getName(); - - /** - * Return the implementations short version string. - * e.g. "1.0" - * - * @return version string - */ - String getVersion(); - - /** - * Return version information about the used OpenPGP backend. - * e.g. "Bouncycastle 1.70" - * - * @return backend version string - */ - String getBackendVersion(); - - /** - * Return an extended version string containing multiple lines of version information. - * The first line MUST match the information produced by {@link #getName()} and {@link #getVersion()}, but the rest of the text - * has no defined structure. - * Example: - *
-     *     "SOP 1.0
-     *     Awesome PGP!
-     *     Using Bouncycastle 1.70
-     *     LibFoo 1.2.2
-     *     See https://pgp.example.org/sop/ for more information"
-     * 
- * - * @return extended version string - */ - String getExtendedVersion(); -} diff --git a/sop-java/src/main/java/sop/operation/package-info.java b/sop-java/src/main/java/sop/operation/package-info.java deleted file mode 100644 index dde4d5bb..00000000 --- a/sop-java/src/main/java/sop/operation/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Different cryptographic operations. - */ -package sop.operation; diff --git a/sop-java/src/main/java/sop/package-info.java b/sop-java/src/main/java/sop/package-info.java deleted file mode 100644 index 5ad4f528..00000000 --- a/sop-java/src/main/java/sop/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - */ -package sop; diff --git a/sop-java/src/main/java/sop/util/HexUtil.java b/sop-java/src/main/java/sop/util/HexUtil.java deleted file mode 100644 index 9b88f53d..00000000 --- a/sop-java/src/main/java/sop/util/HexUtil.java +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2021 Paul Schaub, @maybeWeCouldStealAVan, @Dave L. -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -public class HexUtil { - - private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); - - /** - * Encode a byte array to a hex string. - * - * @see - * How to convert a byte array to a hex string in Java? - * @param bytes bytes - * @return hex encoding - */ - public static String bytesToHex(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = HEX_ARRAY[v >>> 4]; - hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; - } - return new String(hexChars); - } - - /** - * Decode a hex string into a byte array. - * - * @see - * Convert a string representation of a hex dump to a byte array using Java? - * @param s hex string - * @return decoded byte array - */ - public static byte[] hexToBytes(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i + 1), 16)); - } - return data; - } -} diff --git a/sop-java/src/main/java/sop/util/Optional.java b/sop-java/src/main/java/sop/util/Optional.java deleted file mode 100644 index 00eb2012..00000000 --- a/sop-java/src/main/java/sop/util/Optional.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -/** - * Backport of java.util.Optional for older Android versions. - * - * @param item type - */ -public class Optional { - - private final T item; - - public Optional() { - this(null); - } - - public Optional(T item) { - this.item = item; - } - - public static Optional of(T item) { - if (item == null) { - throw new NullPointerException("Item cannot be null."); - } - return new Optional<>(item); - } - - public static Optional ofNullable(T item) { - return new Optional<>(item); - } - - public static Optional ofEmpty() { - return new Optional<>(null); - } - - public T get() { - return item; - } - - public boolean isPresent() { - return item != null; - } - - public boolean isEmpty() { - return item == null; - } -} diff --git a/sop-java/src/main/java/sop/util/ProxyOutputStream.java b/sop-java/src/main/java/sop/util/ProxyOutputStream.java deleted file mode 100644 index 0559e8f4..00000000 --- a/sop-java/src/main/java/sop/util/ProxyOutputStream.java +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -/** - * {@link OutputStream} that buffers data being written into it, until its underlying output stream is being replaced. - * At that point, first all the buffered data is being written to the underlying stream, followed by any successive - * data that may get written to the {@link ProxyOutputStream}. - * - * This class is useful if we need to provide an {@link OutputStream} at one point in time when the final - * target output stream is not yet known. - */ -public class ProxyOutputStream extends OutputStream { - - private final ByteArrayOutputStream buffer; - private OutputStream swapped; - - public ProxyOutputStream() { - this.buffer = new ByteArrayOutputStream(); - } - - public synchronized void replaceOutputStream(OutputStream underlying) throws IOException { - if (underlying == null) { - throw new NullPointerException("Underlying OutputStream cannot be null."); - } - this.swapped = underlying; - - byte[] bufferBytes = buffer.toByteArray(); - swapped.write(bufferBytes); - } - - @Override - public synchronized void write(byte[] b) throws IOException { - if (swapped == null) { - buffer.write(b); - } else { - swapped.write(b); - } - } - - @Override - public synchronized void write(byte[] b, int off, int len) throws IOException { - if (swapped == null) { - buffer.write(b, off, len); - } else { - swapped.write(b, off, len); - } - } - - @Override - public synchronized void flush() throws IOException { - buffer.flush(); - if (swapped != null) { - swapped.flush(); - } - } - - @Override - public synchronized void close() throws IOException { - buffer.close(); - if (swapped != null) { - swapped.close(); - } - } - - @Override - public synchronized void write(int i) throws IOException { - if (swapped == null) { - buffer.write(i); - } else { - swapped.write(i); - } - } -} diff --git a/sop-java/src/main/java/sop/util/UTCUtil.java b/sop-java/src/main/java/sop/util/UTCUtil.java deleted file mode 100644 index 8ef7e773..00000000 --- a/sop-java/src/main/java/sop/util/UTCUtil.java +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; - -/** - * Utility class to parse and format dates as ISO-8601 UTC timestamps. - */ -public class UTCUtil { - - public static final SimpleDateFormat UTC_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - public static final SimpleDateFormat[] UTC_PARSERS = new SimpleDateFormat[] { - UTC_FORMATTER, - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"), - new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'") - }; - - static { - for (SimpleDateFormat f : UTC_PARSERS) { - f.setTimeZone(TimeZone.getTimeZone("UTC")); - } - } - /** - * Parse an ISO-8601 UTC timestamp from a string. - * - * @param dateString string - * @return date - */ - public static Date parseUTCDate(String dateString) { - for (SimpleDateFormat parser : UTC_PARSERS) { - try { - return parser.parse(dateString); - } catch (ParseException e) { - // Try next parser - } - } - return null; - } - - /** - * Format a date as ISO-8601 UTC timestamp. - * - * @param date date - * @return timestamp string - */ - public static String formatUTCDate(Date date) { - return UTC_FORMATTER.format(date); - } -} diff --git a/sop-java/src/main/java/sop/util/package-info.java b/sop-java/src/main/java/sop/util/package-info.java deleted file mode 100644 index 3dd9fc19..00000000 --- a/sop-java/src/main/java/sop/util/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Utility classes. - */ -package sop.util; diff --git a/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java b/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java deleted file mode 100644 index 8ae1859f..00000000 --- a/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.Test; -import sop.ByteArrayAndResult; -import sop.Verification; - -public class ByteArrayAndResultTest { - - @Test - public void testCreationAndGetters() { - byte[] bytes = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); - List result = Collections.singletonList( - new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"), - "C90E6D36200A1B922A1509E77618196529AE5FF8", - "C4BC2DDB38CCE96485EBE9C2F20691179038E5C6") - ); - ByteArrayAndResult> bytesAndResult = new ByteArrayAndResult<>(bytes, result); - - assertArrayEquals(bytes, bytesAndResult.getBytes()); - assertEquals(result, bytesAndResult.getResult()); - } -} diff --git a/sop-java/src/test/java/sop/util/HexUtilTest.java b/sop-java/src/test/java/sop/util/HexUtilTest.java deleted file mode 100644 index 54fc21de..00000000 --- a/sop-java/src/test/java/sop/util/HexUtilTest.java +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.nio.charset.Charset; - -import org.junit.jupiter.api.Test; - -/** - * Test using some test vectors from RFC4648. - * - * @see RFC-4648 §10: Test Vectors - */ -public class HexUtilTest { - - @SuppressWarnings("CharsetObjectCanBeUsed") - private static final Charset ASCII = Charset.forName("US-ASCII"); - - @Test - public void emptyHexEncodeTest() { - assertHexEquals("", ""); - } - - @Test - public void encodeF() { - assertHexEquals("66", "f"); - } - - @Test - public void encodeFo() { - assertHexEquals("666F", "fo"); - } - - @Test - public void encodeFoo() { - assertHexEquals("666F6F", "foo"); - } - - @Test - public void encodeFoob() { - assertHexEquals("666F6F62", "foob"); - } - - @Test - public void encodeFooba() { - assertHexEquals("666F6F6261", "fooba"); - } - - @Test - public void encodeFoobar() { - assertHexEquals("666F6F626172", "foobar"); - } - - private void assertHexEquals(String hex, String ascii) { - assertEquals(hex, HexUtil.bytesToHex(ascii.getBytes(ASCII))); - assertArrayEquals(ascii.getBytes(ASCII), HexUtil.hexToBytes(hex)); - } -} diff --git a/sop-java/src/test/java/sop/util/MicAlgTest.java b/sop-java/src/test/java/sop/util/MicAlgTest.java deleted file mode 100644 index f720c85b..00000000 --- a/sop-java/src/test/java/sop/util/MicAlgTest.java +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.HashMap; -import java.util.Map; - -import org.junit.jupiter.api.Test; -import sop.MicAlg; - -public class MicAlgTest { - - @Test - public void constructorNullArgThrows() { - assertThrows(IllegalArgumentException.class, () -> new MicAlg(null)); - } - - @Test - public void emptyMicAlgIsEmptyString() { - MicAlg empty = MicAlg.empty(); - assertNotNull(empty.getMicAlg()); - assertTrue(empty.getMicAlg().isEmpty()); - } - - @Test - public void fromInvalidAlgorithmIdThrows() { - assertThrows(IllegalArgumentException.class, () -> MicAlg.fromHashAlgorithmId(-1)); - } - - @Test - public void fromHashAlgorithmIdsKnownAlgsMatch() { - Map knownAlgorithmMicalgs = new HashMap<>(); - knownAlgorithmMicalgs.put(1, "pgp-md5"); - knownAlgorithmMicalgs.put(2, "pgp-sha1"); - knownAlgorithmMicalgs.put(3, "pgp-ripemd160"); - knownAlgorithmMicalgs.put(8, "pgp-sha256"); - knownAlgorithmMicalgs.put(9, "pgp-sha384"); - knownAlgorithmMicalgs.put(10, "pgp-sha512"); - knownAlgorithmMicalgs.put(11, "pgp-sha224"); - - for (Integer id : knownAlgorithmMicalgs.keySet()) { - MicAlg micAlg = MicAlg.fromHashAlgorithmId(id); - assertEquals(knownAlgorithmMicalgs.get(id), micAlg.getMicAlg()); - } - } -} diff --git a/sop-java/src/test/java/sop/util/OptionalTest.java b/sop-java/src/test/java/sop/util/OptionalTest.java deleted file mode 100644 index 45900b73..00000000 --- a/sop-java/src/test/java/sop/util/OptionalTest.java +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -public class OptionalTest { - - @Test - public void testEmpty() { - Optional optional = new Optional<>(); - assertEmpty(optional); - } - - @Test - public void testArg() { - String string = "foo"; - Optional optional = new Optional<>(string); - assertFalse(optional.isEmpty()); - assertTrue(optional.isPresent()); - assertEquals(string, optional.get()); - } - - @Test - public void testOfEmpty() { - Optional optional = Optional.ofEmpty(); - assertEmpty(optional); - } - - @Test - public void testNullArg() { - Optional optional = new Optional<>(null); - assertEmpty(optional); - } - - @Test - public void testOfWithNullArgThrows() { - assertThrows(NullPointerException.class, () -> Optional.of(null)); - } - - @Test - public void testOf() { - String string = "Hello, World!"; - Optional optional = Optional.of(string); - assertFalse(optional.isEmpty()); - assertTrue(optional.isPresent()); - assertEquals(string, optional.get()); - } - - @Test - public void testOfNullableWithNull() { - Optional optional = Optional.ofNullable(null); - assertEmpty(optional); - } - - @Test - public void testOfNullableWithArg() { - Optional optional = Optional.ofNullable("bar"); - assertEquals("bar", optional.get()); - assertFalse(optional.isEmpty()); - assertTrue(optional.isPresent()); - } - - private void assertEmpty(Optional optional) { - assertTrue(optional.isEmpty()); - assertFalse(optional.isPresent()); - - assertNull(optional.get()); - } -} diff --git a/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java b/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java deleted file mode 100644 index 9d99fd4f..00000000 --- a/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -import org.junit.jupiter.api.Test; - -public class ProxyOutputStreamTest { - - @Test - public void replaceOutputStreamThrowsNPEForNull() { - ProxyOutputStream proxy = new ProxyOutputStream(); - assertThrows(NullPointerException.class, () -> proxy.replaceOutputStream(null)); - } - - @Test - public void testSwappingStreamPreservesWrittenBytes() throws IOException { - byte[] firstSection = "Foo\nBar\n".getBytes(StandardCharsets.UTF_8); - byte[] secondSection = "Baz\n".getBytes(StandardCharsets.UTF_8); - - ProxyOutputStream proxy = new ProxyOutputStream(); - proxy.write(firstSection); - - ByteArrayOutputStream swappedStream = new ByteArrayOutputStream(); - proxy.replaceOutputStream(swappedStream); - - proxy.write(secondSection); - proxy.close(); - - assertEquals("Foo\nBar\nBaz\n", swappedStream.toString()); - } -} diff --git a/sop-java/src/test/java/sop/util/ReadyTest.java b/sop-java/src/test/java/sop/util/ReadyTest.java deleted file mode 100644 index 07fa0903..00000000 --- a/sop-java/src/test/java/sop/util/ReadyTest.java +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; - -import org.junit.jupiter.api.Test; -import sop.Ready; - -public class ReadyTest { - - @Test - public void readyTest() throws IOException { - byte[] data = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); - Ready ready = new Ready() { - @Override - public void writeTo(OutputStream outputStream) throws IOException { - outputStream.write(data); - } - }; - - assertArrayEquals(data, ready.getBytes()); - } -} diff --git a/sop-java/src/test/java/sop/util/ReadyWithResultTest.java b/sop-java/src/test/java/sop/util/ReadyWithResultTest.java deleted file mode 100644 index 97841fa8..00000000 --- a/sop-java/src/test/java/sop/util/ReadyWithResultTest.java +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.Test; -import sop.ByteArrayAndResult; -import sop.ReadyWithResult; -import sop.Verification; -import sop.exception.SOPGPException; - -public class ReadyWithResultTest { - - @Test - public void testReadyWithResult() throws SOPGPException.NoSignature, IOException { - byte[] data = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); - List result = Collections.singletonList( - new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"), - "C90E6D36200A1B922A1509E77618196529AE5FF8", - "C4BC2DDB38CCE96485EBE9C2F20691179038E5C6") - ); - ReadyWithResult> readyWithResult = new ReadyWithResult>() { - @Override - public List writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { - outputStream.write(data); - return result; - } - }; - - ByteArrayAndResult> bytesAndResult = readyWithResult.toByteArrayAndResult(); - assertArrayEquals(data, bytesAndResult.getBytes()); - assertEquals(result, bytesAndResult.getResult()); - } -} diff --git a/sop-java/src/test/java/sop/util/SessionKeyTest.java b/sop-java/src/test/java/sop/util/SessionKeyTest.java deleted file mode 100644 index 2891d0d4..00000000 --- a/sop-java/src/test/java/sop/util/SessionKeyTest.java +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; -import sop.SessionKey; - -public class SessionKeyTest { - - @Test - public void fromStringTest() { - String string = "9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"; - SessionKey sessionKey = SessionKey.fromString(string); - assertEquals(string, sessionKey.toString()); - } - - @Test - public void toStringTest() { - SessionKey sessionKey = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); - assertEquals("9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD", sessionKey.toString()); - } - - @Test - public void equalsTest() { - SessionKey s1 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); - SessionKey s2 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); - SessionKey s3 = new SessionKey((byte) 4, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); - SessionKey s4 = new SessionKey((byte) 9, HexUtil.hexToBytes("19125CD57392BAB7037C7078359FCA4BEAF687F4025CBF9F7BCD8059CACC14FB")); - SessionKey s5 = new SessionKey((byte) 4, HexUtil.hexToBytes("19125CD57392BAB7037C7078359FCA4BEAF687F4025CBF9F7BCD8059CACC14FB")); - - assertEquals(s1, s1); - assertEquals(s1, s2); - assertEquals(s1.hashCode(), s2.hashCode()); - assertNotEquals(s1, s3); - assertNotEquals(s1.hashCode(), s3.hashCode()); - assertNotEquals(s1, s4); - assertNotEquals(s1.hashCode(), s4.hashCode()); - assertNotEquals(s4, s5); - assertNotEquals(s4.hashCode(), s5.hashCode()); - assertNotEquals(s1, null); - assertNotEquals(s1, "FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"); - } - - @Test - public void fromString_missingAlgorithmIdThrows() { - String missingAlgorithId = "FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"; - assertThrows(IllegalArgumentException.class, () -> SessionKey.fromString(missingAlgorithId)); - } - - @Test - public void fromString_wrongDivider() { - String semicolonDivider = "9;FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"; - assertThrows(IllegalArgumentException.class, () -> SessionKey.fromString(semicolonDivider)); - } -} diff --git a/sop-java/src/test/java/sop/util/SigningResultTest.java b/sop-java/src/test/java/sop/util/SigningResultTest.java deleted file mode 100644 index 0d35cdc1..00000000 --- a/sop-java/src/test/java/sop/util/SigningResultTest.java +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; -import sop.MicAlg; -import sop.SigningResult; - -public class SigningResultTest { - - @Test - public void basicBuilderTest() { - SigningResult result = SigningResult.builder() - .setMicAlg(MicAlg.fromHashAlgorithmId(10)) - .build(); - - assertEquals("pgp-sha512", result.getMicAlg().getMicAlg()); - } -} diff --git a/sop-java/src/test/java/sop/util/UTCUtilTest.java b/sop-java/src/test/java/sop/util/UTCUtilTest.java deleted file mode 100644 index 18de8176..00000000 --- a/sop-java/src/test/java/sop/util/UTCUtilTest.java +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -import java.util.Date; - -import org.junit.jupiter.api.Test; - -/** - * Test parsing some date examples from the stateless OpenPGP CLI spec. - * - * @see OpenPGP Stateless CLI §4.1. Date - */ -public class UTCUtilTest { - - @Test - public void parseExample1() { - String timestamp = "2019-10-29T12:11:04+00:00"; - Date date = UTCUtil.parseUTCDate(timestamp); - assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date)); - } - - @Test - public void parseExample2() { - String timestamp = "2019-10-24T23:48:29Z"; - Date date = UTCUtil.parseUTCDate(timestamp); - assertEquals("2019-10-24T23:48:29Z", UTCUtil.formatUTCDate(date)); - } - - @Test - public void parseExample3() { - String timestamp = "20191029T121104Z"; - Date date = UTCUtil.parseUTCDate(timestamp); - assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date)); - } - - @Test - public void invalidDateReturnsNull() { - String invalidTimestamp = "foobar"; - Date expectNull = UTCUtil.parseUTCDate(invalidTimestamp); - assertNull(expectNull); - } -} From 09c2a84ec1aaea0b1880671634ec9cf874f5c939 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Jan 2022 15:21:58 +0100 Subject: [PATCH 0029/1204] Add reuse header to sop-java*/readme files --- sop-java-picocli/README.md | 7 ++++++- sop-java/README.md | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/sop-java-picocli/README.md b/sop-java-picocli/README.md index 3c9234af..433bd576 100644 --- a/sop-java-picocli/README.md +++ b/sop-java-picocli/README.md @@ -1 +1,6 @@ -# [MOVED](https://github.com/pgpainless/sop-java/tree/master/sop-java-picocli) \ No newline at end of file + +# [MOVED](https://github.com/pgpainless/sop-java/tree/master/sop-java-picocli) diff --git a/sop-java/README.md b/sop-java/README.md index e10261b6..3e30ada9 100644 --- a/sop-java/README.md +++ b/sop-java/README.md @@ -1 +1,6 @@ -# [MOVED](https://github.com/pgpainless/sop-java/tree/master/sop-java) \ No newline at end of file + +# [MOVED](https://github.com/pgpainless/sop-java/tree/master/sop-java) From a7208e6c42be54d04daff6423d45f6d53e3ff12c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Jan 2022 15:23:49 +0100 Subject: [PATCH 0030/1204] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf32f122..798539e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ SPDX-License-Identifier: CC0-1.0 ## 1.0.2-SNAPSHOT - Update SOP implementation to specification revision 03 +- Move `sop-java` and `sop-java-picocli` modules to [its own repository](https://github.com/pgpainless/sop-java) - `OpenPGPV4Fingerprint`: Hex decode bytes in constructor - Add `ArmorUtils.toAsciiArmoredString()` for single key - Fix `ClassCastException` when retrieving `RevocationKey` subpackets from signatures From f4e574011ba9ae761157f28489f8457ce0319fce Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jan 2022 00:26:52 +0100 Subject: [PATCH 0031/1204] Add info about sop modules to root readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index d2c01e3f..697745cd 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,14 @@ PGPainless currently [*scores second place* on Sequoia-PGPs Interoperability Tes > > -Mario @ Cure53.de +## Get Started + +The very easiest way to start using OpenPGP on Java/Kotlin based systems is to use an implementation of [sop-java](https://github.com/pgpainless/sop-java). +`sop-java` defines a very stripped down API and is super easy to get started with. +Luckily PGPainless provides an implementation for the `sop-java` interface definitions in the form of [pgpainless-sop](pgpainless-sop/README.md). + +If you need more flexibility, directly using `pgpainless-core` is the way to go. + ## Features Most of PGPainless' features can be accessed directly from the `PGPainless` class. From ef02163d97bbb89ee314475d33df14ba34a95ec0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jan 2022 00:53:11 +0100 Subject: [PATCH 0032/1204] Remove dependency on picocli --- build.gradle | 1 - pgpainless-cli/build.gradle | 2 -- 2 files changed, 3 deletions(-) diff --git a/build.gradle b/build.gradle index af0936e1..07cb53d4 100644 --- a/build.gradle +++ b/build.gradle @@ -73,7 +73,6 @@ allprojects { slf4jVersion = '1.7.32' logbackVersion = '1.2.9' junitVersion = '5.8.2' - picocliVersion = '4.6.2' sopJavaVersion = '1.1.0' rootConfigDir = new File(rootDir, 'config') gitCommit = getGitCommit() diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle index 9db15a5b..3c8ab8f1 100644 --- a/pgpainless-cli/build.gradle +++ b/pgpainless-cli/build.gradle @@ -42,8 +42,6 @@ dependencies { implementation(project(":pgpainless-sop")) implementation "org.pgpainless:sop-java-picocli:$sopJavaVersion" - implementation "info.picocli:picocli:$picocliVersion" - // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' } From 020c0be8fb3f66d06f52237896a0e41837b5146c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jan 2022 01:00:00 +0100 Subject: [PATCH 0033/1204] Remove further traces of sop-java from the build script --- build.gradle | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 07cb53d4..9a1c6a8b 100644 --- a/build.gradle +++ b/build.gradle @@ -31,13 +31,14 @@ allprojects { apply plugin: 'checkstyle' // Only generate jar for submodules + // without this we would generate an empty pgpainless.jar for the project root // https://stackoverflow.com/a/25445035 jar { onlyIf { !sourceSets.main.allSource.files.isEmpty() } } - // For non-sop modules, enable android api compatibility check - if (it.name.equals('pgpainless-core') || it.name.equals('sop-java') || it.name.equals('pgpainless-sop')) { + // For library modules, enable android api compatibility check + if (it.name != 'pgpainless-cli') { // animalsniffer apply plugin: 'ru.vyarus.animalsniffer' dependencies { From 9b270197c2457cff8b5bea46629e5e33eab593e3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jan 2022 01:12:22 +0100 Subject: [PATCH 0034/1204] Add MIME StreamEncoding enum val --- .../java/org/pgpainless/algorithm/StreamEncoding.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java index 3ea9507b..d47304f6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java @@ -32,6 +32,17 @@ public enum StreamEncoding { */ UTF8(PGPLiteralData.UTF8), + /** + * The literal data packet contains a MIME message body part (RFC2045). + * Introduced in rfc4880-bis10. + * + * TODO: Replace 'm' with 'PGPLiteralData.MIME' once BC 1.71 gets released and contains our fix: + * https://github.com/bcgit/bc-java/pull/1088 + * + * @see RFC4880-bis10 + */ + MIME('m'), + /** * Early versions of PGP also defined a value of 'l' as a 'local' mode for machine-local conversions. * RFC 1991 [RFC1991] incorrectly stated this local mode flag as '1' (ASCII numeral one). From e55fad6078cf0e7a4c0ccfb1dd89253cf4ceedf9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jan 2022 01:28:15 +0100 Subject: [PATCH 0035/1204] Switch pgpainless-sop over to java-library plugin and api-depend on sop-java --- pgpainless-sop/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index 87e893b2..d6b97e64 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 plugins { - id 'java' + id 'java-library' } group 'org.pgpainless' @@ -20,7 +20,7 @@ dependencies { testImplementation "ch.qos.logback:logback-classic:$logbackVersion" implementation(project(":pgpainless-core")) - implementation "org.pgpainless:sop-java:$sopJavaVersion" + api "org.pgpainless:sop-java:$sopJavaVersion" } test { From 7aa48f458b4782b5b003ba47fad11c0f41f6406b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jan 2022 01:48:15 +0100 Subject: [PATCH 0036/1204] Fix pgpainless-cli:jar resolution of pgpainless-sop.jar --- pgpainless-cli/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle index 3c8ab8f1..869eedf7 100644 --- a/pgpainless-cli/build.gradle +++ b/pgpainless-cli/build.gradle @@ -72,4 +72,4 @@ run { } } -tasks."jar".dependsOn(":pgpainless-core:assemble") +tasks."jar".dependsOn(":pgpainless-core:assemble", ":pgpainless-sop:assemble") From 593a6cdb6595b1c6383d28ea2c0999244cc5b4c8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 15 Jan 2022 14:38:36 +0100 Subject: [PATCH 0037/1204] Document how to include pgpainless-sop in build scripts --- pgpainless-sop/README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 067f85c8..3de168c3 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -16,6 +16,33 @@ Implementation of the Stateless OpenPGP Protocol using PGPainless. This module implements `sop-java` using `pgpainless-core`. If your code depends on `sop-java`, this module can be used as a realization of those interfaces. +## Get started + +To start using pgpainless-sop in your code, include the following lines in your build script: +``` +// If you use Gradle +... +dependencies { + ... + implementation "org.pgpainless:pgpainless-sop:1.1.0" + ... +} + +// If you use Maven +... + + ... + + org.pgpainless + pgpainless-sop + 1.1.0 + + ... + +``` + +`pgpainless-sop` will transitively pull in its dependencies, such as `sop-java` and `pgpainless-core`. + ## Usage Examples ```java SOP sop = new SOPImpl(); From 059a38a0a20b8e93527584f49cd4239e18553008 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 5 Feb 2022 14:41:43 +0100 Subject: [PATCH 0038/1204] sop-java, sop-java-picocli placeholder readme: add links to codeberg --- sop-java-picocli/README.md | 4 +++- sop-java/README.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/sop-java-picocli/README.md b/sop-java-picocli/README.md index 433bd576..d67967b9 100644 --- a/sop-java-picocli/README.md +++ b/sop-java-picocli/README.md @@ -3,4 +3,6 @@ SPDX-FileCopyrightText: 2022 Paul Schaub SPDX-License-Identifier: Apache-2.0 --> -# [MOVED](https://github.com/pgpainless/sop-java/tree/master/sop-java-picocli) +# MOVED +* [Github](https://github.com/pgpainless/sop-java/tree/master/sop-java-picocli) +* [Codeberg](https://codeberg.org/PGPainless/sop-java/src/branch/master/sop-java-picocli) diff --git a/sop-java/README.md b/sop-java/README.md index 3e30ada9..dd59c615 100644 --- a/sop-java/README.md +++ b/sop-java/README.md @@ -3,4 +3,6 @@ SPDX-FileCopyrightText: 2022 Paul Schaub SPDX-License-Identifier: Apache-2.0 --> -# [MOVED](https://github.com/pgpainless/sop-java/tree/master/sop-java) +# MOVED +* [Github](https://github.com/pgpainless/sop-java/tree/master/sop-java) +* [Codeberg](https://codeberg.org/PGPainless/sop-java/src/branch/master/sop-java) From 94a9c84434a0f46002038ccf6d400d6f7dab8257 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 5 Feb 2022 14:48:40 +0100 Subject: [PATCH 0039/1204] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 798539e1..577c4743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ SPDX-License-Identifier: CC0-1.0 - `OpenPGPV4Fingerprint`: Hex decode bytes in constructor - Add `ArmorUtils.toAsciiArmoredString()` for single key - Fix `ClassCastException` when retrieving `RevocationKey` subpackets from signatures +- Fix `pgpainless-sop` gradle script + - it now automatically pulls in transitive dependencies ## 1.0.1 - Fix sourcing of preferred algorithms by primary user-id when key is located via key-id From 132981abfa290cc6c5dffaf4b3bef094b47ff00d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 5 Feb 2022 14:55:38 +0100 Subject: [PATCH 0040/1204] PGPainless 1.0.2 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 577c4743..cb711aa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.0.2-SNAPSHOT +## 1.0.2 - Update SOP implementation to specification revision 03 - Move `sop-java` and `sop-java-picocli` modules to [its own repository](https://github.com/pgpainless/sop-java) - `OpenPGPV4Fingerprint`: Hex decode bytes in constructor diff --git a/README.md b/README.md index 697745cd..cf732a2b 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.0.1' + implementation 'org.pgpainless:pgpainless-core:1.0.2' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 3de168c3..d4acd83c 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -24,7 +24,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.1.0" + implementation "org.pgpainless:pgpainless-sop:1.0.2" ... } @@ -35,7 +35,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.1.0 + 1.0.2 ... diff --git a/version.gradle b/version.gradle index 7c12e305..b07a09f3 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.0.2' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From 9b5cc2da5ad6b52202229dfcd3587d2e7066b96c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 5 Feb 2022 15:05:07 +0100 Subject: [PATCH 0041/1204] PGPainless-1.0.3-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index b07a09f3..9ae75440 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.0.2' - isSnapshot = false + shortVersion = '1.0.3' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From 458b4f1f783522e7548f26405f6bed535bcfb812 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Feb 2022 13:58:50 +0100 Subject: [PATCH 0042/1204] Fix detection of unarmored data in detached signature verification --- .../DecryptionStreamFactory.java | 37 ++++++---- .../exception/FinalIOException.java | 17 +++++ .../VerifyDetachedSignatureTest.java | 71 +++++++++++++++++++ 3 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 6d7cb069..bcc9099e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -46,6 +46,7 @@ import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.exception.FinalIOException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.exception.MissingLiteralDataException; @@ -154,7 +155,7 @@ public final class DecryptionStreamFactory { objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); // Parse OpenPGP message inputStream = processPGPPackets(objectFactory, 1); - } catch (EOFException e) { + } catch (EOFException | FinalIOException e) { throw e; } catch (MissingLiteralDataException e) { // Not an OpenPGP message. @@ -174,7 +175,7 @@ public final class DecryptionStreamFactory { objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); inputStream = wrapInVerifySignatureStream(bufferedIn, objectFactory); } else { - throw e; + throw new FinalIOException(e); } } @@ -195,18 +196,28 @@ public final class DecryptionStreamFactory { throw new PGPException("Maximum depth of nested packages exceeded."); } Object nextPgpObject; - while ((nextPgpObject = objectFactory.nextObject()) != null) { - if (nextPgpObject instanceof PGPEncryptedDataList) { - return processPGPEncryptedDataList((PGPEncryptedDataList) nextPgpObject, depth); + try { + while ((nextPgpObject = objectFactory.nextObject()) != null) { + if (nextPgpObject instanceof PGPEncryptedDataList) { + return processPGPEncryptedDataList((PGPEncryptedDataList) nextPgpObject, depth); + } + if (nextPgpObject instanceof PGPCompressedData) { + return processPGPCompressedData((PGPCompressedData) nextPgpObject, depth); + } + if (nextPgpObject instanceof PGPOnePassSignatureList) { + return processOnePassSignatureList(objectFactory, (PGPOnePassSignatureList) nextPgpObject, depth); + } + if (nextPgpObject instanceof PGPLiteralData) { + return processPGPLiteralData(objectFactory, (PGPLiteralData) nextPgpObject, depth); + } } - if (nextPgpObject instanceof PGPCompressedData) { - return processPGPCompressedData((PGPCompressedData) nextPgpObject, depth); - } - if (nextPgpObject instanceof PGPOnePassSignatureList) { - return processOnePassSignatureList(objectFactory, (PGPOnePassSignatureList) nextPgpObject, depth); - } - if (nextPgpObject instanceof PGPLiteralData) { - return processPGPLiteralData(objectFactory, (PGPLiteralData) nextPgpObject, depth); + } catch (FinalIOException e) { + throw e; + } catch (IOException e) { + if (depth == 1 && e.getMessage().contains("unknown object in stream:")) { + throw new MissingLiteralDataException("No Literal Data Packet found."); + } else { + throw new FinalIOException(e); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java new file mode 100644 index 00000000..6b6f86de --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception; + +import java.io.IOException; + +/** + * Wrapper for {@link IOException} indicating that we need to throw this exception up. + */ +public class FinalIOException extends IOException { + + public FinalIOException(IOException e) { + super(e); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java new file mode 100644 index 00000000..01bc547d --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; + +public class VerifyDetachedSignatureTest { + + @Test + public void verify() throws PGPException, IOException { + String signedContent = "Content-Type: multipart/mixed; boundary=\"OSR6TONWKJD9dgyc2XH5AQPNnAs7pdg1t\"\n" + + "\n" + + "--OSR6TONWKJD9dgyc2XH5AQPNnAs7pdg1t\n" + + "Content-Type: text/plain; charset=utf-8\n" + + "Content-Transfer-Encoding: quoted-printable\n" + + "Content-Language: en-US\n" + + "\n" + + "NOT encrypted + signed(detached)\n" + + "\n" + + "\n" + + "\n" + + "--OSR6TONWKJD9dgyc2XH5AQPNnAs7pdg1t--\n"; + String signature = "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "iHUEARYIAB0WIQTBZCjWAcs5N4nPYdTDIInNavjWzgUCYgKPzAAKCRDDIInNavjW\n" + + "zmdoAP0TdFt1OWqosHhXxt2hNYqZQMc6bgQRpJNL029nRyzkPAD/SoYJ4T+aYEhw\n" + + "11qrbXloqkr0G3QaA6/zk31RPMI/bgI=\n" + + "=o5Ze\n" + + "-----END PGP SIGNATURE-----\n"; + String pubkey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "\n" + + "mDMEYIucWBYJKwYBBAHaRw8BAQdAew+8mzMWyf3+Pfy49qa60uKV6e5os7de4TdZ\n" + + "ceAWUq+0F2RlbmJvbmQ3QGZsb3djcnlwdC50ZXN0iHgEExYKACAFAmCLnFgCGwMF\n" + + "FgIDAQAECwkIBwUVCgkICwIeAQIZAQAKCRDDIInNavjWzm3JAQCgFgCEyD58iEa/\n" + + "Rw/DYNoQNoZC1lhw1bxBiOcIbtkdBgEAsDFZu3TBavOMKI7KW+vfMBHtRVbkMNpv\n" + + "unaAldoabgO4OARgi5xYEgorBgEEAZdVAQUBAQdAB1/Mrq5JGYim4KqGTSK4OESQ\n" + + "UwPgK56q0yrkiU9WgyYDAQgHiHUEGBYKAB0FAmCLnFgCGwwFFgIDAQAECwkIBwUV\n" + + "CgkICwIeAQAKCRDDIInNavjWzjMgAQCU+R1fItqdY6lt9jXUqipmXuqVaEFPwNA8\n" + + "YJ1rIwDwVQEAyUc8162KWzA2iQB5akwLwNr/pLDDtOWwhLUkrBb3mAc=\n" + + "=pXF6\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + + DecryptionStream verifier = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(signedContent.getBytes(StandardCharsets.UTF_8))) + .withOptions( + new ConsumerOptions() + .addVerificationOfDetachedSignatures(new ByteArrayInputStream(signature.getBytes(StandardCharsets.UTF_8))) + .addVerificationCerts(PGPainless.readKeyRing().keyRingCollection(pubkey, true).getPgpPublicKeyRingCollection()) + .setMultiPassStrategy(new InMemoryMultiPassStrategy()) + ); + + Streams.drain(verifier); + verifier.close(); + OpenPgpMetadata metadata = verifier.getResult(); + assertTrue(metadata.isVerified()); + } +} From 09c091bfeaadda7c2859f1bdd4c8ba4b3b874657 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Feb 2022 14:09:12 +0100 Subject: [PATCH 0043/1204] PGPainless 1.0.3 --- CHANGELOG.md | 3 +++ README.md | 2 +- version.gradle | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb711aa2..c502e989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.0.3 +- Fix detection of unarmored data in signature verification + ## 1.0.2 - Update SOP implementation to specification revision 03 - Move `sop-java` and `sop-java-picocli` modules to [its own repository](https://github.com/pgpainless/sop-java) diff --git a/README.md b/README.md index cf732a2b..dd4f1374 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.0.2' + implementation 'org.pgpainless:pgpainless-core:1.0.3' } ``` diff --git a/version.gradle b/version.gradle index 9ae75440..73daad1d 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.0.3' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From e276507b653daad0673de1007a327a66261019bc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Feb 2022 14:11:11 +0100 Subject: [PATCH 0044/1204] PGPainless-1.0.4-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 73daad1d..bf9c1eee 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.0.3' - isSnapshot = false + shortVersion = '1.0.4' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From b33885c268b9b255abf0fb3c6e549e16b8182832 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Feb 2022 14:19:46 +0100 Subject: [PATCH 0045/1204] Remove accidental marking of buffered stream in PGPUtilWrapper --- .../src/main/java/org/pgpainless/util/PGPUtilWrapper.java | 1 - 1 file changed, 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/PGPUtilWrapper.java b/pgpainless-core/src/main/java/org/pgpainless/util/PGPUtilWrapper.java index c01dd03d..dd95a3bc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/PGPUtilWrapper.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/PGPUtilWrapper.java @@ -27,7 +27,6 @@ public final class PGPUtilWrapper { * @throws IOException in case of an io error which is unrelated to base64 encoding */ public static InputStream getDecoderStream(BufferedInputStream buf) throws IOException { - buf.mark(512); try { return PGPUtil.getDecoderStream(buf); } catch (IOException e) { From f3cf3456abd1ad57a33018eaffaa06bec9802265 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Feb 2022 14:21:12 +0100 Subject: [PATCH 0046/1204] ConsumerOptions.setIsCleartextSigned -> return this --- .../pgpainless/decryption_verification/ConsumerOptions.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index 436d9356..66b5c4c8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -351,8 +351,9 @@ public class ConsumerOptions { * INTERNAL method to mark cleartext signed messages. * Do not call this manually. */ - public void setIsCleartextSigned() { + public ConsumerOptions setIsCleartextSigned() { this.cleartextSigned = true; + return this; } /** From e8da3b30d84b5ea60133d3d41196b0b31994489b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Feb 2022 14:21:28 +0100 Subject: [PATCH 0047/1204] Yet another patch for ASCII armor detection -.- --- .../DecryptionStreamFactory.java | 6 +- .../VerifyDetachedSignatureTest.java | 77 ++++++++++++++++++- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index bcc9099e..daf94a9a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -172,8 +172,7 @@ public final class DecryptionStreamFactory { LOGGER.debug("The message is apparently not armored."); bufferedIn.reset(); decoderStream = bufferedIn; - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); - inputStream = wrapInVerifySignatureStream(bufferedIn, objectFactory); + inputStream = wrapInVerifySignatureStream(bufferedIn, null); } else { throw new FinalIOException(e); } @@ -214,6 +213,9 @@ public final class DecryptionStreamFactory { } catch (FinalIOException e) { throw e; } catch (IOException e) { + if (depth == 1 && e.getMessage().contains("invalid armor")) { + throw e; + } if (depth == 1 && e.getMessage().contains("unknown object in stream:")) { throw new MissingLiteralDataException("No Literal Data Packet found."); } else { diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java index 01bc547d..fa1427d3 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java @@ -19,7 +19,7 @@ import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMulti public class VerifyDetachedSignatureTest { @Test - public void verify() throws PGPException, IOException { + public void test1() throws PGPException, IOException { String signedContent = "Content-Type: multipart/mixed; boundary=\"OSR6TONWKJD9dgyc2XH5AQPNnAs7pdg1t\"\n" + "\n" + "--OSR6TONWKJD9dgyc2XH5AQPNnAs7pdg1t\n" + @@ -68,4 +68,79 @@ public class VerifyDetachedSignatureTest { OpenPgpMetadata metadata = verifier.getResult(); assertTrue(metadata.isVerified()); } + + @Test + public void test2() throws PGPException, IOException { + String signedContent = "Content-Type: multipart/mixed; boundary=\"------------26m0wPaTDf7nRDIftnMj4qjE\";\r\n" + + " protected-headers=\"v1\"\r\n" + + "From: Denys \r\n" + + "To: default@flowcrypt.test\r\n" + + "Message-ID: \r\n" + + "Subject: Signed + pub key\r\n" + + "\r\n" + + "--------------26m0wPaTDf7nRDIftnMj4qjE\r\n" + + "Content-Type: multipart/mixed; boundary=\"------------RQxi6oNuQI1n8MnuNglORR5s\"\r\n" + + "\r\n" + + "--------------RQxi6oNuQI1n8MnuNglORR5s\r\n" + + "Content-Type: text/plain; charset=UTF-8; format=flowed\r\n" + + "Content-Transfer-Encoding: base64\r\n" + + "\r\n" + + "U29tZSBpbXBvcnRhbnQgdGV4dA0KDQo=\r\n" + + "--------------RQxi6oNuQI1n8MnuNglORR5s\r\n" + + "Content-Type: application/pgp-keys; name=\"OpenPGP_0xC32089CD6AF8D6CE.asc\"\r\n" + + "Content-Disposition: attachment; filename=\"OpenPGP_0xC32089CD6AF8D6CE.asc\"\r\n" + + "Content-Description: OpenPGP public key\r\n" + + "Content-Transfer-Encoding: quoted-printable\r\n" + + "\r\n" + + "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n" + + "\r\n" + + "xjMEYIucWBYJKwYBBAHaRw8BAQdAew+8mzMWyf3+Pfy49qa60uKV6e5os7de4TdZ\r\n" + + "ceAWUq/NF2RlbmJvbmQ3QGZsb3djcnlwdC50ZXN0wngEExYKACAFAmCLnFgCGwMF\r\n" + + "FgIDAQAECwkIBwUVCgkICwIeAQIZAQAKCRDDIInNavjWzm3JAQCgFgCEyD58iEa/\r\n" + + "Rw/DYNoQNoZC1lhw1bxBiOcIbtkdBgEAsDFZu3TBavOMKI7KW+vfMBHtRVbkMNpv\r\n" + + "unaAldoabgPOOARgi5xYEgorBgEEAZdVAQUBAQdAB1/Mrq5JGYim4KqGTSK4OESQ\r\n" + + "UwPgK56q0yrkiU9WgyYDAQgHwnUEGBYKAB0FAmCLnFgCGwwFFgIDAQAECwkIBwUV\r\n" + + "CgkICwIeAQAKCRDDIInNavjWzjMgAQCU+R1fItqdY6lt9jXUqipmXuqVaEFPwNA8\r\n" + + "YJ1rIwDwVQEAyUc8162KWzA2iQB5akwLwNr/pLDDtOWwhLUkrBb3mAc=3D\r\n" + + "=3DyJxA\r\n" + + "-----END PGP PUBLIC KEY BLOCK-----\r\n" + + "\r\n" + + "--------------RQxi6oNuQI1n8MnuNglORR5s--\r\n" + + "\r\n" + + "--------------26m0wPaTDf7nRDIftnMj4qjE--\r\n"; + String signature = "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wnsEABYIACMWIQTBZCjWAcs5N4nPYdTDIInNavjWzgUCYguNRQUDAAAAAAAKCRDDIInNavjWzoxf\n" + + "AQCOCu6bityLBbY1MPF+smwYLjkJvzEHf+ErXC7KkI4mnAEAn7FPPOzJAwWENv8a//0zg4P9Ymdr\n" + + "uyp1EJ1tsavXRQA=\n" + + "=K5yW\n" + + "-----END PGP SIGNATURE-----\n"; + String pubkey = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "\n" + + "mDMEYIucWBYJKwYBBAHaRw8BAQdAew+8mzMWyf3+Pfy49qa60uKV6e5os7de4TdZ\n" + + "ceAWUq+0F2RlbmJvbmQ3QGZsb3djcnlwdC50ZXN0iHgEExYKACAFAmCLnFgCGwMF\n" + + "FgIDAQAECwkIBwUVCgkICwIeAQIZAQAKCRDDIInNavjWzm3JAQCgFgCEyD58iEa/\n" + + "Rw/DYNoQNoZC1lhw1bxBiOcIbtkdBgEAsDFZu3TBavOMKI7KW+vfMBHtRVbkMNpv\n" + + "unaAldoabgO4OARgi5xYEgorBgEEAZdVAQUBAQdAB1/Mrq5JGYim4KqGTSK4OESQ\n" + + "UwPgK56q0yrkiU9WgyYDAQgHiHUEGBYKAB0FAmCLnFgCGwwFFgIDAQAECwkIBwUV\n" + + "CgkICwIeAQAKCRDDIInNavjWzjMgAQCU+R1fItqdY6lt9jXUqipmXuqVaEFPwNA8\n" + + "YJ1rIwDwVQEAyUc8162KWzA2iQB5akwLwNr/pLDDtOWwhLUkrBb3mAc=\n" + + "=pXF6\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + DecryptionStream verifier = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(signedContent.getBytes(StandardCharsets.UTF_8))) + .withOptions( + new ConsumerOptions() + .addVerificationOfDetachedSignatures(new ByteArrayInputStream(signature.getBytes(StandardCharsets.UTF_8))) + .addVerificationCerts(PGPainless.readKeyRing().keyRingCollection(pubkey, true).getPgpPublicKeyRingCollection()) + .setMultiPassStrategy(new InMemoryMultiPassStrategy()) + ); + + Streams.drain(verifier); + verifier.close(); + OpenPgpMetadata metadata = verifier.getResult(); + assertTrue(metadata.isVerified()); + } } From bb9e3e89b16aac97b9455b4b39b7cdfd01bf431e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Feb 2022 14:25:45 +0100 Subject: [PATCH 0048/1204] PGPainless 1.0.4 --- CHANGELOG.md | 3 +++ README.md | 2 +- version.gradle | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c502e989..fb76a07a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.0.4 +- Yet another patch for faulty ASCII armor detection 😒 + ## 1.0.3 - Fix detection of unarmored data in signature verification diff --git a/README.md b/README.md index dd4f1374..3e726b82 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.0.3' + implementation 'org.pgpainless:pgpainless-core:1.0.4' } ``` diff --git a/version.gradle b/version.gradle index bf9c1eee..80cba090 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.0.4' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From 5b4018d2f095687308b60c797bce9caefbdeed20 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Feb 2022 14:27:38 +0100 Subject: [PATCH 0049/1204] PGPainless-1.0.5-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 80cba090..1dfd0e29 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.0.4' - isSnapshot = false + shortVersion = '1.0.5' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From c405a9999432329dd0a2229950db37f834768fc0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Feb 2022 00:13:35 +0100 Subject: [PATCH 0050/1204] Bump sop-java to 1.2.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9a1c6a8b..b0881e63 100644 --- a/build.gradle +++ b/build.gradle @@ -74,7 +74,7 @@ allprojects { slf4jVersion = '1.7.32' logbackVersion = '1.2.9' junitVersion = '5.8.2' - sopJavaVersion = '1.1.0' + sopJavaVersion = '1.2.0' rootConfigDir = new File(rootDir, 'config') gitCommit = getGitCommit() isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI')) From 2b9d92ed8996bac13720e4015f7ff16d892a73ba Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Feb 2022 10:50:36 +0100 Subject: [PATCH 0051/1204] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb76a07a..aabeb14c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.1.0-SNAPSHOT +- `pgpainless-sop`: Update `sop-java` to version 1.2.0 + - Treat passwords and session keys as indirect parameters + ## 1.0.4 - Yet another patch for faulty ASCII armor detection 😒 From 36c5ec8a283471d1cd20aef77f581cd1819879ae Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Feb 2022 14:23:19 +0100 Subject: [PATCH 0052/1204] Host javadoc on javadoc.io --- README.md | 2 +- pgpainless-cli/README.md | 2 ++ pgpainless-core/README.md | 5 ++++- pgpainless-sop/README.md | 3 +-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3e726b82..b55ee7f8 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ SPDX-License-Identifier: Apache-2.0 [![Travis (.com)](https://travis-ci.com/pgpainless/pgpainless.svg?branch=master)](https://travis-ci.com/pgpainless/pgpainless) [![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-core)](https://search.maven.org/artifact/org.pgpainless/pgpainless-core) [![Coverage Status](https://coveralls.io/repos/github/pgpainless/pgpainless/badge.svg?branch=master)](https://coveralls.io/github/pgpainless/pgpainless?branch=master) -[![JavaDoc](https://badgen.net/badge/javadoc/yes/green)](https://pgpainless.org/releases/latest/javadoc/) [![Interoperability Test-Suite](https://badgen.net/badge/Sequoia%20Test%20Suite/%232/green)](https://tests.sequoia-pgp.org/) [![PGP](https://img.shields.io/badge/pgp-A027%20DB2F%203E1E%20118A-blue)](https://keyoxide.org/7F9116FEA90A5983936C7CFAA027DB2F3E1E118A) [![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/pgpainless)](https://api.reuse.software/info/github.com/pgpainless/pgpainless) + ## About PGPainless aims to make using OpenPGP in Java projects as simple as possible. diff --git a/pgpainless-cli/README.md b/pgpainless-cli/README.md index 4f691f56..d1d72afc 100644 --- a/pgpainless-cli/README.md +++ b/pgpainless-cli/README.md @@ -6,6 +6,8 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless-CLI +[![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-cli/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-cli) + PGPainless-CLI is an implementation of the [Stateless OpenPGP Command Line Interface](https://tools.ietf.org/html/draft-dkg-openpgp-stateless-cli-01) specification based on PGPainless. It plugs `pgpainless-sop` into `sop-java-picocli`. diff --git a/pgpainless-core/README.md b/pgpainless-core/README.md index 091722b8..d7b0a036 100644 --- a/pgpainless-core/README.md +++ b/pgpainless-core/README.md @@ -6,6 +6,9 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless-Core +[![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-core/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-core) +[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-core)](https://search.maven.org/artifact/org.pgpainless/pgpainless-core) + Wrapper around Bouncycastle's OpenPGP implementation. ## Protection Against Attacks @@ -50,4 +53,4 @@ It is therefore responsibility of the consumer to ensure that an attacker on the It is highly advised to store both secret and public keys in a secure key storage which protects against modifications. Furthermore, PGPainless cannot verify key authenticity, so it is up to the application that uses PGPainless to check, -if a key really belongs to a certain user. \ No newline at end of file +if a key really belongs to a certain user. diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index d4acd83c..c20b58c4 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -8,8 +8,7 @@ SPDX-License-Identifier: Apache-2.0 [![Spec Revision: 3](https://img.shields.io/badge/Spec%20Revision-3-blue)](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03) [![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-sop)](https://search.maven.org/artifact/org.pgpainless/pgpainless-sop) -[![JavaDoc](https://badgen.net/badge/javadoc/yes/green)](https://pgpainless.org/releases/latest/javadoc/org/pgpainless/sop/package-summary.html) -[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/pgpainless)](https://api.reuse.software/info/github.com/pgpainless/pgpainless) +[![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-sop/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-sop) Implementation of the Stateless OpenPGP Protocol using PGPainless. From 66d81067b51746a4ca0ee6bd7ed65be0d66abc71 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Feb 2022 14:39:11 +0100 Subject: [PATCH 0053/1204] Update README javadoc link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b55ee7f8..4e505ce6 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ If you need more flexibility, directly using `pgpainless-core` is the way to go. Most of PGPainless' features can be accessed directly from the `PGPainless` class. If you want to get started, this class is your friend :) -For further details you should check out the [javadoc](https://pgpainless.org/releases/latest/javadoc/)! +For further details you should check out the [javadoc](https://javadoc.io/doc/org.pgpainless/pgpainless-core)! ### Handle Keys Reading keys from ASCII armored strings or from binary files is easy: From b31742f215d363dac3225ab8daa4ae34eb355841 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Feb 2022 15:00:42 +0100 Subject: [PATCH 0054/1204] PGPainless 1.1.0 --- CHANGELOG.md | 3 ++- README.md | 2 +- version.gradle | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aabeb14c..5e3c57bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,10 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.1.0-SNAPSHOT +## 1.1.0 - `pgpainless-sop`: Update `sop-java` to version 1.2.0 - Treat passwords and session keys as indirect parameters + This means they are no longer treated as string input, but pointers to files or env variables ## 1.0.4 - Yet another patch for faulty ASCII armor detection 😒 diff --git a/README.md b/README.md index 4e505ce6..d3713423 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.0.4' + implementation 'org.pgpainless:pgpainless-core:1.1.0' } ``` diff --git a/version.gradle b/version.gradle index 1dfd0e29..3377af2c 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.0.5' - isSnapshot = true + shortVersion = '1.1.0' + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From 8edd0c6a148fc4041df7bbfec696db59e691328b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Feb 2022 15:03:50 +0100 Subject: [PATCH 0055/1204] PGPainless-1.1.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 3377af2c..e88ae14d 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.1.0' - isSnapshot = false + shortVersion = '1.1.1' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From 5e48a5a786eee433ee51a6c9a664b335b288ec84 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Feb 2022 18:44:58 +0100 Subject: [PATCH 0056/1204] Update SECURITY.md --- SECURITY.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 2448191a..59e9adbf 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -13,9 +13,10 @@ Use this section to tell people about which versions of your project are currently being supported with security updates. | Version | Supported | -| ------- | ------------------ | -| 0.2.x | :white_check_mark: | -| < 0.2.0 | :x: | +|---------| ------------------ | +| 1.1.X | :white_check_mark: | +| 1.0.X | :white_check_mark: | +| < 1.0.0 | :x: | ## Reporting a Vulnerability @@ -23,3 +24,11 @@ If you find a security relevant vulnerability inside PGPainless, please let me k [Here](https://keyoxide.org/7F9116FEA90A5983936C7CFAA027DB2F3E1E118A) you can find my OpenPGP key to email me confidentially. Valid security issues will be fixed ASAP. + +## Audits + +### Cure53 - FLO-04 +PGPainless has received a security audit by [cure53.de](https://cure53.de) in late 2021. +The [penetrationj test and audit](https://cure53.de/pentest-report_pgpainless.pdf) covered PGPainless +release candidate 1.0.0-rc6. +Security fixes for discovered flaws were deployed before the final 1.0.0 release. \ No newline at end of file From 08a3f3e8b041f173d7da7234b9c2228151b9a6dc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Feb 2022 13:50:45 +0100 Subject: [PATCH 0057/1204] s/Bouncycastle/Bouncy Castle --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d3713423..872d1549 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,10 @@ PGPainless aims to make using OpenPGP in Java projects as simple as possible. It does so by introducing an intuitive Builder structure, which allows easy setup of encryption/decryption operations, as well as straight forward key generation. -PGPainless is based around the Bouncycastle java library and can be used on Android down to API level 10. -It can be configured to either use the Java Cryptographic Engine (JCE), or Bouncycastles lightweight reimplementation. +PGPainless is based around the Bouncy Castle java library and can be used on Android down to API level 10. +It can be configured to either use the Java Cryptographic Engine (JCE), or Bouncy Castles lightweight reimplementation. -While signature verification in Bouncycastle is limited to signature correctness, PGPainless goes much further. +While signature verification in Bouncy Castle is limited to signature correctness, PGPainless goes much further. It also checks if signing subkeys are properly bound to their primary key, if keys are expired or revoked, as well as if keys are allowed to create signatures in the first place. From a3f9311d9a9bb080c506af6d08f28d93072c9bfe Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 19 Feb 2022 14:48:17 +0100 Subject: [PATCH 0058/1204] Add some comments to messy DecryptionStreamFactory code --- .../decryption_verification/DecryptionStreamFactory.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index daf94a9a..575a5e5f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -133,6 +133,11 @@ public final class DecryptionStreamFactory { InputStream decoderStream; PGPObjectFactory objectFactory; + // Workaround for cleartext signed data + // If we below threw a WrongConsumingMethodException, the CleartextSignatureProcessor will prepare the + // message for us and will set options.isCleartextSigned() to true. + // That way we can process long messages without running the issue of resetting the bufferedInputStream + // to invalid marks. if (options.isCleartextSigned()) { inputStream = wrapInVerifySignatureStream(bufferedIn, null); return new DecryptionStream(inputStream, resultBuilder, integrityProtectedEncryptedInputStream, @@ -146,6 +151,9 @@ public final class DecryptionStreamFactory { if (decoderStream instanceof ArmoredInputStream) { ArmoredInputStream armor = (ArmoredInputStream) decoderStream; + // Cleartext Signed Message + // Throw a WrongConsumingMethodException to delegate preparation (extraction of signatures) + // to the CleartextSignatureProcessor which will call us again (see comment above) if (armor.isClearText()) { throw new WrongConsumingMethodException("Message appears to be using the Cleartext Signature Framework. " + "Use PGPainless.verifyCleartextSignedMessage() to verify this message instead."); @@ -156,6 +164,7 @@ public final class DecryptionStreamFactory { // Parse OpenPGP message inputStream = processPGPPackets(objectFactory, 1); } catch (EOFException | FinalIOException e) { + // Broken message or invalid decryption session key throw e; } catch (MissingLiteralDataException e) { // Not an OpenPGP message. From 41ed056165f8648fc2d1a0866985dc500a41b036 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 19 Feb 2022 16:05:02 +0100 Subject: [PATCH 0059/1204] By default emit IssuerFingerprint signature subpackets as non-critical --- .../pgpainless/signature/subpackets/SignatureSubpackets.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java index 7076873f..73730fd3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java @@ -136,7 +136,7 @@ public class SignatureSubpackets @Override public SignatureSubpackets setIssuerFingerprint(@Nonnull PGPPublicKey key) { - return setIssuerFingerprint(true, key); + return setIssuerFingerprint(false, key); } @Override From db58280db65b28e8b05fd01ee78026320a1c8c69 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 19 Feb 2022 17:07:56 +0100 Subject: [PATCH 0060/1204] Change default criticality of signature subpackets to mirror those of sequoia --- .../builder/RevocationSignatureBuilder.java | 2 +- .../subpackets/BaseSignatureSubpackets.java | 4 ++++ .../subpackets/SignatureSubpackets.java | 20 ++++++++++++++----- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java index e2e9c0c0..3c2dcab9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java @@ -19,7 +19,7 @@ public class RevocationSignatureBuilder extends AbstractSignatureBuilder algorithms) { - return setPreferredCompressionAlgorithms(true, algorithms); + return setPreferredCompressionAlgorithms(false, algorithms); } @Override @@ -342,7 +342,7 @@ public class SignatureSubpackets @Override public SignatureSubpackets setPreferredSymmetricKeyAlgorithms(Set algorithms) { - return setPreferredSymmetricKeyAlgorithms(true, algorithms); + return setPreferredSymmetricKeyAlgorithms(false, algorithms); } @Override @@ -381,7 +381,7 @@ public class SignatureSubpackets @Override public SignatureSubpackets setPreferredHashAlgorithms(Set algorithms) { - return setPreferredHashAlgorithms(true, algorithms); + return setPreferredHashAlgorithms(false, algorithms); } @Override @@ -465,6 +465,11 @@ public class SignatureSubpackets return new ArrayList<>(intendedRecipientFingerprintList); } + @Override + public SignatureSubpackets setExportable(boolean exportable) { + return setExportable(true, exportable); + } + @Override public SignatureSubpackets setExportable(boolean isCritical, boolean isExportable) { return setExportable(new Exportable(isCritical, isExportable)); @@ -480,6 +485,11 @@ public class SignatureSubpackets return exportable; } + @Override + public SignatureSubpackets setRevocable(boolean revocable) { + return setRevocable(true, revocable); + } + @Override public SignatureSubpackets setRevocable(boolean isCritical, boolean isRevocable) { return setRevocable(new Revocable(isCritical, isRevocable)); @@ -530,7 +540,7 @@ public class SignatureSubpackets @Override public SignatureSubpackets setRevocationReason(RevocationAttributes revocationAttributes) { - return setRevocationReason(true, revocationAttributes); + return setRevocationReason(false, revocationAttributes); } @Override From 1753cef10eaa79f370bf81de2d55d6792b4f46e3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 19 Feb 2022 15:41:43 +0100 Subject: [PATCH 0061/1204] Simplify handling of cleartext-signed data --- .../ConsumerOptions.java | 18 ---- .../DecryptionBuilder.java | 20 +---- .../DecryptionStreamFactory.java | 84 +++++++++++-------- .../CleartextSignatureProcessor.java | 75 ----------------- .../MultiPassStrategy.java | 2 +- .../VerifyCleartextSignatures.java | 36 -------- .../VerifyCleartextSignaturesImpl.java | 30 ------- .../CleartextSignatureVerificationTest.java | 32 ------- 8 files changed, 55 insertions(+), 242 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignatures.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignaturesImpl.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index 66b5c4c8..c23ede3d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -53,7 +53,6 @@ public class ConsumerOptions { private MissingKeyPassphraseStrategy missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE; private MultiPassStrategy multiPassStrategy = new InMemoryMultiPassStrategy(); - private boolean cleartextSigned; /** * Consider signatures on the message made before the given timestamp invalid. @@ -346,21 +345,4 @@ public class ConsumerOptions { public MultiPassStrategy getMultiPassStrategy() { return multiPassStrategy; } - - /** - * INTERNAL method to mark cleartext signed messages. - * Do not call this manually. - */ - public ConsumerOptions setIsCleartextSigned() { - this.cleartextSigned = true; - return this; - } - - /** - * Return true if the message is cleartext signed. - * @return cleartext signed - */ - public boolean isCleartextSigned() { - return this.cleartextSigned; - } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java index 6cc28a7f..68a68847 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java @@ -4,19 +4,14 @@ package org.pgpainless.decryption_verification; -import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPException; -import org.pgpainless.decryption_verification.cleartext_signatures.VerifyCleartextSignaturesImpl; -import org.pgpainless.exception.WrongConsumingMethodException; public class DecryptionBuilder implements DecryptionBuilderInterface { - public static int BUFFER_SIZE = 4096; - @Override public DecryptWith onInputStream(@Nonnull InputStream inputStream) { return new DecryptWithImpl(inputStream); @@ -24,11 +19,10 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { static class DecryptWithImpl implements DecryptWith { - private final BufferedInputStream inputStream; + private final InputStream inputStream; DecryptWithImpl(InputStream inputStream) { - this.inputStream = new BufferedInputStream(inputStream, BUFFER_SIZE); - this.inputStream.mark(BUFFER_SIZE); + this.inputStream = inputStream; } @Override @@ -37,15 +31,7 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { throw new IllegalArgumentException("Consumer options cannot be null."); } - try { - return DecryptionStreamFactory.create(inputStream, consumerOptions); - } catch (WrongConsumingMethodException e) { - inputStream.reset(); - return new VerifyCleartextSignaturesImpl() - .onInputStream(inputStream) - .withOptions(consumerOptions) - .getVerificationStream(); - } + return DecryptionStreamFactory.create(inputStream, consumerOptions); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 575a5e5f..fbdd6229 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -36,6 +36,7 @@ import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; @@ -46,6 +47,8 @@ import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; +import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.exception.FinalIOException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; @@ -53,7 +56,6 @@ import org.pgpainless.exception.MissingLiteralDataException; import org.pgpainless.exception.MissingPassphraseException; import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.exception.UnacceptableAlgorithmException; -import org.pgpainless.exception.WrongConsumingMethodException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; @@ -62,6 +64,7 @@ import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.consumer.DetachedSignatureCheck; import org.pgpainless.signature.consumer.OnePassSignatureCheck; +import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.CRCingArmoredInputStreamWrapper; import org.pgpainless.util.PGPUtilWrapper; import org.pgpainless.util.Passphrase; @@ -77,6 +80,9 @@ public final class DecryptionStreamFactory { // Maximum nesting depth of packets (e.g. compression, encryption...) private static final int MAX_PACKET_NESTING_DEPTH = 16; + // Buffer Size for BufferedInputStreams + public static int BUFFER_SIZE = 4096; + private final ConsumerOptions options; private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); private final List onePassSignatureChecks = new ArrayList<>(); @@ -92,7 +98,8 @@ public final class DecryptionStreamFactory { @Nonnull ConsumerOptions options) throws PGPException, IOException { DecryptionStreamFactory factory = new DecryptionStreamFactory(options); - return factory.parseOpenPGPDataAndCreateDecryptionStream(inputStream); + BufferedInputStream bufferedIn = new BufferedInputStream(inputStream, BUFFER_SIZE); + return factory.parseOpenPGPDataAndCreateDecryptionStream(bufferedIn); } public DecryptionStreamFactory(ConsumerOptions options) { @@ -125,44 +132,34 @@ public final class DecryptionStreamFactory { } } - private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(InputStream inputStream) + private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(BufferedInputStream bufferedIn) throws IOException, PGPException { - // Make sure we handle armored and non-armored data properly - BufferedInputStream bufferedIn = new BufferedInputStream(inputStream, 512); - bufferedIn.mark(512); - InputStream decoderStream; + InputStream pgpInStream; + InputStream outerDecodingStream; PGPObjectFactory objectFactory; - // Workaround for cleartext signed data - // If we below threw a WrongConsumingMethodException, the CleartextSignatureProcessor will prepare the - // message for us and will set options.isCleartextSigned() to true. - // That way we can process long messages without running the issue of resetting the bufferedInputStream - // to invalid marks. - if (options.isCleartextSigned()) { - inputStream = wrapInVerifySignatureStream(bufferedIn, null); - return new DecryptionStream(inputStream, resultBuilder, integrityProtectedEncryptedInputStream, - null); - } - try { - decoderStream = PGPUtilWrapper.getDecoderStream(bufferedIn); - decoderStream = CRCingArmoredInputStreamWrapper.possiblyWrap(decoderStream); + outerDecodingStream = PGPUtilWrapper.getDecoderStream(bufferedIn); + outerDecodingStream = CRCingArmoredInputStreamWrapper.possiblyWrap(outerDecodingStream); - if (decoderStream instanceof ArmoredInputStream) { - ArmoredInputStream armor = (ArmoredInputStream) decoderStream; + if (outerDecodingStream instanceof ArmoredInputStream) { + ArmoredInputStream armor = (ArmoredInputStream) outerDecodingStream; // Cleartext Signed Message // Throw a WrongConsumingMethodException to delegate preparation (extraction of signatures) // to the CleartextSignatureProcessor which will call us again (see comment above) if (armor.isClearText()) { - throw new WrongConsumingMethodException("Message appears to be using the Cleartext Signature Framework. " + - "Use PGPainless.verifyCleartextSignedMessage() to verify this message instead."); + bufferedIn.reset(); + return parseCleartextSignedMessage(bufferedIn); } } - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); + objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); // Parse OpenPGP message - inputStream = processPGPPackets(objectFactory, 1); + pgpInStream = processPGPPackets(objectFactory, 1); + return new DecryptionStream(pgpInStream, + resultBuilder, integrityProtectedEncryptedInputStream, + (outerDecodingStream instanceof ArmoredInputStream) ? outerDecodingStream : null); } catch (EOFException | FinalIOException e) { // Broken message or invalid decryption session key throw e; @@ -172,23 +169,44 @@ public final class DecryptionStreamFactory { // to allow for detached signature verification. LOGGER.debug("The message appears to not be an OpenPGP message. This is probably data signed with detached signatures?"); bufferedIn.reset(); - decoderStream = bufferedIn; - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); - inputStream = wrapInVerifySignatureStream(bufferedIn, objectFactory); + outerDecodingStream = bufferedIn; + objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); + pgpInStream = wrapInVerifySignatureStream(bufferedIn, objectFactory); } catch (IOException e) { if (e.getMessage().contains("invalid armor") || e.getMessage().contains("invalid header encountered")) { // We falsely assumed the data to be armored. LOGGER.debug("The message is apparently not armored."); bufferedIn.reset(); - decoderStream = bufferedIn; - inputStream = wrapInVerifySignatureStream(bufferedIn, null); + outerDecodingStream = CRCingArmoredInputStreamWrapper.possiblyWrap(bufferedIn); + pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null); } else { throw new FinalIOException(e); } } - return new DecryptionStream(inputStream, resultBuilder, integrityProtectedEncryptedInputStream, - (decoderStream instanceof ArmoredInputStream) ? decoderStream : null); + return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, + (outerDecodingStream instanceof ArmoredInputStream) ? outerDecodingStream : null); + } + + private DecryptionStream parseCleartextSignedMessage(BufferedInputStream in) + throws IOException, PGPException { + resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) + .setFileEncoding(StreamEncoding.TEXT); + + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(in); + + MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); + PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream()); + + for (PGPSignature signature : signatures) { + options.addVerificationOfDetachedSignature(signature); + } + + initializeDetachedSignatures(options.getDetachedSignatures()); + + InputStream verifyIn = wrapInVerifySignatureStream(multiPassStrategy.getMessageInputStream(), null); + return new DecryptionStream(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream, + null); } private InputStream wrapInVerifySignatureStream(InputStream bufferedIn, @Nullable PGPObjectFactory objectFactory) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java deleted file mode 100644 index 26e33a96..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/CleartextSignatureProcessor.java +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.IOException; -import java.io.InputStream; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.util.ArmoredInputStreamFactory; - -/** - * Processor for cleartext-signed messages. - */ -public class CleartextSignatureProcessor { - - private final ArmoredInputStream in; - private final ConsumerOptions options; - - public CleartextSignatureProcessor(InputStream inputStream, - ConsumerOptions options) - throws IOException { - if (inputStream instanceof ArmoredInputStream) { - this.in = (ArmoredInputStream) inputStream; - } else { - this.in = ArmoredInputStreamFactory.get(inputStream); - } - this.options = options; - } - - /** - * Perform the first pass of cleartext signed message processing: - * Unpack the message from the ascii armor and detach signatures. - * The plaintext message is being written to cache/disk according to the used {@link MultiPassStrategy}. - * - * The result of this method is a {@link DecryptionStream} which will perform the second pass. - * It again outputs the plaintext message and performs signature verification. - * - * The result of {@link DecryptionStream#getResult()} contains information about the messages signatures. - * - * @return validated signature - * @throws IOException if the signature cannot be read. - * @throws PGPException if the signature cannot be initialized. - * @throws SignatureValidationException if the signature is invalid. - */ - public DecryptionStream getVerificationStream() throws IOException, PGPException { - OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) - .setFileEncoding(StreamEncoding.TEXT); - - MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); - PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(in, multiPassStrategy.getMessageOutputStream()); - - for (PGPSignature signature : signatures) { - options.addVerificationOfDetachedSignature(signature); - } - - options.setIsCleartextSigned(); - return PGPainless.decryptAndOrVerify() - .onInputStream(multiPassStrategy.getMessageInputStream()) - .withOptions(options); - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java index 688105a1..5aa9f548 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java @@ -11,7 +11,7 @@ import java.io.InputStream; import java.io.OutputStream; /** - * Since the {@link CleartextSignatureProcessor} needs to read the whole data twice in order to verify signatures, + * Since for verification of cleartext signed messages, we need to read the whole data twice in order to verify signatures, * a strategy for how to cache the read data is required. * Otherwise, large data kept in memory could cause {@link OutOfMemoryError OutOfMemoryErrors} or other issues. * diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignatures.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignatures.java deleted file mode 100644 index 52360869..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignatures.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.IOException; -import java.io.InputStream; - -import org.pgpainless.decryption_verification.ConsumerOptions; - -/** - * Interface defining the API for verification of cleartext signed documents. - */ -public interface VerifyCleartextSignatures { - - /** - * Provide the {@link InputStream} which contains the cleartext-signed message. - * @param inputStream inputstream - * @return api handle - */ - VerifyWith onInputStream(InputStream inputStream); - - interface VerifyWith { - - /** - * Pass in consumer options like verification certificates, acceptable date ranges etc. - * - * @param options options - * @return processor - * @throws IOException in case of an IO error - */ - CleartextSignatureProcessor withOptions(ConsumerOptions options) throws IOException; - - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignaturesImpl.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignaturesImpl.java deleted file mode 100644 index fde90874..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/VerifyCleartextSignaturesImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.IOException; -import java.io.InputStream; - -import org.pgpainless.decryption_verification.ConsumerOptions; - -public class VerifyCleartextSignaturesImpl implements VerifyCleartextSignatures { - - private InputStream inputStream; - - @Override - public VerifyWithImpl onInputStream(InputStream inputStream) { - VerifyCleartextSignaturesImpl.this.inputStream = inputStream; - return new VerifyWithImpl(); - } - - public class VerifyWithImpl implements VerifyWith { - - @Override - public CleartextSignatureProcessor withOptions(ConsumerOptions options) throws IOException { - return new CleartextSignatureProcessor(inputStream, options); - } - - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java index 4a2a99f5..bcf1321e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java @@ -6,7 +6,6 @@ package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; @@ -30,11 +29,9 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; -import org.pgpainless.decryption_verification.cleartext_signatures.VerifyCleartextSignaturesImpl; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.exception.WrongConsumingMethodException; import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.consumer.CertificateValidator; @@ -180,35 +177,6 @@ public class CleartextSignatureVerificationTest { assertEquals(1, metadata.getVerifiedSignatures().size()); } - @Test - public void consumingInlineSignedMessageWithCleartextSignedVerificationApiThrowsWrongConsumingMethodException() - throws IOException { - String inlineSignedMessage = "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "kA0DAQoTVzbmkxrPNwwBy8BJYgAAAAAAQWgsIEp1bGlldCwgaWYgdGhlIG1lYXN1\n" + - "cmUgb2YgdGh5IGpveQpCZSBoZWFwZWQgbGlrZSBtaW5lLCBhbmQgdGhhdCB0aHkg\n" + - "c2tpbGwgYmUgbW9yZQpUbyBibGF6b24gaXQsIHRoZW4gc3dlZXRlbiB3aXRoIHRo\n" + - "eSBicmVhdGgKVGhpcyBuZWlnaGJvciBhaXIsIGFuZCBsZXQgcmljaCBtdXNpY+KA\n" + - "mXMgdG9uZ3VlClVuZm9sZCB0aGUgaW1hZ2luZWQgaGFwcGluZXNzIHRoYXQgYm90\n" + - "aApSZWNlaXZlIGluIGVpdGhlciBieSB0aGlzIGRlYXIgZW5jb3VudGVyLoh1BAET\n" + - "CgAGBQJhK2q9ACEJEFc25pMazzcMFiEET2ZcTcLEZgvGQl5BVzbmkxrPNwxr8gD+\n" + - "MDfg+qccpsoJVgHIW8mRPBQowXDyw+oNHsf28ii+/pEBAO/RXhFkZBPzlfDJMJVT\n" + - "UwJJeuna1R4yOoWjq0zqRvrg\n" + - "=dBiV\n" + - "-----END PGP MESSAGE-----\n"; - - PGPPublicKeyRing certificate = TestKeys.getEmilPublicKeyRing(); - ConsumerOptions options = new ConsumerOptions() - .addVerificationCert(certificate); - - assertThrows(WrongConsumingMethodException.class, () -> - new VerifyCleartextSignaturesImpl() - .onInputStream(new ByteArrayInputStream(inlineSignedMessage.getBytes(StandardCharsets.UTF_8))) - .withOptions(options) - .getVerificationStream()); - } - @Test public void getDecoderStreamMistakensPlaintextForBase64RegressionTest() throws PGPException, IOException { From 928fa12b514f28d729ac1443e90b9906eef7ede4 Mon Sep 17 00:00:00 2001 From: feri Date: Thu, 24 Feb 2022 00:51:16 +0100 Subject: [PATCH 0062/1204] Add new ProducerOption setComment() for Ascii armored EncryptionStreams. (#254) * Add new ProducerOption setComment() for Ascii armored EncryptionStreams. --- .../encryption_signing/EncryptionStream.java | 4 ++ .../encryption_signing/ProducerOptions.java | 35 ++++++++++ .../java/org/pgpainless/example/Encrypt.java | 67 +++++++++++++++++++ 3 files changed, 106 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 317b8cb2..37b7288e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -23,6 +23,7 @@ import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.ArmoredOutputStreamFactory; import org.pgpainless.util.StreamGeneratorWrapper; import org.slf4j.Logger; @@ -74,6 +75,9 @@ public final class EncryptionStream extends OutputStream { LOGGER.debug("Wrap encryption output in ASCII armor"); armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream); + if (options.hasComment()) { + ArmorUtils.addCommentHeader(armorOutputStream, options.getComment()); + } outermostStream = armorOutputStream; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java index 94b7f0ce..890acc6c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java @@ -25,6 +25,7 @@ public final class ProducerOptions { private CompressionAlgorithm compressionAlgorithmOverride = PGPainless.getPolicy().getCompressionAlgorithmPolicy() .defaultCompressionAlgorithm(); private boolean asciiArmor = true; + private String comment = null; private ProducerOptions(EncryptionOptions encryptionOptions, SigningOptions signingOptions) { this.encryptionOptions = encryptionOptions; @@ -107,6 +108,40 @@ public final class ProducerOptions { return asciiArmor; } + /** + * set the comment for header in ascii armored output. + * The default value is null, which means no comment header is added. + * Multiline comments are possible using '\\n'. + * + * @param comment comment header text + * @return builder + */ + public ProducerOptions setComment(String comment) { + if (!asciiArmor) { + throw new IllegalArgumentException("Comment can only be set when ASCII armoring is enabled."); + } + this.comment = comment; + return this; + } + + /** + * Return comment set for header in ascii armored output. + * + * @return comment + */ + public String getComment() { + return comment; + } + + /** + * Return whether a comment was set (!= null). + * + * @return comment + */ + public boolean hasComment() { + return comment != null; + } + public ProducerOptions setCleartextSigned() { if (signingOptions == null) { throw new IllegalArgumentException("Signing Options cannot be null if cleartext signing is enabled."); diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java index d2f1113c..3a3888a1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java @@ -129,4 +129,71 @@ public class Encrypt { assertEquals(message, plaintext.toString()); } + + /** + * In this example, Alice is sending a signed and encrypted message to Bob. + * She encrypts the message to both bobs certificate and her own. + * A comment header with the text "This comment was added using options." is added + * using the fluent ProducerOption syntax. + * + * Bob subsequently decrypts the message using his key. + */ + @Test + public void encryptWithCommentHeader() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + // Prepare keys + PGPSecretKeyRing keyAlice = PGPainless.generateKeyRing() + .modernKeyRing("alice@pgpainless.org", null); + PGPPublicKeyRing certificateAlice = KeyRingUtils.publicKeyRingFrom(keyAlice); + + PGPSecretKeyRing keyBob = PGPainless.generateKeyRing() + .modernKeyRing("bob@pgpainless.org", null); + PGPPublicKeyRing certificateBob = KeyRingUtils.publicKeyRingFrom(keyBob); + SecretKeyRingProtector protectorBob = SecretKeyRingProtector.unprotectedKeys(); + + // plaintext message to encrypt + String message = "Hello, World!\n"; + String comment = "This comment was added using options."; + ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); + // Encrypt and sign + EncryptionStream encryptor = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertext) + .withOptions(ProducerOptions.encrypt( + // we want to encrypt communication (affects key selection based on key flags) + EncryptionOptions.encryptCommunications() + .addRecipient(certificateBob) + .addRecipient(certificateAlice) + ).setAsciiArmor(true) + .setComment(comment) + ); + + // Pipe data trough and CLOSE the stream (important) + Streams.pipeAll(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)), encryptor); + encryptor.close(); + String encryptedMessage = ciphertext.toString(); + + // check that comment header was added after "BEGIN PGP" and "Version:" + assertEquals(encryptedMessage.split("\n")[2].trim(), "Comment: " + comment); + + // also test, that decryption still works... + + // Decrypt and verify signatures + DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(encryptedMessage.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions() + .addDecryptionKey(keyBob, protectorBob) + .addVerificationCert(certificateAlice) + ); + + ByteArrayOutputStream plaintext = new ByteArrayOutputStream(); + + Streams.pipeAll(decryptor, plaintext); + decryptor.close(); + + // Check the metadata to see how the message was encrypted/signed + OpenPgpMetadata metadata = decryptor.getResult(); + assertTrue(metadata.isEncrypted()); + assertEquals(message, plaintext.toString()); + } + + } From 367a07411d2fd078a77dc13afd47267dee5a567d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Feb 2022 01:01:13 +0100 Subject: [PATCH 0063/1204] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e3c57bf..8e6cdda9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.1.1-SNAPSHOT +- Add `producerOptions.setComment(string)` to allow adding ASCII armor comments when creating OpenPGP messages (thanks @ferenc-hechler) +- Simplify consumption of cleartext-signed data +- Change default criticality of signature subpackets + - Issuer Fingerprint: critical -> non-critical + - Revocable: non-critical -> critical + - Issuer KeyID: critical -> non-critical + - Preferred Algorithms: critical -> non-critical + - Revocation Reason: critical -> non-critical + ## 1.1.0 - `pgpainless-sop`: Update `sop-java` to version 1.2.0 - Treat passwords and session keys as indirect parameters From fc33e56ad86d4d5356b093749d30c4df19679c73 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Feb 2022 01:08:23 +0100 Subject: [PATCH 0064/1204] Some clarifications in javadoc --- .../org/pgpainless/encryption_signing/ProducerOptions.java | 5 ++++- .../java/org/pgpainless/util/ArmoredOutputStreamFactory.java | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java index 890acc6c..ac4f0e6e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java @@ -109,10 +109,13 @@ public final class ProducerOptions { } /** - * set the comment for header in ascii armored output. + * Set the comment header in ASCII armored output. * The default value is null, which means no comment header is added. * Multiline comments are possible using '\\n'. * + * Note: If a default header comment is set using {@link org.pgpainless.util.ArmoredOutputStreamFactory#setComment(String)}, + * then both comments will be written to the produced ASCII armor. + * * @param comment comment header text * @return builder */ diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java index 2ccf7536..2e1377a0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java @@ -61,6 +61,9 @@ public final class ArmoredOutputStreamFactory { * Set a comment header value in the ASCII armor header. * If the comment contains newlines, it will be split into multiple header entries. * + * @see org.pgpainless.encryption_signing.ProducerOptions#setComment(String) for how to set comments for + * individual messages. + * * @param commentString comment */ public static void setComment(String commentString) { From 53f7815778399efef929251add96c1968eb54196 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Feb 2022 13:05:06 +0100 Subject: [PATCH 0065/1204] =?UTF-8?q?=D1=81=D0=BE=D0=BB=D1=96=D0=B4=D0=B0?= =?UTF-8?q?=D1=80=D0=BD=D1=96=D1=81=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/repository-open-graph.png | Bin 58325 -> 51210 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/repository-open-graph.png b/assets/repository-open-graph.png index 93ef34a16e94719fe96960ebcbbc500c32e804f5..4df928e51aefa2b360b980f2d20b3dd0d0359713 100644 GIT binary patch literal 51210 zcmeFZbyQVd+c&yF1(a4%x{>Z~>8?$KfUxOJcY{bMf;32p#0Kf^lJ4&A7LacE*5$IN{PXNF7_7b4oY%bOHGfwx_^hlbh5iix83+VImys3+gFsJ!OGG~uB;Z%; z@pu{tgiYq5rsV`SbS1TOur;-?G9h(xw=*F%akDT5f!t;a5-j2Ny!q$2z*>O*vuzbTy9=yC>+&>n+iOJultXuqnL2zP>p**)ENrWFEzo;@dhns~yUCy2JNF-_ryVzk=*+2R z6{$#9i8P%dMo+Jyr1$tVyPN2j`0u?cV-t1nk6&$E3LG4S&1{~37OwHFJQwU5ypLBr zaalLg5Y%>9u^li#?K;d;B0l)iY`ZME98~?>7kX9{ixpwez8y}o?`gwOadVWFT1M5> zGvVr=np&oQvKx1!8Zk>U*i?Tfee0~TFm!LRVPsHr(z#o>?W1v`da`=HV0}K%UWW_A z-Z`ttIbduosxrrE9q8}~-gZ+h#OY8ee5 zn{Nj0jNIs#&y72;cNu&Rez!M$JRqq*ndBn78(z`HUyyxTUxvqSyR3nO6#c9HKv$*}ESu$Bw-PRr5Z3S46(Xd-MhOCl_J) z-I^08{c3Yl_g(m!p7+F}{)(1sp#j^f0ioy82!0b?+HABE6poSXJxRGgd&r?{R&vIn zXmJ!eMP8fx!%C7Aj`u1H-}QaElJ3U)1S4w~`{sl<_cXO<$;5X1r?Q_>{G?U&&RhtZ zsUF4sRM9C+=5=r!?Nj?lGxa|DLwSInU!+vq&&kT(DYHDM1l^*io|e=JBSps$Uo@hl?0oUp$tz$s@P2yE={eEb%AU7BT~SYRTfU)94^r}y3C zo8C`<&WcA%jjqST2R$w6noK_Io@yhQL8dv{Q?+>-@v`U~_Msh7ozirs&@ugqezOAn z5(bMf2Y-r30`Jr86*P_LdQdIvsIE2FhzKp*m6sOi)nzDqRFVkX^T2deaZ4RI74o5| zLP4VRvlytk%G&%Z-mkU!G3f_+M@&PMf<9mN4xUbPS3nkyzF~cSmC7*RV?$|qJ(peG zMQe2xlZjU=lDmtG!6(_sm6;I`tdsPFFmjl)S=nK9v~*7WNnHg6NoEFT(`IsA_wd;Q zS-a{^JsWOLIj^p-q)miL74JZoqoUM#cG+GgG)m`>xu}hgtU|^naIGL0IaRk5TYwtbRehSaDr5 zX=#9JcfDPfv6cHm_c66fHsvo}hu@p}tF%9E8J@f>iyh5adqZgH`N&4RCWK$B2SuDd zX=FZQLWETEw-5VZP>ZQ2lT7z%-1lJO+*hEJSl4j6w761{@%LVXE>8>8Q#_xKR8|yp zfR_%p-ySd5KH>V7)Z5SgPDrU?wll4Au({i0#4T8FVx_LRGp5lH$5aWg1#9JfzBC45 zQb+Bv=${HuCzlj`Z;2&avy7g3R`Xncq3HD&s>?IJYm;n({x3bo5l@8EeiDKNvF{hA{F>P5cmm}RWUA|s@TRDT73;Il|CXa$qkNl62a?QR9P7P zOdc=tLOjq)%=PiD2uEvCq97_;hMyrJJ!DK-?2KbFKiJE1S53`h*?Zrdpv#jNnUzX@ zZ22*#XjCV~N>|2&v{&V5hxqwbplFh&v8=nhhZ+>$*+`2~GY)EuV7}*%??S$TnP#*U zqDA#SVnTvp6iGdLZ>;8%hlJs{S^CZ8@4Tl&e^5ksV`wU(4?HHy=^mBl#|K1Th&_#~ z?18Pl(|LJ-kinw^9vN-93$uz(H@PPb6u>n(yzsjPWYrGZdrMLcvP*sHfLpwmm!`_ ze3S<726uR%ed^bwQ6TTZllXEuFwKKPK z__`;3#d@vG{~ndvbc@)Syb$A0il{l~(4iPNXLMmi67Pz1PO+jmK2rV}S^SR1ywpgthXO?D_vXe9 zoxtuBa(&$m$B02b(L9bG?Ocjt0m4^%F3CuZ{OoD_cr4nRKCGvOJo5PVx&$Av{SMa2 zvL3rIC7aowAt-+Qsk#T!r zvuBrt&r^)pB6B%E#br;FU#wzO_&y<+`31kAu_1?#>@CC;IU_m2)wHxASCz}Kt^5d= zZ}={Y#cc>U1jk4T&1{)Ojd{|g>U>bE>5kA5QBuqJYW@!8YFm*{)aXORiZ;}bUo(w- zIfqcmh-|c^<5GS72&u}t9sB;B#_#%yAx#mbDF{?-qapizrK5BZmpehO_NiPi1CcRt zz=tZzJ>CKPwzpU75}*dN4&P}<21b$mq{k3^PC-^egSFuQ>3#O)jmgJH zhIN6m;;gT4*!1ZXIXMWCFz$n-dnnSlE{gFzSf2-KObKyeQkobL5vu27Q~Xl+hT=5P zg6l`}8&g6cw{KPmWjA^4peZT@;kOi^#}{C>EfXm=Gt{stCLv z{B`Pf@^6?we>{366HY<@6tN6vzu1h2e-v*GxwqZsPY90JsD*WRg~DW21}_gWKAlWC zTfj#VP9a9rMZqM@hqNh5Y`n{|D+#J ze+4vW+T|dAx+sZ~9(vL-|CZj}ocEBaAReuXSQf7XUDvOqK^g;p8i@;o9hIVq=3_o_ z7Zw?^749QGw2~eBw_dNpa1_z9Hkb$=1yh}Xp7}H6`VM`9cKMguryQXbESC02`Cjw* z+f3L;|H1AV!ijoTujcls>U`!^Cu5))xv&$6`dN2ia*%e}pHH8TV)E-hw3nPlSY1eA zybNJYbId?792bXFa_pHaZ>3C|r$;9E`6A1SOa)2$1z<0bgL3Tm(|&sAF$WEkH?IVs zqBLxO(YFcyL}w`pdHRPSqeW^6R{%RjGg(1ctKBF?Th$5G-Vb>I%yQma&8WKO<~g(+ z^D?w|ua@y*aMjI5O1RmF_~nPA!UGH8x!@3)>WP5w7-*Q6l1QzY9Z!+N_Cj4AhZMZ_ z9}wA)X4kp@&6q>M{rItUyE{21i-XhDUL$hp+^2hS8(j=cD)|yrFU8Ted9tF917{+X z%gAeQk}*sjmTH@+O#(cIG?v?0Wls)N{YO2#^x7#|UM(BHmmMJ+x)Xn!W6yuW$X;Wp z_Z1i6G1{2;7>Od;j8C#qx!3SD3_Db!43Snrp7hDd9Os}(8{x_`FkUFQU!dqn$2XZ? z{ITz4ug0t)i4GMHDr#eEWaI%8rZ>+s*(X=?k1_I)6O@dH{Zrp>ObhbxQttQrg*wP3 z`x%%zUWETj{uq)RnEnX#?rdrf!;w_`JySa&F7_{PS}-5?XCKX+6-vpbUh$W!(ih*q zoQQcz8+*AC&)s}8-m3`UvQrdlOvlnUs0A1 zIcMBKl-mqL1PJkKusVOQlnrMJnb%U+hdD0=2O$zR6IA!h^JG|@iN%AHMoraT;vuzV zy&3u6?eyPTiX)Rya}sbP3hj7%IOXKKSYhGNbbNSOxNNO`5$cG= zz&dL5<0G@GSe9^%Lhp5W(CTFqKX%KPSd$r^ixA6tSJ-&-#~8gfCy8AuAEEGL=8dZjD6rs+i{y@=K6UhRjEjBv5H zwTALCsf!Xjb`;N2Q6M6$_#uM&YxPSNN@>mP7X0W117ZCUfE?e>nNjEkKm$97+-$U`WYLDG=wGpCE7|n_0#(@()TtH z=%GaIx8S!yN=Jv^Bb^}#FZI#|v%PYbKg#H5M!oX=efCu&T|K*@`tZr6Si-1Rs+{_Y z;BbY)vhs;vfPU%H`}nk9!blB!*a}ZMzQ}|X;qG|&qa9%sp&!QuesCNL;FT@9T-xgYkM7DPdztJ_Um+g`Dib_CT%>)zks?IIt(R4^GIOGXrRs3XAq%TM5IaPH}vxxYor{tWOasZWcHJr*NuBJ z_Z1(Qw%qStE6PnsilF+_fItWzEyTo>WyHk(Q7i#UB+1^9g3_&T3EK@+VdOZ-{4edh zl-ryAe}2l)NJGI9Ef~uE*bOrn$|RGqw}1H#6~l)?gXmqdv$Gw}<7P}ODQaqo*R+Se zGHDW_$ep)tt_Sw#7+Cl?S=xH5ep$1odpDZ3ahl30b zRs?$Q=TbA2`gPGzKDCwgu~@I5uWT#KF4Zd$8d7Of2~iWXZ~M1}zta@(S$*fEy_A5> z9{rvAE7q5G3=O_-vGQSVAv{@@eCw#%;pzjr$rZ9;75m)=xC6WNz3;Ml11MjWJbp_4 za!gtB3D-*bPNtUhs836$@YWTT1MM2!$4Cf*x_N}2@CC`u{sf)8qMOX}=UWq{=usrnZ2`o^zD+eVPV2Z>g_$3o&EeojNp<3@+Tp@QrbFjM3yoQ z=)mkmf%pB=t1Z1_59r?N2||~$W)8v95*98k_^fuY&Xdpc`dmJ*iG=NiLzwuTIwerk z+OYs?S6T}4d=Oh}CPQOeBNHY!YdfH}1p)~OyV)5+pe9bFMkZz!HiF~_bxq`?7RG|) znw$zO3U*>9<`&W(4koG|ifRxKD1_ITTv+ItfEyn`z}m#gkkrlE%Epn;O_2P7E+23W zzs*ce`Y^-^DoC!SpiC-e>tI64!NkGD!YJWp;lf5P^o&%%!Pt}!EH3#s3E-O`xw(^* z9Un8ZtE(%MD?5{|gBdd`FE1}M3mY>V8zV4+(b3(;$S*7C;>8qO&o2V9UvwWE+#fk6#q`b81m2gcFqn~58E+@Fq>GJSOY^HfmK=m zV@W9)1?7LvfQ!J)!rJa(79i~Z80lnT`Y&Pq2XF8v58L^7Mu6%6r28ME|FQRn!N4d5 z1wL_Gh%=l$8F4{!`1*Xtwh#+rzK2UrUUoJX9*7Ae8>_JiBL|l;J0p)V2M?nuyD^s$ z#FUqp#f0nMNXghZIvLtPOyHyd;!G9*9WHKePELq1gprM#g@uvBh?Ses(1^#Bk%xU)p7Is!f4t7omBQLM1F(Zc| zumn3R7aKb-?*l1g2%n^_gS8=WoEFxGW+u#bHf9eG-~#6pQI-)TXJcad*BxamLnl*U zf*`rPg^jb@zaFSrSevLi8N#K>%EiIS&B6h!!NtbG&C33-K^i6wjsOzjG+9}g*#Dvn zKNdbJ(bR;6iydNQ%4KT$*Ear1-O<+6$<@%oM8pi( zDX<#=Ko7eirFjq~?SJy(YHk8Zf}VwqkA;Pt`G0>u9IVDh>_&hkF=R0T@XNu<1t11u z%4o#S#c9OK#li-F`9C@Qf3qJp7GMPyHbyoMHCA>$7Ir>PUV0YRf7_w}Gu%r3;{*km z{~u@k*MNUBYk+9}x(Ap`!1^-()BOG|GdL{%7e9Yv>wj?tfb@SC`5(#mzvB9@xc)~H z_#X}aFS`CKuK$q){zrrVi?09A#P#f737m-y5Y@Q?>6unyV+oMjAsNX@i9aNHpi8kj zU*PJgowT+i2!#3q{(}HYOvVLnB0I?_NFZ+@;-Wp`xBDiT4+4>bWW+_(+-82yyL+jP zH{Tx~7{BopeU3=>cIw<)T?9cyJWs-20(-18OiBVF&)goSq4q=l3**I>k_dge+7}HK z8XP+jEb$3aq)(+!P~^HAantgblvM5cZ$T_uc#!Z zVL#1j`9wlw*c#JUsXWoFQQ*^9(1BTxdsZWJLo!D)z{3bRH`Qv@nlr~~w(FTr(TQh~ z&j|<&WE5ZlPKYA#32)y5TmQvO3=ualiFC5cF8BGmCh%MWcPR?3SJI6d{52bB`69>6{ z0PIe^x;h=vtp?v8Sj@G)-9A}u$=Izh)&o*#@6^cT~lA=rMB&uLckM!Kr|50+z`^e-;IpC~jtXgil(9W#|^^3sRW& z?niJ-9*F#+1r}OTBd&51y=Xse8a{aWnL#yD70kD(*%k|dP@mD6vFH%SWbg+5Kr7*=-Hc;(dyDptq z+@^v>Yhy;D1_=QgAo2)>NruxQG!Xg7XTg!ud-VL!Z}K_UoE<22nevWO)4aWzt93BJ zRN2J_5-n{#n}}LHs%gl@++Si~LBx7%oXbO2kn}~T9y7H^UZu% zq^HGRW>M;KQ@*W!G`_1L-Cr9gT|h(-Cw8qUw=GaBp1xh);iU0iqTu$6n!~q2k_S!!jq~eL)n?ecoV!2voTqhe=$V?IG|kJ-t`M^v>qbF< zBwcGtj+5os6_sp1fQ8fuILJ>709Yt>`Oac}H@SJGEZO^}ma7`4A)&O*X7lU5c)E)m zS;BC6g2<8JD}c2bti5}i9P1V+Q#eIJ6flgQSV^MroG9I;=%0i+aY!8%-vglr2qgXt z5ja2C@1JU=qvyv_{4$J#aVrEtzoPkdEXPE49IJdc4=&Xl__tBvqp`>U;EH3!tM+KG zEY>u1$f73(E9(6&0e>l@(hWV>B-SFs*&Ke@DXA!&Z9*z!m>|iOtIcX*6k{84!lA{; zlEqz0-Mt(I)&oejh~exm*~@jhUc2rW3(K*7PSY+r`3lb7^%Az7Y=ZTWUp)*I2f-O_ zR;R+*KfbNE;g&<6l_?E0Z7_<;)cEKvTy*58rt-E4&ZHm8TM!Bw=do-4t%y@pfU^cnNMGZ^({1}YI=;JNW#$p zij#!Xin(T;udooN<)S*-H7x_I0l?dVB^M%m$is5Kt^{myPCgxMRtQKrv;|2KMJHbP zDBj_BYfIk4$$-89angHl;wrU>-D~%5h1Tq^tTJF(?#018rfgsM0Kax@5Mr@VxNeU- za1g&smQ|&6W5&vh4mQ;_M}*-ceyso;Yo&IsFS=KtA#~fqansD(z4~FT902_tHK8Z9%1gu~E|ud-xqt6CgN{#`2_^ z8RA>`_hLTa_rzw-*>OEf67ep>@oMt3D`fZ+EkXoR(C(KQ5t~rZgE_i^!g;-ChQ8PJ3}{ytnoP;QO8;y2$jFEmm#4{Y75&_9&}t-d}H~tt9!UGN3#da8|H# z0f7ai<{E9c32k36vnIQ15TR%V*J?=7<=whFs0s zSDo%camO`@l?t9LfUq3k0Xo@km&mOCXx#Z-=RKlP7VYLBzk(R?@FjK?u zJ8!rIBIUd_B$AMjAS*AgW@VM1o}Rwu#HX&JlD66zzI%Ppr0L>P8S>>z>&5X5Bhd_v z33^2O#l(YeN(-ps0HtNuf@iJ+W2)G!YFMtuzqvdh{35 z=nHeBmi0`zO7t6rmqbKF<({+*3}YW2A(wk!yUcqX_or34EV&QLhSd$lBLoP+u@)nX zA#*tkD`396Ik;Y=hgHNzB}%i(l*bDTB^eGx5l=F}7!d%yg#z$kjBO{wxuc?{hTh#= z;&WMj1FNciUVQ2-O8hjJS&K7 zygJ?v=RG|@5+pM@i}+nqP6Ps>@b^JMSVqjoFfvE_gU0JZ2S-Qe%k_AAMn*=;mL^qR zLW~OAMg7q%1yx1GuWyvT6XJ7gs8H4?i7Df`X%NOv0i5FQgN{$G!-!TqTqC_*icN* z>#%)iG&_91c9*H)e8IE%8coo-ygt8^k?}Hh2znFO6j_QSErTanjFZGL4QV-FG<2>} z^Jvqm_g%Ge>|yAOYwWwbyq`Rk%+VI6&B=M0lts##lcx;83xE_?ELr<;dvaP898&RJP8QrFPv0pMF(tHTs8H#|HX5)u+M zZ=-2sl>Yq13qTbS7gVNfs^|f=J>DFg1Z3*e%UET8e|PKk%h~z>yt=DqCP>3+MNefK zpcJ91&OkKF=zVp4RkLuW8OAKL!>(dEF=<{TJ?B17=S?#fOMJU7Npg2_gS~xJHH{1C zSO7Wze(ZzK)Zd4y?N`Tg)#;mh|F{XzF*C=HZjVJ$NR_)D=oe_0NtT%nYWVo&2Kd~7I%;X5DPV4n7-790Jv`&QtzX=(8OT4)g&HRmvK>GI? zlF!I7H)QS>)+s)H`qU@oz@f(J&ytAsT(wFPm-WcbcwU9oG+Sz~cD-B4D`tHLnVzn$ zq4w7X`GBV8Y8L75jO9cFs^!hZ{^pHjYY@8XY^60^uW(+M*?A9n9r+z@Mr3D7retmf z3{^9S%hAm-7o=Fr52HWP)n{o$x>d0pQFSO@wUjb0IzUkUMg?epv)4rh}A|E+i_ve1hT`O?UXWFlJY#;dE*L@}8-x#gg zo2eKLc=}=t@N;}3G+NwZR~xBd6G5eZrh(`?=M?P1z>h6f+qb)6L(kKOgIly)F*agx zX8q0p@EU#uETVXVuy3Pe82#F%I zF6$Zd)17eu+vitv8~sVp?GYH?LH+R-TH5>Typh*~V~aXQK*ES=i!oTKb6u~dEuQ++ zh2)uQ4r4z(eH!-OQxK>H9<_h6KWI3=;@GPK)V9v`U_uO2FthHUk$-JR_1(LCk&j5* zdsT}QpI=bd^L3h*<*4Urc$^GN#YRSoWTQN}gl3*Hk9k+CmmBDK=(<&n4un%`;&{K& z^6q-}9AAz|5?OyhTXi}T4^U1VxN;f-?k9Hk=4J-63|VfAU-Mx3=X*fr)bb3__TD65hXXvTXc7|RcU|<% z%#*q5c^pn#LqCd6H}ewM%`AUUmB3e=-77r37#mXyBNo&#OSVT!p;YraH3Oo?W`-&t@+E%)S|7e zEuHYZ!omsR`%9%T0&XU&-CCDD9k`d!DK-8)Q)O$gd#I4aH6E=bl!HwnIiw=m-Wi?Q z`zoQV)x7z9WO8tP@75G8W(F-b{Y4mUBv6%Tf4Y6G6iVc^+p_N(3e9r;(KO~vQd)Wu z1TL}%2cyFL4>EsO`A(bZhR5i9hdd5CL{B&a(zcdYa6!N=*a%FnLFc@vv=qEMQ7}s6 zI){CBI?&xcJXhn$X}=;~VL6`L0kP&v8sOa|Y&hznuf5zzqR9JLPGlng`WBs7IeA` z5L+)c>H!|MM1DTieFF~&7IzOjNi=0;Wy3l5FrMf~yi2P5XmnfSk3vpHi1mb;nu-ET4WD8$-(} zEOuoBD?gQR+=sEuPEA)E@?R@g?pz|-=mhHCY90%1G8@;y$8Xel? z`mbbL&60HYZ>4>@EDNK1X)a1;L4~zU|0cVy5S00Cg=4AVe)rMketZ$_o=d zIy!P{Mdxo@MjR?M7*yDXWn5o=>m?s zNkJPVh^)BsodwHIw4lo z_6`tq&rnwM8Ek0SAk$d#4MQ${=8h}`^1`*^5Ko*p)TOFZX85q^ei_Hk0oSX zK0KJ%Iei2oIV2+STc2|pRR{^mb@y{rSC8egnp!%|dX4&;Nv+A}b}V0;o$AdS`~yv< z_&18lKj-UQq0RnJJeC7r%NO;+!x?yRBJt&w<P$Jqh&J*?Bj)YNjhB@mz86pg5x0z+(k1A?d7h9N@bqSeh(cnPn-#zVqW3+#&|GXFf|e=iZY*Nm z&TDCqB*n8MIb+juR+niVens{LKs`||bqv5K-wogQFv z)lE&a5;-i=pP;__SzSFB-j6wm1pY4>T5Z6w1R~{wBZvNc%3OP znW(?gr3Rt7kmQEt^`ImimBG*l6IC~I4AnbX!FeJbilfdriB{U?inIEw>rI^Tsyb&) zAM~JY21+C#4IDWs7wOdNn&yFS^pAteKNkH-gN3?wXK=X<-#yEta}3PapYV0d?hHV5Y{46&=?* zM!+GCz!q-p?l+QbcS?JiRIRLjTZETPTftK+p}Px6iu*=F)))kc6>9IUcA;pyl(az7 zFeq>wnyi=&gvReyONyHAYTqa%W&-*->Glqg8t408paQ+RIE?jhn&7d^=f^EcI}S4X zLENT==Uh&4CA<=Pvep)gye3jGr2cIHO=*ex#Y$eb0cMxbox*)yR9}1pzlY=5zEb`2}Kv$~mHN6P6} zrp>XqmB)7B4fCzqHTdI8nOr60IdoCh>6WuT-NDSZ!$(u*aSQtOn}pQ@5u_0r8w*oR z;Tx|Vm@k2JmfJ0h7WM2dHu;7vFXnihY^Av$ZzO=@flz^tlXE-|k^}%}I77(NLyL^p^4mX>((JZFSfJ(dMDFyFr5Md`z74qGHLLFj@MGuR_L>Bz$^J zKpI?-L~(0oG#})9zZZ?1>8o#(581MpIAdRL5^n7wx9Ggy2=vYF(WYq-*5GUGP0krf z>(OH*x|H;Abpj@3Uu|%1w%CZCJbqXUaupbu~|l^Mp3Sw`AYI8VG8ExOn+=gS^QUz^;%% zm7&-Hx8+o+$rqg}8%AC9(eOlWTVmjC(Zqy0%i3i9#-T)>#7v9StJ!8Ys2^)TyRuEZ zan)!r;D4J;B`T->TMb8hn+iJG>epN?+>DdOj3oer<~F`6_*2VBkBc=U(L0 z{EoID%z38t_otKfkCgzo&3rSy{21Z@(Bh@?Hk2 z01xj1G);S>2m!&!oA_RJ=sdOjx3}3x*L8WHMbw;OuAK?IIJ719w-xG^^b~ezC%wdC zB+tq*Q`A&wybtBK0(_~x??0S$zv#e7jeO%9#(H!B6xHr$?i*YAREgw{!$6!tci!H4@}@L~P0`XH`&iBa{>S-bYgi|IceaW-g)ZVX&S;~mC*I>& zLbij%;m=+{kyF8x5X$Wi_cWY{_@&=#Z;<8f)o=y&=@HY@YaRW*b zx7$j-&#IXK{|yiIa&vR*`5H4b#q%@@j<)erz0b4@*0VD+he~eNcVl`JdFrPEXZARl z3C&r5+)dnzZJm&HPhnRSa>udsUTFg@+AA4vzD9n5gV|L!#8qX?N~-N$_l^zzMmt^@ z(fza3LCEx8_#%tCur)b4LOyLp-h-s;F0V|P*8PJE{w1h(eg zQB+xDW_{mS^}}}fYw`{=!i9EJFX=T4)l^jap?okH3~&-(2G+wvM2~xpO!*3DEf!9n zk6hl6mtd;Dl1-f>(YG#l%&GAv;k>>v>{iF|IoJEsP_k>o)1_;X@3`5^G1Ag7G9a;h znn1W9dkZ2(naV}+qZ)gq2BZZ<0&YW_4hCL_65bcf!SGxP$gAjrTMxfDC?)fx1){#T z?1`bxn%D;N*m3_SsPL4%z2nZ|v66~PWE_KbI*{WIY?caN|7MSki_2BXkj%bETDCL4 zxi}OG{(?QY$|?y?pqly8Z~pF05b3bY0~(K-Seu9oq-4=&37}Z7yqNcVI_HjDn<$*G z3S#C$g;WjbP+C={G5kTYlZ_J@z}z`K{&l#4orqz>NREfZDmb3nj<$THr_n z$)~~5OggOY? ?e)$qRo(@(Xk$B3@tjs7x19ScK8xAmdE9~j#&zws3Kz@8?Ar@}H z?r~x+10MV?7z>pB7XHW9yz8c;7k0jeK#!otE!4svo({frKv zK%mX8pYOfmBD?5osqaoCDTl#~_|~<;ee%Sm6-6r>GnRyVQwQ%6tn47tNU4l0nnKYs z!sPJ>8HLP#N5rNLP2VUT^pL(V=#^p|TP^;cbvWn5{z-M1keWJO$`n+;*mI;r4`@a& z4-3TxZcGS6evF+u0F~L{#orhZh_O<|T7E=NINs<;zZTSP^%#824r|kCnMl9=U;y2c zr_GSsgEq~iX9n$M9-GPCC*WQ7d8o|P-*Wgo}F@q>xc zU|-?uZFWVVF`Nuij>?rKrw9EOn7}1^mFjaf$i>x;i|2mHFNsy+Qp-EJaH5&BDiOtq z*2qmbZK$vPF=g&kEGn=Zq32fWQtlHW{NtQ2V z?`Do`PNu~+;*#d(>Q+%e+hRWFWZ^OAvAmPAmMga#?FVYn|A>TM&sUi4PtxXdHgnL<^pT4| zRa0?yjiau`dnA{3BYx36|7HBjdP8+zU13vdOu-?v7q=@2)B#ptjI>Px=R% z!a43{ejKfyzfviYP3@& z4Jed=^KPok?Oo;<43Z3mLOYHhHwmVgj5HOZE}Syo2fuuwKyp-pnZW76!8CsEnEs-_ z#>JwMTr^%~VQ=dOYNVRE;EJc}_GsND$b}#9WDDaPG2@oS_oFGJGmm|4{SOpLK#7O8 zliQoj)_pS!xmgi|KtAe63cTZkbrup*PjHL%^6Iny0Ov4a=*< z^Yy^jCuF`?5rUGnI|gqAly5Gce9>l}Hnlc-euTLPnw*DD4J2Q)I8GkcE-}QqFrd^Y z3%!6LE!SCtS42e!6mjK4eP`U1J>(KGG60q(3l1n2o+D=vz6C@1ti z;hUeecNK^)LI!6EO3?dgJNr1Lc13G=34C%a4t|Nz7)-Pa5Sd+C_!P4)d%77`#++9i z^h!@zNsYw}c~qFF|NE|Juo2RpAIg^4 zo3O96eHzm8A2FS?6rfIlE`nb$zNspbX59PSNJ3jU&PNLhEV(T-T3`D{H0qM6;{hA6y=6hhrg95sj^YZfQKX9|D0$P}&V&mBWdhq7Gg~R)Gu*^yPPp9Sa z)d_)rjv+`4R00v1vpcPU2DW**`?`fo4d7TF$3eW^)p|0W*ldwzkY++1aDY**~q%jjLu0&~8=UrE7ly#1eEXLS7*GX;vC zjcfLkL)lx#}pOds+olC)TLuXcBpFI8!vRE*Nx{*=8YBZ2R0)eX}WIWhK zQ&zC>)HhfuDPi}?^B!?ylB3|fo~heuZBu#Gd>*n`qG$|jR_Xu*FLgb@VBt_BGG~<1 z{9$``j&C?#l#`cp$Nw77fVzT+3g}V;Z=XSBB&olZNg7x`MAl{k(;%&uQj=Z|tEpmq zS0HbzWJEvxNG29ER!b`@BJyFPP)`mrUctmIog($5QX?jZ0yOXS1_(mycLc zoM?g0+B>pR$~>o#BRg4%7$t?zWDrQmZ+8dXrShE?*hfMZ$M*!+8qK^?c9J_`F|WX0 ziLCyGiVJFt@aWzQUZ61C=u7bUUD6p7VRtpmjE9G}GgXoSG^p$8j_ovUZZ}=uDq4={$(ma&n6gKL0cND&%w%L{ zW_EyBxVSWczu-4;8kTtNuC5OVgU@w8A9xf|bPWi~i?0!A_f!xc0}=gj&W`L~1$Myj zNghZfT!^mZt{i`E6FPzB(!>1corSNEmM+!V!>>=|1g9XGHX`z!BkbOOf zdguMhlwdH_6;OnH?g=E$j}L$hfM)~7W@eKf@;W*?1<=J5%gI7LhdMD&g>s;23RHT! zc2}8e)}Bj@m$Dnm$b9||1loMD2|)r>dS{q9mv~^O`WllYJW$;0KiU#TBv-8Fc5+gc za&k5?jrV?BQf~Hj8?>v563@rmfDVb_;8*cONCBWe8`Rs%*4!S(xRETCSkCZC2N zOp?Nji_s=rYAUMj^>~Act>KIYx?=zX@EY!Tz58)Qep&DONR~oUxl#MGqOXHMvAfD< z-g1sXA_{|8;3vQ^pZbq0`Iy8L7MEA~y%KHx$%tNF*<(h22b&Vq((w#`_L4(hj2t~Z ziB{eci^9GmnxpYP!`l1$XU$&1<@N;uf!w3D6vl!SV*K-y?nmI=Nbor{^g6=_gv9{| z$m+CN>Nvc63-n$`g--}E&W#Ccm*l>V@vQ zsZ-T7G&I6G)qrw+UQuJZ*k#7xd`Ed-TL;+cS%i z4cv4}+S`|umX(cHTI){lWiR4z3JU6R&pSnDDW>eG*QjR65C2rjuta= zebe)+Wc>z@@!D%!L&G#;p9{lX*Kh3m@%S9ahnX~xPCeZi>{?!vb~p z*}v?YhPRQRJA$M2?b|*l`b?FjG71=|>^{hb$Im$PaNJd1PUBoPr?)c|YCYb})#oRd@{ku4 z>=UdN@G_Y3O(L2ftS^~YH5h>8?R>eJ((O94?_up+_ID3i0&W|$1auML{ectU?U!q( z0ibK70`=Q=sXOiGUL1;SQIRioHzDdz6Qq!7wxP9!@tUB!N!MCMj;*uhs8B;<6pezg zmG5U>zu!qhhYdDPrtjnnK>aAS`qi&x=_}l{h*I_cP<;KmTJ#pi+Gi>S`;i0 zuZmgWXUhmXPW|rCDR{a)qJAGm_a(D5qeHQB`go^^FIAXM@9t~OVih_3NVlqF5NZ%;Z?{||fT{ncdfw)u#?fL#-x-KsmjP2 zM>K z{hyNhh)_&{g|MQd+mVV5bR9OrJ12SKDYM0z$(M$tQkY>y!FF)k>l1=OucK{_h70db zgszwYPDhE`2s?lQcoR?9>eG_O9hcnhdLWnsQe%mfeE!_IzwaZ5_DL6^D zC&^aUJp+T64v>ga@fbTzkVn+P^8y+V_TxqW z_jfr=s*y>mID<%A$&Cak2hg?(z{3qgVu6Pf!=nlV8K$`U)aQzdzEZHt^K%ED?Az~> z{l&)23>~MkbN!|)Ud(a6Hxl98q_! zJFK=ucL8WqubJ6sJeZITi*qK8R2EJ|?7HWcdIR(IFB%EM12I~^2?1)fu1q=C?=Ozl z&81q0DO7{vr4A4mF5?^Ki3Np)z_S$|pqIaS>oK2VmgcVi(zo8(T;!&!pRSFtd(`iq zceV0!z=z<9W1(EUY`PVep(WgPa0m@&cAd`a`ltmj>47I)=9gbn$DbC+N99f1F$1oG&OU3P8A?T!0rI0u)PT5^4J0blJ1vo?^hFF(7)d$F82_y!8%%hQ0ShH?ozj z7Q(T$4iO4w?Ykp;2Db|?`g53am%|Ll=@XFLeVv5Y$nFdmLrVZ4Mat@NS4T0H$M8K| znp=W8dUUOV>y-D9%s#2F{EBb(+f4R5%H`2BdoG+d2j%&UD+^Fi7nU2yhVH{(S_*H7 z10xh*MZK=2*!jUq7=q@{%B^IF^P$1xRgUo zLpK+D+ISE}NV^Ej=E#%gKvR(ksX&m}9UL6sKo?_HoF6{8AR3UVsZ9G1lv*;`KU_d@ zW)&x0-HGE#ZM40X!hQgj+9AX>a!rg2=XBmwpm@}LnjrmaMoERGFXnB@e98v#c`Ld7 z?dfu$bFR1$XMd?O7}ZU@X2?)GL#xrR%l6IXjkcgDSUdirH-@_kQ- z)>+8wb0A#S01@&%sk8}JR#w@xHL?`ozcOJ9ChA@(3@h!>o*ZA;yR$*+FLyF(`4Pmh zo(CY`U*IXB)Gg=-hr}y6A2*l|r0mJeyURYivUkpTrh_Qhggm73$nC<}BuYn9pgj5w z3!I?<@3reHjn>NRa(<=3=cw(Za-}%);kw*}r7A6O^d7W%6Y93#{y-5hQ{!Y+R_ulg zd6HhKn(zLU(aMtk8;2*v9k`FAw&}8C-QKED0+R9z&cF#;Ppj=$>J1rsNB%YS8y|TL zAXgH{K!5QjCZ=Cz&}P=m*|B_>$sB?OwUDSM-ec{FMYnN>ihlC{+1`$478d|*D+Gv{ z%F?X{CtMDXobc@fuMJYg(|Bo_U)h_!bMtQ0w^>MZoN%Q(?+ugKgeof~&yR!c3Uik- zh5Gp8>OBHnG`7ABqa10ZO^(_Xomv6p_S$3O2Zo@`&w}k5O6euYX15wQg3qv3E?`Z0hUNH*s;)g{-u+i~b9J zI!S^?0|1Pp05jZmd8~FLWD=N^8A`!ln2-fv)wZM;08{uEw3s2}F1Za*5A+JR&l<*~w?UY8g@kc>UG5BRI>sA>k2gYR>mDW>?M|*0ou^J)tZQe)+ z)d5}asohGS9Sfj@-m1Md+`D>FL}tKUhWDJJ8MHTpG4bDGQUbH(0rLF98XDqR%450OJ`dG3oyoNL$URY%`RZpf9;&b_NsLx~F&OR{Vz7Y7 zJZ+k72#}AqKq0lL^mC!|{AF?jwDKRWP#Gfqq*(5h5~ecNb9& zqRSzja6#)*59L<9@zT`A2Dj~&(;}=4y{*A!vlgeQ-tb=8JQiyDMHrQ9^xYj0Y7kBVC1eFzBA^B{l%U1ei=+?qLx z0IR9+B^ZKKp$CiuX6AJj<>lp7=hccejpJ7C!aeMxCovl$JP-1_F(^1oZwLc$}vv&GDtV5$z+CIFLJ0Vp1W ztXub#Lny|k$%yA4&z?Dx93TI#s7Q2UWzr7NmWieyH#0G2GqYby0Ucma0hR&a4R3F6 zaM|DDE(hP$(AciPv=Ps-vy%X1+80h8&j)4+`RZ&Wa_Sr}wNfac@GRk` zs!Mm+`>dIUvk@?yuT%^@BU^;brI_~t)>jPHU;*|9JXN(p9NAh7s0VWaEa&&&CICAO zEw-wJMqdzPLOU{UL&j{8vu*bKSs6Cs$biM6_lLQI)COUp?q1(jt&Pn)BllP;WP2py`>%Lw0n1ODAB=bZ7atgZmEnC*R-^`o{V9TBy-D}VQJbMM_ZN*XmN{pC7F15UJrhi zHs7*S4;#G)yde(6p@nF8VtoegRHg3^oh6C*SFeWhwAFKDzyC_(Q_UsagF1S`w-$dF zm`C>DKUK4wZ2Kq19d3BP?}(VpQh>P;6C-1y`%Hw%5BS0Ho?jLgT$m3b$oh$g*f>7{ z#PuNc)uY5`+<(67d1G4paL4U#3}hYCz^)`}HVRbMf4zu)RhNQ?a%=~ZHRt;&_%Yui zu=B9;)@atvLfx&sskzJX>Mr=+pejG^%VVKLrGc8bJS+=i0$#2Yqrma8r}N0bewm=U zKG&iZUJl+$Fw<=o`%kGkN9iQGNhS)`C@Rd)&%4b0U;>-DCAIS$l^)|(7!2mi=+f0* zQ{ZodD$E3IlxdCSDg^HQ@0zlC8o#8Bl-tlEV4H}0MCddO-f>3=VX(SF>O2hg6QZ~L zG?NFaf3)JC)T0~9(UrYj5>1oJ(88-w%1K`Nq?=tr)F0eJQYpBTDwK^An6`_&FJN+7 z!ks0Pq+CXWB!^=AAM-CL0cdU}`T{{a-*8a=f3g6%V|I+C5vLEy{;A2oKYK)Qe+Gru zyI8PU;D{J#Rnn?@U=eCQ_OC0j9fnTIZ3x^l!$MRdi0=4u!;Onr8Rh1E_aSgmf|b|t{@py3-IZLo!p zH|J(fc&G}pF}QfSWH2DPk1hxGk+L`6R0z#$(@j@KcT6T6^FhauNh(vEtMPp`(QBS{ zB?xbOrkZA}qyUhKtAbedpX2y9=S7%8ng8O@J$6{o`x5M?)yCqmNmNu+-r|V$WkA0G z;TaD4bY`(%`Kaa1?@Mce1%~G3`(ZWZZ5MmhOssU>U+Fk##Uz;dpKhJS4RWiYCBg!byAN=U z+=IH=n!4z1ApPO*pOtNcE4E+U$msiF5puPF+wR+=Gcl~vno43RJYX}7w)Mmf3DL`2 zpVy+BwkWxp8v}yO)!Vt(WB8w4t#{s(+w3@ckJArL9AGk-QaB}MQUn`t41WFk1(Ls< zLxz`w1G>Gv-PG9l9q0tY>DvU5KJWZR1Q1ju(ADfFk3@eiC1yD!miKLyfBA9buu-Ge zlI>21Z{5Uoe(9T<@j$)C+&m1<M=rR@D_ts@0BTIZMr45a&fFUCky!SI4z-%=<47L!4yNPDBGr#_dluMirKmD?(sOU)4i(~iN zh2577NsGP_h=%&cLAp(77A|w}HpE zKC*?SY}lR;Aan7vNC8+0ND_-~CSqbhzyvnEu#nI_AjJdKEg69FS!N;6Q-+Nfc5n3~ z_Cdg6KYAK+Aim)Z+2GV_JtFDCuOgTerr8V{d`!ymfmEx$>WP5A6T{NB;k@ zg#2Iq5a|ioVSEz*>%splsQbV3Ax||t1J)TrNNXo|bNe_RldZ!G7RuKW=E{2T{++Q^ zrIl6PogKOprdJXg8Gt`sj9L`MAKo;v;G1LD-hK(yi88$}jz;TVB0>$TwmYVdo;h!0 znO@sSVAp=xB2bF#__INg76OO$&idAL?}xZOYmTx2oztEcLcWOY=F;!7HMSNA*( zQ%AUjW!$fub@9_`oJ`+_YCcS$-~x!Hm3WZ3g-Q?5yNBLGb4#EddC=!|Jx3#a83!SJ ze_*gi=p5gE$m&gY_vbr0%@Rm9Poe%#_a%G();r*%kXyO*r9voS^E@NS6gZTC)lr>NMyu?w2y!v5nIUC0+Ns**o3CO!g z@6VossOfiR@%lGf6b$QFeI7EcEO1s~$bKQJeG$H%*tlg_Fv`$69|vkjv!>nZu3EJkC2#~vEj9aqK`=eX zjDz0ZI8&1m=VsOOPMM)O7a#2CaGINnT36`}zD;U1jO|)4Z8Qm4wd~Ei68|Yw(>sc? z?1YD#w+38}Q_{04fBNq(L9B9QT)Vvm)+H;c!Z#~guZlk z#EVBi89Isq-pnaeRoJ)hNqflt&(1HxHW4Tjg#*=DiO}BiM-wpOZZ6t&g>5hUXft}W zG}Y%zqH6ZBV+Jy%{7u@&Q?DQFE8aBd`+O)&# z^;k(dWt(HN(7wdNSvdxNRWm@(ccIf`GcljxFef6YqO>JHQIh0mFNqtmxUH{ zObo8$Z^9=d&WdJ|U;1_MksQY5?r`{5CMSHOu|wj2P<=oi`r46|ug!8tF;I`(gxr~Y ztUS5Lj$Y2b#de++#Jio6qEm|Cvkl4I2a+$bffr+?cdGFvw$SJc*S6#x! zO^J*5L-zDWu#ps&M@u;Wfp1ORTxf8fK|tgo_Pr!jkv{RyENW8aOHL_i&Q$(x*zzR@ z2YjTRmBj40Ao@1p5D)mJ3Ta=DJ^8_q#>{0Ye)n1i3ncM8NWE$lPjL)%Fcx1FSxng7 zkiZ{%jed5G*qC2L2qr!>nUmv;VRyI{_+dE&oU5-)E(P~tA6CB7a5 z)s#+f>Az2gme)~wq#3y@fX>l-MPc7C?AGH%B17B~1< zdnydRdXX`iNlrE`4fr#POcA1y!Wg}tVDEZm{-b<;$k)cdNLI)x6D=M`nHM!9%|-Mudlsh9WhE$v4ip`BN1AL7is!h!E0vhyeY^#zGwC;#iu+eboBVK zuC<>M(%*wJ2|v(5`_&mBPmV_ zG8TK$(t9eTZ7tS>oy~Z#iuwy33N&8MM6aN7%q`q-x{ThUkl?W!lh_f5+X-+*mv4`V zBlR?dXZx*Q#mA)isH4}neXiJ9)$8ocYY`U|iwWWDhP|?8AntDzBotSF)*e}nqjap` z5isc`PfPHCn}nB|NntcAe^M2DNGDtENJaT_t{2BQD&6gsX}RtKE3bUJYYv_4=s=4p zqUM_Ps795ByC-ucmCI!d*mt&b|AyIFmR#+raSGc_Vr;K)2=ten#x4~mx{jRXl>J7f z7>uISPnJ!BHEO)ZtPg*fzC(8IJ)24`_m7mOx{_RAIox3rd-AX_iCg0bSQ0fX(uR>w+vKy4h@TRnXSZrs zgy5SEQ%!;Ww+Mvl+#9#!y;!`XV2JC%q$MMo5n}1y*`KfTLMqX8>|TCH2J65+H4%*# z0Z5Hgt~6DI_`&+wE^N}Zi@`3SOAFEDKB8;dSUoGlXGMLx$3lAN)Rfc1O4r3-%1_&r z3im*0wixI^o?v?pU4dN5!?TRRb#+;c7#`_;Q^~)dAsS&}mjja;tr9s|bJ@@9@q`I+ z^~lw*2bxO}w2O`8&~ayaruI^~@IUg^k3S1qP#HSVQ?+!0t_YH?L}zMj{gsNT%zUzE za(6waQkwXe&kCf!cA(8l-?s4M zBafrbN<>X;`d#J=ue(up`|JK~Jc&n_V8b))Q+Facf+p2CN+(!hriJjzCcJh7Kj3%` zxpaK?)5Q^9+Af@!vDPyjx2=cb#U-Ye9PyyvEmbuQ-G4xX#fwmDY09=g}=Cx>~qxoTIeTngQ^#PKX8mZZ#qeEXT_-|7$+{gRVI$~SrP z96np!=WGMaFzJ?;UDTd!3BioCT{Yqsv1Q+ zrF@m@UKs1~`~KwXzIbpwoKHip#RNEUvOQ&qzTYPQTmG_`IvwX15x@9vwOWix1tk6eD*)VtvW-~I4| z^5k!6@3VXA`dY_!$!Hw=LAx%$ei!14kM{Ll)>Vgj4qWOpS{S$|EPT{MHLu}XcE9>U zZ$FjtQuP2&SmrIL%gFBoK@&TCUIfm>zLMOE&@$7HEL z5r6~?`+Fq+M0@x zYyFRiizeX>YJx$u>5;|`Mumyd+lc%lY?t;Goa6o(7siE~5ZU<4CsX3dk{;U-Fna22 z5Y0WtZ(WV#U~>yS^NWt`Cvvrq^CoYoq zjCf@37A$}4>#ge==m@9kz<)gGJyq}InIR-JuhBp|jURZK_aa`EF!2Dkes-?e3Dq;0 zhL6ta?}M5cNR(yANj^O$Vgfrl#&-X%M1lx<%|vuS)i8uU35u`GS)ye@flG;BkRI`= zwZYUtZd}v)@;Un@O_DoEX@0}<0vG=b=a^X)m=?b3!zNMM+6Fp^f!GypNwB!R+$ZJ{ zR-$^?L&vVnv1?ZCy)4ycC`?iWvXYi^2B9YC5gBw+Y?3U3pOx#>)KK`t@uPaz$k_3c zt7Q#N76&0BzDKLQR+=D?-D}L^FzVSIP*N2UYCn~(Jo%PO+C$`PXbPJ;Lk^Y9w1+{hvx7xtERxr3-}L~17x?g9wGG)gFNDeYyt(XpG{ty7I)IAj zncm7Pf3)I$zj(<#6^0q#+3b}iKh~%XW&7Uy46^yaCgU2j%vp>U{t9{WX*AslrlP6x zKzVXnEIe+qp(WXH6-|iB`(yOkO8V#?e)Lv%|BqK}@!huoCw`o+=xz;CD*H#xJ%*K& zGOhLY`-8~~eTgdJ-H6JO$w6Jgx@2Q(dL+XEyAk9pv9-ijnX_ET`vOD)8xhee#njsg zYQc4%_!{O6F9~LQ={rxtM~rve>ikAhE^LQlg=Es)wkHe+<1HqoxHC_KE%D4gK%cpq zu0ih$q@-pPRMfAd&La*D+7s|{v?evX@OzXh3FFVaBiUyT%qBLzKPbtMv3ioIxw~%f z>o!^WNnz`^J7~b(OHi*zs|{u03{o_-{M)TAxSPJrcxC3Jj}byZ!~KpV{N0G`C?H2B z_x*rLeDA4>Nj}|bLRxb#&PP;yttUr6{z(m}ef5Ksb6PBvs+BJsK~q5SL#}L}0?9Rq z7_8xjWPHEd>v3P{z{27K#?u3QUKgL{sC|pmF1Wu_?v(3)+1V-Jc&Str$A#OKQN1X| zk|ZDBPOrjwI)V0}(QhQ`sbY>-*KxweF?Z{;*oeD%j{d&b+DAnz#Myl9YP&gZeZ5y< zc0DCm18SWP^ah*WXg5zxg*^GA-f@He!qmz)wYH*{&E@dFcTstl#+?&y-I*yF;D6--XJZ zU@LJiiT3d3i6Gu4{%uAHu)7;m{)jcfeEs|%%4&+=H&_slb|jJyrJGugOil7+30kXK&_GChJV z5%{*Fey{gK+wd?MwfB9f;^2i(A>JNm4ndAbCn-+);y&Lme8qtGPQ-Ixz8u!*in!~g zycMx#yBI<;c=g|TG_29s(yZ`JW%yxKeeUNRr@s(lMl{Ni6kFt*U6bNu!Q_6TZ!ZA0B+!n6>=efWXaYG*GC}Xt6|mb{RYp7TayV7T4`qJ$1BDRj6>EBMVkW+vdZ_6wxZ~y>ZM{_UhK<8eF|~K@ z<}kO|j`?DG{>I<5G~usLNBBFt3vO+5Pq-X#p7e0yh97`DxqB2$cj9ol(SlSTgK{!A zA;j<65c|oCh<`<_9bd5d{dVU;tMjA|ef2CepwxuLl`K=n4jcDv` z*kQ=KYcp#HA@QcN?v*(cb3w4i*|~TDdVW_9VLa63%g=6Ues#r)q*fad_cRFuZhKmG zz8$L#wCF>T-A%&JZ7j`*M_O~FUbT+c7>@kKPOpIHr0so{P^*8D!w(U0X)Gr$9{R zOG!cOEBAzwcaIrRGxEr_)HaSFAIMlB+8IOMM0Na>UKnuQ9pXf?UD6ng*Q?9Si zjb}S|*U46qjOs0+sM2D7<(22$wV-_5@WaLGoXK7}T_?a=l+QCcDJ=oclyYDOp?K4X z%Ig}YFz%i8@7ru|6=aI@a|6)H?9IppYEwK%zy?i@vHKfoF7i6ZI|)up&^1bWMWL#O zZz}a(cz|l}K$jw|aK8Ams9(+CYh7D+b5$I&j&=)}G;00*QjR((RZQ6Sk)-a2sLlQ7 z>qe?JRhLF{QD07t>R30WOS78a&!Z512gssZDir&fg_RgQu4y|$(z9oXK~ZseaJ);$sAB8>gr}w@DET*;^2pHJ!K*OI?noFk- z-W+g?x7th`$rTi{oyMLh6rSvzID%!Z=c+Hx53M@*?-~N~q?dOf1RDu>v+A@M16=Xx z@M3~|Q{W#i>KOeqr7Q0JT=_;O8m3-EfKS}oURVBZf-RtoyAF^!eM>54;(p>A-Wl(> ziV;~MpMsdhLE;;;lUa$@zpq7Kx(TN-FPZ0Le7Z;aELliL|>+R=1P@!M*y)AGeB;Tv@E#K&%ETo#RR;1 zwI$BCPadB?slBUSG5ou8oLJ1k2g$l`lw7ldi33rPzGYp9DDZ2>(vkg5$i8>6 zyEPyQx||HfK$~*#pCIVz*m|{)zw+>Y7vB@g&!K1sC{P~LV^G#hzO%LG6$DY1rCurZOwRKYpb(LLLS-}by*xC=GaRy{L zm5W}8L?&m<);T_>j6_0vx$#2~bo>4}aGKW{{%k&A`6XHPqotDs!+|~c?_7XtZlw6d zX+%y>T$-v*h+lMW`mjfV*P)LU zrn&~+Y&_v<(hqB-Bi=ApvSy(z8;z&)tehUcuLugBx)R@9Lqfkjv9bn=kP6dZ2_E&j zJFeLq-kgfVm!|&q+~e@dGsV6kQXXxEe=A*?|Av1@ZcrS0eF(RdrZ9n`{yhB5u9aOP zgQWRoKJ-Ot04u!pIh33;K<=`;mhF@78`O7-xO!gzH&4(*6s2&70SH1hLa+@2iO>4? z$m;68>HFxrN}jwa^kt)XPfsE4%;kNR9^zu*QeGGd^Y;fH^PSyV3V8NwKU?}GJS$Si zF3c4+wEq${=y#b)`Q%__xn*tc~P(Ybr29WpATR|&lO@{fp{@p zTA^d1rziOg#cA=+5T(&YtF1C6wL7-!3qeIBJG>xydN%);f($zSWQ!x#XhO&qZ?0=Z zRy6_*i2>_^HUn#y;pgg>sv7*(Z?a3E>jNBL+*4>3Ogbebr?^d4zSdimP_cB`L|AUX zQCU>^=Bp_l$d%fEp#`04fL0}FR=oux_pp(6c%2ol+EbOt^)~q&ajObHg3}i&b+)AE z^pW^TSG0Ry^&rv_ByVCn9hx%sl`IfK>oF~?^Xq*8r$_hLE)PoRG#_fslbbyoo0?N= z&Y>v1=K#t#cdIM?r6%6%%GXS)Q;%+S=bp}-qptANb{a9lb}~rHAqbxwQwTxz*VR^# ztxYs;n&vh=7*Q&;mGZK-8x2etc<(%65#p6hSt?8rBVYy5*^@d;Y%*MC2go-O9jA!( zgDi6Eo^y?-@fpE+3xS6Boxi=fTcVorVgH zm7EJZfs<*X2DKtzxTDUdtej3?6vgZw6^M)2()UXpZbD)>RAz4ze-1!7+D=1^_pN5~ zLat!?3$EG*KlO8z11cTy8)L^4>9%OxJ4&36Ok0ufD|wh_KskHMf|AsicM#6?CQB6K z8%q2dhi@&$uTP@Mo+tLOyP9eC!yj~l`tSOF(cg9FQ4uK~cMf?T?iVsuwIFZ;X}I*S z+N}=Ql2~Lel>TA|mV|ot%$!Pu#Z_BL*;Ohm!q^4z0^?|jb=j+`2Hm)(Ih=HdNR`1a z8ll?ul9w02T9WE>a{}HJ++O0|Hg`gyNeAb1tR9+`k+x|kape8ol|PdBs5YH0jtu*Y z(QFGo7Wyr?^#)VhcX>OeEkzSbB2sebs5M^@E8g0_!YYpA?QQvxzBX<(q=87 zk7DgZGPXP9Yb4696OleB1{};Z%cmUO4*m1=@6QLH<8LO-%MS}!T#Q?&jB2O8?k?+) zxPfTl@7{dS+nYu33P|0KHdoAsKzYA zkueMNv^Mbi{QnqX@XsQKeDydMjoQ0|X|QS6NBa1plF#A6sqLnQ$pBRGYj5v7Zhd`| zD}H_RB^~>Cvxh}!PMa(jbZ(nvqP=4&g&<-zf=o$Z`{g{yO{7Vn7B`{AhR~&MXg?c1 zb{xfe#%UT#P*$GP##AIrYo|)TZk6I|V^`Bq16BV~b7MaDEiKST?}T9daHdib_+sGA zLOTSYvxlG|87nIi3|oaA8*=b+IdmmSXl`GqumlqLB=iYBF>9f&kjb|^UU_|VFN}i} z=Ao@nChKNaXKaXC^J6!nq!CemgExG$ zUHbK1dWJ+*6Gkr;(YEpUx$bFMF;wfu*`xu*06h`97aLEHZh@U(18v7C(~l@NKNM8( zm!L5LOQLdG$sseOr|K+*HGIUMJ845wm~HD(R>!?c!#aWve4cZK_>spuA_q9!a|fZ= zkqNT$S|XWz$y{$YRoBc%XVRHl@G$qAvrae_V(&}*OI~b#?IFdyNr?>-{F!*luM#E9 z?$qx_DXJx=!~1C|JHst$V#|Us?jzN444SE$Vy16=5O9bwzfM z@OAHghX)s`vU=veiAJ}hOm-$n@Lo*ekO-N;5#s%RV|;MIpB{fynp z-I@-q^fQi8`Xp^;H^0^3y9Ij8=hb?IjYM%J^#WLm(#cSrAGL0B%q!EszVFDDtFoR+ zh8L$exq&-lpN8p9PPv+*rw}heE4#nom*Csjb5aL4HVf-2F_JS`$g8>QGHj|#Xg_p$ z7@?MNLnCATjqlhZLpwV*p};H!`glabs!Deuv^ZS_5!F`_SwU9B+}~(V_YhHCW!p+l z%}7knMUYPXRhCsRxUa$CTb!IUr^1M}g_3&5+r-k~x?5(yB+Yb>zd^+?v;%2c8OpfM zUg9-)^WIc}lcv3*dZm3eFpO8$TxGS}xYN@rVh`bD=?dH}G|1+KCEwbcGndY&Ieh15 zQ@ayZ-5I|1bhKf#=vQaF;jNJd8}8uLOLD%k((88kwWTbwgOjfNXnfx1+;tz5x@MUz zYO{47R&{W*v%oxsqu*E6oUhk#{5-ohJmqr(;$sLj#-%ZzYeH9na*Ejb-f=4jlQ-hm z_A!f`46NwzKXj}rAtPWA$XL2EBicr>frlXc#Em#q=ApwLf#Ue36KyMqU2aBe8@?w} z2M%8|AUO+N_v4Axh(E0@(4&GB;mx_a&s4y4-=R~1KnmNIQ-~BiQSI@0=l2Et>eKFN zDnZNB`dGBSPVaF<3eSZfwfAExE?8Y_Kq) zvoP5qzuZ!7pyD<3MZInsMp$hiV4ovOIQmj9U>7(W9iV@(-G|Dvr%^=v-t6Y+Nw0{U zTLvR_Qgk?1S7`LEPC^>VOef0k-Id^weg0!j>&MRy`uZzXUCki9{xf1?6|l;2ah4Lh zQJbX?KeZ@-K8_F@#AOG0eGtGp+S;F6`X5GG;Br$1Z2j^u>sw8cw~>d&*~L@t7fa?! zNuh2N>Y7d&B0A*5Ms%R%ukZ`3c=k!%!HK1;&GqNdxbE%;k99DVU5?(h?>`Uk;RxF- z3w4buwgzqdh4=MVI+I$BybX6L@|fm(J(v%*?)ey&5QZuB&?-U6L09k92=7$>akPIX z+9%F_W5A}@KId_4TTf)Ik%WXCmw@PQm*q^`bfuO7CijBDcaa2;^Rbj1-z}6qjp4%B z%*S^tk!u~!VhB2ogwaO59XOgo5vAg-&uV43+>WeH_9>surL&1Je0Y|9Y^Gu@Vm(PJ zd(FqF8*RdIuaXY&qk*&F6~c~zt0z$#twdQlX0v2z5tzYWjv>;29_tL+E+{RVz!K=; zh2_;FW`*MfRq@vge(p*vFRTN%mTRtOj9w}*upTGx+>6v%Q4z`xeSQhH7Ae%jZqPGv z*gQBCEk{tO9`C0Ncb_rN4@}6Ac{8!mPF7loc-GfYR`G`&)8XJawriy4;f>_^+j#xD%`FuGgC@NIVTk-OOoO>*;Qh3J>6w<0AJdQDei{Vvax z<(AcLF5-|8JNYr7trrdVjj8#GJvwSicF5dsLDY!*R+S)&FwXJRbDs@gi??8$w z;68xJOM{T=8iB{sM89e)HNo&12w$c>;&5)V8LSSf^_yuKfLA=o>ze~@_-QT}O|6b1 zOA2m8jGOiZgr}!5vio+0Nh{bJJq@Dsk0S%JK;X&R6n|P=y?5AwJM`PIIR0{y!C^>F zYH?z$@?=PM^I_fDSLt|LI?Uox0zDR);hkil!}jX{f7=Px62bZO72W5yu!%QF#r^X= zogWkOZhJ>16B8p-vG&g#?*}^%D*}HhG>-A`W#9B2!s_x0&0NplqVCSZ-*qmn%%~hV zg8YCu-SafgZunawiVY6W3$ZM!lH&C2PC3NcK<^44XrM5#_gsk3J1bw~1_(|S)Q7Vd zdL7Zt3q}UkHs6LSwF<`NSHlqD*D75uU`Z*2;0otd9j!sqd?c79rS&A*i)-+0-_t< zQx|8xG)8(P72HpFW#E;YA8Z7tKEh~TR5pzue*_0jQN7E#Wl!RfF+Q;**`GV##!YA= z__v4k{WC;XykdD2`?kLmt!|by*kn*+sHH1X-0?T@=#x!;D!1$;2mGwOyENtpH}6_t zCzQ7W2X7GHmIzps2PFA@d;L84&v}!XwQ)>ioNp1{?q+{B=f3MY3dEWzyF}&ZEW4X} zGKFmdahg)BBe{ez!~V0KYl&P^#cmGkG%)#ZnmCOtnsk(ZazefvY# zWWXzzl?W<(UsZ7z0tdZwxhgG>J<*#d488sKIe!I`u{?2nm9m>HF;zl}ZrNBs>0#Sm zp6z?nSCtJdHyLjWU#$>|stZR-{l2QhwNroCx?Bn!m#-7fme&!Qt0TxcmrbSecrm<- zT(zj-F^`#xu%~g8^ZE@gnczc_d9iX_({Cew>>N&gERReQV`a%USqoX#{;_uOiN$+X zP*eeDN}70|u=?{b>QH`~UB#fsx<{vS#)Z!0bwr{^jXq$rDpn5nR%J9V2II|UJ0)7{ zo8+)(CvR!D+X;kkB(^ngs6HK(qgs~7j+P{r)c+{jB@>s)qC1ilnnlyF^6GkzboruQ zcjBQd}Y3l$90gj0|1L<>;!4m&APkX${96! zDvtJczS4>4LygU7%zD@uI?g|T$$kY�q{4&9e6w7qsWtQp}&(7)y9PdmK`B2O#lx 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 Date: Thu, 24 Feb 2022 17:04:30 +0100 Subject: [PATCH 0066/1204] Support multiline comments in ProducerOption.setComment(). --- .../encryption_signing/EncryptionStream.java | 7 ++++++- .../test/java/org/pgpainless/example/Encrypt.java | 15 +++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 37b7288e..d3f0cd54 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -76,7 +76,12 @@ public final class EncryptionStream extends OutputStream { LOGGER.debug("Wrap encryption output in ASCII armor"); armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream); if (options.hasComment()) { - ArmorUtils.addCommentHeader(armorOutputStream, options.getComment()); + String[] commentLines = options.getComment().split("\n"); + for (String commentLine : commentLines) { + if (!commentLine.trim().isEmpty()) { + ArmorUtils.addCommentHeader(armorOutputStream, commentLine); + } + } } outermostStream = armorOutputStream; } diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java index 3a3888a1..385198e9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java @@ -133,8 +133,7 @@ public class Encrypt { /** * In this example, Alice is sending a signed and encrypted message to Bob. * She encrypts the message to both bobs certificate and her own. - * A comment header with the text "This comment was added using options." is added - * using the fluent ProducerOption syntax. + * A multiline comment header is added using the fluent ProducerOption syntax. * * Bob subsequently decrypts the message using his key. */ @@ -152,7 +151,13 @@ public class Encrypt { // plaintext message to encrypt String message = "Hello, World!\n"; - String comment = "This comment was added using options."; + String[] comments = { + "This comment was added using options.", + "And it has three lines.", + " ", + "Empty lines are skipped." + }; + String comment = comments[0] + "\n" + comments[1] + "\n" + comments[2] + "\n" + comments[3]; ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); // Encrypt and sign EncryptionStream encryptor = PGPainless.encryptAndOrSign() @@ -172,7 +177,9 @@ public class Encrypt { String encryptedMessage = ciphertext.toString(); // check that comment header was added after "BEGIN PGP" and "Version:" - assertEquals(encryptedMessage.split("\n")[2].trim(), "Comment: " + comment); + assertEquals(encryptedMessage.split("\n")[2].trim(), "Comment: " + comments[0]); + assertEquals(encryptedMessage.split("\n")[3].trim(), "Comment: " + comments[1]); + assertEquals(encryptedMessage.split("\n")[4].trim(), "Comment: " + comments[3]); // also test, that decryption still works... From a1deb531a496ec694b8e105aa5c5bf427ba00f10 Mon Sep 17 00:00:00 2001 From: feri Date: Thu, 24 Feb 2022 17:20:57 +0100 Subject: [PATCH 0067/1204] trim comment lines. --- .../org/pgpainless/encryption_signing/EncryptionStream.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index d3f0cd54..27b8958a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -79,7 +79,7 @@ public final class EncryptionStream extends OutputStream { String[] commentLines = options.getComment().split("\n"); for (String commentLine : commentLines) { if (!commentLine.trim().isEmpty()) { - ArmorUtils.addCommentHeader(armorOutputStream, commentLine); + ArmorUtils.addCommentHeader(armorOutputStream, commentLine.trim()); } } } From a681a27bb71bc510a5c61a1c674225164b8f5ed3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Feb 2022 15:56:56 +0100 Subject: [PATCH 0068/1204] Add logo svg --- assets/pgpainless.svg | 110 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 assets/pgpainless.svg diff --git a/assets/pgpainless.svg b/assets/pgpainless.svg new file mode 100644 index 00000000..47588bd8 --- /dev/null +++ b/assets/pgpainless.svg @@ -0,0 +1,110 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 69c0a1bfa43c8ffe6ad6fa051811ff7b4963fa99 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Feb 2022 16:06:01 +0100 Subject: [PATCH 0069/1204] PGPainless 1.1.1 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 2 +- version.gradle | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e6cdda9..a91a90c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.1.1-SNAPSHOT +## 1.1.1 - Add `producerOptions.setComment(string)` to allow adding ASCII armor comments when creating OpenPGP messages (thanks @ferenc-hechler) - Simplify consumption of cleartext-signed data - Change default criticality of signature subpackets diff --git a/README.md b/README.md index 872d1549..4dc5d2f2 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.1.0' + implementation 'org.pgpainless:pgpainless-core:1.1.1' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index c20b58c4..f7f3856d 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.0.2" + implementation "org.pgpainless:pgpainless-sop:1.1.1" ... } diff --git a/version.gradle b/version.gradle index e88ae14d..0e0f97a3 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.1.1' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From 37be70e0f36ba080b04fcc2cb4a15eaeda01d5f2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Feb 2022 16:11:00 +0100 Subject: [PATCH 0070/1204] PGPainless-1.1.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 0e0f97a3..03d66410 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.1.1' - isSnapshot = false + shortVersion = '1.1.2' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From d876f770a61ee1d1c6a590b55e69b61bf3f9a025 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Feb 2022 16:12:56 +0100 Subject: [PATCH 0071/1204] Bump version in sop readme --- pgpainless-sop/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index f7f3856d..00196f93 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.0.2 + 1.1.1 ... @@ -75,4 +75,4 @@ byte[] decrypted = messageAndVerifications.getBytes(); // Signature Verifications DecryptionResult messageInfo = messageAndVerifications.getResult(); List signatureVerifications = messageInfo.getVerifications(); -``` \ No newline at end of file +``` From 1088b6c8ae0fae7bc20202982bfb6daeb8a9d88e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Feb 2022 16:23:27 +0100 Subject: [PATCH 0072/1204] Add dep5 license info for pgpainless.svg --- .reuse/dep5 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.reuse/dep5 b/.reuse/dep5 index 99b0abe2..f820f003 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -19,6 +19,10 @@ Files: assets/repository-open-graph.png Copyright: 2021 Paul Schaub License: CC-BY-3.0 +Files: assets/pgpainless.svg +Copyright: 2021 Paul Schaub +License: CC-BY-3.0 + Files: assets/test_vectors/* Copyright: 2018 Paul Schaub License: CC0-1.0 From d55d6a16866e7984fa81a43d6082f1a100bb3018 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 1 Mar 2022 12:14:09 +0100 Subject: [PATCH 0073/1204] Improve RegExs for extracting email addresses from keys Based on https://github.com/pgpainless/pgpainless/pull/257/ Thanks @bratkartoffel for the initial proposed changes --- .../org/pgpainless/key/info/KeyRingInfo.java | 12 +++- .../pgpainless/key/info/KeyRingInfoTest.java | 58 +++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 74b72e61..8b47e2a2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -50,7 +50,8 @@ import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; */ public class KeyRingInfo { - private static final Pattern PATTERN_EMAIL = Pattern.compile("[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}"); + private static final Pattern PATTERN_EMAIL_FROM_USERID = Pattern.compile("<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)>"); + private static final Pattern PATTERN_EMAIL_EXPLICIT = Pattern.compile("^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)$"); private final PGPKeyRing keys; private final Signatures signatures; @@ -421,9 +422,14 @@ public class KeyRingInfo { List userIds = getUserIds(); List emails = new ArrayList<>(); for (String userId : userIds) { - Matcher matcher = PATTERN_EMAIL.matcher(userId); + Matcher matcher = PATTERN_EMAIL_FROM_USERID.matcher(userId); if (matcher.find()) { - emails.add(matcher.group()); + emails.add(matcher.group(1)); + } else { + matcher = PATTERN_EMAIL_EXPLICIT.matcher(userId); + if (matcher.find()) { + emails.add(matcher.group(1)); + } } } return emails; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java index 0cb351b7..8bd053f2 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java @@ -50,6 +50,7 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.UserId; +import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.DateUtil; import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.Passphrase; @@ -701,4 +702,61 @@ public class KeyRingInfoTest { assertTrue(unboundKeyCreation.after(latestModification)); assertTrue(unboundKeyCreation.after(latestKeyCreation)); } + + @Test + public void getEmailsTest() throws IOException { + String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: B4A8 9FE8 9D59 31E6 BCF7 DC2F 6BA1 2CC7 9A08 8D73\n" + + "Comment: Alice Anderson [Primary Mail Address]\n" + + "Comment: Alice A. \n" + + "Comment: \n" + + "Comment: alice@rfc4880.spec\n" + + "Comment: alice anderson@invalid.mail\n" + + "Comment: Alice Anderson \n" + + "\n" + + "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\n" + + "=ZRAy\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + + List emails = info.getEmailAddresses(); + assertEquals(emails, Arrays.asList("alice@email.tld", "alice@pgpainless.org", "alice@openpgp.org", "alice@rfc4880.spec")); + } } From 63b39c56bda22567688b373b9a1618a8363a9209 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 1 Mar 2022 17:18:20 +0100 Subject: [PATCH 0074/1204] Fix README --- pgpainless-core/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/README.md b/pgpainless-core/README.md index d7b0a036..82afdfb5 100644 --- a/pgpainless-core/README.md +++ b/pgpainless-core/README.md @@ -9,7 +9,7 @@ SPDX-License-Identifier: Apache-2.0 [![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-core/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-core) [![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-core)](https://search.maven.org/artifact/org.pgpainless/pgpainless-core) -Wrapper around Bouncycastle's OpenPGP implementation. +Wrapper around Bouncy Castle's OpenPGP implementation. ## Protection Against Attacks From 35dd4f9a67e14d5869dffe579e489966ede9ccec Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 1 Mar 2022 17:37:24 +0100 Subject: [PATCH 0075/1204] Fix unused import --- .../src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java index 8bd053f2..8379e0cc 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java @@ -50,10 +50,9 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.UserId; -import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.DateUtil; -import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.TestAllImplementations; public class KeyRingInfoTest { From 1949cc5eeae94a99be103e3c25b0e0d788e7af0c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 2 Mar 2022 11:15:07 +0100 Subject: [PATCH 0076/1204] Fix generics of CertificationSubpackets callback --- .../signature/subpackets/CertificationSubpackets.java | 2 +- .../builder/ThirdPartyCertificationSignatureBuilderTest.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/CertificationSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/CertificationSubpackets.java index 59356bba..24614882 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/CertificationSubpackets.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/CertificationSubpackets.java @@ -6,7 +6,7 @@ package org.pgpainless.signature.subpackets; public interface CertificationSubpackets extends BaseSignatureSubpackets { - interface Callback extends SignatureSubpacketCallback { + interface Callback extends SignatureSubpacketCallback { } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java index 89732991..bfc83df4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java @@ -13,7 +13,6 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.subpackets.BaseSignatureSubpackets; import org.pgpainless.signature.subpackets.CertificationSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; @@ -53,7 +52,7 @@ public class ThirdPartyCertificationSignatureBuilderTest { signatureBuilder.applyCallback(new CertificationSubpackets.Callback() { @Override - public void modifyHashedSubpackets(BaseSignatureSubpackets hashedSubpackets) { + public void modifyHashedSubpackets(CertificationSubpackets hashedSubpackets) { hashedSubpackets.setExportable(true, false); } }); From 2e6ae5c117cc00e854846768bee155ad3031b00d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 1 Mar 2022 12:26:29 +0100 Subject: [PATCH 0077/1204] Update README --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a91a90c8..0e200863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.1.2-SNAPSHOT +- Fix `keyRingInfo.getEmailAddresses()` incorrectly matching some mail addresses (thanks @bratkartoffel for reporting and initial patch proposal) + ## 1.1.1 - Add `producerOptions.setComment(string)` to allow adding ASCII armor comments when creating OpenPGP messages (thanks @ferenc-hechler) - Simplify consumption of cleartext-signed data From 54b443f18302e239ac99358213e905767c0d3cb2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 2 Mar 2022 11:36:55 +0100 Subject: [PATCH 0078/1204] Document generics fix in CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e200863..3cf07aa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ SPDX-License-Identifier: CC0-1.0 ## 1.1.2-SNAPSHOT - Fix `keyRingInfo.getEmailAddresses()` incorrectly matching some mail addresses (thanks @bratkartoffel for reporting and initial patch proposal) +- Fix generic type of `CertificationSubpackets.Callback`5 ## 1.1.1 - Add `producerOptions.setComment(string)` to allow adding ASCII armor comments when creating OpenPGP messages (thanks @ferenc-hechler) From afad3fc7470faa0796ddc23ba17536ba5c1fd44e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Mar 2022 14:35:52 +0100 Subject: [PATCH 0079/1204] Fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cf07aa2..8c8cbdca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ SPDX-License-Identifier: CC0-1.0 ## 1.1.2-SNAPSHOT - Fix `keyRingInfo.getEmailAddresses()` incorrectly matching some mail addresses (thanks @bratkartoffel for reporting and initial patch proposal) -- Fix generic type of `CertificationSubpackets.Callback`5 +- Fix generic type of `CertificationSubpackets.Callback` ## 1.1.1 - Add `producerOptions.setComment(string)` to allow adding ASCII armor comments when creating OpenPGP messages (thanks @ferenc-hechler) From 5b9e72d42c761de781852232c3921c0c1cac63bf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Mar 2022 14:58:36 +0100 Subject: [PATCH 0080/1204] Add KeyRingInfo.isUsableForEncryption() --- .../org/pgpainless/key/info/KeyRingInfo.java | 19 ++++ .../pgpainless/key/info/KeyRingInfoTest.java | 100 +++++++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 8b47e2a2..a1818902 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -1009,6 +1009,25 @@ public class KeyRingInfo { return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)).getPreferredCompressionAlgorithms(); } + /** + * Returns true, if the certificate has at least one usable encryption subkey. + * + * @return true if usable for encryption + */ + public boolean isUsableForEncryption() { + return isUsableForEncryption(EncryptionPurpose.ANY); + } + + /** + * Returns true, if the certificate has at least one usable encryption subkey for the given purpose. + * + * @param purpose purpose of encryption + * @return true if usable for encryption + */ + public boolean isUsableForEncryption(@Nonnull EncryptionPurpose purpose) { + return !getEncryptionSubkeys(purpose).isEmpty(); + } + private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) { if (getPublicKey(keyID) == null) { throw new NoSuchElementException("No subkey with key id " + Long.toHexString(keyID) + " found on this key."); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java index 8379e0cc..bfececee 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java @@ -218,7 +218,7 @@ public class KeyRingInfoTest { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder( KeyType.ECDH(EllipticCurve._BRAINPOOLP384R1), KeyFlag.ENCRYPT_STORAGE)) @@ -758,4 +758,102 @@ public class KeyRingInfoTest { List emails = info.getEmailAddresses(); assertEquals(emails, Arrays.asList("alice@email.tld", "alice@pgpainless.org", "alice@openpgp.org", "alice@rfc4880.spec")); } + + @Test + public void isUsableForEncryptionTest_base() throws IOException { + String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9B6A C43E A67C 11BB C023 4CC3 69D5 9A7C 29C0 F858\n" + + "Comment: Usable \n" + + "\n" + + "mDMEYiS54BYJKwYBBAHaRw8BAQdAr0FXsDQtIpF54UwfjQb+8XJ3jxt3LkpCh0e7\n" + + "lH59Vzy0HlVzYWJsZSA8dXNhYmxlQHBncGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJi\n" + + "JLngCRBp1Zp8KcD4WBYhBJtqxD6mfBG7wCNMw2nVmnwpwPhYAp4BApsBBRYCAwEA\n" + + "BAsJCAcFFQoJCAsCmQEAACuNAQDX+7/ffM2B9qaW+F9MkeUJeq9u8MLk+BcaotQZ\n" + + "/c+8pQD/RhaVmKTLjm+RmpG2O1lrkta4L5CQQBXYdNMnebhlLAu4OARiJLngEgor\n" + + "BgEEAZdVAQUBAQdA8Et257jQXR0oJOimAWU9Z5Erq5OcfguBI28ixgw5z2IDAQgH\n" + + "iHUEGBYKAB0FAmIkueACngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRBp1Zp8KcD4\n" + + "WDQYAQDtJG06gAiFk7D1EqdtoTgBeIXi6pdKJ8VQA17/Sel1PgEAjO7Gy+RishFG\n" + + "eT0WwimGAGWOFgyIB8GCmuk1sEN+9wO4MwRiJLngFgkrBgEEAdpHDwEBB0BNGWZx\n" + + "IiCzs6Acu/e7Di9E+uUZmEA7geObWgwPleedLYjVBBgWCgB9BQJiJLngAp4BApsC\n" + + "BRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUCYiS54AAKCRBsyz3UPPzzw6bTAQCZ\n" + + "4NnXfhuyw2itPKNnVSvPl72GgHzfVb2MZi2QBPFJyQD+K7Xl6qNcaI9VyMos8zSy\n" + + "VT74iE7Sraqu2Fck27y1wgMACgkQadWafCnA+FjLFwEAxb/GFdAoUgmY6DGIbatO\n" + + "LOIorswrgSQVZ8B1yLh1gxcA/2K3XO1Tl68O961SW60CijoBY/16EFC+mkQIzxTT\n" + + "J5wP\n" + + "=nFoO\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(CERT); + KeyRingInfo info = PGPainless.inspectKeyRing(cert); + assertTrue(info.isUsableForEncryption()); + } + + @Test + public void isUsableForEncryptionTest_commsOnly() throws IOException { + String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: B2EE 493D 1DAC 943A 1CBD B151 5F15 42D1 ACB7 D26F\n" + + "Comment: Comms Only \n" + + "\n" + + "mG8EYiS7mhMFK4EEACIDAwTENCF226L9l1i24ZpHuTK9P9kEc7neMZ1cQbJFSX9p\n" + + "ZP89dp4dnjZcAop5jzdvqjU98BgX9STZB6q2qYEG46luZoanDA0dpwzm0TENAvcr\n" + + "KoeIMqjv6dkKs5k11qtFx/K0JkNvbW1zIE9ubHkgPGNvbW1zLW9ubHlAcGdwYWlu\n" + + "bGVzcy5vcmc+iK8EExMKAEEFAmIku5sJEF8VQtGst9JvFiEEsu5JPR2slDocvbFR\n" + + "XxVC0ay30m8CngECmwMFFgIDAQAECwkIBwUVCgkICwKZAQAA3u4BgOl888SnxXys\n" + + "Ft/sPRh/hT8n0ObrxDHUgaAR5J7Sc3097u1r3ecCYaY045FYKKb23QGAjGSEEFG1\n" + + "TLbM1JMsE5H7xjjjJ5tTM6l45vkkrk3uMhsCL+QLv9pp251ctTF/JSCvuHMEYiS7\n" + + "mxIFK4EEACIDAwToE6c42GWSI0zmalisYewWvV/2Sfdo9KKgxfzX3rfldrOWFkN1\n" + + "fkLy6b01AUt3RqfwEBIJK6OrSXOlmdCiRV1Oqf20f2MGsDNXAttDApSSDJIHwV24\n" + + "3i6qylin0ujQ9KIDAQgHiJUEGBMKAB0FAmIku5sCngECmwQFFgIDAQAECwkIBwUV\n" + + "CgkICwAKCRBfFULRrLfSbwoYAYCzcZ29xIRUEHzZvAXWeHselBLdLGztZSBZKd9T\n" + + "m045mewePa780jk5o2z5Nt4Bj0EBfRxoiWt/czpy0nWpyfEeTHOx32jHHoTStjIF\n" + + "2XO/hpB2T8VXFfFKwj7U9LGkX+ciLg==\n" + + "=etPP\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(CERT); + KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys); + + assertTrue(info.isUsableForEncryption(EncryptionPurpose.COMMUNICATIONS)); + assertTrue(info.isUsableForEncryption(EncryptionPurpose.ANY)); + + assertFalse(info.isUsableForEncryption(EncryptionPurpose.STORAGE)); + } + + @Test + public void isUsableForEncryptionTest_encryptionKeyRevoked() throws IOException { + // encryption subkey is revoked + String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: CE65 608D 8639 E20C 61BF 077B F010 3226 1C64 5EA7\n" + + "Comment: Revoked \n" + + "\n" + + "mDMEYiS8+hYJKwYBBAHaRw8BAQdATvSKAaY5yvyOdJtZXBEXbyiWSsExOwnP2L35\n" + + "AyMPe7u0IFJldm9rZWQgPHJldm9rZWRAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEF\n" + + "AmIkvPoJEPAQMiYcZF6nFiEEzmVgjYY54gxhvwd78BAyJhxkXqcCngECmwEFFgID\n" + + "AQAECwkIBwUVCgkICwKZAQAAYFQA/02fMgRnneYK17Vsxc8DJEj0pVmTDHIOQH8K\n" + + "O8BuTkvhAP9zXtnJ7BsWO3Kg/ajIlaZEzMl6/lK2FTnAzBhs1UtrD7g4BGIkvPoS\n" + + "CisGAQQBl1UBBQEBB0AO8Bzm66ydlFhKtesh9EX66k4yyODeO0X3y3JUbrAnFQMB\n" + + "CAeIdQQYFgoAHQUCYiS8+gKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEPAQMiYc\n" + + "ZF6nTB0BAPjF6pUUrS3wv8CvrIM3S4BCtCOp+oQyPsie72As+47SAP41KfnvzYF3\n" + + "Y0WBp94Dqiy1MkvMZ9Q2x8BQt/L1UsoTBIh7BCgWCgAtBQJiJLz8CRDwEDImHGRe\n" + + "pxYhBM5lYI2GOeIMYb8He/AQMiYcZF6nAocAAh0DAAABqgD/TJpSDZ5fX3zNHqmN\n" + + "4TOuJ1GEkiYpPjBhem2C+U9jHjoBAJxQqzDB2VMiUDfe2+LLVIYa4EwhT2rT12qg\n" + + "aJ+TXWAJuDMEYiS8+hYJKwYBBAHaRw8BAQdAR0y6K6GPt4ddNyaRX16duqDFZwQi\n" + + "jeflFZ+UGLQ5GgSI1QQYFgoAfQUCYiS8+gKeAQKbAgUWAgMBAAQLCQgHBRUKCQgL\n" + + "XyAEGRYKAAYFAmIkvPoACgkQCX8koK2POrbPywEA3mbeGX8vWwnENtiFeMBjXNox\n" + + "oHAIuULBsvOdc1xrH0QBALezsulAJoziQ/t+EUrNHgTELDq3F8Y8tmLAJykb/nQB\n" + + "AAoJEPAQMiYcZF6n6CAA/0HadYoqOUbMjgu3Tle0HSXiTCJfBrTox5trTOKUsQ8z\n" + + "AQCjeV+3VT+u1movwIYv4XkzB6gB+B2C+DK9nvG5sXZhBg==\n" + + "=uqmO\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(CERT); + KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys); + + assertFalse(info.isUsableForEncryption()); + assertFalse(info.isUsableForEncryption(EncryptionPurpose.ANY)); + assertFalse(info.isUsableForEncryption(EncryptionPurpose.COMMUNICATIONS)); + assertFalse(info.isUsableForEncryption(EncryptionPurpose.STORAGE)); + } } From 126cc9df70b522e157a02e5dfd0e2630f46badaa Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Mar 2022 10:24:08 +0100 Subject: [PATCH 0081/1204] Make toSecondsPrecision() more readable and improv performance --- .../main/java/org/pgpainless/util/DateUtil.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java index 8a1b610e..0cec35d9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java @@ -46,21 +46,23 @@ public final class DateUtil { } /** - * "Round" a date down to seconds precision. + * Floor a date down to seconds precision. * @param date date - * @return rounded date + * @return floored date */ public static Date toSecondsPrecision(Date date) { - long seconds = date.getTime() / 1000; - return new Date(seconds * 1000); + long millis = date.getTime(); + long seconds = millis / 1000; + long floored = seconds * 1000; + return new Date(floored); } /** - * Return the current date "rounded" to UTC precision. + * Return the current date "floored" to UTC precision. * * @return now */ public static Date now() { - return parseUTCDate(formatUTCDate(new Date())); + return toSecondsPrecision(new Date()); } } From a7d1f09b5ce469037cd13916aae9378d3b4c756e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Mar 2022 10:26:24 +0100 Subject: [PATCH 0082/1204] Document SimpleDateFormat not thread-safe --- pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java index 0cec35d9..ad1fce09 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java @@ -15,6 +15,7 @@ public final class DateUtil { } + // Java's SimpleDateFormat is not thread-safe, therefore we return a new instance on every invocation. public static SimpleDateFormat getParser() { SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); parser.setTimeZone(TimeZone.getTimeZone("UTC")); From a6dcf027c02055161cdc05286442252019189f5f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Mar 2022 10:36:20 +0100 Subject: [PATCH 0083/1204] Add and document PGPainless.inspectKeyRing(key, date) --- .../src/main/java/org/pgpainless/PGPainless.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 57dd29d5..bd353f24 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -116,7 +116,7 @@ public final class PGPainless { * This method can be used to determine expiration dates, key flags and other information about a key. * * To evaluate a key at a given date (e.g. to determine if the key was allowed to create a certain signature) - * use {@link KeyRingInfo#KeyRingInfo(PGPKeyRing, Date)} instead. + * use {@link #inspectKeyRing(PGPKeyRing, Date)} instead. * * @param keyRing key ring * @return access object @@ -125,6 +125,18 @@ public final class PGPainless { return new KeyRingInfo(keyRing); } + /** + * Quickly access information about a {@link org.bouncycastle.openpgp.PGPPublicKeyRing} / {@link PGPSecretKeyRing}. + * This method can be used to determine expiration dates, key flags and other information about a key at a specific time. + * + * @param keyRing key ring + * @param inspectionDate date of inspection + * @return access object + */ + public static KeyRingInfo inspectKeyRing(PGPKeyRing keyRing, Date inspectionDate) { + return new KeyRingInfo(keyRing, inspectionDate); + } + /** * Access, and make changes to PGPainless policy on acceptable/default algorithms etc. * From 10e72f6773a75a56a6bb9fdaea63b0f0e576e772 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Mar 2022 11:08:59 +0100 Subject: [PATCH 0084/1204] Allow custom key creation dates during generation --- .../key/generation/KeyRingBuilder.java | 4 +- .../pgpainless/key/generation/KeySpec.java | 13 +++++- .../key/generation/KeySpecBuilder.java | 9 +++- .../generation/KeySpecBuilderInterface.java | 4 ++ ...GenerateKeyWithCustomCreationDateTest.java | 46 +++++++++++++++++++ 5 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java index 537bc255..88ed6ecd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java @@ -305,9 +305,11 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { // Create raw Key Pair KeyPair keyPair = certKeyGenerator.generateKeyPair(); + Date keyCreationDate = spec.getKeyCreationDate() != null ? spec.getKeyCreationDate() : new Date(); + // Form PGP key pair PGPKeyPair pgpKeyPair = ImplementationFactory.getInstance() - .getPGPKeyPair(type.getAlgorithm(), keyPair, new Date()); + .getPGPKeyPair(type.getAlgorithm(), keyPair, keyCreationDate); return pgpKeyPair; } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java index bd5a5063..63645edd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java @@ -5,6 +5,7 @@ package org.pgpainless.key.generation; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; import org.pgpainless.algorithm.KeyFlag; @@ -12,18 +13,23 @@ import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.signature.subpackets.SignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper; +import java.util.Date; + public class KeySpec { private final KeyType keyType; private final SignatureSubpackets subpacketGenerator; private final boolean inheritedSubPackets; + private final Date keyCreationDate; KeySpec(@Nonnull KeyType type, @Nonnull SignatureSubpackets subpacketGenerator, - boolean inheritedSubPackets) { + boolean inheritedSubPackets, + @Nullable Date keyCreationDate) { this.keyType = type; this.subpacketGenerator = subpacketGenerator; this.inheritedSubPackets = inheritedSubPackets; + this.keyCreationDate = keyCreationDate; } @Nonnull @@ -45,6 +51,11 @@ public class KeySpec { return inheritedSubPackets; } + @Nullable + public Date getKeyCreationDate() { + return keyCreationDate; + } + public static KeySpecBuilder getBuilder(KeyType type, KeyFlag flag, KeyFlag... flags) { return new KeySpecBuilder(type, flag, flags); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java index 07b53383..2d7010d8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java @@ -5,6 +5,7 @@ package org.pgpainless.key.generation; import java.util.Arrays; +import java.util.Date; import java.util.LinkedHashSet; import java.util.Set; import javax.annotation.Nonnull; @@ -31,6 +32,7 @@ public class KeySpecBuilder implements KeySpecBuilderInterface { private Set preferredCompressionAlgorithms = algorithmSuite.getCompressionAlgorithms(); private Set preferredHashAlgorithms = algorithmSuite.getHashAlgorithms(); private Set preferredSymmetricAlgorithms = algorithmSuite.getSymmetricKeyAlgorithms(); + private Date keyCreationDate; KeySpecBuilder(@Nonnull KeyType type, KeyFlag flag, KeyFlag... flags) { if (flag == null) { @@ -66,6 +68,11 @@ public class KeySpecBuilder implements KeySpecBuilderInterface { return this; } + @Override + public KeySpecBuilder setKeyCreationDate(@Nonnull Date creationDate) { + this.keyCreationDate = creationDate; + return this; + } @Override public KeySpec build() { @@ -75,6 +82,6 @@ public class KeySpecBuilder implements KeySpecBuilderInterface { this.hashedSubpackets.setPreferredSymmetricKeyAlgorithms(preferredSymmetricAlgorithms); this.hashedSubpackets.setFeatures(Feature.MODIFICATION_DETECTION); - return new KeySpec(type, (SignatureSubpackets) hashedSubpackets, false); + return new KeySpec(type, (SignatureSubpackets) hashedSubpackets, false, keyCreationDate); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilderInterface.java index cd68e8b4..4a99bc8d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilderInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilderInterface.java @@ -10,6 +10,8 @@ import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import java.util.Date; + public interface KeySpecBuilderInterface { KeySpecBuilder overridePreferredCompressionAlgorithms(@Nonnull CompressionAlgorithm... compressionAlgorithms); @@ -18,5 +20,7 @@ public interface KeySpecBuilderInterface { KeySpecBuilder overridePreferredSymmetricKeyAlgorithms(@Nonnull SymmetricKeyAlgorithm... preferredSymmetricKeyAlgorithms); + KeySpecBuilder setKeyCreationDate(@Nonnull Date creationDate); + KeySpec build(); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java new file mode 100644 index 00000000..e77602dd --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.util.DateUtil; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Date; +import java.util.Iterator; + +public class GenerateKeyWithCustomCreationDateTest { + + @Test + public void generateKeyWithCustomCreationDateTest() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + Date creationDate = DateUtil.parseUTCDate("2018-06-11 14:12:09 UTC"); + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) + .setKeyCreationDate(creationDate)) // primary key with custom creation time + .addUserId("Alice") + .build(); + + Iterator iterator = secretKeys.iterator(); + PGPPublicKey primaryKey = iterator.next().getPublicKey(); + PGPPublicKey subkey = iterator.next().getPublicKey(); + + JUtils.assertDateEquals(creationDate, primaryKey.getCreationTime()); + // subkey has no creation date override, so it was generated "just now" + JUtils.assertDateNotEquals(creationDate, subkey.getCreationTime()); + } +} From c3f5b997ab72bdd01ea9a25ecacebdb892ed7185 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Mar 2022 11:11:04 +0100 Subject: [PATCH 0085/1204] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c8cbdca..cac498d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ SPDX-License-Identifier: CC0-1.0 ## 1.1.2-SNAPSHOT - Fix `keyRingInfo.getEmailAddresses()` incorrectly matching some mail addresses (thanks @bratkartoffel for reporting and initial patch proposal) - Fix generic type of `CertificationSubpackets.Callback` +- Add `KeyRingInfo.isUsableForEncryption()` +- Add `PGPainless.inspectKeyRing(key, date)` +- Allow custom key creation dates during key generation ## 1.1.1 - Add `producerOptions.setComment(string)` to allow adding ASCII armor comments when creating OpenPGP messages (thanks @ferenc-hechler) From 5d3646cd3626de1125a51106dc5661adfa0291a5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Mar 2022 11:27:21 +0100 Subject: [PATCH 0086/1204] Add missing @throws documentation --- .../secretkeyring/SecretKeyRingEditorInterface.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java index e999a163..ec579c9e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java @@ -348,7 +348,7 @@ public interface SecretKeyRingEditorInterface { * @param secretKeyRingProtector protector to unlock the primary secret key * @param revocationAttributes revocation attributes * @return builder - * @throws PGPException + * @throws PGPException if the revocation signatures cannot be generated */ SecretKeyRingEditorInterface revokeUserIds( @Nonnull SelectUserId userIdSelector, @@ -370,7 +370,7 @@ public interface SecretKeyRingEditorInterface { * @param secretKeyRingProtector protector to unlock the primary secret key * @param subpacketsCallback callback to modify the revocations subpackets * @return builder - * @throws PGPException + * @throws PGPException if the revocation signatures cannot be generated */ SecretKeyRingEditorInterface revokeUserIds( @Nonnull SelectUserId userIdSelector, From 9d160ef0476f7d6a0dc3c6830c26ab7d52e7d5a1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Mar 2022 12:17:45 +0100 Subject: [PATCH 0087/1204] Reject subkeys with predating binding signatures --- .../signature/consumer/SignaturePicker.java | 1 + .../consumer/SignatureValidator.java | 16 +++++++++- .../signature/consumer/SignatureVerifier.java | 3 ++ ...GenerateKeyWithCustomCreationDateTest.java | 31 ++++++++++++++++--- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java index ee6dfc89..5ab81099 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java @@ -337,6 +337,7 @@ public final class SignaturePicker { try { SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_BINDING).verify(signature); SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); + SignatureValidator.signatureDoesNotPredateSignee(subkey).verify(signature); SignatureValidator.signatureIsAlreadyEffective(validationDate).verify(signature); // if the currently latest signature is not yet expired, check if the next candidate is not yet expired if (latestSubkeyBinding != null && !SignatureUtils.isSignatureExpired(latestSubkeyBinding, validationDate)) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java index d0de0d46..a8f1ec5b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java @@ -355,6 +355,10 @@ public abstract class SignatureValidator { }; } + public static SignatureValidator signatureDoesNotPredateSignee(PGPPublicKey signee) { + return signatureDoesNotPredateKeyCreation(signee); + } + /** * Verify that a signature has a hashed creation time subpacket. * @@ -379,6 +383,16 @@ public abstract class SignatureValidator { * @return validator */ public static SignatureValidator signatureDoesNotPredateSigningKey(PGPPublicKey key) { + return signatureDoesNotPredateKeyCreation(key); + } + + /** + * Verify that a signature does not predate the creation time of the given key. + * + * @param key key + * @return validator + */ + public static SignatureValidator signatureDoesNotPredateKeyCreation(PGPPublicKey key) { return new SignatureValidator() { @Override public void verify(PGPSignature signature) throws SignatureValidationException { @@ -386,7 +400,7 @@ public abstract class SignatureValidator { Date signatureCreationTime = signature.getCreationTime(); if (keyCreationTime.after(signatureCreationTime)) { - throw new SignatureValidationException("Signature predates its signing key (key creation: " + keyCreationTime + ", signature creation: " + signatureCreationTime + ")"); + throw new SignatureValidationException("Signature predates key (key creation: " + keyCreationTime + ", signature creation: " + signatureCreationTime + ")"); } } }; diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java index b0db7c9b..1cfff7db 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java @@ -243,6 +243,7 @@ public final class SignatureVerifier { throws SignatureValidationException { SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_BINDING).verify(signature); SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); + SignatureValidator.signatureDoesNotPredateSignee(subkey).verify(signature); SignatureValidator.signatureIsEffective(validationDate).verify(signature); SignatureValidator.hasValidPrimaryKeyBindingSignatureIfRequired(primaryKey, subkey, policy, validationDate).verify(signature); SignatureValidator.correctSubkeyBindingSignature(primaryKey, subkey).verify(signature); @@ -265,6 +266,7 @@ public final class SignatureVerifier { public static boolean verifySubkeyBindingRevocation(PGPSignature signature, PGPPublicKey primaryKey, PGPPublicKey subkey, Policy policy, Date validationDate) throws SignatureValidationException { SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_REVOCATION).verify(signature); SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); + SignatureValidator.signatureDoesNotPredateSignee(subkey).verify(signature); SignatureValidator.signatureIsEffective(validationDate).verify(signature); SignatureValidator.correctSignatureOverKey(primaryKey, subkey).verify(signature); @@ -303,6 +305,7 @@ public final class SignatureVerifier { throws SignatureValidationException { SignatureValidator.signatureIsOfType(SignatureType.DIRECT_KEY).verify(signature); SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); + SignatureValidator.signatureDoesNotPredateSignee(signedKey).verify(signature); SignatureValidator.signatureIsEffective(validationDate).verify(signature); SignatureValidator.correctSignatureOverKey(signingKey, signedKey).verify(signature); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java index e77602dd..d2697b82 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java @@ -4,6 +4,14 @@ package org.pgpainless.key.generation; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Calendar; +import java.util.Date; +import java.util.Iterator; + import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; @@ -13,15 +21,11 @@ import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.ecc.EllipticCurve; import org.pgpainless.key.generation.type.eddsa.EdDSACurve; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.util.DateUtil; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Date; -import java.util.Iterator; - public class GenerateKeyWithCustomCreationDateTest { @Test @@ -43,4 +47,21 @@ public class GenerateKeyWithCustomCreationDateTest { // subkey has no creation date override, so it was generated "just now" JUtils.assertDateNotEquals(creationDate, subkey.getCreationTime()); } + + @Test + public void generateSubkeyWithFutureKeyCreationDate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.YEAR, 20); + Date future = calendar.getTime(); + + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .addSubkey(KeySpec.getBuilder(KeyType.ECDH(EllipticCurve._P384), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE).setKeyCreationDate(future)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.ECDSA(EllipticCurve._P384), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .addUserId("Captain Future ") + .build(); + + // Subkey has future key creation date, so its binding will predate the key -> no usable encryption key left + assertFalse(PGPainless.inspectKeyRing(secretKeys) + .isUsableForEncryption()); + } } From fc65bb449672fd6558abcb9c6fb54ce75ebfce78 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Mar 2022 14:55:45 +0100 Subject: [PATCH 0088/1204] Raise readable error message when trying to encrypt for key without acceptable self-sigs --- .../encryption_signing/EncryptionOptions.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index ed748e3e..48107b23 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -11,6 +11,7 @@ import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Set; import javax.annotation.Nonnull; @@ -184,15 +185,24 @@ public class EncryptionOptions { * @return this */ public EncryptionOptions addRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy) { - KeyRingInfo info = new KeyRingInfo(key, new Date()); - Date primaryKeyExpiration = info.getPrimaryKeyExpirationDate(); - if (primaryKeyExpiration != null && primaryKeyExpiration.before(new Date())) { + Date evaluationDate = new Date(); + KeyRingInfo info; + info = new KeyRingInfo(key, evaluationDate); + + Date primaryKeyExpiration; + try { + primaryKeyExpiration = info.getPrimaryKeyExpirationDate(); + } catch (NoSuchElementException e) { + throw new IllegalArgumentException("Provided key " + OpenPgpFingerprint.of(key) + " does not have a valid/acceptable signature carrying a primary key expiration date."); + } + if (primaryKeyExpiration != null && primaryKeyExpiration.before(evaluationDate)) { throw new IllegalArgumentException("Provided key " + OpenPgpFingerprint.of(key) + " is expired: " + primaryKeyExpiration); } + List encryptionSubkeys = encryptionKeySelectionStrategy .selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose)); if (encryptionSubkeys.isEmpty()) { - throw new IllegalArgumentException("Key has no suitable encryption subkeys."); + throw new IllegalArgumentException("Key " + OpenPgpFingerprint.of(key) + " has no suitable encryption subkeys."); } for (PGPPublicKey encryptionSubkey : encryptionSubkeys) { From f1f7dec8b6b5cae50c8ffea596418e241fe709ab Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Mar 2022 14:56:37 +0100 Subject: [PATCH 0089/1204] Fix accidental verification of thirdparty user-id revocations using primary key --- .../org/pgpainless/signature/consumer/SignaturePicker.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java index 5ab81099..be0c87b7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java @@ -169,6 +169,11 @@ public final class SignaturePicker { PGPSignature latestUserIdRevocation = null; for (PGPSignature signature : signatures) { + PGPPublicKey signer = keyRing.getPublicKey(signature.getKeyID()); + if (signer == null) { + // Signature made by external key. Skip. + continue; + } try { SignatureVerifier.verifyUserIdRevocation(userId, signature, primaryKey, policy, validationDate); } catch (SignatureValidationException e) { From 3fe78ab12a05a2474b4e4aaba60aef5e4bd56b18 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Mar 2022 14:56:56 +0100 Subject: [PATCH 0090/1204] Fix NPE when validating broken signature --- .../signature/subpackets/SignatureSubpacketsUtil.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index 396311c6..17add09a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -575,6 +575,10 @@ public final class SignatureSubpacketsUtil { * @return last occurrence of the subpacket in the vector */ public static

P getSignatureSubpacket(PGPSignatureSubpacketVector vector, SignatureSubpacket type) { + if (vector == null) { + // Almost never happens, but may be caused by broken signatures. + return null; + } org.bouncycastle.bcpg.SignatureSubpacket[] allPackets = vector.getSubpackets(type.getCode()); if (allPackets.length == 0) { return null; From db02106518c1ad586b89e9fa84fb002b676420f1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Mar 2022 14:57:00 +0100 Subject: [PATCH 0091/1204] Fix typo --- .../signature/subpackets/SignatureSubpacketsUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index 17add09a..bbf8972b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -567,7 +567,7 @@ public final class SignatureSubpacketsUtil { } /** - * Return the last occurence of a subpacket type in the given signature subpacket vector. + * Return the last occurrence of a subpacket type in the given signature subpacket vector. * * @param vector subpacket vector (hashed/unhashed) * @param type subpacket type From 8563cf0969a7afe729f691dc4bd011c62a9896c7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Mar 2022 15:00:33 +0100 Subject: [PATCH 0092/1204] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cac498d7..8f23ab77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ SPDX-License-Identifier: CC0-1.0 - Add `KeyRingInfo.isUsableForEncryption()` - Add `PGPainless.inspectKeyRing(key, date)` - Allow custom key creation dates during key generation +- Reject subkeys with bindings that predate key generation +- `EncryptionOptions.addRecipient()`: Transform `NoSuchElementException` into `IllegalArgumentException` with proper error message +- Fix `ClassCastException` by preventing accidental verification of 3rd-party-issued user-id revocation with primary key. +- Fix `NullPointerException` when trying to verify malformed signature ## 1.1.1 - Add `producerOptions.setComment(string)` to allow adding ASCII armor comments when creating OpenPGP messages (thanks @ferenc-hechler) From 95aed9bf2222d89503887a3d321d1b0993ba87d4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Mar 2022 15:02:24 +0100 Subject: [PATCH 0093/1204] PGPainless 1.1.2 --- CHANGELOG.md | 2 +- README.md | 2 +- version.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f23ab77..2f39e50c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.1.2-SNAPSHOT +## 1.1.2 - Fix `keyRingInfo.getEmailAddresses()` incorrectly matching some mail addresses (thanks @bratkartoffel for reporting and initial patch proposal) - Fix generic type of `CertificationSubpackets.Callback` - Add `KeyRingInfo.isUsableForEncryption()` diff --git a/README.md b/README.md index 4dc5d2f2..29454fd0 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.1.1' + implementation 'org.pgpainless:pgpainless-core:1.1.2' } ``` diff --git a/version.gradle b/version.gradle index 03d66410..3bf96de1 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.1.2' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From 5b43cfaf8c49c6b1f12acc5d34eb10d6105d2345 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Mar 2022 15:05:02 +0100 Subject: [PATCH 0094/1204] PGPainless-1.1.3-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 3bf96de1..34931747 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.1.2' - isSnapshot = false + shortVersion = '1.1.3' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From b34866b012d45eca06ea1b81da275a51ea8999f6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Mar 2022 21:03:31 +0100 Subject: [PATCH 0095/1204] Make SigningOptions.getSigningMethods package visible --- .../java/org/pgpainless/encryption_signing/SigningOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java index 45b9f521..ab3898dc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -385,7 +385,7 @@ public final class SigningOptions { * * @return signing methods */ - public Map getSigningMethods() { + Map getSigningMethods() { return Collections.unmodifiableMap(signingMethods); } From 26d79679f0be855b54ad644eeb88d926cd86afba Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Mar 2022 21:05:00 +0100 Subject: [PATCH 0096/1204] Fix crash when validating unmatched signer's user-id subpacket TODO: We might want to deprecate Signer's UserID subpackets completely and ignore them. See results of sequoias test suite once PR below gets merged. https://gitlab.com/sequoia-pgp/openpgp-interoperability-test-suite/-/merge_requests/28 --- .../signature/consumer/CertificateValidator.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java index 7080e368..93d12fc5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java @@ -144,7 +144,13 @@ public final class CertificateValidator { // Specific signer user-id SignerUserID signerUserID = SignatureSubpacketsUtil.getSignerUserID(signature); if (signerUserID != null) { - PGPSignature userIdSig = userIdSignatures.get(signerUserID.getID()).get(0); + List signerUserIdSigs = userIdSignatures.get(signerUserID.getID()); + if (signerUserIdSigs == null || signerUserIdSigs.isEmpty()) { + throw new SignatureValidationException("Signature was allegedly made by user-id '" + signerUserID.getID() + + "' but we have no valid signatures for that on the certificate."); + } + + PGPSignature userIdSig = signerUserIdSigs.get(0); if (userIdSig.getSignatureType() == SignatureType.CERTIFICATION_REVOCATION.getCode()) { throw new SignatureValidationException("Signature was made with user-id '" + signerUserID.getID() + "' which is revoked."); } From 0824bbd37c31cd9b7af8413fc84d548f01205fa0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Mar 2022 21:05:17 +0100 Subject: [PATCH 0097/1204] Add investigative test for signers user-ids --- .../investigations/WrongSignerUserIdTest.java | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 pgpainless-core/src/test/java/investigations/WrongSignerUserIdTest.java diff --git a/pgpainless-core/src/test/java/investigations/WrongSignerUserIdTest.java b/pgpainless-core/src/test/java/investigations/WrongSignerUserIdTest.java new file mode 100644 index 00000000..ccccdb2d --- /dev/null +++ b/pgpainless-core/src/test/java/investigations/WrongSignerUserIdTest.java @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package investigations; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Iterator; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.util.io.Streams; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.exception.SignatureValidationException; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; + +public class WrongSignerUserIdTest { + + private static final String CERT = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + " Comment: Alice's OpenPGP Transferable Secret Key\n" + + " Comment: https://www.ietf.org/id/draft-bre-openpgp-samples-01.html\n" + + "\n" + + " lFgEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U\n" + + " b7O1u10AAP9XBeW6lzGOLx7zHH9AsUDUTb2pggYGMzd0P3ulJ2AfvQ4RtCZBbGlj\n" + + " ZSBMb3ZlbGFjZSA8YWxpY2VAb3BlbnBncC5leGFtcGxlPoiQBBMWCAA4AhsDBQsJ\n" + + " CAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE64W7X6M6deFelE5j8jFVDE9H444FAl2l\n" + + " nzoACgkQ8jFVDE9H447pKwD6A5xwUqIDprBzrHfahrImaYEZzncqb25vkLV2arYf\n" + + " a78A/R3AwtLQvjxwLDuzk4dUtUwvUYibL2sAHwj2kGaHnfICnF0EXEcE6RIKKwYB\n" + + " BAGXVQEFAQEHQEL/BiGtq0k84Km1wqQw2DIikVYrQrMttN8d7BPfnr4iAwEIBwAA\n" + + " /3/xFPG6U17rhTuq+07gmEvaFYKfxRB6sgAYiW6TMTpQEK6IeAQYFggAIBYhBOuF\n" + + " u1+jOnXhXpROY/IxVQxPR+OOBQJcRwTpAhsMAAoJEPIxVQxPR+OOWdABAMUdSzpM\n" + + " hzGs1O0RkWNQWbUzQ8nUOeD9wNbjE3zR+yfRAQDbYqvtWQKN4AQLTxVJN5X5AWyb\n" + + " Pnn+We1aTBhaGa86AQ==\n" + + " =n8OM\n" + + " -----END PGP PRIVATE KEY BLOCK-----"; + private static final String USER_ID = "Alice Lovelace "; + + public static void main(String[] args) throws Exception { + WrongSignerUserIdTest test = new WrongSignerUserIdTest(); + test.execute(); + } + + public void execute() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(CERT); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + assertEquals(USER_ID, certificate.getPublicKey().getUserIDs().next()); + + Iterator keys = secretKeys.getSecretKeys(); + PGPSecretKey signingKey = keys.next(); + PGPSecretKey encryptionKey = keys.next(); + + PGPPrivateKey signingPrivKey = UnlockSecretKey.unlockSecretKey(signingKey, Passphrase.emptyPassphrase()); + + // ARMOR + ByteArrayOutputStream cipherText = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(cipherText); + + // ENCRYPTION + PGPDataEncryptorBuilder dataEncryptorBuilder = new BcPGPDataEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_256); + dataEncryptorBuilder.setWithIntegrityPacket(true); + + PGPEncryptedDataGenerator encDataGenerator = new PGPEncryptedDataGenerator(dataEncryptorBuilder); + encDataGenerator.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(encryptionKey.getPublicKey())); + OutputStream encStream = encDataGenerator.open(armorOut, new byte[4096]); + + // COMPRESSION + PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZLIB); + BCPGOutputStream bOut = new BCPGOutputStream(compressedDataGenerator.open(encStream)); + + // SIGNING + PGPSignatureGenerator sigGen = new PGPSignatureGenerator( + new BcPGPContentSignerBuilder(signingKey.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId())); + sigGen.init(PGPSignature.BINARY_DOCUMENT, signingPrivKey); + + PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(); + subpacketGenerator.addSignerUserID(false, "Albert Lovelace "); + sigGen.setHashedSubpackets(subpacketGenerator.generate()); + + sigGen.generateOnePassVersion(false).encode(bOut); + + // LITERAL DATA + PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator(); + OutputStream lOut = literalDataGenerator.open(bOut, PGPLiteralDataGenerator.BINARY, + PGPLiteralDataGenerator.CONSOLE, new Date(), new byte[4096]); + + // write msg + ByteArrayInputStream msgIn = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + int ch; + while ((ch = msgIn.read()) >= 0) { + lOut.write(ch); + sigGen.update((byte) ch); + } + + lOut.close(); + sigGen.generate().encode(bOut); + compressedDataGenerator.close(); + encStream.close(); + armorOut.close(); + + try { + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify().onInputStream( + new ByteArrayInputStream(cipherText.toByteArray())) + .withOptions(new ConsumerOptions() + .addDecryptionKey(secretKeys) + .addVerificationCert(certificate)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + + decryptionStream.close(); + } catch (SignatureValidationException e) { + // expected + } + } +} From 8f473b513f9ede165f6f0c81dff311492b7d35a5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Mar 2022 12:01:12 +0100 Subject: [PATCH 0098/1204] Add support for OpenPGP v5 fingerprints. Obviously we need support for key.getFingerprint() in BC, but once that is there, this should magically start working. --- .../pgpainless/key/OpenPgpFingerprint.java | 3 + .../pgpainless/key/OpenPgpV4Fingerprint.java | 80 ++++++------ .../pgpainless/key/OpenPgpV5Fingerprint.java | 117 ++++++++++++++++++ .../key/OpenPgpV5FingerprintTest.java | 25 ++++ 4 files changed, 185 insertions(+), 40 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java index af0051c8..b00e4d6f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java @@ -33,6 +33,9 @@ public abstract class OpenPgpFingerprint implements CharSequence, Comparable - * XEP-0373 §4.1: The OpenPGP Public-Key Data Node about how to obtain the fingerprint - * @param fingerprint hexadecimal representation of the fingerprint. + * + * @param fingerprint uppercase hexadecimal fingerprint of length 40 */ public OpenPgpV4Fingerprint(@Nonnull String fingerprint) { super(fingerprint); } - @Override - public int getVersion() { - return 4; - } - - @Override - protected boolean isValid(@Nonnull String fp) { - return fp.matches("[0-9A-F]{40}"); - } - - @Override - public long getKeyId() { - byte[] bytes = Hex.decode(toString().getBytes(utf8)); - ByteBuffer buf = ByteBuffer.wrap(bytes); - - // We have to cast here in order to be compatible with java 8 - // https://github.com/eclipse/jetty.project/issues/3244 - ((Buffer) buf).position(12); - - return buf.getLong(); - } - - @Override - public String prettyPrint() { - String fp = toString(); - StringBuilder pretty = new StringBuilder(); - for (int i = 0; i < 5; i++) { - pretty.append(fp, i * 4, (i + 1) * 4).append(' '); - } - pretty.append(' '); - for (int i = 5; i < 9; i++) { - pretty.append(fp, i * 4, (i + 1) * 4).append(' '); - } - pretty.append(fp, 36, 40); - return pretty.toString(); - } - public OpenPgpV4Fingerprint(@Nonnull byte[] bytes) { super(Hex.encode(bytes)); } @@ -95,6 +57,44 @@ public class OpenPgpV4Fingerprint extends OpenPgpFingerprint { super(ring); } + @Override + public int getVersion() { + return 4; + } + + @Override + protected boolean isValid(@Nonnull String fp) { + return fp.matches("^[0-9A-F]{40}$"); + } + + @Override + public long getKeyId() { + byte[] bytes = Hex.decode(toString().getBytes(utf8)); + ByteBuffer buf = ByteBuffer.wrap(bytes); + + // The key id is the right-most 8 bytes (conveniently a long) + // We have to cast here in order to be compatible with java 8 + // https://github.com/eclipse/jetty.project/issues/3244 + ((Buffer) buf).position(12); // 20 - 8 bytes = offset 12 + + return buf.getLong(); + } + + @Override + public String prettyPrint() { + String fp = toString(); + StringBuilder pretty = new StringBuilder(); + for (int i = 0; i < 5; i++) { + pretty.append(fp, i * 4, (i + 1) * 4).append(' '); + } + pretty.append(' '); + for (int i = 5; i < 9; i++) { + pretty.append(fp, i * 4, (i + 1) * 4).append(' '); + } + pretty.append(fp, 36, 40); + return pretty.toString(); + } + @Override public boolean equals(Object other) { if (other == null) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java new file mode 100644 index 00000000..fafbc34c --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.encoders.Hex; + +import javax.annotation.Nonnull; +import java.nio.Buffer; +import java.nio.ByteBuffer; + +/** + * This class represents a hex encoded, upper case OpenPGP v5 fingerprint. + */ +public class OpenPgpV5Fingerprint extends OpenPgpFingerprint { + + /** + * Create an {@link OpenPgpV5Fingerprint}. + * + * @param fingerprint uppercase hexadecimal fingerprint of length 64 + */ + public OpenPgpV5Fingerprint(@Nonnull String fingerprint) { + super(fingerprint); + } + + public OpenPgpV5Fingerprint(@Nonnull byte[] bytes) { + super(Hex.encode(bytes)); + } + + public OpenPgpV5Fingerprint(@Nonnull PGPPublicKey key) { + super(key); + } + + public OpenPgpV5Fingerprint(@Nonnull PGPSecretKey key) { + this(key.getPublicKey()); + } + + public OpenPgpV5Fingerprint(@Nonnull PGPPublicKeyRing ring) { + super(ring); + } + + public OpenPgpV5Fingerprint(@Nonnull PGPSecretKeyRing ring) { + super(ring); + } + + public OpenPgpV5Fingerprint(@Nonnull PGPKeyRing ring) { + super(ring); + } + + @Override + public int getVersion() { + return 5; + } + + @Override + protected boolean isValid(@Nonnull String fp) { + return fp.matches("^[0-9A-F]{64}$"); + } + + @Override + public long getKeyId() { + byte[] bytes = Hex.decode(toString().getBytes(utf8)); + ByteBuffer buf = ByteBuffer.wrap(bytes); + + // The key id is the left-most 8 bytes (conveniently a long). + // We have to cast here in order to be compatible with java 8 + // https://github.com/eclipse/jetty.project/issues/3244 + ((Buffer) buf).position(0); + + return buf.getLong(); + } + + @Override + public String prettyPrint() { + String fp = toString(); + StringBuilder pretty = new StringBuilder(); + + for (int i = 0; i < 4; i++) { + pretty.append(fp, i * 8, (i + 1) * 8).append(' '); + } + pretty.append(' '); + for (int i = 4; i < 7; i++) { + pretty.append(fp, i * 8, (i + 1) * 8).append(' '); + } + pretty.append(fp, 56, 64); + return pretty.toString(); + } + + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + + if (!(other instanceof CharSequence)) { + return false; + } + + return this.toString().equals(other.toString()); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public int compareTo(OpenPgpFingerprint openPgpFingerprint) { + return toString().compareTo(openPgpFingerprint.toString()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java new file mode 100644 index 00000000..f3c10cf7 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class OpenPgpV5FingerprintTest { + + @Test + public void testFingerprintFormatting() { + String pretty = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + String fp = pretty.replace(" ", ""); + + OpenPgpV5Fingerprint fingerprint = new OpenPgpV5Fingerprint(fp); + assertEquals(fp, fingerprint.toString()); + assertEquals(pretty, fingerprint.prettyPrint()); + + long id = fingerprint.getKeyId(); + assertEquals("76543210abcdefab", Long.toHexString(id)); + } +} From 6b9b956c2cd40615c251bc367d101300de425a02 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Mar 2022 12:22:02 +0100 Subject: [PATCH 0099/1204] Add OpenPgpFingerprint.parse(String) --- .../org/pgpainless/key/OpenPgpFingerprint.java | 17 +++++++++++++++++ .../key/OpenPgpV4FingerprintTest.java | 11 +++++++++++ .../key/OpenPgpV5FingerprintTest.java | 11 +++++++++++ 3 files changed, 39 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java index b00e4d6f..d5a1daad 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java @@ -50,6 +50,23 @@ public abstract class OpenPgpFingerprint implements CharSequence, Comparable Date: Sun, 13 Mar 2022 15:12:38 +0100 Subject: [PATCH 0100/1204] Add comment about hash algorithm header --- .../java/org/pgpainless/encryption_signing/EncryptionStream.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 27b8958a..6fc112ba 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -152,6 +152,7 @@ public final class EncryptionStream extends OutputStream { private void prepareLiteralDataProcessing() throws IOException { if (options.isCleartextSigned()) { + // Begin cleartext with hash algorithm of first signing method SigningOptions.SigningMethod firstMethod = options.getSigningOptions().getSigningMethods().values().iterator().next(); armorOutputStream.beginClearText(firstMethod.getHashAlgorithm().getAlgorithmId()); return; From 661c043cdc489f8b7728b00535ae4e2e52f0e006 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 13 Mar 2022 16:52:57 +0100 Subject: [PATCH 0101/1204] DFix KeyRingInfo.getValidAndExpiredUserIds considering unbound user-ids as valid --- .../src/main/java/org/pgpainless/key/info/KeyRingInfo.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index a1818902..971d5a8e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -347,6 +347,11 @@ public class KeyRingInfo { PGPSignature certification = signatures.userIdCertifications.get(userId); PGPSignature revocation = signatures.userIdRevocations.get(userId); + // Unbound user-id + if (certification == null) { + continue; + } + // Not revoked -> valid if (revocation == null) { probablyExpired.add(userId); From ffdbd2149162f8d1f1877f623a1a0e47ad62ec90 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Mar 2022 18:23:40 +0100 Subject: [PATCH 0102/1204] Implement configuration option for SignerUserId subpacket verification level. By default we ignore SignerUserId subpackets on signatures. This behavior can be changed by calling Policy.setSignerUserIdValidationLevel(). Right now, STRICT and DISABLED are available as options, but it may make sense to implement another option PARTIALLY, which will accept signatures made by key with user-id 'A ' but where the sig contains a signer user id of value 'foo@bar' for example. --- .../java/org/pgpainless/policy/Policy.java | 44 +++++++++++ .../consumer/CertificateValidator.java | 2 +- .../WrongSignerUserIdTest.java | 78 +++++++++++++------ 3 files changed, 99 insertions(+), 25 deletions(-) rename pgpainless-core/src/test/java/{investigations => org/pgpainless/decryption_verification}/WrongSignerUserIdTest.java (69%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java index 85948546..58d6f6a2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java @@ -42,6 +42,25 @@ public final class Policy { private AlgorithmSuite keyGenerationAlgorithmSuite = AlgorithmSuite.getDefaultAlgorithmSuite(); + // Signers User-ID is soon to be deprecated. + private SignerUserIdValidationLevel signerUserIdValidationLevel = SignerUserIdValidationLevel.DISABLED; + + public enum SignerUserIdValidationLevel { + /** + * PGPainless will verify {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets in signatures strictly. + * This means, that signatures with Signer's User-ID subpackets containing a value that does not match the signer key's + * user-id exactly, will be rejected. + * E.g. Signer's user-id "alice@pgpainless.org", User-ID: "Alice <alice@pgpainless.org>" does not + * match exactly and is therefore rejected. + */ + STRICT, + + /** + * PGPainless will ignore {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets on signature. + */ + DISABLED + } + Policy() { } @@ -468,4 +487,29 @@ public final class Policy { public void setKeyGenerationAlgorithmSuite(@Nonnull AlgorithmSuite algorithmSuite) { this.keyGenerationAlgorithmSuite = algorithmSuite; } + + /** + * Return the level of validation PGPainless shall do on {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets. + * By default, this value is {@link SignerUserIdValidationLevel#DISABLED}. + * + * @return the level of validation + */ + public SignerUserIdValidationLevel getSignerUserIdValidationLevel() { + return signerUserIdValidationLevel; + } + + /** + * Specify, how {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets on signatures shall be validated. + * + * @param signerUserIdValidationLevel level of verification PGPainless shall do on + * {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets. + * @return policy instance + */ + public Policy setSignerUserIdValidationLevel(SignerUserIdValidationLevel signerUserIdValidationLevel) { + if (signerUserIdValidationLevel == null) { + throw new NullPointerException("SignerUserIdValidationLevel cannot be null."); + } + this.signerUserIdValidationLevel = signerUserIdValidationLevel; + return this; + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java index 93d12fc5..65a1a41d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java @@ -143,7 +143,7 @@ public final class CertificateValidator { // Specific signer user-id SignerUserID signerUserID = SignatureSubpacketsUtil.getSignerUserID(signature); - if (signerUserID != null) { + if (signerUserID != null && policy.getSignerUserIdValidationLevel() == Policy.SignerUserIdValidationLevel.STRICT) { List signerUserIdSigs = userIdSignatures.get(signerUserID.getID()); if (signerUserIdSigs == null || signerUserIdSigs.isEmpty()) { throw new SignatureValidationException("Signature was allegedly made by user-id '" + signerUserID.getID() + diff --git a/pgpainless-core/src/test/java/investigations/WrongSignerUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java similarity index 69% rename from pgpainless-core/src/test/java/investigations/WrongSignerUserIdTest.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java index ccccdb2d..b3ecabc8 100644 --- a/pgpainless-core/src/test/java/investigations/WrongSignerUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java @@ -2,9 +2,11 @@ // // SPDX-License-Identifier: Apache-2.0 -package investigations; +package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -34,17 +36,17 @@ import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.policy.Policy; import org.pgpainless.util.Passphrase; public class WrongSignerUserIdTest { - private static final String CERT = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + " Comment: Alice's OpenPGP Transferable Secret Key\n" + " Comment: https://www.ietf.org/id/draft-bre-openpgp-samples-01.html\n" + "\n" + @@ -63,13 +65,54 @@ public class WrongSignerUserIdTest { " -----END PGP PRIVATE KEY BLOCK-----"; private static final String USER_ID = "Alice Lovelace "; - public static void main(String[] args) throws Exception { - WrongSignerUserIdTest test = new WrongSignerUserIdTest(); - test.execute(); + @Test + public void verificationSucceedsWithDisabledCheck() throws PGPException, IOException { + executeTest(false, true); } - public void execute() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(CERT); + @Test + public void verificationFailsWithEnabledCheck() throws PGPException, IOException { + executeTest(true, false); + } + + @AfterAll + public static void resetDefault() { + PGPainless.getPolicy().setSignerUserIdValidationLevel(Policy.SignerUserIdValidationLevel.DISABLED); + } + + public void executeTest(boolean enableCheck, boolean expectSucessfulVerification) throws IOException, PGPException { + PGPainless.getPolicy().setSignerUserIdValidationLevel(enableCheck ? Policy.SignerUserIdValidationLevel.STRICT : Policy.SignerUserIdValidationLevel.DISABLED); + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + assertEquals(USER_ID, secretKeys.getPublicKey().getUserIDs().next()); + + String messageWithWrongUserId = generateTestMessage(secretKeys); + verifyTestMessage(messageWithWrongUserId, secretKeys, expectSucessfulVerification); + } + + private void verifyTestMessage(String messageWithWrongUserId, PGPSecretKeyRing secretKeys, boolean expectSuccessfulVerification) throws IOException, PGPException { + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify().onInputStream( + new ByteArrayInputStream(messageWithWrongUserId.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions() + .addDecryptionKey(secretKeys) + .addVerificationCert(certificate)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + + decryptionStream.close(); + OpenPgpMetadata metadata = decryptionStream.getResult(); + + if (expectSuccessfulVerification) { + assertTrue(metadata.isVerified()); + } else { + assertFalse(metadata.isVerified()); + } + + } + + private String generateTestMessage(PGPSecretKeyRing secretKeys) throws PGPException, IOException { PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); assertEquals(USER_ID, certificate.getPublicKey().getUserIDs().next()); @@ -126,19 +169,6 @@ public class WrongSignerUserIdTest { encStream.close(); armorOut.close(); - try { - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify().onInputStream( - new ByteArrayInputStream(cipherText.toByteArray())) - .withOptions(new ConsumerOptions() - .addDecryptionKey(secretKeys) - .addVerificationCert(certificate)); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - - decryptionStream.close(); - } catch (SignatureValidationException e) { - // expected - } + return cipherText.toString(); } } From 0819592b3aeb78157548f20c9ecf7c4ed2d9ef5e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Mar 2022 11:12:21 +0100 Subject: [PATCH 0103/1204] Update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f39e50c..484671e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.1.3-SNAPSHOT +- Make `SigningOptions.getSigningMethods()` part of internal API +- Fix crash when trying to do verification of unmatched `SignersUserId` signature subpacket + - For now, verification of `SignersUserId` is disabled but can be enabled via `Policy.setSignerUserIdValidationLevel()` +- Initial support for `OpenPgpV5Fingerprint` +- Security: Fix `KeyRingInfo.getValidAndExpiredUserIds()` accidentally including unbound user-ids + ## 1.1.2 - Fix `keyRingInfo.getEmailAddresses()` incorrectly matching some mail addresses (thanks @bratkartoffel for reporting and initial patch proposal) - Fix generic type of `CertificationSubpackets.Callback` From d4d29553ec273185f0f6f854dce8d99399a28abe Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Mar 2022 15:10:23 +0100 Subject: [PATCH 0104/1204] Add decryption example --- .../pgpainless/example/DecryptOrVerify.java | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java index d0f3461c..d85d667e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java @@ -5,6 +5,8 @@ package org.pgpainless.example; 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.assertTrue; import java.io.ByteArrayInputStream; @@ -54,6 +56,11 @@ public class DecryptOrVerify { "=JHMt\n" + "-----END PGP PRIVATE KEY BLOCK-----\n"; + // The key above is not password protected. + private static final SecretKeyRingProtector keyProtector = SecretKeyRingProtector.unprotectedKeys(); + + private static final String PLAINTEXT = "Hello, World!\n"; + private static final String INBAND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + @@ -76,6 +83,27 @@ public class DecryptOrVerify { "QUibivG5Slahz8l7PWnGkxbB2naQxgw=\n" + "=oNIK\n" + "-----END PGP SIGNATURE-----"; + private static final String ENCRYPTED = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DwqNy0B3ItTkSAQdArkuJHqPTVX+UaqQtHzppwOZDK0TfH1f/fAjrZaso/DUw\n" + + "ne6Xc1HYG+gTBWEQUw09m5b/f0E7DSeIg/ai/HKnF8mBSIQhphPR4yVAWypOOUmh\n" + + "0kABCiGjaJQyAzF/VtzC+ZVU67DfBl24CEPaRMumxieVUqo/VYWy3zyzE6H1zMqq\n" + + "/lWeVnK7NwtfArlhpRcph0S8\n" + + "=1cyl\n" + + "-----END PGP MESSAGE-----\n"; + private static final String ENCRYPTED_AND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DwqNy0B3ItTkSAQdAGqwFJ6SRW6It9w+RBudeGbdUj8OZqwApqyvwbKUzJiYw\n" + + "WAcJOrGIbrK9bKzJdCLbVYkegILb6vqTuamU8iYDCccstV4Y2w0kT5ynHHPVFKfg\n" + + "0r8BUe/Mi8zL0Af6K2r6A9gq/Q8vmscoOB5mI5Yxrk48+rPcp0rZbSu9rC9pHZfs\n" + + "hhvxwGwG8EZm14pseHUZdoKldUD8tCbhkS7wDMOHzA1Fo1m1Yyjhe4kBaCrn9zhP\n" + + "YSeOzHtMxk5JBcrZW+LMMuRGNBzxc0R1yirqk8yymF1qzTTuYqziO0QxbW1gU00F\n" + + "ewdovd7Cx1Il8ONgRzGS3Wyb+iORNuhLpw+w2SV74Kg8XWLD7pDFgOuFZw39b+0X\n" + + "Nw==\n" + + "=9PiO\n" + + "-----END PGP MESSAGE-----"; private static PGPSecretKeyRing secretKey; private static PGPPublicKeyRing certificate; @@ -86,6 +114,53 @@ public class DecryptOrVerify { certificate = PGPainless.extractCertificate(secretKey); } + @Test + public void decryptMessage() throws PGPException, IOException { + ConsumerOptions consumerOptions = new ConsumerOptions() + .addDecryptionKey(secretKey, keyProtector); + + ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ENCRYPTED.getBytes(StandardCharsets.UTF_8)); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(consumerOptions); + + Streams.pipeAll(decryptionStream, plaintextOut); + decryptionStream.close(); + + OpenPgpMetadata metadata = decryptionStream.getResult(); + assertTrue(metadata.isEncrypted()); // message was encrypted + assertFalse(metadata.isVerified()); // We did not do any signature verification + + assertEquals(PLAINTEXT, plaintextOut.toString()); + } + + @Test + public void decryptMessageAndVerifySignatures() throws PGPException, IOException { + ConsumerOptions consumerOptions = new ConsumerOptions() + .addDecryptionKey(secretKey, keyProtector) + .addVerificationCert(certificate); + + ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ENCRYPTED_AND_SIGNED.getBytes(StandardCharsets.UTF_8)); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(consumerOptions); + + Streams.pipeAll(decryptionStream, plaintextOut); + decryptionStream.close(); + + OpenPgpMetadata metadata = decryptionStream.getResult(); + assertTrue(metadata.isEncrypted()); + assertTrue(metadata.isSigned()); + assertTrue(metadata.isVerified()); + assertTrue(metadata.containsVerifiedSignatureFrom(certificate)); + + assertEquals(PLAINTEXT, plaintextOut.toString()); + } + @Test public void verifySignatures() throws PGPException, IOException { ConsumerOptions options = new ConsumerOptions() @@ -104,7 +179,7 @@ public class DecryptOrVerify { OpenPgpMetadata metadata = verificationStream.getResult(); assertTrue(metadata.isVerified()); - assertArrayEquals("Hello, World!\n".getBytes(StandardCharsets.UTF_8), out.toByteArray()); + assertEquals(PLAINTEXT, out.toString()); } } From bfe140294ca46e06f6cf1d04a99fe4a280f5baeb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Mar 2022 15:38:07 +0100 Subject: [PATCH 0105/1204] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 484671e6..e0c0352d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ SPDX-License-Identifier: CC0-1.0 - Initial support for `OpenPgpV5Fingerprint` - Security: Fix `KeyRingInfo.getValidAndExpiredUserIds()` accidentally including unbound user-ids +## 1.0.5 +- Security: Fix `KeyRingInfo.getValidAndExpiredUserIds()` accidentally including unbound user-ids + ## 1.1.2 - Fix `keyRingInfo.getEmailAddresses()` incorrectly matching some mail addresses (thanks @bratkartoffel for reporting and initial patch proposal) - Fix generic type of `CertificationSubpackets.Callback` From 655f4ae09ae185a0f1d2e5b5c0287b874eeb79c9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Mar 2022 15:41:30 +0100 Subject: [PATCH 0106/1204] PGPainless 1.1.3 --- CHANGELOG.md | 1 + README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0c0352d..28d2483f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ SPDX-License-Identifier: CC0-1.0 - Fix crash when trying to do verification of unmatched `SignersUserId` signature subpacket - For now, verification of `SignersUserId` is disabled but can be enabled via `Policy.setSignerUserIdValidationLevel()` - Initial support for `OpenPgpV5Fingerprint` +- Add `OpenPgpFingerprint.parse(string)` - Security: Fix `KeyRingInfo.getValidAndExpiredUserIds()` accidentally including unbound user-ids ## 1.0.5 diff --git a/README.md b/README.md index 29454fd0..933ba781 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.1.2' + implementation 'org.pgpainless:pgpainless-core:1.1.3' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 00196f93..af3c5e52 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.1.1" + implementation "org.pgpainless:pgpainless-sop:1.1.3" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.1.1 + 1.1.3 ... diff --git a/version.gradle b/version.gradle index 34931747..b91ac185 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.1.3' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From f155768539077c461152945e8e4174d48792fc3c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Mar 2022 15:43:50 +0100 Subject: [PATCH 0107/1204] PGPainless-1.1.4-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index b91ac185..57acae3f 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.1.3' - isSnapshot = false + shortVersion = '1.1.4' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From ecfa3823fb3c1f47a6ea8d46d176ab0324cca041 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Mar 2022 16:51:56 +0100 Subject: [PATCH 0108/1204] Add utility method to remove secret subkey from key ring This might be useful for offline primary keys --- .../org/pgpainless/key/util/KeyRingUtils.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index af06928e..1648d5d8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -4,6 +4,7 @@ package org.pgpainless.key.util; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -25,6 +26,7 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; import org.pgpainless.PGPainless; import org.pgpainless.exception.NotYetImplementedException; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; @@ -290,4 +292,24 @@ public final class KeyRingUtils { return newSecretKey; } + public static PGPSecretKeyRing removeSecretKey(PGPSecretKeyRing secretKeys, long secretKeyId) + throws IOException, PGPException { + if (secretKeys.getSecretKey(secretKeyId) == null) { + throw new NoSuchElementException("PGPSecretKeyRing does not contain secret key " + Long.toHexString(secretKeyId)); + } + + ByteArrayOutputStream encoded = new ByteArrayOutputStream(); + for (PGPSecretKey secretKey : secretKeys) { + if (secretKey.getKeyID() == secretKeyId) { + secretKey.getPublicKey().encode(encoded); + } else { + secretKey.encode(encoded); + } + } + for (Iterator it = secretKeys.getExtraPublicKeys(); it.hasNext(); ) { + PGPPublicKey extra = it.next(); + extra.encode(encoded); + } + return new PGPSecretKeyRing(encoded.toByteArray(), ImplementationFactory.getInstance().getKeyFingerprintCalculator()); + } } From 29dc20d0bc330afa27e0e0f6b4335b88771736e3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Mar 2022 16:52:29 +0100 Subject: [PATCH 0109/1204] Add EncryptionResult.isEncryptedFor(certificate) --- .../encryption_signing/EncryptionResult.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java index 566238d3..d1bc3d7f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java @@ -11,6 +11,7 @@ import java.util.Set; import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.StreamEncoding; @@ -130,6 +131,25 @@ public final class EncryptionResult { return PGPLiteralData.CONSOLE.equals(getFileName()); } + /** + * Returns true, if the message was encrypted for at least one subkey of the given certificate. + * + * @param certificate certificate + * @return true if encrypted for 1+ subkeys, false otherwise. + */ + public boolean isEncryptedFor(PGPPublicKeyRing certificate) { + for (SubkeyIdentifier recipient : recipients) { + if (certificate.getPublicKey().getKeyID() != recipient.getPrimaryKeyId()) { + continue; + } + + if (certificate.getPublicKey(recipient.getSubkeyId()) != null) { + return true; + } + } + return false; + } + /** * Create a builder for the encryption result class. * From 2dba981e075c79085c1d58856d85c2d18222f8b7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Mar 2022 17:20:55 +0100 Subject: [PATCH 0110/1204] Update README --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 933ba781..a4157c6d 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,23 @@ PGPainless is developed in - and accepts contributions from - the following plac * [Github](https://github.com/pgpainless/pgpainless) * [Codeberg](https://codeberg.org/PGPainless/pgpainless) +We are using SemVer (MAJOR.MINOR.PATCH) versioning, although MINOR releases could contain breaking changes from time to time. + +If you want to contribute a bug fix, please check the `release/X.Y` branches first to see, what the oldest release is +which contains the bug you are fixing. That way we can update older revisions of the library easily. + +### Branches +* `release/X.Y` contains the state of the latest `X.Y.Z` PATCH release + next PATCH snapshot definition. +* `master` contains the state of the latest MINOR release + some smaller changes that will make it into the next PATCH release. +* `development` contains new features that will make it into the next MINOR release. + +#### Example: +Latest release: 1.1.3 +* `release/1.0` contains the state of `1.0.5-SNAPSHOT` +* `release/1.1` contains the state of `1.1.4-SNAPSHOT` +* `master` contains the state `release/1.1` plus patch level changes that will make it into `1.1.4`. +* `development` contains the state which will at some point become `1.2.0`. + Please follow the [code of conduct](CODE_OF_CONDUCT.md) if you want to be part of the project. ## Acknowledgements From 9e0aa95a5a1138b1d7266831def013341737e97f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Mar 2022 21:29:34 +0100 Subject: [PATCH 0111/1204] Add documentation for the DecryptOrVerify examples --- .../pgpainless/example/DecryptOrVerify.java | 133 ++++++++++++++---- 1 file changed, 106 insertions(+), 27 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java index d85d667e..074fc206 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java @@ -30,8 +30,14 @@ import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.key.protection.SecretKeyRingProtector; +/** + * This class contains examples on how to decrypt encrypted, and verify signed messages. + */ public class DecryptOrVerify { + /** + * The secret key. + */ private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: AA21 9149 3B35 E679 8876 DE43 B0D7 8185 F639 B6C9\n" + @@ -56,11 +62,22 @@ public class DecryptOrVerify { "=JHMt\n" + "-----END PGP PRIVATE KEY BLOCK-----\n"; - // The key above is not password protected. + /** + * Protector to unlock the secret key. + * Since the key is not protected, it is enough to use an unprotectedKeys implementation. + * + * For more info on how to use the {@link SecretKeyRingProtector}, see {@link UnlockSecretKeys}. + */ private static final SecretKeyRingProtector keyProtector = SecretKeyRingProtector.unprotectedKeys(); + /** + * The plaintext message. + */ private static final String PLAINTEXT = "Hello, World!\n"; + /** + * The {@link #PLAINTEXT} message, but signed using inband signatures. + */ private static final String INBAND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + @@ -70,6 +87,10 @@ public class DecryptOrVerify { "M8e7ufwA\n" + "=RDiy\n" + "-----END PGP MESSAGE-----"; + + /** + * The {@link #PLAINTEXT} message, but signed using the cleartext signature framework. + */ private static final String CLEARTEXT_SIGNED = "-----BEGIN PGP SIGNED MESSAGE-----\n" + "Hash: SHA512\n" + "\n" + @@ -83,6 +104,10 @@ public class DecryptOrVerify { "QUibivG5Slahz8l7PWnGkxbB2naQxgw=\n" + "=oNIK\n" + "-----END PGP SIGNATURE-----"; + + /** + * The {@link #PLAINTEXT} message, but encrypted for the {@link #certificate}. + */ private static final String ENCRYPTED = "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + @@ -92,6 +117,10 @@ public class DecryptOrVerify { "/lWeVnK7NwtfArlhpRcph0S8\n" + "=1cyl\n" + "-----END PGP MESSAGE-----\n"; + + /** + * The {@link #PLAINTEXT} message signed by the {@link #secretKey} and encrypted for the {@link #certificate}. + */ private static final String ENCRYPTED_AND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + @@ -110,37 +139,55 @@ public class DecryptOrVerify { @BeforeAll public static void prepare() throws IOException { + // read the secret key secretKey = PGPainless.readKeyRing().secretKeyRing(KEY); + // certificate is the public part of the key certificate = PGPainless.extractCertificate(secretKey); } + /** + * This example demonstrates how to decrypt an encrypted message using a secret key. + * + * @throws PGPException + * @throws IOException + */ @Test public void decryptMessage() throws PGPException, IOException { ConsumerOptions consumerOptions = new ConsumerOptions() - .addDecryptionKey(secretKey, keyProtector); + .addDecryptionKey(secretKey, keyProtector); // add the decryption key ring ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ENCRYPTED.getBytes(StandardCharsets.UTF_8)); + // The decryption stream is an input stream from which we read the decrypted data DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(ciphertextIn) .withOptions(consumerOptions); Streams.pipeAll(decryptionStream, plaintextOut); - decryptionStream.close(); + decryptionStream.close(); // remember to close the stream! + // The metadata object contains information about the message OpenPgpMetadata metadata = decryptionStream.getResult(); assertTrue(metadata.isEncrypted()); // message was encrypted assertFalse(metadata.isVerified()); // We did not do any signature verification + // The output stream now contains the decrypted message assertEquals(PLAINTEXT, plaintextOut.toString()); } + /** + * In this example, an encrypted and signed message is processed. + * The message gets decrypted using the secret key and the signatures are verified using the certificate. + * + * @throws PGPException + * @throws IOException + */ @Test public void decryptMessageAndVerifySignatures() throws PGPException, IOException { ConsumerOptions consumerOptions = new ConsumerOptions() - .addDecryptionKey(secretKey, keyProtector) - .addVerificationCert(certificate); + .addDecryptionKey(secretKey, keyProtector) // provide the secret key of the recipient for decryption + .addVerificationCert(certificate); // provide the signers public key for signature verification ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ENCRYPTED_AND_SIGNED.getBytes(StandardCharsets.UTF_8)); @@ -150,66 +197,98 @@ public class DecryptOrVerify { .withOptions(consumerOptions); Streams.pipeAll(decryptionStream, plaintextOut); - decryptionStream.close(); + decryptionStream.close(); // remember to close the stream to finish signature verification + // metadata with information on the message, like signatures OpenPgpMetadata metadata = decryptionStream.getResult(); - assertTrue(metadata.isEncrypted()); - assertTrue(metadata.isSigned()); - assertTrue(metadata.isVerified()); - assertTrue(metadata.containsVerifiedSignatureFrom(certificate)); + assertTrue(metadata.isEncrypted()); // messages was in fact encrypted + assertTrue(metadata.isSigned()); // message contained some signatures + assertTrue(metadata.isVerified()); // the signatures were actually correct + assertTrue(metadata.containsVerifiedSignatureFrom(certificate)); // the signatures could be verified using the certificate assertEquals(PLAINTEXT, plaintextOut.toString()); } + /** + * In this example, signed messages are verified. + * The example shows that verification of inband signed, and cleartext signed messages works the same. + * @throws PGPException + * @throws IOException + */ @Test public void verifySignatures() throws PGPException, IOException { ConsumerOptions options = new ConsumerOptions() - .addVerificationCert(certificate); + .addVerificationCert(certificate); // provide the signers certificate for verification of signatures for (String signed : new String[] {INBAND_SIGNED, CLEARTEXT_SIGNED}) { ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayInputStream in = new ByteArrayInputStream(signed.getBytes(StandardCharsets.UTF_8)); - DecryptionStream verificationStream; - verificationStream = PGPainless.decryptAndOrVerify() - .onInputStream(in) - .withOptions(options); + + DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(options); Streams.pipeAll(verificationStream, out); - verificationStream.close(); + verificationStream.close(); // remember to close the stream to finish sig verification + // Get the metadata object for information about the message OpenPgpMetadata metadata = verificationStream.getResult(); - assertTrue(metadata.isVerified()); + assertTrue(metadata.isVerified()); // signatures were verified successfully + // The output stream we piped to now contains the message assertEquals(PLAINTEXT, out.toString()); } } - + /** + * This example shows how to create - and verify - cleartext signed messages. + * @throws PGPException + * @throws IOException + */ @Test - public void createVerifyCleartextSignedMessage() throws PGPException, IOException { + public void createAndVerifyCleartextSignedMessage() throws PGPException, IOException { + // In this example we sign and verify a number of different messages one after the other for (String msg : new String[] {"Hello World!", "- Hello - World -", "Hello, World!\n", "Hello\nWorld!"}) { + // we need to read the plaintext message from somewhere ByteArrayInputStream in = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); + // and write the signed message to an output stream ByteArrayOutputStream out = new ByteArrayOutputStream(); + + SigningOptions signingOptions = SigningOptions.get(); + // for cleartext signed messages, we need to add a detached signature... + signingOptions.addDetachedSignature(keyProtector, secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT); + ProducerOptions producerOptions = ProducerOptions.sign(signingOptions) + .setCleartextSigned(); // and declare that the message will be cleartext signed + + // Create the signing stream EncryptionStream signingStream = PGPainless.encryptAndOrSign() - .onOutputStream(out) - .withOptions(ProducerOptions.sign(SigningOptions.get() - .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) - ).setCleartextSigned()); + .onOutputStream(out) // on the output stream + .withOptions(producerOptions); // with the options - Streams.pipeAll(in, signingStream); - signingStream.close(); + Streams.pipeAll(in, signingStream); // pipe the plaintext message into the signing stream + signingStream.close(); // remember to close the stream to finish the signatures - ByteArrayInputStream signedIn = new ByteArrayInputStream(out.toByteArray()); + // Now the output stream contains the signed message + byte[] signedMessage = out.toByteArray(); + // Verification + // we need to read the signed message + ByteArrayInputStream signedIn = new ByteArrayInputStream(signedMessage); + + // and pass it to the decryption stream DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() .onInputStream(signedIn) .withOptions(new ConsumerOptions().addVerificationCert(certificate)); + // plain will receive the plaintext message ByteArrayOutputStream plain = new ByteArrayOutputStream(); Streams.pipeAll(verificationStream, plain); - verificationStream.close(); + verificationStream.close(); // as always, remember to close the stream + + // Metadata will confirm that the message was in fact signed OpenPgpMetadata metadata = verificationStream.getResult(); assertTrue(metadata.isVerified()); + // compare the plaintext to what we originally signed assertArrayEquals(msg.getBytes(StandardCharsets.UTF_8), plain.toByteArray()); } } From d6cf1c6609de195f67fb2549dc15140b45150dc4 Mon Sep 17 00:00:00 2001 From: Simon Frankenberger Date: Thu, 3 Mar 2022 11:55:34 +0100 Subject: [PATCH 0112/1204] fix "Easily Generate Keys" example code missing passphrase wrapper class --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a4157c6d..2627b1f9 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ There are some predefined key archetypes, but it is possible to fully customize KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE) ).addUserId("Juliet ") .addUserId("xmpp:juliet@capulet.lit") - .setPassphrase("romeo_oh_Romeo<3") + .setPassphrase(Passphrase.fromPassword("romeo_oh_Romeo<3")) .build(); ``` From e569c2c99152a202d2019a2bd3a2b240f033924e Mon Sep 17 00:00:00 2001 From: Simon Frankenberger Date: Thu, 3 Mar 2022 11:56:28 +0100 Subject: [PATCH 0113/1204] ArmorUtils now prints out the primary user-id and brief information about other user-ids --- .../java/org/pgpainless/util/ArmorUtils.java | 31 ++++++++++- .../org/pgpainless/util/ArmorUtilsTest.java | 55 +++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index 9d73635e..122dd961 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -24,6 +24,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.bouncycastle.util.io.Streams; @@ -106,14 +107,38 @@ public final class ArmorUtils { return sb.toString(); } + private static Tuple getPrimaryUserIdAndUserIdCount(PGPPublicKey publicKey) { + Iterator userIds = publicKey.getUserIDs(); + int countIdentities = 0; + String primary = null; + while (userIds.hasNext()) { + countIdentities++; + String userId = userIds.next(); + if (primary == null) { + Iterator signatures = publicKey.getSignaturesForID(userId); + while (signatures.hasNext()) { + PGPSignature signature = signatures.next(); + if (signature.getHashedSubPackets().isPrimaryUserID()) { + primary = userId; + break; + } + } + } + } + return new Tuple<>(primary, countIdentities); + } + private static MultiMap keyToHeader(PGPPublicKey publicKey) { MultiMap header = new MultiMap<>(); OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); - Iterator userIds = publicKey.getUserIDs(); header.put(HEADER_COMMENT, fingerprint.prettyPrint()); - if (userIds.hasNext()) { - header.put(HEADER_COMMENT, userIds.next()); + Tuple idCount = getPrimaryUserIdAndUserIdCount(publicKey); + if (idCount.getA() != null) { + header.put(HEADER_COMMENT, idCount.getA() + " (Primary)"); + } + if (idCount.getB() != 1) { + header.put(HEADER_COMMENT, String.format("Public key contains %d identities", idCount.getB())); } return header; } diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java index f421277b..d9ac2ca5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java @@ -21,16 +21,24 @@ import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; +import org.pgpainless.PGPainless; import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.TestKeys; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.ecc.EllipticCurve; +import org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA; public class ArmorUtilsTest { @@ -144,6 +152,53 @@ public class ArmorUtilsTest { "-----END PGP MESSAGE-----\n", out.toString()); } + @Test + public void testMultipleIdentitiesInHeader() throws Exception { + PGPSecretKeyRing secretKeyRing = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(ECDSA.fromCurve(EllipticCurve._P256), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) + .addUserId("Juliet ") + .addUserId("xmpp:juliet@capulet.lit") + .setPassphrase(Passphrase.fromPassword("test")) + .build(); + PGPPublicKey publicKey = secretKeyRing.getPublicKey(); + PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); + String armored = PGPainless.asciiArmor(publicKeyRing); + Assertions.assertTrue(armored.contains("Comment: Juliet (Primary)")); + Assertions.assertTrue(armored.contains("Comment: Public key contains 2 identities")); + } + + @Test + public void testSingleIdentityInHeader() throws Exception { + PGPSecretKeyRing secretKeyRing = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(ECDSA.fromCurve(EllipticCurve._P256), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) + .addUserId("Juliet ") + .setPassphrase(Passphrase.fromPassword("test")) + .build(); + PGPPublicKey publicKey = secretKeyRing.getPublicKey(); + PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); + String armored = PGPainless.asciiArmor(publicKeyRing); + Assertions.assertTrue(armored.contains("Comment: Juliet (Primary)")); + Assertions.assertFalse(armored.contains("Comment: Public key contains 1 identities")); + } + + @Test + public void testWithoutIdentityInHeader() throws Exception { + PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing("-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "xsBNBGIgzE0BCACwxaYg6bpmp0POq1T6yalGE9XaL2IG9d9khDBweZ63s3Pu1pHB\n" + + "JtmjgN7Tx3ts6hLzQm3YKYA6zu1MXQ8k2vqtdtGUpZPp18Pbars7yUDqh8QIdFjO\n" + + "GeE+c8So0MQgTgoBuyZiSmslwp1WO78ozf/0rCayFdy73dPUntuLE6c2ZKO8nw/g\n" + + "uyk2ozsqLN/TBpgbuJUyMedJtXV10DdT9QxH/66LmdjFKXTkc74qI8YAm/pmJeOh\n" + + "36qZ5ehAgz9MthPQINnZKpnqidqkGFvjwVFlCMlVSmNCNJmpgGDH3gvkklZHzGsf\n" + + "dfzQswd/BQjPsFH9cK+QFYMG6q2zrvM0X9mdABEBAAE=\n" + + "=njg8\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"); + PGPPublicKey publicKey = publicKeys.getPublicKey(); + PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); + String armored = PGPainless.asciiArmor(publicKeyRing); + Assertions.assertTrue(armored.contains("Comment: Public key contains 0 identities")); + } + @TestTemplate @ExtendWith(TestAllImplementations.class) public void decodeExampleTest() throws IOException, PGPException { From 3585203557066a22ac40c2fd5cefbc01974c628f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 21 Mar 2022 16:44:59 +0100 Subject: [PATCH 0114/1204] Prettify user-id info on armor --- .../java/org/pgpainless/util/ArmorUtils.java | 36 ++++++++++++++----- .../org/pgpainless/util/ArmorUtilsTest.java | 33 +++++++++++++---- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index 122dd961..a46dd211 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -108,12 +108,21 @@ public final class ArmorUtils { } private static Tuple getPrimaryUserIdAndUserIdCount(PGPPublicKey publicKey) { + // Quickly determine the primary user-id + number of total user-ids + // NOTE: THIS METHOD DOES NOT CRYPTOGRAPHICALLY VERIFY THE SIGNATURES + // DO NOT RELY ON IT! Iterator userIds = publicKey.getUserIDs(); int countIdentities = 0; + String first = null; String primary = null; while (userIds.hasNext()) { countIdentities++; String userId = userIds.next(); + // remember the first user-id + if (first == null) { + first = userId; + } + if (primary == null) { Iterator signatures = publicKey.getSignaturesForID(userId); while (signatures.hasNext()) { @@ -125,7 +134,10 @@ public final class ArmorUtils { } } } - return new Tuple<>(primary, countIdentities); + // It may happen that no user-id is marked as primary + // in that case print the first one + String printed = primary != null ? primary : first; + return new Tuple<>(printed, countIdentities); } private static MultiMap keyToHeader(PGPPublicKey publicKey) { @@ -133,16 +145,24 @@ public final class ArmorUtils { OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); header.put(HEADER_COMMENT, fingerprint.prettyPrint()); - Tuple idCount = getPrimaryUserIdAndUserIdCount(publicKey); - if (idCount.getA() != null) { - header.put(HEADER_COMMENT, idCount.getA() + " (Primary)"); - } - if (idCount.getB() != 1) { - header.put(HEADER_COMMENT, String.format("Public key contains %d identities", idCount.getB())); - } + setUserIdInfoOnHeader(header, publicKey); return header; } + private static void setUserIdInfoOnHeader(MultiMap header, PGPPublicKey publicKey) { + Tuple idCount = getPrimaryUserIdAndUserIdCount(publicKey); + String primary = idCount.getA(); + int totalCount = idCount.getB(); + if (primary != null) { + header.put(HEADER_COMMENT, primary); + } + if (totalCount == 2) { + header.put(HEADER_COMMENT, "1 further identity"); + } else if (totalCount > 2) { + header.put(HEADER_COMMENT, String.format("%d further identities", totalCount - 1)); + } + } + private static MultiMap keysToHeader(PGPKeyRing keyRing) { PGPPublicKey publicKey = keyRing.getPublicKey(); return keyToHeader(publicKey); diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java index d9ac2ca5..c320c649 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java @@ -163,10 +163,27 @@ public class ArmorUtilsTest { PGPPublicKey publicKey = secretKeyRing.getPublicKey(); PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); String armored = PGPainless.asciiArmor(publicKeyRing); - Assertions.assertTrue(armored.contains("Comment: Juliet (Primary)")); - Assertions.assertTrue(armored.contains("Comment: Public key contains 2 identities")); + Assertions.assertTrue(armored.contains("Comment: Juliet ")); + Assertions.assertTrue(armored.contains("Comment: 1 further identity")); } + @Test + public void testEvenMoreIdentitiesInHeader() throws Exception { + PGPSecretKeyRing secretKeyRing = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(ECDSA.fromCurve(EllipticCurve._P256), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) + .addUserId("Juliet ") + .addUserId("xmpp:juliet@capulet.lit") + .addUserId("Juliet Montague ") + .setPassphrase(Passphrase.fromPassword("test")) + .build(); + PGPPublicKey publicKey = secretKeyRing.getPublicKey(); + PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); + String armored = PGPainless.asciiArmor(publicKeyRing); + Assertions.assertTrue(armored.contains("Comment: Juliet ")); + Assertions.assertTrue(armored.contains("Comment: 2 further identities")); + } + + @Test public void testSingleIdentityInHeader() throws Exception { PGPSecretKeyRing secretKeyRing = PGPainless.buildKeyRing() @@ -177,13 +194,13 @@ public class ArmorUtilsTest { PGPPublicKey publicKey = secretKeyRing.getPublicKey(); PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); String armored = PGPainless.asciiArmor(publicKeyRing); - Assertions.assertTrue(armored.contains("Comment: Juliet (Primary)")); - Assertions.assertFalse(armored.contains("Comment: Public key contains 1 identities")); + Assertions.assertTrue(armored.contains("Comment: Juliet ")); + Assertions.assertFalse(armored.contains("Comment: 1 total identities")); } @Test public void testWithoutIdentityInHeader() throws Exception { - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing("-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "\n" + "xsBNBGIgzE0BCACwxaYg6bpmp0POq1T6yalGE9XaL2IG9d9khDBweZ63s3Pu1pHB\n" + "JtmjgN7Tx3ts6hLzQm3YKYA6zu1MXQ8k2vqtdtGUpZPp18Pbars7yUDqh8QIdFjO\n" + @@ -192,11 +209,13 @@ public class ArmorUtilsTest { "36qZ5ehAgz9MthPQINnZKpnqidqkGFvjwVFlCMlVSmNCNJmpgGDH3gvkklZHzGsf\n" + "dfzQswd/BQjPsFH9cK+QFYMG6q2zrvM0X9mdABEBAAE=\n" + "=njg8\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"); + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(CERT); PGPPublicKey publicKey = publicKeys.getPublicKey(); PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); String armored = PGPainless.asciiArmor(publicKeyRing); - Assertions.assertTrue(armored.contains("Comment: Public key contains 0 identities")); + Assertions.assertFalse(armored.contains("Comment: 0 total identities")); } @TestTemplate From b1eb33eb2c0f6daf036d56ca10c35b8054dc867b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Mar 2022 12:44:36 +0100 Subject: [PATCH 0115/1204] Update CHANGELOG --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28d2483f..9d644157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,13 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.1.3-SNAPSHOT +## 1.1.4-SNAPSHOT +- Add utility method `KeyRingUtils.removeSecretKey()` to remove secret key part from key ring + - This can come in handy when using primary keys stored offline +- Add `EncryptionResult.isEncryptedFor(certificate)` +- `ArmorUtils.toAsciiArmoredString()` methods now print out primary user-id and brief information about further user-ids (thanks @bratkartoffel for the patch) + +## 1.1.3 - Make `SigningOptions.getSigningMethods()` part of internal API - Fix crash when trying to do verification of unmatched `SignersUserId` signature subpacket - For now, verification of `SignersUserId` is disabled but can be enabled via `Policy.setSignerUserIdValidationLevel()` From b5ccb23a62680f1af63c557792210275a0102db5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Mar 2022 12:49:30 +0100 Subject: [PATCH 0116/1204] Add documentation for KeyRingUtils.removeSecretKey() --- .../org/pgpainless/key/util/KeyRingUtils.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 1648d5d8..6be05289 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -292,17 +292,34 @@ public final class KeyRingUtils { return newSecretKey; } + /** + * Remove the secret key of the subkey identified by the given secret key id from the key ring. + * The public part stays attached to the key ring, so that it can still be used for encryption / verification of signatures. + * + * This method is intended to be used to remove secret primary keys from live keys when those are kept in offline storage. + * + * @param secretKeys secret key ring + * @param secretKeyId id of the secret key to remove + * @return secret key ring with removed secret key + * + * @throws IOException + * @throws PGPException + */ public static PGPSecretKeyRing removeSecretKey(PGPSecretKeyRing secretKeys, long secretKeyId) throws IOException, PGPException { if (secretKeys.getSecretKey(secretKeyId) == null) { throw new NoSuchElementException("PGPSecretKeyRing does not contain secret key " + Long.toHexString(secretKeyId)); } + // Since BCs constructors for secret key rings are mostly private, we need to encode the key ring how we want it + // and then parse it again. ByteArrayOutputStream encoded = new ByteArrayOutputStream(); for (PGPSecretKey secretKey : secretKeys) { if (secretKey.getKeyID() == secretKeyId) { + // only encode the public part of the target key secretKey.getPublicKey().encode(encoded); } else { + // otherwise, encode secret + public key secretKey.encode(encoded); } } @@ -310,6 +327,7 @@ public final class KeyRingUtils { PGPPublicKey extra = it.next(); extra.encode(encoded); } + // Parse the key back into an object return new PGPSecretKeyRing(encoded.toByteArray(), ImplementationFactory.getInstance().getKeyFingerprintCalculator()); } } From 4bae2e74c4d77c295e4073f7dc7fd487aa2e9c1a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Mar 2022 13:05:27 +0100 Subject: [PATCH 0117/1204] Add documentation for further KeyRingUtils methods --- .../org/pgpainless/key/util/KeyRingUtils.java | 81 ++++++++++++++++++- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 6be05289..327701ad 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -143,24 +143,57 @@ public final class KeyRingUtils { return UnlockSecretKey.unlockSecretKey(secretKey, protector); } - /* - PGPXxxKeyRing -> PGPXxxKeyRingCollection - */ + /** + * Create a new {@link PGPPublicKeyRingCollection} from an array of {@link PGPPublicKeyRing PGPPublicKeyRings}. + * + * @param rings array of public key rings + * @return key ring collection + * + * @throws IOException in case of an io error + * @throws PGPException in case of a broken key + */ public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) throws IOException, PGPException { return new PGPPublicKeyRingCollection(Arrays.asList(rings)); } + /** + * Create a new {@link PGPSecretKeyRingCollection} from an array of {@link PGPSecretKeyRing PGPSecretKeyRings}. + * + * @param rings array of secret key rings + * @return secret key ring collection + * + * @throws IOException in case of an io error + * @throws PGPException in case of a broken key + */ public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) throws IOException, PGPException { return new PGPSecretKeyRingCollection(Arrays.asList(rings)); } + /** + * Return true, if the given {@link PGPPublicKeyRing} contains a {@link PGPPublicKey} for the given key id. + * + * @param ring public key ring + * @param keyId id of the key in question + * @return true if ring contains said key, false otherwise + */ public static boolean keyRingContainsKeyWithId(@Nonnull PGPPublicKeyRing ring, long keyId) { return ring.getPublicKey(keyId) != null; } + /** + * Inject a key certification into the given key ring. + * + * @param keyRing key ring + * @param certifiedKey signed public key + * @param certification key signature + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @return key ring with injected signature + * + * @throws NoSuchElementException in case that the signed key is not part of the key ring + */ public static T injectCertification(T keyRing, PGPPublicKey certifiedKey, PGPSignature certification) { PGPSecretKeyRing secretKeys = null; PGPPublicKeyRing publicKeys; @@ -197,6 +230,15 @@ public final class KeyRingUtils { } } + /** + * Inject a user-id certification into the given key ring. + * + * @param keyRing key ring + * @param userId signed user-id + * @param certification signature + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @return key ring with injected certification + */ public static T injectCertification(T keyRing, String userId, PGPSignature certification) { PGPSecretKeyRing secretKeys = null; PGPPublicKeyRing publicKeys; @@ -226,6 +268,15 @@ public final class KeyRingUtils { } } + /** + * Inject a user-attribute vector certification into the given key ring. + * + * @param keyRing key ring + * @param userAttributes certified user attributes + * @param certification certification signature + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @return key ring with injected user-attribute certification + */ public static T injectCertification(T keyRing, PGPUserAttributeSubpacketVector userAttributes, PGPSignature certification) { PGPSecretKeyRing secretKeys = null; PGPPublicKeyRing publicKeys; @@ -255,6 +306,17 @@ public final class KeyRingUtils { } } + /** + * Inject a {@link PGPPublicKey} into the given key ring. + * + * Note: Right now this method is broken and will throw a {@link NotYetImplementedException}. + * TODO: Fix with BC 171 + * + * @param keyRing key ring + * @param publicKey public key + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @return key ring with injected public key + */ public static T keysPlusPublicKey(T keyRing, PGPPublicKey publicKey) { if (true) // Is currently broken beyond repair @@ -281,10 +343,23 @@ public final class KeyRingUtils { } } + /** + * Inject a {@link PGPSecretKey} into a {@link PGPSecretKeyRing}. + * + * @param secretKeys secret key ring + * @param secretKey secret key + * @return secret key ring with injected secret key + */ public static PGPSecretKeyRing keysPlusSecretKey(PGPSecretKeyRing secretKeys, PGPSecretKey secretKey) { return PGPSecretKeyRing.insertSecretKey(secretKeys, secretKey); } + /** + * Inject the given signature into the public part of the given secret key. + * @param secretKey secret key + * @param signature signature + * @return secret key with the signature injected in its public key + */ public static PGPSecretKey secretKeyPlusSignature(PGPSecretKey secretKey, PGPSignature signature) { PGPPublicKey publicKey = secretKey.getPublicKey(); publicKey = PGPPublicKey.addCertification(publicKey, signature); From e89e0f216c3cc37826fada4dfe35df756f27ecf7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Mar 2022 13:20:36 +0100 Subject: [PATCH 0118/1204] Annotate KeyRingUtils methods with Nullable and Nonnull --- .../org/pgpainless/key/util/KeyRingUtils.java | 85 +++++++++++++++---- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 327701ad..502dbff8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -12,6 +12,7 @@ import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyRing; @@ -43,7 +44,8 @@ public final class KeyRingUtils { * @param secretKeys secret keys * @return primary secret key */ - public static PGPSecretKey requirePrimarySecretKeyFrom(PGPSecretKeyRing secretKeys) { + @Nonnull + public static PGPSecretKey requirePrimarySecretKeyFrom(@Nonnull PGPSecretKeyRing secretKeys) { PGPSecretKey primarySecretKey = getPrimarySecretKeyFrom(secretKeys); if (primarySecretKey == null) { throw new NoSuchElementException("Provided PGPSecretKeyRing has no primary secret key."); @@ -57,7 +59,8 @@ public final class KeyRingUtils { * @param secretKeys secret key ring * @return primary secret key */ - public static PGPSecretKey getPrimarySecretKeyFrom(PGPSecretKeyRing secretKeys) { + @Nullable + public static PGPSecretKey getPrimarySecretKeyFrom(@Nonnull PGPSecretKeyRing secretKeys) { PGPSecretKey secretKey = secretKeys.getSecretKey(); if (secretKey.isMasterKey()) { return secretKey; @@ -72,7 +75,8 @@ public final class KeyRingUtils { * @param keyRing key ring * @return primary public key */ - public static PGPPublicKey requirePrimaryPublicKeyFrom(PGPKeyRing keyRing) { + @Nonnull + public static PGPPublicKey requirePrimaryPublicKeyFrom(@Nonnull PGPKeyRing keyRing) { PGPPublicKey primaryPublicKey = getPrimaryPublicKeyFrom(keyRing); if (primaryPublicKey == null) { throw new NoSuchElementException("Provided PGPKeyRing has no primary public key."); @@ -86,7 +90,8 @@ public final class KeyRingUtils { * @param keyRing key ring * @return primary public key */ - public static PGPPublicKey getPrimaryPublicKeyFrom(PGPKeyRing keyRing) { + @Nullable + public static PGPPublicKey getPrimaryPublicKeyFrom(@Nonnull PGPKeyRing keyRing) { PGPPublicKey primaryPublicKey = keyRing.getPublicKey(); if (primaryPublicKey.isMasterKey()) { return primaryPublicKey; @@ -94,11 +99,28 @@ public final class KeyRingUtils { return null; } - public static PGPPublicKey getPublicKeyFrom(PGPKeyRing keyRing, long subKeyId) { + /** + * Return the public key with the given subKeyId from the keyRing. + * If no such subkey exists, return null. + * @param keyRing key ring + * @param subKeyId subkey id + * @return subkey or null + */ + @Nullable + public static PGPPublicKey getPublicKeyFrom(@Nonnull PGPKeyRing keyRing, long subKeyId) { return keyRing.getPublicKey(subKeyId); } - public static PGPPublicKey requirePublicKeyFrom(PGPKeyRing keyRing, long subKeyId) { + /** + * Require the public key with the given subKeyId from the keyRing. + * If no such subkey exists, throw an {@link NoSuchElementException}. + * + * @param keyRing key ring + * @param subKeyId subkey id + * @return subkey + */ + @Nonnull + public static PGPPublicKey requirePublicKeyFrom(@Nonnull PGPKeyRing keyRing, long subKeyId) { PGPPublicKey publicKey = getPublicKeyFrom(keyRing, subKeyId); if (publicKey == null) { throw new NoSuchElementException("KeyRing does not contain public key with keyID " + Long.toHexString(subKeyId)); @@ -106,7 +128,16 @@ public final class KeyRingUtils { return publicKey; } - public static PGPSecretKey requireSecretKeyFrom(PGPSecretKeyRing keyRing, long subKeyId) { + /** + * Require the secret key with the given secret subKeyId from the secret keyRing. + * If no such subkey exists, throw an {@link NoSuchElementException}. + * + * @param keyRing secret key ring + * @param subKeyId subkey id + * @return secret subkey + */ + @Nonnull + public static PGPSecretKey requireSecretKeyFrom(@Nonnull PGPSecretKeyRing keyRing, long subKeyId) { PGPSecretKey secretKey = keyRing.getSecretKey(subKeyId); if (secretKey == null) { throw new NoSuchElementException("KeyRing does not contain secret key with keyID " + Long.toHexString(subKeyId)); @@ -120,7 +151,8 @@ public final class KeyRingUtils { * @param secretKeys secret key ring * @return public key ring */ - public static PGPPublicKeyRing publicKeyRingFrom(PGPSecretKeyRing secretKeys) { + @Nonnull + public static PGPPublicKeyRing publicKeyRingFrom(@Nonnull PGPSecretKeyRing secretKeys) { List publicKeyList = new ArrayList<>(); Iterator publicKeyIterator = secretKeys.getPublicKeys(); while (publicKeyIterator.hasNext()) { @@ -139,7 +171,9 @@ public final class KeyRingUtils { * * @throws PGPException if something goes wrong (e.g. wrong passphrase) */ - public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, SecretKeyRingProtector protector) throws PGPException { + @Nonnull + public static PGPPrivateKey unlockSecretKey(@Nonnull PGPSecretKey secretKey, @Nonnull SecretKeyRingProtector protector) + throws PGPException { return UnlockSecretKey.unlockSecretKey(secretKey, protector); } @@ -152,6 +186,7 @@ public final class KeyRingUtils { * @throws IOException in case of an io error * @throws PGPException in case of a broken key */ + @Nonnull public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) throws IOException, PGPException { return new PGPPublicKeyRingCollection(Arrays.asList(rings)); @@ -166,6 +201,7 @@ public final class KeyRingUtils { * @throws IOException in case of an io error * @throws PGPException in case of a broken key */ + @Nonnull public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) throws IOException, PGPException { return new PGPSecretKeyRingCollection(Arrays.asList(rings)); @@ -194,7 +230,10 @@ public final class KeyRingUtils { * * @throws NoSuchElementException in case that the signed key is not part of the key ring */ - public static T injectCertification(T keyRing, PGPPublicKey certifiedKey, PGPSignature certification) { + @Nonnull + public static T injectCertification(@Nonnull T keyRing, + @Nonnull PGPPublicKey certifiedKey, + @Nonnull PGPSignature certification) { PGPSecretKeyRing secretKeys = null; PGPPublicKeyRing publicKeys; if (keyRing instanceof PGPSecretKeyRing) { @@ -239,7 +278,10 @@ public final class KeyRingUtils { * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} * @return key ring with injected certification */ - public static T injectCertification(T keyRing, String userId, PGPSignature certification) { + @Nonnull + public static T injectCertification(@Nonnull T keyRing, + @Nonnull String userId, + @Nonnull PGPSignature certification) { PGPSecretKeyRing secretKeys = null; PGPPublicKeyRing publicKeys; if (keyRing instanceof PGPSecretKeyRing) { @@ -277,7 +319,10 @@ public final class KeyRingUtils { * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} * @return key ring with injected user-attribute certification */ - public static T injectCertification(T keyRing, PGPUserAttributeSubpacketVector userAttributes, PGPSignature certification) { + @Nonnull + public static T injectCertification(@Nonnull T keyRing, + @Nonnull PGPUserAttributeSubpacketVector userAttributes, + @Nonnull PGPSignature certification) { PGPSecretKeyRing secretKeys = null; PGPPublicKeyRing publicKeys; if (keyRing instanceof PGPSecretKeyRing) { @@ -317,7 +362,9 @@ public final class KeyRingUtils { * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} * @return key ring with injected public key */ - public static T keysPlusPublicKey(T keyRing, PGPPublicKey publicKey) { + @Nonnull + public static T keysPlusPublicKey(@Nonnull T keyRing, + @Nonnull PGPPublicKey publicKey) { if (true) // Is currently broken beyond repair throw new NotYetImplementedException(); @@ -350,7 +397,9 @@ public final class KeyRingUtils { * @param secretKey secret key * @return secret key ring with injected secret key */ - public static PGPSecretKeyRing keysPlusSecretKey(PGPSecretKeyRing secretKeys, PGPSecretKey secretKey) { + @Nonnull + public static PGPSecretKeyRing keysPlusSecretKey(@Nonnull PGPSecretKeyRing secretKeys, + @Nonnull PGPSecretKey secretKey) { return PGPSecretKeyRing.insertSecretKey(secretKeys, secretKey); } @@ -360,7 +409,9 @@ public final class KeyRingUtils { * @param signature signature * @return secret key with the signature injected in its public key */ - public static PGPSecretKey secretKeyPlusSignature(PGPSecretKey secretKey, PGPSignature signature) { + @Nonnull + public static PGPSecretKey secretKeyPlusSignature(@Nonnull PGPSecretKey secretKey, + @Nonnull PGPSignature signature) { PGPPublicKey publicKey = secretKey.getPublicKey(); publicKey = PGPPublicKey.addCertification(publicKey, signature); PGPSecretKey newSecretKey = PGPSecretKey.replacePublicKey(secretKey, publicKey); @@ -380,7 +431,9 @@ public final class KeyRingUtils { * @throws IOException * @throws PGPException */ - public static PGPSecretKeyRing removeSecretKey(PGPSecretKeyRing secretKeys, long secretKeyId) + @Nonnull + public static PGPSecretKeyRing removeSecretKey(@Nonnull PGPSecretKeyRing secretKeys, + long secretKeyId) throws IOException, PGPException { if (secretKeys.getSecretKey(secretKeyId) == null) { throw new NoSuchElementException("PGPSecretKeyRing does not contain secret key " + Long.toHexString(secretKeyId)); From 16b0d0730ef726915c6935c9ee84b1c7db7ccf9d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Mar 2022 14:17:35 +0100 Subject: [PATCH 0119/1204] Annotate and document ArmorUtils class --- .../java/org/pgpainless/util/ArmorUtils.java | 465 ++++++++++++++---- .../org/pgpainless/sop/ExtractCertImpl.java | 2 +- 2 files changed, 367 insertions(+), 100 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index a46dd211..8e93ce6d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -31,6 +31,9 @@ import org.bouncycastle.util.io.Streams; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.key.OpenPgpFingerprint; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + public final class ArmorUtils { // MessageIDs are 32 printable characters @@ -46,27 +49,79 @@ public final class ArmorUtils { } - public static String toAsciiArmoredString(PGPSecretKey secretKey) throws IOException { + /** + * Return the ASCII armored encoding of the given {@link PGPSecretKey}. + * + * @param secretKey secret key + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull PGPSecretKey secretKey) + throws IOException { MultiMap header = keyToHeader(secretKey.getPublicKey()); return toAsciiArmoredString(secretKey.getEncoded(), header); } - public static String toAsciiArmoredString(PGPPublicKey publicKey) throws IOException { + /** + * Return the ASCII armored encoding of the given {@link PGPPublicKey}. + * + * @param publicKey public key + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull PGPPublicKey publicKey) + throws IOException { MultiMap header = keyToHeader(publicKey); return toAsciiArmoredString(publicKey.getEncoded(), header); } - public static String toAsciiArmoredString(PGPSecretKeyRing secretKeys) throws IOException { + /** + * Return the ASCII armored encoding of the given {@link PGPSecretKeyRing}. + * + * @param secretKeys secret key ring + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull PGPSecretKeyRing secretKeys) + throws IOException { MultiMap header = keysToHeader(secretKeys); return toAsciiArmoredString(secretKeys.getEncoded(), header); } - public static String toAsciiArmoredString(PGPPublicKeyRing publicKeys) throws IOException { + /** + * Return the ASCII armored encoding of the given {@link PGPPublicKeyRing}. + * + * @param publicKeys public key ring + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull PGPPublicKeyRing publicKeys) + throws IOException { MultiMap header = keysToHeader(publicKeys); return toAsciiArmoredString(publicKeys.getEncoded(), header); } - public static String toAsciiArmoredString(PGPSecretKeyRingCollection secretKeyRings) throws IOException { + /** + * Return the ASCII armored encoding of the given {@link PGPSecretKeyRingCollection}. + * The encoding will use per-key ASCII armors protecting each {@link PGPSecretKeyRing} individually. + * Those armors are then concatenated with newlines in between. + * + * @param secretKeyRings secret key ring collection + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull PGPSecretKeyRingCollection secretKeyRings) + throws IOException { StringBuilder sb = new StringBuilder(); for (Iterator iterator = secretKeyRings.iterator(); iterator.hasNext(); ) { PGPSecretKeyRing secretKeyRing = iterator.next(); @@ -78,24 +133,19 @@ public final class ArmorUtils { return sb.toString(); } - public static ArmoredOutputStream toAsciiArmoredStream(PGPKeyRing keyRing, OutputStream outputStream) { - MultiMap header = keysToHeader(keyRing); - return toAsciiArmoredStream(outputStream, header); - } - - public static ArmoredOutputStream toAsciiArmoredStream(OutputStream outputStream, MultiMap header) { - ArmoredOutputStream armoredOutputStream = ArmoredOutputStreamFactory.get(outputStream); - if (header != null) { - for (String headerKey : header.keySet()) { - for (String headerValue : header.get(headerKey)) { - armoredOutputStream.addHeader(headerKey, headerValue); - } - } - } - return armoredOutputStream; - } - - public static String toAsciiArmoredString(PGPPublicKeyRingCollection publicKeyRings) throws IOException { + /** + * Return the ASCII armored encoding of the given {@link PGPPublicKeyRingCollection}. + * The encoding will use per-key ASCII armors protecting each {@link PGPPublicKeyRing} individually. + * Those armors are then concatenated with newlines in between. + * + * @param publicKeyRings public key ring collection + * @return ascii armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull PGPPublicKeyRingCollection publicKeyRings) + throws IOException { StringBuilder sb = new StringBuilder(); for (Iterator iterator = publicKeyRings.iterator(); iterator.hasNext(); ) { PGPPublicKeyRing publicKeyRing = iterator.next(); @@ -107,7 +157,204 @@ public final class ArmorUtils { return sb.toString(); } - private static Tuple getPrimaryUserIdAndUserIdCount(PGPPublicKey publicKey) { + /** + * Return the ASCII armored encoding of the given OpenPGP data bytes. + * + * @param bytes openpgp data + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull byte[] bytes) + throws IOException { + return toAsciiArmoredString(bytes, null); + } + + /** + * Return the ASCII armored encoding of the given OpenPGP data bytes. + * The ASCII armor will include headers from the header map. + * + * @param bytes OpenPGP data + * @param additionalHeaderValues header map + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull byte[] bytes, + @Nullable MultiMap additionalHeaderValues) + throws IOException { + return toAsciiArmoredString(new ByteArrayInputStream(bytes), additionalHeaderValues); + } + + /** + * Return the ASCII armored encoding of the {@link InputStream} containing OpenPGP data. + * + * @param inputStream input stream of OpenPGP data + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull InputStream inputStream) + throws IOException { + return toAsciiArmoredString(inputStream, null); + } + + /** + * Return the ASCII armored encoding of the OpenPGP data from the given {@link InputStream}. + * The ASCII armor will include armor headers from the given header map. + * + * @param inputStream input stream of OpenPGP data + * @param additionalHeaderValues ASCII armor header map + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull InputStream inputStream, + @Nullable MultiMap additionalHeaderValues) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armor = toAsciiArmoredStream(out, additionalHeaderValues); + Streams.pipeAll(inputStream, armor); + armor.close(); + + return out.toString(); + } + + /** + * Return an {@link ArmoredOutputStream} prepared with headers for the given key ring, which wraps the given + * {@link OutputStream}. + * + * The armored output stream can be used to encode the key ring by calling {@link PGPKeyRing#encode(OutputStream)} + * with the armored output stream as an argument. + * + * @param keyRing key ring + * @param outputStream wrapped output stream + * @return armored output stream + */ + @Nonnull + public static ArmoredOutputStream toAsciiArmoredStream(@Nonnull PGPKeyRing keyRing, + @Nonnull OutputStream outputStream) { + MultiMap header = keysToHeader(keyRing); + return toAsciiArmoredStream(outputStream, header); + } + + /** + * Create an {@link ArmoredOutputStream} wrapping the given {@link OutputStream}. + * The armored output stream will be prepared with armor headers given by header. + * + * Note: Since the armored output stream is retrieved from {@link ArmoredOutputStreamFactory#get(OutputStream)}, + * it may already come with custom headers. Hence, the header entries given by header are appended below those + * already populated headers. + * + * @param outputStream output stream to wrap + * @param header map of header entries + * @return armored output stream + */ + @Nonnull + public static ArmoredOutputStream toAsciiArmoredStream(@Nonnull OutputStream outputStream, + @Nullable MultiMap header) { + ArmoredOutputStream armoredOutputStream = ArmoredOutputStreamFactory.get(outputStream); + if (header != null) { + for (String headerKey : header.keySet()) { + for (String headerValue : header.get(headerKey)) { + armoredOutputStream.addHeader(headerKey, headerValue); + } + } + } + return armoredOutputStream; + } + + /** + * Return an {@link ArmoredOutputStream} prepared with headers for the given key ring, which wraps the given + * {@link OutputStream}. + * + * The armored output stream can be used to encode the key ring by calling {@link PGPKeyRing#encode(OutputStream)} + * with the armored output stream as an argument. + * + * @param keyRing key ring + * @param outputStream wrapped output stream + * @return armored output stream + * + * @deprecated use {@link #toAsciiArmoredStream(PGPKeyRing, OutputStream)} instead + */ + @Deprecated + @Nonnull + public static ArmoredOutputStream createArmoredOutputStreamFor(@Nonnull PGPKeyRing keyRing, + @Nonnull OutputStream outputStream) { + return toAsciiArmoredStream(keyRing, outputStream); + } + + /** + * Generate a header map for ASCII armor from the given {@link PGPKeyRing}. + * + * @param keyRing key ring + * @return header map + */ + @Nonnull + private static MultiMap keysToHeader(@Nonnull PGPKeyRing keyRing) { + PGPPublicKey publicKey = keyRing.getPublicKey(); + return keyToHeader(publicKey); + } + + /** + * Generate a header map for ASCII armor from the given {@link PGPPublicKey}. + * The header map consists of a comment field of the keys pretty-printed fingerprint, + * as well as some optional user-id information (see {@link #setUserIdInfoOnHeader(MultiMap, PGPPublicKey)}. + * + * @param publicKey public key + * @return header map + */ + @Nonnull + private static MultiMap keyToHeader(@Nonnull PGPPublicKey publicKey) { + MultiMap header = new MultiMap<>(); + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); + + header.put(HEADER_COMMENT, fingerprint.prettyPrint()); + setUserIdInfoOnHeader(header, publicKey); + return header; + } + + /** + * Add user-id information to the header map. + * If the key is carrying at least one user-id, we add a comment for the probable primary user-id. + * If the key carries more than one user-id, we further add a comment stating how many further identities + * the key has. + * + * @param header header map + * @param publicKey public key + */ + private static void setUserIdInfoOnHeader(@Nonnull MultiMap header, + @Nonnull PGPPublicKey publicKey) { + Tuple idCount = getPrimaryUserIdAndUserIdCount(publicKey); + String primary = idCount.getA(); + int totalCount = idCount.getB(); + if (primary != null) { + header.put(HEADER_COMMENT, primary); + } + if (totalCount == 2) { + header.put(HEADER_COMMENT, "1 further identity"); + } else if (totalCount > 2) { + header.put(HEADER_COMMENT, String.format("%d further identities", totalCount - 1)); + } + } + + /** + * Determine a probable primary user-id, as well as the total number of user-ids on the given {@link PGPPublicKey}. + * This method is trimmed for efficiency and does not do any cryptographic validation of signatures. + * + * The key might not have any user-id at all, in which case {@link Tuple#getA()} will return null. + * The key might have some user-ids, but none of it marked as primary, in which case {@link Tuple#getA()} + * will return the first user-id of the key. + * + * @param publicKey public key + * @return tuple consisting of a primary user-id candidate, and the total number of user-ids on the key. + */ + @Nonnull + private static Tuple getPrimaryUserIdAndUserIdCount(@Nonnull PGPPublicKey publicKey) { // Quickly determine the primary user-id + number of total user-ids // NOTE: THIS METHOD DOES NOT CRYPTOGRAPHICALLY VERIFY THE SIGNATURES // DO NOT RELY ON IT! @@ -140,98 +387,93 @@ public final class ArmorUtils { return new Tuple<>(printed, countIdentities); } - private static MultiMap keyToHeader(PGPPublicKey publicKey) { - MultiMap header = new MultiMap<>(); - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); - - header.put(HEADER_COMMENT, fingerprint.prettyPrint()); - setUserIdInfoOnHeader(header, publicKey); - return header; - } - - private static void setUserIdInfoOnHeader(MultiMap header, PGPPublicKey publicKey) { - Tuple idCount = getPrimaryUserIdAndUserIdCount(publicKey); - String primary = idCount.getA(); - int totalCount = idCount.getB(); - if (primary != null) { - header.put(HEADER_COMMENT, primary); - } - if (totalCount == 2) { - header.put(HEADER_COMMENT, "1 further identity"); - } else if (totalCount > 2) { - header.put(HEADER_COMMENT, String.format("%d further identities", totalCount - 1)); - } - } - - private static MultiMap keysToHeader(PGPKeyRing keyRing) { - PGPPublicKey publicKey = keyRing.getPublicKey(); - return keyToHeader(publicKey); - } - - public static String toAsciiArmoredString(byte[] bytes) throws IOException { - return toAsciiArmoredString(bytes, null); - } - - public static String toAsciiArmoredString(byte[] bytes, MultiMap additionalHeaderValues) throws IOException { - return toAsciiArmoredString(new ByteArrayInputStream(bytes), additionalHeaderValues); - } - - public static String toAsciiArmoredString(InputStream inputStream) throws IOException { - return toAsciiArmoredString(inputStream, null); - } - - public static void addHashAlgorithmHeader(ArmoredOutputStream armor, HashAlgorithm hashAlgorithm) { + /** + * Add an ASCII armor header entry about the used hash algorithm into the {@link ArmoredOutputStream}. + * + * @param armor armored output stream + * @param hashAlgorithm hash algorithm + * + * @see + * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + */ + public static void addHashAlgorithmHeader(@Nonnull ArmoredOutputStream armor, + @Nonnull HashAlgorithm hashAlgorithm) { armor.addHeader(HEADER_HASH, hashAlgorithm.getAlgorithmName()); } - public static void addCommentHeader(ArmoredOutputStream armor, String comment) { + /** + * Add an ASCII armor comment header entry into the {@link ArmoredOutputStream}. + * + * @param armor armored output stream + * @param comment free-text comment + * + * @see + * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + */ + public static void addCommentHeader(@Nonnull ArmoredOutputStream armor, + @Nonnull String comment) { armor.addHeader(HEADER_COMMENT, comment); } - public static void addMessageIdHeader(ArmoredOutputStream armor, String messageId) { - if (messageId == null) { - throw new NullPointerException("MessageID cannot be null."); - } + /** + * Add an ASCII armor message-id header entry into the {@link ArmoredOutputStream}. + * + * @param armor armored output stream + * @param messageId message id + * + * @see + * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + */ + public static void addMessageIdHeader(@Nonnull ArmoredOutputStream armor, + @Nonnull String messageId) { if (!PATTERN_MESSAGE_ID.matcher(messageId).matches()) { throw new IllegalArgumentException("MessageIDs MUST consist of 32 printable characters."); } armor.addHeader(HEADER_MESSAGEID, messageId); } - public static String toAsciiArmoredString(InputStream inputStream, MultiMap additionalHeaderValues) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ArmoredOutputStream armor = toAsciiArmoredStream(out, additionalHeaderValues); - Streams.pipeAll(inputStream, armor); - armor.close(); - - return out.toString(); - } - - public static ArmoredOutputStream createArmoredOutputStreamFor(PGPKeyRing keyRing, OutputStream outputStream) { - ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(outputStream); - MultiMap headerMap = keysToHeader(keyRing); - for (String header : headerMap.keySet()) { - for (String value : headerMap.get(header)) { - armor.addHeader(header, value); - } - } - - return armor; - } - - public static List getCommentHeaderValues(ArmoredInputStream armor) { + /** + * Extract all ASCII armor header values of type comment from the given {@link ArmoredInputStream}. + * + * @param armor armored input stream + * @return list of comment headers + */ + @Nonnull + public static List getCommentHeaderValues(@Nonnull ArmoredInputStream armor) { return getArmorHeaderValues(armor, HEADER_COMMENT); } - public static List getMessageIdHeaderValues(ArmoredInputStream armor) { + /** + * Extract all ASCII armor header values of type message id from the given {@link ArmoredInputStream}. + * + * @param armor armored input stream + * @return list of message-id headers + */ + @Nonnull + public static List getMessageIdHeaderValues(@Nonnull ArmoredInputStream armor) { return getArmorHeaderValues(armor, HEADER_MESSAGEID); } - public static List getHashHeaderValues(ArmoredInputStream armor) { + /** + * Return all ASCII armor header values of type hash-algorithm from the given {@link ArmoredInputStream}. + * + * @param armor armored input stream + * @return list of hash headers + */ + @Nonnull + public static List getHashHeaderValues(@Nonnull ArmoredInputStream armor) { return getArmorHeaderValues(armor, HEADER_HASH); } - public static List getHashAlgorithms(ArmoredInputStream armor) { + /** + * Return a list of {@link HashAlgorithm} enums extracted from the hash header entries of the given + * {@link ArmoredInputStream}. + * + * @param armor armored input stream + * @return list of hash algorithms from the ASCII header + */ + @Nonnull + public static List getHashAlgorithms(@Nonnull ArmoredInputStream armor) { List algorithmNames = getHashHeaderValues(armor); List algorithms = new ArrayList<>(); for (String name : algorithmNames) { @@ -243,15 +485,38 @@ public final class ArmorUtils { return algorithms; } - public static List getVersionHeaderValues(ArmoredInputStream armor) { + /** + * Return all ASCII armor header values of type version from the given {@link ArmoredInputStream}. + * + * @param armor armored input stream + * @return list of version headers + */ + @Nonnull + public static List getVersionHeaderValues(@Nonnull ArmoredInputStream armor) { return getArmorHeaderValues(armor, HEADER_VERSION); } - public static List getCharsetHeaderValues(ArmoredInputStream armor) { + /** + * Return all ASCII armor header values of type charset from the given {@link ArmoredInputStream}. + * + * @param armor armored input stream + * @return list of charset headers + */ + @Nonnull + public static List getCharsetHeaderValues(@Nonnull ArmoredInputStream armor) { return getArmorHeaderValues(armor, HEADER_CHARSET); } - public static List getArmorHeaderValues(ArmoredInputStream armor, String headerKey) { + /** + * Return all ASCII armor header values of the given headerKey from the given {@link ArmoredInputStream}. + * + * @param armor armored input stream + * @param headerKey ASCII armor header key + * @return list of values for the header key + */ + @Nonnull + public static List getArmorHeaderValues(@Nonnull ArmoredInputStream armor, + @Nonnull String headerKey) { String[] header = armor.getArmorHeaders(); String key = headerKey + ": "; List values = new ArrayList<>(); @@ -278,7 +543,9 @@ public final class ArmorUtils { * @param inputStream input stream * @return BufferedInputStreamExt */ - public static InputStream getDecoderStream(InputStream inputStream) throws IOException { + @Nonnull + public static InputStream getDecoderStream(@Nonnull InputStream inputStream) + throws IOException { BufferedInputStream buf = new BufferedInputStream(inputStream, 512); InputStream decoderStream = PGPUtilWrapper.getDecoderStream(buf); // Data is not armored -> return diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java index 6c3c825f..5f694208 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java @@ -54,7 +54,7 @@ public class ExtractCertImpl implements ExtractCert { public void writeTo(OutputStream outputStream) throws IOException { for (PGPPublicKeyRing cert : certs) { - OutputStream out = armor ? ArmorUtils.createArmoredOutputStreamFor(cert, outputStream) : outputStream; + OutputStream out = armor ? ArmorUtils.toAsciiArmoredStream(cert, outputStream) : outputStream; cert.encode(out); if (armor) { From e8b03834cb325fe30013cdaea7c4edda9b0b8342 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Mar 2022 15:09:09 +0100 Subject: [PATCH 0120/1204] Annotate fromId(code) methods with Nullable and add Nonnull requireFromId(code) methods --- .../algorithm/CompressionAlgorithm.java | 22 ++++++++++++ .../org/pgpainless/algorithm/Feature.java | 35 +++++++++++++++++++ .../pgpainless/algorithm/HashAlgorithm.java | 23 ++++++++++++ .../algorithm/PublicKeyAlgorithm.java | 24 ++++++++++++- .../algorithm/SignatureSubpacket.java | 28 +++++++++++++-- .../pgpainless/algorithm/SignatureType.java | 3 ++ .../pgpainless/algorithm/StreamEncoding.java | 24 +++++++++++++ .../algorithm/SymmetricKeyAlgorithm.java | 23 ++++++++++++ .../DecryptionStreamFactory.java | 13 ++++--- .../encryption_signing/SigningOptions.java | 2 +- .../BcImplementationFactory.java | 2 +- .../java/org/pgpainless/key/info/KeyInfo.java | 2 +- .../org/pgpainless/key/info/KeyRingInfo.java | 3 +- .../secretkeyring/SecretKeyRingEditor.java | 6 ++-- .../key/util/OpenPgpKeyAttributeUtil.java | 5 ++- .../PublicKeyParameterValidationUtil.java | 4 +-- .../java/org/pgpainless/policy/Policy.java | 35 +++++++++++++++---- .../consumer/SignatureValidator.java | 22 +++++++----- .../subpackets/SignatureSubpacketsHelper.java | 6 ++-- .../subpackets/SignatureSubpacketsUtil.java | 15 ++++++-- .../java/org/pgpainless/util/SessionKey.java | 2 +- .../org/pgpainless/algorithm/FeatureTest.java | 17 +++++++++ .../signature/SignatureStructureTest.java | 2 +- .../java/org/pgpainless/sop/DecryptImpl.java | 2 +- 24 files changed, 278 insertions(+), 42 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.java index b1f11185..a2e78c5f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.java @@ -5,10 +5,14 @@ package org.pgpainless.algorithm; import java.util.Map; +import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * Enumeration of possible compression algorithms. * @@ -37,10 +41,28 @@ public enum CompressionAlgorithm { * @param id id * @return compression algorithm */ + @Nullable public static CompressionAlgorithm fromId(int id) { return MAP.get(id); } + /** + * Return the {@link CompressionAlgorithm} value that corresponds to the provided numerical id. + * If an invalid id is provided, thrown an {@link NoSuchElementException}. + * + * @param id id + * @return compression algorithm + * @throws NoSuchElementException in case of an unmapped id + */ + @Nonnull + public static CompressionAlgorithm requireFromId(int id) { + CompressionAlgorithm algorithm = fromId(id); + if (algorithm == null) { + throw new NoSuchElementException("No CompressionAlgorithm found for id " + id); + } + return algorithm; + } + private final int algorithmId; CompressionAlgorithm(int id) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java index 9ec7e362..52de27bf 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java @@ -7,10 +7,14 @@ package org.pgpainless.algorithm; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; import org.bouncycastle.bcpg.sig.Features; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * An enumeration of features that may be set in the {@link Features} subpacket. * @@ -60,16 +64,46 @@ public enum Feature { } } + /** + * Return the {@link Feature} encoded by the given id. + * If the id does not match any known features, return null. + * + * @param id feature id + * @return feature + */ + @Nullable public static Feature fromId(byte id) { return MAP.get(id); } + /** + * Return the {@link Feature} encoded by the given id. + * If the id does not match any known features, throw an {@link NoSuchElementException}. + * + * @param id feature id + * @return feature + * @throws NoSuchElementException if an unmatched feature id is encountered + */ + @Nonnull + public static Feature requireFromId(byte id) { + Feature feature = fromId(id); + if (feature == null) { + throw new NoSuchElementException("Unknown feature id encountered: " + id); + } + return feature; + } + private final byte featureId; Feature(byte featureId) { this.featureId = featureId; } + /** + * Return the id of the feature. + * + * @return feature id + */ public byte getFeatureId() { return featureId; } @@ -80,6 +114,7 @@ public enum Feature { * @param bitmask bitmask * @return list of key flags encoded by the bitmask */ + @Nonnull public static List fromBitmask(int bitmask) { List features = new ArrayList<>(); for (Feature f : Feature.values()) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java index b8c97d7e..bcd69cc0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java @@ -6,9 +6,13 @@ package org.pgpainless.algorithm; import java.util.HashMap; import java.util.Map; +import java.util.NoSuchElementException; import org.bouncycastle.bcpg.HashAlgorithmTags; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * An enumeration of different hashing algorithms. * @@ -42,10 +46,28 @@ public enum HashAlgorithm { * @param id numeric id * @return enum value */ + @Nullable public static HashAlgorithm fromId(int id) { return ID_MAP.get(id); } + /** + * Return the {@link HashAlgorithm} value that corresponds to the provided algorithm id. + * If an invalid algorithm id was provided, throw a {@link NoSuchElementException}. + * + * @param id algorithm id + * @return enum value + * @throws NoSuchElementException in case of an unknown algorithm id + */ + @Nonnull + public static HashAlgorithm requireFromId(int id) { + HashAlgorithm algorithm = fromId(id); + if (algorithm == null) { + throw new NoSuchElementException("No HashAlgorithm found for id " + id); + } + return algorithm; + } + /** * Return the {@link HashAlgorithm} value that corresponds to the provided name. * If an invalid algorithm name was provided, null is returned. @@ -56,6 +78,7 @@ public enum HashAlgorithm { * @param name text name * @return enum value */ + @Nullable public static HashAlgorithm fromName(String name) { return NAME_MAP.get(name); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java index fcb1801c..baa8f92e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java @@ -5,10 +5,14 @@ package org.pgpainless.algorithm; import java.util.Map; +import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * Enumeration of public key algorithms as defined in RFC4880. * @@ -96,12 +100,30 @@ public enum PublicKeyAlgorithm { * If an invalid id is provided, null is returned. * * @param id numeric algorithm id - * @return algorithm + * @return algorithm or null */ + @Nullable public static PublicKeyAlgorithm fromId(int id) { return MAP.get(id); } + /** + * Return the {@link PublicKeyAlgorithm} that corresponds to the provided algorithm id. + * If an invalid id is provided, throw a {@link NoSuchElementException}. + * + * @param id numeric algorithm id + * @return algorithm + * @throws NoSuchElementException in case of an unmatched algorithm id + */ + @Nonnull + public static PublicKeyAlgorithm requireFromId(int id) { + PublicKeyAlgorithm algorithm = fromId(id); + if (algorithm == null) { + throw new NoSuchElementException("No PublicKeyAlgorithm found for id " + id); + } + return algorithm; + } + private final int algorithmId; private final boolean signingCapable; private final boolean encryptionCapable; diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java index e3a754ae..9429f0c6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java @@ -4,6 +4,9 @@ package org.pgpainless.algorithm; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + import static org.bouncycastle.bcpg.SignatureSubpacketTags.ATTESTED_CERTIFICATIONS; import static org.bouncycastle.bcpg.SignatureSubpacketTags.CREATION_TIME; import static org.bouncycastle.bcpg.SignatureSubpacketTags.EMBEDDED_SIGNATURE; @@ -36,6 +39,7 @@ import static org.bouncycastle.bcpg.SignatureSubpacketTags.TRUST_SIG; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; /** @@ -412,14 +416,28 @@ public enum SignatureSubpacket { /** * Return the {@link SignatureSubpacket} that corresponds to the provided id. + * If an unmatched code is presented, return null. * * @param code id * @return signature subpacket */ + @Nullable public static SignatureSubpacket fromCode(int code) { - SignatureSubpacket tag = MAP.get(code); + return MAP.get(code); + } + + /** + * Return the {@link SignatureSubpacket} that corresponds to the provided code. + * + * @param code code + * @return signature subpacket + * @throws NoSuchElementException in case of an unmatched subpacket tag + */ + @Nonnull + public static SignatureSubpacket requireFromCode(int code) { + SignatureSubpacket tag = fromCode(code); if (tag == null) { - throw new IllegalArgumentException("No SignatureSubpacket tag found with code " + code); + throw new NoSuchElementException("No SignatureSubpacket tag found with code " + code); } return tag; } @@ -433,7 +451,11 @@ public enum SignatureSubpacket { public static List fromCodes(int[] codes) { List tags = new ArrayList<>(); for (int code : codes) { - tags.add(fromCode(code)); + try { + tags.add(requireFromCode(code)); + } catch (NoSuchElementException e) { + // skip + } } return tags; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.java index a31dd31a..c2f02989 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.java @@ -6,6 +6,7 @@ package org.pgpainless.algorithm; import org.bouncycastle.openpgp.PGPSignature; +import javax.annotation.Nonnull; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -167,7 +168,9 @@ public enum SignatureType { * * @param code numeric id * @return signature type enum + * @throws IllegalArgumentException in case of an unmatched signature type code */ + @Nonnull public static SignatureType valueOf(int code) { SignatureType type = map.get(code); if (type != null) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java index d47304f6..ad3de600 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java @@ -5,10 +5,14 @@ package org.pgpainless.algorithm; import java.util.Map; +import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; import org.bouncycastle.openpgp.PGPLiteralData; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * Enumeration of possible encoding formats of the content of the literal data packet. * @@ -78,11 +82,31 @@ public enum StreamEncoding { /** * Return the {@link StreamEncoding} corresponding to the provided code identifier. + * If no matching encoding is found, return null. * * @param code identifier * @return encoding enum */ + @Nullable public static StreamEncoding fromCode(int code) { return MAP.get((char) code); } + + /** + * Return the {@link StreamEncoding} corresponding to the provided code identifier. + * If no matching encoding is found, throw a {@link NoSuchElementException}. + * + * @param code identifier + * @return encoding enum + * + * @throws NoSuchElementException in case of an unmatched identifier + */ + @Nonnull + public static StreamEncoding requireFromCode(int code) { + StreamEncoding encoding = fromCode(code); + if (encoding == null) { + throw new NoSuchElementException("No StreamEncoding found for code " + code); + } + return encoding; + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.java index dcbf34cf..e04f21a5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.java @@ -5,10 +5,14 @@ package org.pgpainless.algorithm; import java.util.Map; +import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * Enumeration of possible symmetric encryption algorithms. * @@ -106,10 +110,29 @@ public enum SymmetricKeyAlgorithm { * @param id numeric algorithm id * @return symmetric key algorithm enum */ + @Nullable public static SymmetricKeyAlgorithm fromId(int id) { return MAP.get(id); } + /** + * Return the {@link SymmetricKeyAlgorithm} enum that corresponds to the provided numeric id. + * If an invalid id is provided, throw a {@link NoSuchElementException}. + * + * @param id numeric algorithm id + * @return symmetric key algorithm enum + * + * @throws NoSuchElementException if an unmatched id is provided + */ + @Nonnull + public static SymmetricKeyAlgorithm requireFromId(int id) { + SymmetricKeyAlgorithm algorithm = fromId(id); + if (algorithm == null) { + throw new NoSuchElementException("No SymmetricKeyAlgorithm found for id " + id); + } + return algorithm; + } + private final int algorithmId; SymmetricKeyAlgorithm(int algorithmId) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index fbdd6229..d552ead8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -14,6 +14,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -310,9 +311,13 @@ public final class DecryptionStreamFactory { private InputStream processPGPCompressedData(PGPCompressedData pgpCompressedData, int depth) throws PGPException, IOException { - CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.fromId(pgpCompressedData.getAlgorithm()); - LOGGER.debug("Depth {}: Encountered PGPCompressedData: {}", depth, compressionAlgorithm); - resultBuilder.setCompressionAlgorithm(compressionAlgorithm); + try { + CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.requireFromId(pgpCompressedData.getAlgorithm()); + LOGGER.debug("Depth {}: Encountered PGPCompressedData: {}", depth, compressionAlgorithm); + resultBuilder.setCompressionAlgorithm(compressionAlgorithm); + } catch (NoSuchElementException e) { + throw new PGPException("Unknown compression algorithm encountered.", e); + } InputStream inflatedDataStream = pgpCompressedData.getDataStream(); InputStream decodedDataStream = PGPUtil.getDecoderStream(inflatedDataStream); @@ -340,7 +345,7 @@ public final class DecryptionStreamFactory { resultBuilder.setFileName(pgpLiteralData.getFileName()) .setModificationDate(pgpLiteralData.getModificationTime()) - .setFileEncoding(StreamEncoding.fromCode(pgpLiteralData.getFormat())); + .setFileEncoding(StreamEncoding.requireFromCode(pgpLiteralData.getFormat())); if (onePassSignatureChecks.isEmpty() && onePassSignaturesWithMissingCert.isEmpty()) { LOGGER.debug("No OnePassSignatures found -> We are done"); diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java index ab3898dc..0db93407 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -320,7 +320,7 @@ public final class SigningOptions { throws PGPException { SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(secretKey, signingSubkey.getKeyID()); PGPSecretKey signingSecretKey = secretKey.getSecretKey(signingSubkey.getKeyID()); - PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.fromId(signingSecretKey.getPublicKey().getAlgorithm()); + PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(signingSecretKey.getPublicKey().getAlgorithm()); int bitStrength = secretKey.getPublicKey().getBitStrength(); if (!PGPainless.getPolicy().getPublicKeyAlgorithmPolicy().isAcceptable(publicKeyAlgorithm, bitStrength)) { throw new IllegalArgumentException("Public key algorithm policy violation: " + diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java index 67cc6881..c8e20521 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java @@ -58,7 +58,7 @@ public class BcImplementationFactory extends ImplementationFactory { int keyEncryptionAlgorithm = secretKey.getKeyEncryptionAlgorithm(); if (secretKey.getS2K() == null) { - return getPBESecretKeyEncryptor(SymmetricKeyAlgorithm.fromId(keyEncryptionAlgorithm), passphrase); + return getPBESecretKeyEncryptor(SymmetricKeyAlgorithm.requireFromId(keyEncryptionAlgorithm), passphrase); } int hashAlgorithm = secretKey.getS2K().getHashAlgorithm(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java index a37eda24..d1a5f748 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java @@ -65,7 +65,7 @@ public class KeyInfo { } public static String getCurveName(PGPPublicKey publicKey) { - PublicKeyAlgorithm algorithm = PublicKeyAlgorithm.fromId(publicKey.getAlgorithm()); + PublicKeyAlgorithm algorithm = PublicKeyAlgorithm.requireFromId(publicKey.getAlgorithm()); ECPublicBCPGKey key; switch (algorithm) { case ECDSA: { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 971d5a8e..f1611aff 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -567,8 +567,9 @@ public class KeyRingInfo { * * @return public key algorithm */ + @Nonnull public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.fromId(getPublicKey().getAlgorithm()); + return PublicKeyAlgorithm.requireFromId(getPublicKey().getAlgorithm()); } /** diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index 7eb5a92f..f5a5292c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -272,11 +272,11 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { KeyFlag... additionalKeyFlags) throws PGPException, IOException, NoSuchAlgorithmException { KeyFlag[] flags = concat(keyFlag, additionalKeyFlags); - PublicKeyAlgorithm subkeyAlgorithm = PublicKeyAlgorithm.fromId(subkey.getPublicKey().getAlgorithm()); + PublicKeyAlgorithm subkeyAlgorithm = PublicKeyAlgorithm.requireFromId(subkey.getPublicKey().getAlgorithm()); SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm); // check key against public key algorithm policy - PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.fromId(subkey.getPublicKey().getAlgorithm()); + PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(subkey.getPublicKey().getAlgorithm()); int bitStrength = subkey.getPublicKey().getBitStrength(); if (!PGPainless.getPolicy().getPublicKeyAlgorithmPolicy().isAcceptable(publicKeyAlgorithm, bitStrength)) { throw new IllegalArgumentException("Public key algorithm policy violation: " + @@ -285,7 +285,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing); - PublicKeyAlgorithm signingKeyAlgorithm = PublicKeyAlgorithm.fromId(primaryKey.getPublicKey().getAlgorithm()); + PublicKeyAlgorithm signingKeyAlgorithm = PublicKeyAlgorithm.requireFromId(primaryKey.getPublicKey().getAlgorithm()); HashAlgorithm hashAlgorithm = HashAlgorithmNegotiator .negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) .negotiateHashAlgorithm(info.getPreferredHashAlgorithms()); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/OpenPgpKeyAttributeUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/OpenPgpKeyAttributeUtil.java index a775bc08..e97a2d7a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/OpenPgpKeyAttributeUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/OpenPgpKeyAttributeUtil.java @@ -42,7 +42,10 @@ public final class OpenPgpKeyAttributeUtil { continue; } for (int h : hashAlgos) { - hashAlgorithms.add(HashAlgorithm.fromId(h)); + HashAlgorithm algorithm = HashAlgorithm.fromId(h); + if (algorithm != null) { + hashAlgorithms.add(algorithm); + } } // Exit the loop after the first key signature with hash algorithms. break; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java index 5ee8135b..fbddb080 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java @@ -42,7 +42,7 @@ public class PublicKeyParameterValidationUtil { public static void verifyPublicKeyParameterIntegrity(PGPPrivateKey privateKey, PGPPublicKey publicKey) throws KeyIntegrityException { - PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.fromId(publicKey.getAlgorithm()); + PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(publicKey.getAlgorithm()); boolean valid = true; // Algorithm specific validations @@ -97,7 +97,7 @@ public class PublicKeyParameterValidationUtil { */ private static boolean verifyCanSign(PGPPrivateKey privateKey, PGPPublicKey publicKey) { SecureRandom random = new SecureRandom(); - PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.fromId(publicKey.getAlgorithm()); + PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(publicKey.getAlgorithm()); PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( ImplementationFactory.getInstance().getPGPContentSignerBuilder(publicKeyAlgorithm, HashAlgorithm.SHA256) ); diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java index 58d6f6a2..e64899c0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java @@ -9,6 +9,7 @@ import java.util.Collections; import java.util.EnumMap; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import javax.annotation.Nonnull; @@ -232,8 +233,13 @@ public final class Policy { * @return true if algorithm is acceptable, false otherwise */ public boolean isAcceptable(int algorithmId) { - SymmetricKeyAlgorithm algorithm = SymmetricKeyAlgorithm.fromId(algorithmId); - return isAcceptable(algorithm); + try { + SymmetricKeyAlgorithm algorithm = SymmetricKeyAlgorithm.requireFromId(algorithmId); + return isAcceptable(algorithm); + } catch (NoSuchElementException e) { + // Unknown algorithm is not acceptable + return false; + } } /** @@ -329,8 +335,13 @@ public final class Policy { * @return true if the hash algorithm is acceptable, false otherwise */ public boolean isAcceptable(int algorithmId) { - HashAlgorithm algorithm = HashAlgorithm.fromId(algorithmId); - return isAcceptable(algorithm); + try { + HashAlgorithm algorithm = HashAlgorithm.requireFromId(algorithmId); + return isAcceptable(algorithm); + } catch (NoSuchElementException e) { + // Unknown algorithm is not acceptable + return false; + } } /** @@ -382,7 +393,13 @@ public final class Policy { } public boolean isAcceptable(int compressionAlgorithmTag) { - return isAcceptable(CompressionAlgorithm.fromId(compressionAlgorithmTag)); + try { + CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.requireFromId(compressionAlgorithmTag); + return isAcceptable(compressionAlgorithm); + } catch (NoSuchElementException e) { + // Unknown algorithm is not acceptable + return false; + } } public boolean isAcceptable(CompressionAlgorithm compressionAlgorithm) { @@ -408,7 +425,13 @@ public final class Policy { } public boolean isAcceptable(int algorithmId, int bitStrength) { - return isAcceptable(PublicKeyAlgorithm.fromId(algorithmId), bitStrength); + try { + PublicKeyAlgorithm algorithm = PublicKeyAlgorithm.requireFromId(algorithmId); + return isAcceptable(algorithm, bitStrength); + } catch (NoSuchElementException e) { + // Unknown algorithm is not acceptable + return false; + } } public boolean isAcceptable(PublicKeyAlgorithm algorithm, int bitStrength) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java index a8f1ec5b..3572fff0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java @@ -9,6 +9,7 @@ import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; import org.bouncycastle.bcpg.sig.KeyFlags; @@ -92,7 +93,7 @@ public abstract class SignatureValidator { return new SignatureValidator() { @Override public void verify(PGPSignature signature) throws SignatureValidationException { - if (!PublicKeyAlgorithm.fromId(signature.getKeyAlgorithm()).isSigningCapable()) { + if (!PublicKeyAlgorithm.requireFromId(signature.getKeyAlgorithm()).isSigningCapable()) { // subkey is not signing capable -> No need to process embedded sigs return; } @@ -168,7 +169,7 @@ public abstract class SignatureValidator { return new SignatureValidator() { @Override public void verify(PGPSignature signature) throws SignatureValidationException { - PublicKeyAlgorithm algorithm = PublicKeyAlgorithm.fromId(signingKey.getAlgorithm()); + PublicKeyAlgorithm algorithm = PublicKeyAlgorithm.requireFromId(signingKey.getAlgorithm()); int bitStrength = signingKey.getBitStrength(); if (bitStrength == -1) { throw new SignatureValidationException("Cannot determine bit strength of signing key."); @@ -191,11 +192,14 @@ public abstract class SignatureValidator { return new SignatureValidator() { @Override public void verify(PGPSignature signature) throws SignatureValidationException { - HashAlgorithm hashAlgorithm = HashAlgorithm.fromId(signature.getHashAlgorithm()); - Policy.HashAlgorithmPolicy hashAlgorithmPolicy = getHashAlgorithmPolicyForSignature(signature, policy); - - if (!hashAlgorithmPolicy.isAcceptable(signature.getHashAlgorithm())) { - throw new SignatureValidationException("Signature uses unacceptable hash algorithm " + hashAlgorithm); + try { + HashAlgorithm hashAlgorithm = HashAlgorithm.requireFromId(signature.getHashAlgorithm()); + Policy.HashAlgorithmPolicy hashAlgorithmPolicy = getHashAlgorithmPolicyForSignature(signature, policy); + if (!hashAlgorithmPolicy.isAcceptable(signature.getHashAlgorithm())) { + throw new SignatureValidationException("Signature uses unacceptable hash algorithm " + hashAlgorithm); + } + } catch (NoSuchElementException e) { + throw new SignatureValidationException("Signature uses unknown hash algorithm " + signature.getHashAlgorithm()); } } }; @@ -255,8 +259,8 @@ public abstract class SignatureValidator { PGPSignatureSubpacketVector hashedSubpackets = signature.getHashedSubPackets(); for (int criticalTag : hashedSubpackets.getCriticalTags()) { try { - SignatureSubpacket.fromCode(criticalTag); - } catch (IllegalArgumentException e) { + SignatureSubpacket.requireFromCode(criticalTag); + } catch (NoSuchElementException e) { throw new SignatureValidationException("Signature contains unknown critical subpacket of type " + Long.toHexString(criticalTag)); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java index ee34a6fc..2118c49c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java @@ -33,7 +33,7 @@ public class SignatureSubpacketsHelper { public static SignatureSubpackets applyFrom(PGPSignatureSubpacketVector vector, SignatureSubpackets subpackets) { for (SignatureSubpacket subpacket : vector.toArray()) { - org.pgpainless.algorithm.SignatureSubpacket type = org.pgpainless.algorithm.SignatureSubpacket.fromCode(subpacket.getType()); + org.pgpainless.algorithm.SignatureSubpacket type = org.pgpainless.algorithm.SignatureSubpacket.requireFromCode(subpacket.getType()); switch (type) { case signatureCreationTime: case issuerKeyId: @@ -102,8 +102,8 @@ public class SignatureSubpacketsHelper { case signatureTarget: SignatureTarget target = (SignatureTarget) subpacket; subpackets.setSignatureTarget(target.isCritical(), - PublicKeyAlgorithm.fromId(target.getPublicKeyAlgorithm()), - HashAlgorithm.fromId(target.getHashAlgorithm()), + PublicKeyAlgorithm.requireFromId(target.getPublicKeyAlgorithm()), + HashAlgorithm.requireFromId(target.getHashAlgorithm()), target.getHashData()); break; case embeddedSignature: diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index bbf8972b..12c6f1de 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -257,7 +257,10 @@ public final class SignatureSubpacketsUtil { PreferredAlgorithms preferences = getPreferredSymmetricAlgorithms(signature); if (preferences != null) { for (int code : preferences.getPreferences()) { - algorithms.add(SymmetricKeyAlgorithm.fromId(code)); + SymmetricKeyAlgorithm algorithm = SymmetricKeyAlgorithm.fromId(code); + if (algorithm != null) { + algorithms.add(algorithm); + } } } return algorithms; @@ -286,7 +289,10 @@ public final class SignatureSubpacketsUtil { PreferredAlgorithms preferences = getPreferredHashAlgorithms(signature); if (preferences != null) { for (int code : preferences.getPreferences()) { - algorithms.add(HashAlgorithm.fromId(code)); + HashAlgorithm algorithm = HashAlgorithm.fromId(code); + if (algorithm != null) { + algorithms.add(algorithm); + } } } return algorithms; @@ -315,7 +321,10 @@ public final class SignatureSubpacketsUtil { PreferredAlgorithms preferences = getPreferredCompressionAlgorithms(signature); if (preferences != null) { for (int code : preferences.getPreferences()) { - algorithms.add(CompressionAlgorithm.fromId(code)); + CompressionAlgorithm algorithm = CompressionAlgorithm.fromId(code); + if (algorithm != null) { + algorithms.add(algorithm); + } } } return algorithms; diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java b/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java index 72bab826..397e1f0a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java @@ -24,7 +24,7 @@ public class SessionKey { * @param sessionKey BC session key */ public SessionKey(@Nonnull PGPSessionKey sessionKey) { - this(SymmetricKeyAlgorithm.fromId(sessionKey.getAlgorithm()), sessionKey.getKey()); + this(SymmetricKeyAlgorithm.requireFromId(sessionKey.getAlgorithm()), sessionKey.getKey()); } /** diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/FeatureTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/FeatureTest.java index 347322f8..e9d0b86e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/algorithm/FeatureTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/FeatureTest.java @@ -6,20 +6,37 @@ package org.pgpainless.algorithm; import org.junit.jupiter.api.Test; +import java.util.NoSuchElementException; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; public class FeatureTest { + @Test + public void testAll() { + for (Feature feature : Feature.values()) { + assertEquals(feature, Feature.fromId(feature.getFeatureId())); + assertEquals(feature, Feature.requireFromId(feature.getFeatureId())); + } + } + @Test public void testModificationDetection() { Feature modificationDetection = Feature.MODIFICATION_DETECTION; assertEquals(0x01, modificationDetection.getFeatureId()); assertEquals(modificationDetection, Feature.fromId((byte) 0x01)); + assertEquals(modificationDetection, Feature.requireFromId((byte) 0x01)); } @Test public void testFromInvalidIdIsNull() { assertNull(Feature.fromId((byte) 0x99)); } + + @Test + public void testRequireFromInvalidThrows() { + assertThrows(NoSuchElementException.class, () -> Feature.requireFromId((byte) 0x99)); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureStructureTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureStructureTest.java index 84eecdb5..cf118822 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureStructureTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureStructureTest.java @@ -64,7 +64,7 @@ public class SignatureStructureTest { @Test public void testGetHashAlgorithm() { - assertEquals(HashAlgorithm.SHA256, HashAlgorithm.fromId(signature.getHashAlgorithm())); + assertEquals(HashAlgorithm.SHA256, HashAlgorithm.requireFromId(signature.getHashAlgorithm())); } @Test diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index bef260c4..38656c9b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -70,7 +70,7 @@ public class DecryptImpl implements Decrypt { public DecryptImpl withSessionKey(SessionKey sessionKey) throws SOPGPException.UnsupportedOption { consumerOptions.setSessionKey( new org.pgpainless.util.SessionKey( - SymmetricKeyAlgorithm.fromId(sessionKey.getAlgorithm()), + SymmetricKeyAlgorithm.requireFromId(sessionKey.getAlgorithm()), sessionKey.getKey())); return this; } From aeb321b576e3fb4cbb3f5c15c6d60bf0ec3da3b5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Mar 2022 13:40:14 +0100 Subject: [PATCH 0121/1204] Add short project description to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2627b1f9..b4cc1089 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ SPDX-License-Identifier: Apache-2.0 [![PGP](https://img.shields.io/badge/pgp-A027%20DB2F%203E1E%20118A-blue)](https://keyoxide.org/7F9116FEA90A5983936C7CFAA027DB2F3E1E118A) [![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/pgpainless)](https://api.reuse.software/info/github.com/pgpainless/pgpainless) +**PGPainless is an easy-to-use OpenPGP library for Java and Android applications** + ## About PGPainless aims to make using OpenPGP in Java projects as simple as possible. From 405c7225f67f5937d4cabceb2770ef7aed79ab82 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Mar 2022 15:17:29 +0100 Subject: [PATCH 0122/1204] Deprecate ProducerOptions.setForYourEyesOnly() Use of this special file name is deprecated since at least crypto-refresh-05 --- .../org/pgpainless/encryption_signing/ProducerOptions.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java index ac4f0e6e..3fe284c8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java @@ -193,7 +193,10 @@ public final class ProducerOptions { * Note: Therefore this method cannot be used simultaneously with {@link #setFileName(String)}. * * @return this + * @deprecated deprecated since at least crypto-refresh-05. It is not recommended using this special filename in + * newly generated literal data packets */ + @Deprecated public ProducerOptions setForYourEyesOnly() { this.fileName = PGPLiteralData.CONSOLE; return this; From 8ff405d6ad7ff72c2ad4ccb2edf38bd8b7794467 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Mar 2022 14:16:13 +0100 Subject: [PATCH 0123/1204] Add toString() to SessionKey --- .../src/main/java/org/pgpainless/util/SessionKey.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java b/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java index 397e1f0a..1e71bb03 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java @@ -7,6 +7,7 @@ package org.pgpainless.util; import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPSessionKey; +import org.bouncycastle.util.encoders.Hex; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; /** @@ -57,4 +58,9 @@ public class SessionKey { System.arraycopy(key, 0, copy, 0, copy.length); return copy; } + + @Override + public String toString() { + return "" + getAlgorithm().getAlgorithmId() + ":" + Hex.toHexString(getKey()); + } } From 80d97b1bc02497c9dd723a497932065b7975a191 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 27 Mar 2022 16:35:20 +0200 Subject: [PATCH 0124/1204] Fix malformed signature packets --- .../org/pgpainless/encryption_signing/EncryptionStream.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 6fc112ba..68aef2ce 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -47,6 +47,8 @@ public final class EncryptionStream extends OutputStream { private static final int BUFFER_SIZE = 1 << 9; OutputStream outermostStream; + OutputStream signatureLayerStream; + private ArmoredOutputStream armorOutputStream = null; private OutputStream publicKeyEncryptedStream = null; private PGPCompressedDataGenerator compressedDataGenerator; @@ -130,6 +132,7 @@ public final class EncryptionStream extends OutputStream { } private void prepareOnePassSignatures() throws IOException, PGPException { + signatureLayerStream = outermostStream; SigningOptions signingOptions = options.getSigningOptions(); if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { // No singing options/methods -> no signing @@ -274,7 +277,7 @@ public final class EncryptionStream extends OutputStream { resultBuilder.addDetachedSignature(signingKey, signature); } if (!signingMethod.isDetached() || options.isCleartextSigned()) { - signature.encode(outermostStream); + signature.encode(signatureLayerStream); } } } From 82936c5499bce2f86177ba596d244cfa3d205930 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Mar 2022 14:16:46 +0100 Subject: [PATCH 0125/1204] Add investigative test for broken messages when using different data/sig encodings --- .../CanonicalizedDataEncryptionTest.java | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java diff --git a/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java b/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java new file mode 100644 index 00000000..4f6dd15c --- /dev/null +++ b/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java @@ -0,0 +1,219 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package investigations; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.key.protection.SecretKeyRingProtector; + +public class CanonicalizedDataEncryptionTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9AF4 29C4 C389 CC11 1739 98E9 9F8E E9C5 3AE5 C1A4\n" + + "Comment: Test \n" + + "\n" + + "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\n" + + "=EyDf\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + String message = "Hello, World!\n"; + + private static PGPSecretKeyRing secretKeys; + private static PGPPublicKeyRing publicKeys; + + @BeforeAll + public static void readKeys() throws IOException { + secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + publicKeys = PGPainless.extractCertificate(secretKeys); + System.out.println(PGPainless.asciiArmor(secretKeys)); + } + + @Test + public void binaryDataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + fail(); + } + } + + @Test + public void binaryDataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + fail(); + } + } + + @Test + @Disabled("Fails") + public void textDataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + fail(); + } + } + + @Test + public void textDataTextSig() throws PGPException, IOException { + System.out.println("SignatureType: Text, LiteralData: Text"); + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + fail(); + } + } + + @Test + @Disabled("Fails") + public void utf8DataBinarySig() throws PGPException, IOException { + System.out.println("SignatureType: Binary, LiteralData: UTF8"); + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + fail(); + } + } + + @Test + public void utf8DataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + fail(); + } + } + + private String encryptAndSign(String message, DocumentSignatureType sigType, StreamEncoding dataFormat) + throws PGPException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions + .signAndEncrypt( + EncryptionOptions.encryptCommunications() + .addRecipient(publicKeys), + SigningOptions.get() + .addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, sigType) + ) + .setEncoding(dataFormat) + ); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); + Streams.pipeAll(inputStream, encryptionStream); + encryptionStream.close(); + + String msg = out.toString(); + return msg; + } + + private OpenPgpMetadata decryptAndVerify(String msg) throws PGPException, IOException { + ByteArrayInputStream in = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(new ConsumerOptions() + .addDecryptionKey(secretKeys, SecretKeyRingProtector.unprotectedKeys()) + .addVerificationCert(publicKeys)); + + Streams.drain(decryptionStream); + decryptionStream.close(); + + return decryptionStream.getResult(); + } +} From 1cb3e559b528ac944148feaa390bb2b10c97355c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 27 Mar 2022 17:29:42 +0200 Subject: [PATCH 0126/1204] Eliminate removed 'm' StreamEncoding --- .../java/org/pgpainless/algorithm/StreamEncoding.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java index ad3de600..b0617bbb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java @@ -36,17 +36,6 @@ public enum StreamEncoding { */ UTF8(PGPLiteralData.UTF8), - /** - * The literal data packet contains a MIME message body part (RFC2045). - * Introduced in rfc4880-bis10. - * - * TODO: Replace 'm' with 'PGPLiteralData.MIME' once BC 1.71 gets released and contains our fix: - * https://github.com/bcgit/bc-java/pull/1088 - * - * @see RFC4880-bis10 - */ - MIME('m'), - /** * Early versions of PGP also defined a value of 'l' as a 'local' mode for machine-local conversions. * RFC 1991 [RFC1991] incorrectly stated this local mode flag as '1' (ASCII numeral one). From 620deaa1f9f63918c8de8a674f203b51da21af51 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 27 Mar 2022 17:34:24 +0200 Subject: [PATCH 0127/1204] Deprecate ProducerOptions.setEncoding() The reason is that values other than BINARY oftentimes cause issues (see https://github.com/pgpainless/pgpainless/issues/264), and further experts recommended to ignore the metadata of the LiteralData packet and only produce with ('b'/0/) as metadata values. --- .../org/pgpainless/encryption_signing/ProducerOptions.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java index 3fe284c8..77901e34 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java @@ -230,7 +230,11 @@ public final class ProducerOptions { * * @param encoding encoding * @return this + * + * @deprecated this option will be removed in the near future, as values other than {@link StreamEncoding#BINARY} + * are causing issues. See https://github.com/pgpainless/pgpainless/issues/264 for details */ + @Deprecated public ProducerOptions setEncoding(@Nonnull StreamEncoding encoding) { this.streamEncoding = encoding; return this; @@ -239,6 +243,7 @@ public final class ProducerOptions { public StreamEncoding getEncoding() { return streamEncoding; } + /** * Override which compression algorithm shall be used. * From a8fa501a7a2673d20d9deef281dd523745eead1d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Mar 2022 12:08:29 +0200 Subject: [PATCH 0128/1204] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d644157..48dafad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,16 @@ SPDX-License-Identifier: CC0-1.0 - This can come in handy when using primary keys stored offline - Add `EncryptionResult.isEncryptedFor(certificate)` - `ArmorUtils.toAsciiArmoredString()` methods now print out primary user-id and brief information about further user-ids (thanks @bratkartoffel for the patch) +- Methods of `KeyRingUtils` and `ArmorUtils` classes are now annotated with `@Nonnull/@Nullable` +- Enums `fromId(code)` methods are now annotated with `@Nullable` and there are now `requireFromId(code)` counterparts which are `@Nonnull`. +- `ProducerOptions.setForYourEyesOnly()` is now deprecated (reason is deprecation in the +- [crypto-refresh-05](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-05.html#name-special-filename-_console-d) document) +- Add `SessionKey.toString()` +- Partially fix generation of malformed signature packets when using different combinations of `StreamEncoding` and `DocumentSignatureType` values + - Unfortunately PGPainless still produces broken signatures when using either `StreamEncoding.TEXT` or `StreamEncoding.UTF8` in combination with `DocumentSignatureType.BINARY_DOCUMENT`. +- Deprecate `ProducerOptions.setEncoding(StreamEncoding)` + - Will be removed in a future release +- Remove `StreamEncoding.MIME` (was removed from the standard) ## 1.1.3 - Make `SigningOptions.getSigningMethods()` part of internal API From 87e6b044d9fd4c28a25dfe0c117e7a34ba2ae74f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Mar 2022 12:18:03 +0200 Subject: [PATCH 0129/1204] Add EncryptionStream class description --- .../org/pgpainless/encryption_signing/EncryptionStream.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 68aef2ce..34fd05ca 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -30,6 +30,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** + * OutputStream that produces an OpenPGP message. The message can be encrypted, signed, or both, + * depending on its configuration. + * * This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. * @see Source */ From b0eb32d550456bb2e9cf1980f84c7e76d3d773c5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Mar 2022 12:21:53 +0200 Subject: [PATCH 0130/1204] Fix checkstyle --- .../CanonicalizedDataEncryptionTest.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java b/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java index 4f6dd15c..9be3569e 100644 --- a/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java +++ b/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java @@ -100,7 +100,9 @@ public class CanonicalizedDataEncryptionTest { public static void readKeys() throws IOException { secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); publicKeys = PGPainless.extractCertificate(secretKeys); + // CHECKSTYLE:OFF System.out.println(PGPainless.asciiArmor(secretKeys)); + // CHECKSTYLE:ON } @Test @@ -109,8 +111,10 @@ public class CanonicalizedDataEncryptionTest { OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { + // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); + // CHECKSTYLE:ON fail(); } } @@ -121,8 +125,10 @@ public class CanonicalizedDataEncryptionTest { OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { + // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); + // CHECKSTYLE:ON fail(); } } @@ -134,21 +140,24 @@ public class CanonicalizedDataEncryptionTest { OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { + // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); + // CHECKSTYLE:ON fail(); } } @Test public void textDataTextSig() throws PGPException, IOException { - System.out.println("SignatureType: Text, LiteralData: Text"); String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT); OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { + // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); + // CHECKSTYLE:ON fail(); } } @@ -156,13 +165,14 @@ public class CanonicalizedDataEncryptionTest { @Test @Disabled("Fails") public void utf8DataBinarySig() throws PGPException, IOException { - System.out.println("SignatureType: Binary, LiteralData: UTF8"); String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8); OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { + // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); + // CHECKSTYLE:ON fail(); } } @@ -173,8 +183,10 @@ public class CanonicalizedDataEncryptionTest { OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { + // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); + // CHECKSTYLE:ON fail(); } } From ccee24dd9370193caf517aca24a42f656979d298 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Mar 2022 12:26:50 +0200 Subject: [PATCH 0131/1204] PGPainless 1.1.4 --- CHANGELOG.md | 2 +- README.md | 8 ++++---- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48dafad2..b17b071e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.1.4-SNAPSHOT +## 1.1.4 - Add utility method `KeyRingUtils.removeSecretKey()` to remove secret key part from key ring - This can come in handy when using primary keys stored offline - Add `EncryptionResult.isEncryptedFor(certificate)` diff --git a/README.md b/README.md index b4cc1089..514dbd84 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.1.3' + implementation 'org.pgpainless:pgpainless-core:1.1.4' } ``` @@ -221,10 +221,10 @@ which contains the bug you are fixing. That way we can update older revisions of * `development` contains new features that will make it into the next MINOR release. #### Example: -Latest release: 1.1.3 +Latest release: 1.1.4 * `release/1.0` contains the state of `1.0.5-SNAPSHOT` -* `release/1.1` contains the state of `1.1.4-SNAPSHOT` -* `master` contains the state `release/1.1` plus patch level changes that will make it into `1.1.4`. +* `release/1.1` contains the state of `1.1.5-SNAPSHOT` +* `master` contains the state `release/1.1` plus patch level changes that will make it into `1.1.5`. * `development` contains the state which will at some point become `1.2.0`. Please follow the [code of conduct](CODE_OF_CONDUCT.md) if you want to be part of the project. diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index af3c5e52..9ec6c8cd 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.1.3" + implementation "org.pgpainless:pgpainless-sop:1.1.4" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.1.3 + 1.1.4 ... diff --git a/version.gradle b/version.gradle index 57acae3f..99bdb588 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.1.4' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From 30a62daec9462b1fb85cb449a2b65d1aa9cf2dc0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Mar 2022 12:28:37 +0200 Subject: [PATCH 0132/1204] PGPainless-1.1.5-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 99bdb588..3b94fb87 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.1.4' - isSnapshot = false + shortVersion = '1.1.5' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From 4782868bc125a57f6aa9ade8aadc2a962754ec27 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Mar 2022 12:49:26 +0200 Subject: [PATCH 0133/1204] SOP encrypt: match signature type when using --as= option --- .../src/main/java/org/pgpainless/sop/EncryptImpl.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index bb0af660..51624214 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -60,7 +60,11 @@ public class EncryptImpl implements Encrypt { signingOptions = SigningOptions.get(); } try { - signingOptions.addInlineSignatures(SecretKeyRingProtector.unprotectedKeys(), keys, DocumentSignatureType.BINARY_DOCUMENT); + signingOptions.addInlineSignatures( + SecretKeyRingProtector.unprotectedKeys(), + keys, + (encryptAs == EncryptAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) + ); } catch (IllegalArgumentException e) { throw new SOPGPException.KeyCannotSign(); } catch (WrongPassphraseException e) { From 9497cbaeb15803ac2f3005f4c17db9dbb3cd2da0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Mar 2022 12:50:29 +0200 Subject: [PATCH 0134/1204] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b17b071e..7f609c7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.1.5-SNAPSHOT +- SOP encrypt: match signature type when using `encrypt --as=` option + ## 1.1.4 - Add utility method `KeyRingUtils.removeSecretKey()` to remove secret key part from key ring - This can come in handy when using primary keys stored offline From c31fd7d5e0f224ff010f8fa6c000eb4d8b209696 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Mar 2022 13:14:36 +0200 Subject: [PATCH 0135/1204] SOP: Fix mapping of encryption format --- .../src/main/java/org/pgpainless/sop/EncryptImpl.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 51624214..d6bd7709 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -125,10 +125,9 @@ public class EncryptImpl implements Encrypt { private static StreamEncoding encryptAsToStreamEncoding(EncryptAs encryptAs) { switch (encryptAs) { case Binary: + case MIME: return StreamEncoding.BINARY; case Text: - return StreamEncoding.TEXT; - case MIME: return StreamEncoding.UTF8; } throw new IllegalArgumentException("Invalid value encountered: " + encryptAs); From 6bef376992817ba9cc7bf065b8842dbbd05a8b85 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Mar 2022 16:13:08 +0200 Subject: [PATCH 0136/1204] Fix signature generation with all format and signature type combinations This comes at the cost of that we no longer CR/LF encode literal data before encryption/signing. That means that applications that rely on PGPainless to do the CR/LF encoding must manually do the encoding before feeding the message to PGPainless. The newly introduced CRLFGeneratorStream has documentation on how to do that. Fixes #264 --- .../CRLFGeneratorStream.java | 64 +++++++++++++ .../encryption_signing/EncryptionStream.java | 12 +-- .../encryption_signing/ProducerOptions.java | 3 +- .../util/StreamGeneratorWrapper.java | 91 ------------------- .../CanonicalizedDataEncryptionTest.java | 90 +++++++++++++++++- 5 files changed, 158 insertions(+), 102 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/StreamGeneratorWrapper.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java new file mode 100644 index 00000000..f5b13f2a --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2021 David Hook +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import org.pgpainless.algorithm.StreamEncoding; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * {@link OutputStream} which applies CR-LF encoding of its input data, based on the desired {@link StreamEncoding}. + * + * + * If you need PGPainless to CRLF encode signed data for you, you could do the following: + * {@code + *

+ *     InputStream plaintext = ...
+ *     EncryptionStream signerOrEncryptor = PGPainless.signAndOrEncrypt(...);
+ *     CRLFGeneratorStream crlfOut = new CRLFGeneratorStream(signerOrEncryptor, streamEncoding);
+ *
+ *     Streams.pipeAll(plaintext, crlfOut);
+ *     crlfOut.close;
+ *
+ *     EncryptionResult result = signerOrEncryptor.getResult();
+ * 
+ * } + * This implementation originates from the Bouncy Castle library. + */ +public class CRLFGeneratorStream extends OutputStream { + + protected final OutputStream crlfOut; + private final boolean isBinary; + private int lastB = 0; + + public CRLFGeneratorStream(OutputStream crlfOut, StreamEncoding encoding) { + this.crlfOut = crlfOut; + this.isBinary = encoding == StreamEncoding.BINARY; + } + + public void write(int b) throws IOException { + if (!isBinary) { + if (b == '\n' && lastB != '\r') { // Unix + crlfOut.write('\r'); + } else if (lastB == '\r') { // MAC + if (b != '\n') { + crlfOut.write('\n'); + } + } + lastB = b; + } + + crlfOut.write(b); + } + + public void close() throws IOException { + if (!isBinary && lastB == '\r') { // MAC + crlfOut.write('\n'); + } + crlfOut.close(); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 34fd05ca..66fd1d24 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -15,6 +15,7 @@ import org.bouncycastle.bcpg.BCPGOutputStream; import org.bouncycastle.openpgp.PGPCompressedDataGenerator; import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; @@ -25,7 +26,6 @@ import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.ArmoredOutputStreamFactory; -import org.pgpainless.util.StreamGeneratorWrapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,7 +56,7 @@ public final class EncryptionStream extends OutputStream { private OutputStream publicKeyEncryptedStream = null; private PGPCompressedDataGenerator compressedDataGenerator; private BCPGOutputStream basicCompressionStream; - private StreamGeneratorWrapper streamGeneratorWrapper; + private PGPLiteralDataGenerator literalDataGenerator; private OutputStream literalDataStream; EncryptionStream(@Nonnull OutputStream targetOutputStream, @@ -164,8 +164,8 @@ public final class EncryptionStream extends OutputStream { return; } - streamGeneratorWrapper = StreamGeneratorWrapper.forStreamEncoding(options.getEncoding()); - literalDataStream = streamGeneratorWrapper.open(outermostStream, + literalDataGenerator = new PGPLiteralDataGenerator(); + literalDataStream = literalDataGenerator.open(outermostStream, options.getEncoding().getCode(), options.getFileName(), options.getModificationDate(), new byte[BUFFER_SIZE]); outermostStream = literalDataStream; @@ -226,8 +226,8 @@ public final class EncryptionStream extends OutputStream { literalDataStream.flush(); literalDataStream.close(); } - if (streamGeneratorWrapper != null) { - streamGeneratorWrapper.close(); + if (literalDataGenerator != null) { + literalDataGenerator.close(); } if (options.isCleartextSigned()) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java index 77901e34..9ee3c03e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java @@ -231,8 +231,7 @@ public final class ProducerOptions { * @param encoding encoding * @return this * - * @deprecated this option will be removed in the near future, as values other than {@link StreamEncoding#BINARY} - * are causing issues. See https://github.com/pgpainless/pgpainless/issues/264 for details + * @deprecated options other than the default value of {@link StreamEncoding#BINARY} are discouraged. */ @Deprecated public ProducerOptions setEncoding(@Nonnull StreamEncoding encoding) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/StreamGeneratorWrapper.java b/pgpainless-core/src/main/java/org/pgpainless/util/StreamGeneratorWrapper.java deleted file mode 100644 index eded853b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/StreamGeneratorWrapper.java +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.Date; - -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPCanonicalizedDataGenerator; -import org.bouncycastle.openpgp.PGPLiteralDataGenerator; -import org.pgpainless.algorithm.StreamEncoding; - -/** - * Literal Data can be encoded in different ways. - * BINARY encoding leaves the data as is and is generated through the {@link PGPLiteralDataGenerator}. - * However, if the data is encoded in TEXT or UTF8 encoding, we need to use the {@link PGPCanonicalizedDataGenerator} - * instead. - * - * This wrapper class acts as a handle for both options and provides a unified interface for them. - */ -public final class StreamGeneratorWrapper { - - private final StreamEncoding encoding; - private final PGPLiteralDataGenerator literalDataGenerator; - private final PGPCanonicalizedDataGenerator canonicalizedDataGenerator; - - /** - * Create a new instance for the given encoding. - * - * @param encoding stream encoding - * @return wrapper - */ - public static StreamGeneratorWrapper forStreamEncoding(@Nonnull StreamEncoding encoding) { - if (encoding == StreamEncoding.BINARY) { - return new StreamGeneratorWrapper(encoding, new PGPLiteralDataGenerator()); - } else { - return new StreamGeneratorWrapper(encoding, new PGPCanonicalizedDataGenerator()); - } - } - - private StreamGeneratorWrapper(@Nonnull StreamEncoding encoding, @Nonnull PGPLiteralDataGenerator literalDataGenerator) { - if (encoding != StreamEncoding.BINARY) { - throw new IllegalArgumentException("PGPLiteralDataGenerator can only be used with BINARY encoding."); - } - this.encoding = encoding; - this.literalDataGenerator = literalDataGenerator; - this.canonicalizedDataGenerator = null; - } - - private StreamGeneratorWrapper(@Nonnull StreamEncoding encoding, @Nonnull PGPCanonicalizedDataGenerator canonicalizedDataGenerator) { - if (encoding != StreamEncoding.TEXT && encoding != StreamEncoding.UTF8) { - throw new IllegalArgumentException("PGPCanonicalizedDataGenerator can only be used with TEXT or UTF8 encoding."); - } - this.encoding = encoding; - this.canonicalizedDataGenerator = canonicalizedDataGenerator; - this.literalDataGenerator = null; - } - - /** - * Open a new encoding stream. - * - * @param outputStream wrapped output stream - * @param filename file name - * @param modificationDate modification date - * @param buffer buffer - * @return encoding stream - */ - public OutputStream open(OutputStream outputStream, String filename, Date modificationDate, byte[] buffer) throws IOException { - if (literalDataGenerator != null) { - return literalDataGenerator.open(outputStream, encoding.getCode(), filename, modificationDate, buffer); - } else { - return canonicalizedDataGenerator.open(outputStream, encoding.getCode(), filename, modificationDate, buffer); - } - } - - /** - * Close all encoding streams opened by this generator wrapper. - */ - public void close() throws IOException { - if (literalDataGenerator != null) { - literalDataGenerator.close(); - } - if (canonicalizedDataGenerator != null) { - canonicalizedDataGenerator.close(); - } - } -} diff --git a/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java b/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java index 9be3569e..7cae970e 100644 --- a/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java +++ b/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java @@ -4,31 +4,46 @@ package investigations; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.encryption_signing.CRLFGeneratorStream; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; public class CanonicalizedDataEncryptionTest { @@ -134,7 +149,6 @@ public class CanonicalizedDataEncryptionTest { } @Test - @Disabled("Fails") public void textDataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT); OpenPgpMetadata metadata = decryptAndVerify(msg); @@ -163,7 +177,6 @@ public class CanonicalizedDataEncryptionTest { } @Test - @Disabled("Fails") public void utf8DataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8); OpenPgpMetadata metadata = decryptAndVerify(msg); @@ -228,4 +241,75 @@ public class CanonicalizedDataEncryptionTest { return decryptionStream.getResult(); } + + @Test + public void testManualSignWithAllCombinations() throws PGPException, IOException { + for (StreamEncoding streamEncoding : StreamEncoding.values()) { + for (DocumentSignatureType sigType : DocumentSignatureType.values()) { + manualSignAndVerify(sigType, streamEncoding); + } + } + } + + public void manualSignAndVerify(DocumentSignatureType sigType, StreamEncoding streamEncoding) + throws IOException, PGPException { + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(out); + + PGPCompressedDataGenerator compressor = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZLIB); + OutputStream compressedOut = compressor.open(armorOut); + + PGPSignatureGenerator signer = new PGPSignatureGenerator( + new BcPGPContentSignerBuilder( + secretKeys.getPublicKey().getAlgorithm(), + HashAlgorithm.SHA256.getAlgorithmId())); + signer.init(sigType.getSignatureType().getCode(), privateKey); + + PGPOnePassSignature ops = signer.generateOnePassVersion(false); + ops.encode(compressedOut); + + PGPLiteralDataGenerator author = new PGPLiteralDataGenerator(); + OutputStream literalOut = author.open(compressedOut, streamEncoding.getCode(), "", PGPLiteralData.NOW, new byte[4096]); + + byte[] msg = message.getBytes(StandardCharsets.UTF_8); + + ByteArrayOutputStream crlfed = new ByteArrayOutputStream(); + CRLFGeneratorStream crlfOut = new CRLFGeneratorStream(crlfed, streamEncoding); + crlfOut.write(msg); + msg = crlfed.toByteArray(); + + for (byte b : msg) { + literalOut.write(b); + signer.update(b); + } + + literalOut.close(); + PGPSignature signature = signer.generate(); + + signature.encode(compressedOut); + compressor.close(); + + armorOut.close(); + + String ciphertext = out.toString(); + // CHECKSTYLE:OFF + System.out.println(sigType + " " + streamEncoding); + System.out.println(ciphertext); + // CHECKSTYLE:ON + + ByteArrayInputStream cipherIn = new ByteArrayInputStream(ciphertext.getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(cipherIn) + .withOptions(new ConsumerOptions() + .addVerificationCert(publicKeys)); + + Streams.pipeAll(decryptionStream, decrypted); + decryptionStream.close(); + OpenPgpMetadata metadata = decryptionStream.getResult(); + assertTrue(metadata.isVerified(), "Not verified! Sig Type: " + sigType + " StreamEncoding: " + streamEncoding); + + assertArrayEquals(msg, decrypted.toByteArray()); + } } From ade07bde85385791cb918f96446dac2770404605 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Mar 2022 16:43:23 +0200 Subject: [PATCH 0137/1204] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f609c7e..797d7d64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ SPDX-License-Identifier: CC0-1.0 ## 1.1.5-SNAPSHOT - SOP encrypt: match signature type when using `encrypt --as=` option +- `ProducerOptions.setEncoding()`: The encoding is henceforth only considered metadata and will no longer trigger CRLF encoding + - This fixes broken signature generation for mismatching (`StreamEncoding`,`DocumentSignatureType`) tuples. + - Applications that rely on CRLF-encoding must now apply that encoding themselves (see [#264](https://github.com/pgpainless/pgpainless/issues/264#issuecomment-1083206738) for details). ## 1.1.4 - Add utility method `KeyRingUtils.removeSecretKey()` to remove secret key part from key ring From f8e66f4d611c523ecc508d087a2125bcba7c3b96 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Mar 2022 15:03:50 +0200 Subject: [PATCH 0138/1204] Add ProducerOptions.applyCRLFEncoding() Enabling it will automatically apply CRLF encoding to input data. Further, disentangle signing from the encryption stream --- .../encryption_signing/EncryptionStream.java | 35 +++-- .../encryption_signing/ProducerOptions.java | 37 +++++- .../SignatureGenerationStream.java | 61 +++++++++ .../CanonicalizedDataEncryptionTest.java | 122 +++++++++++++++--- 4 files changed, 214 insertions(+), 41 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java rename pgpainless-core/src/test/java/{investigations => org/pgpainless/decryption_verification}/CanonicalizedDataEncryptionTest.java (75%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 66fd1d24..3b737702 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -21,6 +21,7 @@ import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.SubkeyIdentifier; @@ -70,6 +71,8 @@ public final class EncryptionStream extends OutputStream { prepareCompression(); prepareOnePassSignatures(); prepareLiteralDataProcessing(); + prepareSigningStream(); + prepareInputEncoding(); } private void prepareArmor() { @@ -174,20 +177,19 @@ public final class EncryptionStream extends OutputStream { .setFileEncoding(options.getEncoding()); } + public void prepareSigningStream() { + outermostStream = new SignatureGenerationStream(outermostStream, options.getSigningOptions()); + } + + public void prepareInputEncoding() { + CRLFGeneratorStream crlfGeneratorStream = new CRLFGeneratorStream(outermostStream, + options.isApplyCRLFEncoding() ? StreamEncoding.UTF8 : StreamEncoding.BINARY); + outermostStream = crlfGeneratorStream; + } + @Override public void write(int data) throws IOException { outermostStream.write(data); - SigningOptions signingOptions = options.getSigningOptions(); - if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { - return; - } - - for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) { - SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey); - PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); - byte asByte = (byte) (data & 0xff); - signatureGenerator.update(asByte); - } } @Override @@ -199,15 +201,6 @@ public final class EncryptionStream extends OutputStream { @Override public void write(@Nonnull byte[] buffer, int off, int len) throws IOException { outermostStream.write(buffer, 0, len); - SigningOptions signingOptions = options.getSigningOptions(); - if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { - return; - } - for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) { - SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey); - PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); - signatureGenerator.update(buffer, 0, len); - } } @Override @@ -221,6 +214,8 @@ public final class EncryptionStream extends OutputStream { return; } + outermostStream.close(); + // Literal Data if (literalDataStream != null) { literalDataStream.flush(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java index 9ee3c03e..41d9ca85 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java @@ -19,7 +19,8 @@ public final class ProducerOptions { private final SigningOptions signingOptions; private String fileName = ""; private Date modificationDate = PGPLiteralData.NOW; - private StreamEncoding streamEncoding = StreamEncoding.BINARY; + private StreamEncoding encodingField = StreamEncoding.BINARY; + private boolean applyCRLFEncoding = false; private boolean cleartextSigned = false; private CompressionAlgorithm compressionAlgorithmOverride = PGPainless.getPolicy().getCompressionAlgorithmPolicy() @@ -223,9 +224,12 @@ public final class ProducerOptions { } /** - * Set the format of the literal data packet. + * Set format metadata field of the literal data packet. * Defaults to {@link StreamEncoding#BINARY}. * + * This does not change the encoding of the wrapped data itself. + * To apply CR/LF encoding to your input data before processing, use {@link #applyCRLFEncoding(boolean)} instead. + * * @see RFC4880 §5.9. Literal Data Packet * * @param encoding encoding @@ -235,12 +239,37 @@ public final class ProducerOptions { */ @Deprecated public ProducerOptions setEncoding(@Nonnull StreamEncoding encoding) { - this.streamEncoding = encoding; + this.encodingField = encoding; return this; } public StreamEncoding getEncoding() { - return streamEncoding; + return encodingField; + } + + /** + * Apply special encoding of line endings to the input data. + * By default, this is set to
false
, which means that the data is not altered. + * + * Setting it to
true
will change the line endings to CR/LF. + * Note: The encoding will not be reversed when decrypting, so applying CR/LF encoding will result in + * the identity "decrypt(encrypt(data)) == data == verify(sign(data))". + * + * @param applyCRLFEncoding apply crlf encoding + * @return this + */ + public ProducerOptions applyCRLFEncoding(boolean applyCRLFEncoding) { + this.applyCRLFEncoding = applyCRLFEncoding; + return this; + } + + /** + * Return the input encoding that will be applied before signing / encryption. + * + * @return input encoding + */ + public boolean isApplyCRLFEncoding() { + return applyCRLFEncoding; } /** diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java new file mode 100644 index 00000000..69ae1346 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.pgpainless.key.SubkeyIdentifier; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.OutputStream; + +class SignatureGenerationStream extends OutputStream { + + private final OutputStream wrapped; + private final SigningOptions options; + + SignatureGenerationStream(OutputStream wrapped, SigningOptions signingOptions) { + this.wrapped = wrapped; + this.options = signingOptions; + } + + @Override + public void write(int b) throws IOException { + wrapped.write(b); + if (options == null || options.getSigningMethods().isEmpty()) { + return; + } + + for (SubkeyIdentifier signingKey : options.getSigningMethods().keySet()) { + SigningOptions.SigningMethod signingMethod = options.getSigningMethods().get(signingKey); + PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); + byte asByte = (byte) (b & 0xff); + signatureGenerator.update(asByte); + } + } + + @Override + public void write(@Nonnull byte[] buffer) throws IOException { + write(buffer, 0, buffer.length); + } + + @Override + public void write(@Nonnull byte[] buffer, int off, int len) throws IOException { + wrapped.write(buffer, 0, len); + if (options == null || options.getSigningMethods().isEmpty()) { + return; + } + for (SubkeyIdentifier signingKey : options.getSigningMethods().keySet()) { + SigningOptions.SigningMethod signingMethod = options.getSigningMethods().get(signingKey); + PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); + signatureGenerator.update(buffer, 0, len); + } + } + + @Override + public void close() throws IOException { + wrapped.close(); + } +} diff --git a/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java similarity index 75% rename from pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java index 7cae970e..e1722343 100644 --- a/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package investigations; +package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -34,9 +34,6 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.encryption_signing.CRLFGeneratorStream; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; @@ -120,9 +117,11 @@ public class CanonicalizedDataEncryptionTest { // CHECKSTYLE:ON } + // NO CR/LF ENCODING PRIOR TO PROCESSING + @Test - public void binaryDataBinarySig() throws PGPException, IOException { - String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY); + public void noInputEncodingBinaryDataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY, false); OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { @@ -135,8 +134,8 @@ public class CanonicalizedDataEncryptionTest { } @Test - public void binaryDataTextSig() throws PGPException, IOException { - String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY); + public void noInputEncodingBinaryDataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY, false); OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { @@ -149,8 +148,8 @@ public class CanonicalizedDataEncryptionTest { } @Test - public void textDataBinarySig() throws PGPException, IOException { - String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT); + public void noInputEncodingTextDataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, false); OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { @@ -163,8 +162,8 @@ public class CanonicalizedDataEncryptionTest { } @Test - public void textDataTextSig() throws PGPException, IOException { - String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT); + public void noInputEncodingTextDataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT, false); OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { @@ -177,8 +176,8 @@ public class CanonicalizedDataEncryptionTest { } @Test - public void utf8DataBinarySig() throws PGPException, IOException { - String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8); + public void noInputEncodingUtf8DataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8, false); OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { @@ -191,8 +190,23 @@ public class CanonicalizedDataEncryptionTest { } @Test - public void utf8DataTextSig() throws PGPException, IOException { - String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8); + public void noInputEncodingUtf8DataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8, false); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + // APPLY CR/LF ENCODING PRIOR TO PROCESSING + + @Test + public void inputEncodingBinaryDataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY, true); OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { @@ -204,7 +218,80 @@ public class CanonicalizedDataEncryptionTest { } } - private String encryptAndSign(String message, DocumentSignatureType sigType, StreamEncoding dataFormat) + @Test + public void inputEncodingBinaryDataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY, true); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void inputEncodingTextDataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, true); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void inputEncodingTextDataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT, true); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void inputEncodingUtf8DataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8, true); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void inputEncodingUtf8DataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8, true); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + private String encryptAndSign(String message, + DocumentSignatureType sigType, + StreamEncoding dataFormat, + boolean applyCRLFEncoding) throws PGPException, IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -218,6 +305,7 @@ public class CanonicalizedDataEncryptionTest { .addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, sigType) ) .setEncoding(dataFormat) + .applyCRLFEncoding(applyCRLFEncoding) ); ByteArrayInputStream inputStream = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); From 131c0c6d036c19d4d3f7eed3ddc44cfb93e92676 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Mar 2022 15:10:50 +0200 Subject: [PATCH 0139/1204] Add javadoc header to SignatureGenerationStream --- .../encryption_signing/SignatureGenerationStream.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java index 69ae1346..55ee943f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java @@ -11,6 +11,9 @@ import javax.annotation.Nonnull; import java.io.IOException; import java.io.OutputStream; +/** + * OutputStream which has the task of updating signature generators for written data. + */ class SignatureGenerationStream extends OutputStream { private final OutputStream wrapped; From 39382c7de6d28c3c63ff58cae8aa1a71505ac1a2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Mar 2022 15:29:06 +0200 Subject: [PATCH 0140/1204] Add annotations to SignatureGenerationStream constructor --- .../encryption_signing/SignatureGenerationStream.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java index 55ee943f..7a96f2e1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java @@ -8,6 +8,7 @@ import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.pgpainless.key.SubkeyIdentifier; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.IOException; import java.io.OutputStream; @@ -19,7 +20,7 @@ class SignatureGenerationStream extends OutputStream { private final OutputStream wrapped; private final SigningOptions options; - SignatureGenerationStream(OutputStream wrapped, SigningOptions signingOptions) { + SignatureGenerationStream(@Nonnull OutputStream wrapped, @Nullable SigningOptions signingOptions) { this.wrapped = wrapped; this.options = signingOptions; } From 50bcb6a1357d9390a81595bce7acfe96c68986cc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Mar 2022 21:56:24 +0200 Subject: [PATCH 0141/1204] Fix changelog and change method signature --- CHANGELOG.md | 4 +- .../CRLFGeneratorStream.java | 15 ----- .../encryption_signing/ProducerOptions.java | 9 ++- .../CanonicalizedDataEncryptionTest.java | 64 ++++++++++++++++--- 4 files changed, 60 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 797d7d64..44686127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,9 @@ SPDX-License-Identifier: CC0-1.0 ## 1.1.5-SNAPSHOT - SOP encrypt: match signature type when using `encrypt --as=` option -- `ProducerOptions.setEncoding()`: The encoding is henceforth only considered metadata and will no longer trigger CRLF encoding +- `ProducerOptions.setEncoding()`: The encoding is henceforth only considered metadata and will no longer trigger CRLF encoding. - This fixes broken signature generation for mismatching (`StreamEncoding`,`DocumentSignatureType`) tuples. - - Applications that rely on CRLF-encoding must now apply that encoding themselves (see [#264](https://github.com/pgpainless/pgpainless/issues/264#issuecomment-1083206738) for details). + - Applications that rely on CRLF-encoding can request PGPainless to apply this encoding by calling `ProducerOptions.applyCRLFEncoding(true)`. ## 1.1.4 - Add utility method `KeyRingUtils.removeSecretKey()` to remove secret key part from key ring diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java index f5b13f2a..9743f6f9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java @@ -12,21 +12,6 @@ import java.io.OutputStream; /** * {@link OutputStream} which applies CR-LF encoding of its input data, based on the desired {@link StreamEncoding}. - * - * - * If you need PGPainless to CRLF encode signed data for you, you could do the following: - * {@code - *
- *     InputStream plaintext = ...
- *     EncryptionStream signerOrEncryptor = PGPainless.signAndOrEncrypt(...);
- *     CRLFGeneratorStream crlfOut = new CRLFGeneratorStream(signerOrEncryptor, streamEncoding);
- *
- *     Streams.pipeAll(plaintext, crlfOut);
- *     crlfOut.close;
- *
- *     EncryptionResult result = signerOrEncryptor.getResult();
- * 
- * } * This implementation originates from the Bouncy Castle library. */ public class CRLFGeneratorStream extends OutputStream { diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java index 41d9ca85..ae335db4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java @@ -249,17 +249,16 @@ public final class ProducerOptions { /** * Apply special encoding of line endings to the input data. - * By default, this is set to
false
, which means that the data is not altered. + * By default, this is disabled, which means that the data is not altered. * - * Setting it to
true
will change the line endings to CR/LF. + * Enabling it will change the line endings to CR/LF. * Note: The encoding will not be reversed when decrypting, so applying CR/LF encoding will result in * the identity "decrypt(encrypt(data)) == data == verify(sign(data))". * - * @param applyCRLFEncoding apply crlf encoding * @return this */ - public ProducerOptions applyCRLFEncoding(boolean applyCRLFEncoding) { - this.applyCRLFEncoding = applyCRLFEncoding; + public ProducerOptions applyCRLFEncoding() { + this.applyCRLFEncoding = true; return this; } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java index e1722343..c427af99 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java @@ -288,6 +288,47 @@ public class CanonicalizedDataEncryptionTest { } } + @Test + public void resultOfDecryptionIsCRLFEncoded() throws PGPException, IOException { + String before = "Foo\nBar!\n"; + String after = "Foo\r\nBar!\r\n"; + + String encrypted = encryptAndSign(before, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, true); + + ByteArrayInputStream in = new ByteArrayInputStream(encrypted.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(new ConsumerOptions() + .addDecryptionKey(secretKeys, SecretKeyRingProtector.unprotectedKeys()) + .addVerificationCert(publicKeys)); + + ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, decrypted); + decryptionStream.close(); + + assertArrayEquals(after.getBytes(StandardCharsets.UTF_8), decrypted.toByteArray()); + } + + @Test + public void resultOfDecryptionIsNotCRLFEncoded() throws PGPException, IOException { + String beforeAndAfter = "Foo\nBar!\n"; + + String encrypted = encryptAndSign(beforeAndAfter, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, false); + + ByteArrayInputStream in = new ByteArrayInputStream(encrypted.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(new ConsumerOptions() + .addDecryptionKey(secretKeys, SecretKeyRingProtector.unprotectedKeys()) + .addVerificationCert(publicKeys)); + + ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, decrypted); + decryptionStream.close(); + + assertArrayEquals(beforeAndAfter.getBytes(StandardCharsets.UTF_8), decrypted.toByteArray()); + } + private String encryptAndSign(String message, DocumentSignatureType sigType, StreamEncoding dataFormat, @@ -295,18 +336,21 @@ public class CanonicalizedDataEncryptionTest { throws PGPException, IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); + ProducerOptions options = ProducerOptions + .signAndEncrypt( + EncryptionOptions.encryptCommunications() + .addRecipient(publicKeys), + SigningOptions.get() + .addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, sigType) + ) + .setEncoding(dataFormat); + if (applyCRLFEncoding) { + options.applyCRLFEncoding(); + } + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() .onOutputStream(out) - .withOptions(ProducerOptions - .signAndEncrypt( - EncryptionOptions.encryptCommunications() - .addRecipient(publicKeys), - SigningOptions.get() - .addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, sigType) - ) - .setEncoding(dataFormat) - .applyCRLFEncoding(applyCRLFEncoding) - ); + .withOptions(options); ByteArrayInputStream inputStream = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); Streams.pipeAll(inputStream, encryptionStream); From 8ec86e6464e647fc986a962c208f5a17fc112449 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 2 Apr 2022 17:03:38 +0200 Subject: [PATCH 0142/1204] Rename KeyRingUtil.removeSecretKey() to stripSecretKey() --- .../org/pgpainless/key/util/KeyRingUtils.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 502dbff8..0fe6df89 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -430,11 +430,35 @@ public final class KeyRingUtils { * * @throws IOException * @throws PGPException + * + * @deprecated use {@link #stripSecretKey(PGPSecretKeyRing, long)} instead. + * TODO: Remove in 1.2.X */ @Nonnull + @Deprecated public static PGPSecretKeyRing removeSecretKey(@Nonnull PGPSecretKeyRing secretKeys, long secretKeyId) throws IOException, PGPException { + return stripSecretKey(secretKeys, secretKeyId); + } + + /** + * Remove the secret key of the subkey identified by the given secret key id from the key ring. + * The public part stays attached to the key ring, so that it can still be used for encryption / verification of signatures. + * + * This method is intended to be used to remove secret primary keys from live keys when those are kept in offline storage. + * + * @param secretKeys secret key ring + * @param secretKeyId id of the secret key to remove + * @return secret key ring with removed secret key + * + * @throws IOException + * @throws PGPException + */ + @Nonnull + public static PGPSecretKeyRing stripSecretKey(@Nonnull PGPSecretKeyRing secretKeys, + long secretKeyId) + throws IOException, PGPException { if (secretKeys.getSecretKey(secretKeyId) == null) { throw new NoSuchElementException("PGPSecretKeyRing does not contain secret key " + Long.toHexString(secretKeyId)); } From 6869c669378a4f6c244474b174e47eb701745d1c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 2 Apr 2022 17:12:12 +0200 Subject: [PATCH 0143/1204] Add TODOs to remove deprecated methods in 1.2.X --- .../org/pgpainless/encryption_signing/EncryptionResult.java | 2 ++ .../org/pgpainless/key/protection/SecretKeyRingProtector.java | 2 ++ .../src/main/java/org/pgpainless/key/util/KeyRingUtils.java | 1 + .../pgpainless/signature/consumer/DetachedSignatureCheck.java | 2 ++ .../src/main/java/org/pgpainless/util/ArmorUtils.java | 2 ++ 5 files changed, 9 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java index d1bc3d7f..10342a3c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java @@ -51,6 +51,8 @@ public final class EncryptionResult { * @return symmetric encryption algorithm * * @deprecated use {@link #getEncryptionAlgorithm()} instead. + * + * TODO: Remove in 1.2.X */ @Deprecated public SymmetricKeyAlgorithm getSymmetricKeyAlgorithm() { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java index 2709b143..84f47155 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java @@ -77,6 +77,8 @@ public interface SecretKeyRingProtector { * @param keys key ring * @return protector * @deprecated use {@link #unlockEachKeyWith(Passphrase, PGPSecretKeyRing)} instead. + * + * TODO: Remove in 1.2.X */ @Deprecated static SecretKeyRingProtector unlockAllKeysWith(@Nonnull Passphrase passphrase, @Nonnull PGPSecretKeyRing keys) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 0fe6df89..f64133b0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -385,6 +385,7 @@ public final class KeyRingUtils { // TODO: Replace with PGPSecretKeyRing.insertOrReplacePublicKey() once available // Right now replacePublicKeys looses extra public keys. // See https://github.com/bcgit/bc-java/pull/1068 for a possible fix + // Fix once BC 171 gets released. secretKeys = PGPSecretKeyRing.replacePublicKeys(secretKeys, publicKeys); return (T) secretKeys; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/DetachedSignatureCheck.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/DetachedSignatureCheck.java index 1f6114bb..a431c5de 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/DetachedSignatureCheck.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/DetachedSignatureCheck.java @@ -63,6 +63,8 @@ public class DetachedSignatureCheck { * * @return fingerprint of the signing key * @deprecated use {@link #getSigningKeyIdentifier()} instead. + * + * TODO: Remove in 1.2.X */ @Deprecated public OpenPgpFingerprint getFingerprint() { diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index 8e93ce6d..1d3c17cc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -280,6 +280,8 @@ public final class ArmorUtils { * @return armored output stream * * @deprecated use {@link #toAsciiArmoredStream(PGPKeyRing, OutputStream)} instead + * + * TODO: Remove in 1.2.X */ @Deprecated @Nonnull From 7eb2f5fb4d84768b4424470794b5f9a9fd015d85 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 2 Apr 2022 17:16:37 +0200 Subject: [PATCH 0144/1204] Document how PGPainlessCLI works --- .../src/main/java/org/pgpainless/cli/PGPainlessCLI.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java index b199ec72..35791a3e 100644 --- a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java +++ b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java @@ -7,6 +7,10 @@ package org.pgpainless.cli; import org.pgpainless.sop.SOPImpl; import sop.cli.picocli.SopCLI; +/** + * This class merely binds PGPainless to {@link SopCLI} by injecting a {@link SOPImpl} instance. + * CLI command calls are then simply forwarded to {@link SopCLI#execute(String[])}. + */ public class PGPainlessCLI { static { From 4bd01578fbecf02d39e49b8e7157cb9103986a2f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 2 Apr 2022 18:14:17 +0200 Subject: [PATCH 0145/1204] Fix javadoc generation --- .../java/org/pgpainless/encryption_signing/ProducerOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java index ae335db4..c0e60181 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java @@ -228,7 +228,7 @@ public final class ProducerOptions { * Defaults to {@link StreamEncoding#BINARY}. * * This does not change the encoding of the wrapped data itself. - * To apply CR/LF encoding to your input data before processing, use {@link #applyCRLFEncoding(boolean)} instead. + * To apply CR/LF encoding to your input data before processing, use {@link #applyCRLFEncoding()} instead. * * @see RFC4880 §5.9. Literal Data Packet * From 58dee0d970b055e4304f3d1ffd885dff1cc00b83 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 2 Apr 2022 18:56:05 +0200 Subject: [PATCH 0146/1204] Fix javadoc warnings --- .../main/java/org/pgpainless/PGPainless.java | 2 + .../MessageInspector.java | 6 ++ .../ClearsignedMessageUtil.java | 2 + .../EncryptionBuilderInterface.java | 5 +- .../encryption_signing/EncryptionOptions.java | 1 + .../encryption_signing/SigningOptions.java | 2 + .../key/generation/KeyRingTemplates.java | 36 ++++++++++ .../SecretKeyRingEditorInterface.java | 66 ++++++++++++++++++- .../pgpainless/key/parsing/KeyRingReader.java | 10 +++ .../protection/SecretKeyRingProtector.java | 3 + .../org/pgpainless/key/util/KeyRingUtils.java | 8 +-- .../pgpainless/signature/SignatureUtils.java | 2 + .../builder/AbstractSignatureBuilder.java | 2 + ...irdPartyCertificationSignatureBuilder.java | 4 ++ .../signature/consumer/SignaturePicker.java | 12 ++++ .../consumer/SignatureValidityComparator.java | 2 + .../subpackets/SignatureSubpacketsUtil.java | 2 + .../java/org/pgpainless/util/ArmorUtils.java | 2 + 18 files changed, 160 insertions(+), 7 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index bd353f24..46dd35c2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -71,6 +71,8 @@ public final class PGPainless { * * @param key key or certificate * @return ascii armored string + * + * @throws IOException in case of an error in the {@link org.bouncycastle.bcpg.ArmoredOutputStream} */ public static String asciiArmor(@Nonnull PGPKeyRing key) throws IOException { if (key instanceof PGPSecretKeyRing) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java index a9948f1d..3dda0f5d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java @@ -75,6 +75,9 @@ public final class MessageInspector { * * @param message OpenPGP message * @return encryption info + * + * @throws PGPException in case the message is broken + * @throws IOException in case of an IO error */ public static EncryptionInfo determineEncryptionInfoForMessage(String message) throws PGPException, IOException { @SuppressWarnings("CharsetObjectCanBeUsed") @@ -88,6 +91,9 @@ public final class MessageInspector { * * @param dataIn openpgp message * @return encryption information + * + * @throws IOException in case of an IO error + * @throws PGPException if the message is broken */ public static EncryptionInfo determineEncryptionInfoForMessage(InputStream dataIn) throws IOException, PGPException { InputStream decoded = ArmorUtils.getDecoderStream(dataIn); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java index d2b514bb..ee166c99 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java @@ -35,7 +35,9 @@ public final class ClearsignedMessageUtil { * @param clearsignedInputStream input stream containing a clearsigned message * @param messageOutputStream output stream to which the dearmored message shall be written * @return signatures + * * @throws IOException if the message is not clearsigned or some other IO error happens + * @throws WrongConsumingMethodException in case the armored message is not cleartext signed */ public static PGPSignatureList detachSignaturesFromInbandClearsignedMessage(InputStream clearsignedInputStream, OutputStream messageOutputStream) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java index 9ba0bb78..c705c0b1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java @@ -27,7 +27,10 @@ public interface EncryptionBuilderInterface { * Create an {@link EncryptionStream} with the given options (recipients, signers, algorithms...). * * @param options options - * @return encryption strea + * @return encryption stream + * + * @throws PGPException if something goes wrong during encryption stream preparation + * @throws IOException if something goes wrong during encryption stream preparation (writing headers) */ EncryptionStream withOptions(ProducerOptions options) throws PGPException, IOException; diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 48107b23..af557703 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -282,6 +282,7 @@ public class EncryptionOptions { * If the algorithm is not overridden, a suitable algorithm will be negotiated. * * @param encryptionAlgorithm encryption algorithm override + * @return this */ public EncryptionOptions overrideEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) { if (encryptionAlgorithm == SymmetricKeyAlgorithm.NULL) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java index 0db93407..0608b22e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -58,6 +58,7 @@ public final class SigningOptions { * The resulting signature will be written into the message itself, together with a one-pass-signature packet. * * @param signatureGenerator signature generator + * @param hashAlgorithm hash algorithm used to generate the signature * @return inline signing method */ public static SigningMethod inlineSignature(PGPSignatureGenerator signatureGenerator, HashAlgorithm hashAlgorithm) { @@ -70,6 +71,7 @@ public final class SigningOptions { * to the signed message. * * @param signatureGenerator signature generator + * @param hashAlgorithm hash algorithm used to generate the signature * @return detached signing method */ public static SigningMethod detachedSignature(PGPSignatureGenerator signatureGenerator, HashAlgorithm hashAlgorithm) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java index 0917b110..0d663ff9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java @@ -33,6 +33,10 @@ public final class KeyRingTemplates { * @param length length in bits. * * @return {@link PGPSecretKeyRing} containing the KeyPair. + * + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error */ public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull UserId userId, @Nonnull RsaLength length) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { @@ -47,6 +51,10 @@ public final class KeyRingTemplates { * @param length length in bits. * * @return {@link PGPSecretKeyRing} containing the KeyPair. + * + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error */ public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull String userId, @Nonnull RsaLength length) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { @@ -62,6 +70,10 @@ public final class KeyRingTemplates { * @param password Password of the key. Can be null for unencrypted keys. * * @return {@link PGPSecretKeyRing} containing the KeyPair. + * + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error */ public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull UserId userId, @Nonnull RsaLength length, String password) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { @@ -77,6 +89,10 @@ public final class KeyRingTemplates { * @param password Password of the key. Can be null for unencrypted keys. * * @return {@link PGPSecretKeyRing} containing the KeyPair. + * + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error */ public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull String userId, @Nonnull RsaLength length, String password) throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { @@ -98,6 +114,10 @@ public final class KeyRingTemplates { * @param userId user-id * * @return {@link PGPSecretKeyRing} containing the key pairs. + * + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error */ public PGPSecretKeyRing simpleEcKeyRing(@Nonnull UserId userId) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { @@ -112,6 +132,10 @@ public final class KeyRingTemplates { * @param userId user-id * * @return {@link PGPSecretKeyRing} containing the key pairs. + * + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error */ public PGPSecretKeyRing simpleEcKeyRing(@Nonnull String userId) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { @@ -127,6 +151,10 @@ public final class KeyRingTemplates { * @param password Password of the private key. Can be null for an unencrypted key. * * @return {@link PGPSecretKeyRing} containing the key pairs. + * + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error */ public PGPSecretKeyRing simpleEcKeyRing(@Nonnull UserId userId, String password) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { @@ -142,6 +170,10 @@ public final class KeyRingTemplates { * @param password Password of the private key. Can be null for an unencrypted key. * * @return {@link PGPSecretKeyRing} containing the key pairs. + * + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error */ public PGPSecretKeyRing simpleEcKeyRing(@Nonnull String userId, String password) throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { @@ -163,6 +195,10 @@ public final class KeyRingTemplates { * @param userId primary user id * @param password passphrase or null if the key should be unprotected. * @return key ring + * + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error */ public PGPSecretKeyRing modernKeyRing(String userId, String password) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java index ec579c9e..6f4d34b8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java @@ -34,6 +34,8 @@ public interface SecretKeyRingEditorInterface { * @param userId user-id * @param secretKeyRingProtector protector to unlock the secret key * @return the builder + * + * @throws PGPException in case we cannot generate a signature for the user-id */ SecretKeyRingEditorInterface addUserId( @Nonnull CharSequence userId, @@ -48,6 +50,8 @@ public interface SecretKeyRingEditorInterface { * certification signature. * @param protector protector to unlock the primary secret key * @return the builder + * + * @throws PGPException in case we cannot generate a signature for the user-id */ SecretKeyRingEditorInterface addUserId( @Nonnull CharSequence userId, @@ -62,6 +66,8 @@ public interface SecretKeyRingEditorInterface { * @param userId user id * @param protector protector to unlock the secret key * @return the builder + * + * @throws PGPException in case we cannot generate a signature for the user-id */ SecretKeyRingEditorInterface addPrimaryUserId( @Nonnull CharSequence userId, @@ -76,6 +82,8 @@ public interface SecretKeyRingEditorInterface { * @param userIdSelector selector to select user-ids * @param protector protector to unlock the primary key * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id */ SecretKeyRingEditorInterface removeUserId(SelectUserId userIdSelector, SecretKeyRingProtector protector) @@ -89,6 +97,8 @@ public interface SecretKeyRingEditorInterface { * @param userId user-id to revoke * @param protector protector to unlock the primary key * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id */ SecretKeyRingEditorInterface removeUserId(CharSequence userId, SecretKeyRingProtector protector) @@ -102,6 +112,11 @@ public interface SecretKeyRingEditorInterface { * @param subKeyPassphrase passphrase to encrypt the sub key * @param secretKeyRingProtector protector to unlock the secret key of the key ring * @return the builder + * + * @throws InvalidAlgorithmParameterException in case the user wants to use invalid parameters for the key + * @throws NoSuchAlgorithmException in case of missing algorithm support in the crypto backend + * @throws PGPException in case we cannot generate a binding signature for the subkey + * @throws IOException in case of an IO error */ SecretKeyRingEditorInterface addSubKey( @Nonnull KeySpec keySpec, @@ -118,6 +133,11 @@ public interface SecretKeyRingEditorInterface { * @param subpacketsCallback callback to modify the subpackets of the subkey binding signature * @param secretKeyRingProtector protector to unlock the primary key * @return builder + * + * @throws InvalidAlgorithmParameterException in case the user wants to use invalid parameters for the key + * @throws NoSuchAlgorithmException in case of missing algorithm support in the crypto backend + * @throws PGPException in case we cannot generate a binding signature for the subkey + * @throws IOException in case of an IO error */ SecretKeyRingEditorInterface addSubKey( @Nonnull KeySpec keySpec, @@ -136,6 +156,10 @@ public interface SecretKeyRingEditorInterface { * @param keyFlag first key flag for the subkey * @param additionalKeyFlags optional additional key flags * @return builder + * + * @throws PGPException in case we cannot generate a binding signature for the subkey + * @throws IOException in case of an IO error + * @throws NoSuchAlgorithmException in case of missing algorithm support in the crypto backend */ SecretKeyRingEditorInterface addSubKey( @Nonnull PGPKeyPair subkey, @@ -152,6 +176,8 @@ public interface SecretKeyRingEditorInterface { * * @param secretKeyRingProtector protector of the primary key * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature */ default SecretKeyRingEditorInterface revoke( @Nonnull SecretKeyRingProtector secretKeyRingProtector) @@ -166,6 +192,8 @@ public interface SecretKeyRingEditorInterface { * @param secretKeyRingProtector protector of the primary key * @param revocationAttributes reason for the revocation * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature */ SecretKeyRingEditorInterface revoke( @Nonnull SecretKeyRingProtector secretKeyRingProtector, @@ -180,6 +208,8 @@ public interface SecretKeyRingEditorInterface { * @param secretKeyRingProtector protector to unlock the primary secret key * @param subpacketsCallback callback to modify the revocations subpackets * @return builder + * + * @throws PGPException in case we cannot generate a revocation signature */ SecretKeyRingEditorInterface revoke( @Nonnull SecretKeyRingProtector secretKeyRingProtector, @@ -198,6 +228,8 @@ public interface SecretKeyRingEditorInterface { * @param fingerprint fingerprint of the subkey to be revoked * @param secretKeyRingProtector protector to unlock the secret key ring * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey */ default SecretKeyRingEditorInterface revokeSubKey( @Nonnull OpenPgpFingerprint fingerprint, @@ -215,6 +247,8 @@ public interface SecretKeyRingEditorInterface { * @param secretKeyRingProtector protector to unlock the primary key * @param revocationAttributes reason for the revocation * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey */ default SecretKeyRingEditorInterface revokeSubKey( OpenPgpFingerprint fingerprint, @@ -235,6 +269,8 @@ public interface SecretKeyRingEditorInterface { * @param secretKeyRingProtector protector to unlock the primary key * @param revocationAttributes reason for the revocation * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey */ SecretKeyRingEditorInterface revokeSubKey( long subKeyId, @@ -255,6 +291,8 @@ public interface SecretKeyRingEditorInterface { * @param subKeyId id of the subkey * @param secretKeyRingProtector protector to unlock the secret key ring * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey */ default SecretKeyRingEditorInterface revokeSubKey( long subKeyId, @@ -279,6 +317,8 @@ public interface SecretKeyRingEditorInterface { * @param subpacketsCallback callback which can be used to modify the subpackets of the revocation * signature * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey */ SecretKeyRingEditorInterface revokeSubKey( long keyID, @@ -296,6 +336,8 @@ public interface SecretKeyRingEditorInterface { * @param userId userId to revoke * @param secretKeyRingProtector protector to unlock the primary key * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id */ default SecretKeyRingEditorInterface revokeUserId( @Nonnull CharSequence userId, @@ -311,6 +353,8 @@ public interface SecretKeyRingEditorInterface { * @param secretKeyRingProtector protector to unlock the primary key * @param revocationAttributes reason for the revocation * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id */ SecretKeyRingEditorInterface revokeUserId( @Nonnull CharSequence userId, @@ -329,6 +373,8 @@ public interface SecretKeyRingEditorInterface { * @param secretKeyRingProtector protector to unlock the primary secret key * @param subpacketCallback callback to modify the revocations subpackets * @return builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id */ SecretKeyRingEditorInterface revokeUserId( @Nonnull CharSequence userId, @@ -348,7 +394,8 @@ public interface SecretKeyRingEditorInterface { * @param secretKeyRingProtector protector to unlock the primary secret key * @param revocationAttributes revocation attributes * @return builder - * @throws PGPException if the revocation signatures cannot be generated + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id */ SecretKeyRingEditorInterface revokeUserIds( @Nonnull SelectUserId userIdSelector, @@ -370,7 +417,8 @@ public interface SecretKeyRingEditorInterface { * @param secretKeyRingProtector protector to unlock the primary secret key * @param subpacketsCallback callback to modify the revocations subpackets * @return builder - * @throws PGPException if the revocation signatures cannot be generated + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id */ SecretKeyRingEditorInterface revokeUserIds( @Nonnull SelectUserId userIdSelector, @@ -385,6 +433,8 @@ public interface SecretKeyRingEditorInterface { * @param expiration new expiration date or null * @param secretKeyRingProtector to unlock the secret key * @return the builder + * + * @throws PGPException in case we cannot generate a new self-signature with the changed expiration date */ SecretKeyRingEditorInterface setExpirationDate( @Nullable Date expiration, @@ -397,6 +447,8 @@ public interface SecretKeyRingEditorInterface { * @param secretKeyRingProtector protector to unlock the primary key. * @param revocationAttributes reason for the revocation * @return revocation certificate + * + * @throws PGPException in case we cannot generate a revocation certificate */ PGPSignature createRevocationCertificate( @Nonnull SecretKeyRingProtector secretKeyRingProtector, @@ -410,6 +462,8 @@ public interface SecretKeyRingEditorInterface { * @param secretKeyRingProtector protector to unlock the primary key. * @param revocationAttributes reason for the revocation * @return revocation certificate + * + * @throws PGPException in case we cannot generate a revocation certificate */ PGPSignature createRevocationCertificate( long subkeyId, @@ -424,6 +478,8 @@ public interface SecretKeyRingEditorInterface { * @param secretKeyRingProtector protector to unlock the primary key. * @param certificateSubpacketsCallback callback to modify the subpackets of the revocation certificate. * @return revocation certificate + * + * @throws PGPException in case we cannot generate a revocation certificate */ PGPSignature createRevocationCertificate( long subkeyId, @@ -438,6 +494,8 @@ public interface SecretKeyRingEditorInterface { * @param secretKeyRingProtector protector to unlock the primary key. * @param revocationAttributes reason for the revocation * @return revocation certificate + * + * @throws PGPException in case we cannot generate a revocation certificate */ default PGPSignature createRevocationCertificate( OpenPgpFingerprint subkeyFingerprint, @@ -521,6 +579,8 @@ public interface SecretKeyRingEditorInterface { * * @param passphrase passphrase * @return editor builder + * + * @throws PGPException in case the passphrase cannot be changed */ SecretKeyRingEditorInterface toNewPassphrase(Passphrase passphrase) throws PGPException; @@ -529,6 +589,8 @@ public interface SecretKeyRingEditorInterface { * Leave the key unprotected. * * @return editor builder + * + * @throws PGPException in case the passphrase cannot be changed */ SecretKeyRingEditorInterface toNoPassphrase() throws PGPException; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java index 5c347f75..0021f74e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java @@ -107,6 +107,8 @@ public class KeyRingReader { * @param inputStream input stream * @param maxIterations max iterations before abort * @return public key ring + * + * @throws IOException in case of an IO error or exceeding of max iterations */ public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( @@ -142,6 +144,9 @@ public class KeyRingReader { * @param inputStream input stream * @param maxIterations max iterations before abort * @return public key ring collection + * + * @throws IOException in case of an IO error or exceeding of max iterations + * @throws PGPException in case of a broken key */ public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream, int maxIterations) throws IOException, PGPException { @@ -186,6 +191,8 @@ public class KeyRingReader { * @param inputStream input stream * @param maxIterations max iterations before abort * @return public key ring + * + * @throws IOException in case of an IO error or exceeding of max iterations */ public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { InputStream decoderStream = ArmorUtils.getDecoderStream(inputStream); @@ -222,6 +229,9 @@ public class KeyRingReader { * @param inputStream input stream * @param maxIterations max iterations before abort * @return secret key ring collection + * + * @throws IOException in case of an IO error or exceeding of max iterations + * @throws PGPException in case of a broken secret key */ public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream, int maxIterations) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java index 84f47155..ee461a4e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java @@ -37,6 +37,8 @@ public interface SecretKeyRingProtector { * * @param keyId id of the key * @return decryptor for the key + * + * @throws PGPException if the decryptor cannot be created for some reason */ @Nullable PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException; @@ -46,6 +48,7 @@ public interface SecretKeyRingProtector { * * @param keyId id of the key * @return encryptor for the key + * * @throws PGPException if the encryptor cannot be created for some reason */ @Nullable PBESecretKeyEncryptor getEncryptor(Long keyId) throws PGPException; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index f64133b0..375e3527 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -429,8 +429,8 @@ public final class KeyRingUtils { * @param secretKeyId id of the secret key to remove * @return secret key ring with removed secret key * - * @throws IOException - * @throws PGPException + * @throws IOException in case of an error during serialization / deserialization of the key + * @throws PGPException in case of a broken key * * @deprecated use {@link #stripSecretKey(PGPSecretKeyRing, long)} instead. * TODO: Remove in 1.2.X @@ -453,8 +453,8 @@ public final class KeyRingUtils { * @param secretKeyId id of the secret key to remove * @return secret key ring with removed secret key * - * @throws IOException - * @throws PGPException + * @throws IOException in case of an error during serialization / deserialization of the key + * @throws PGPException in case of a broken key */ @Nonnull public static PGPSecretKeyRing stripSecretKey(@Nonnull PGPSecretKeyRing secretKeys, diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java index 420efc26..1ebbdda4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java @@ -193,7 +193,9 @@ public final class SignatureUtils { * * @param encodedSignatures ASCII armored signature list * @return signature list + * * @throws IOException if the signatures cannot be read + * @throws PGPException in case of a broken signature */ public static List readSignatures(String encodedSignatures) throws IOException, PGPException { @SuppressWarnings("CharsetObjectCanBeUsed") diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java index 08e496ac..1079f92f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java @@ -114,6 +114,8 @@ public abstract class AbstractSignatureBuilder { /** * Create a new {@link SignatureValidityComparator} which orders signatures following the passed ordering. * Still, hard revocations will come first. + * + * @param order order of creation dates */ public SignatureValidityComparator(SignatureCreationDateComparator.Order order) { this.creationDateComparator = new SignatureCreationDateComparator(order); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index 12c6f1de..7d3b4622 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -506,6 +506,8 @@ public final class SignatureSubpacketsUtil { * * @param signature signature * @return embedded signature + * + * @throws PGPException in case the embedded signatures cannot be parsed */ public static PGPSignatureList getEmbeddedSignature(PGPSignature signature) throws PGPException { PGPSignatureList hashed = signature.getHashedSubPackets().getEmbeddedSignatures(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index 1d3c17cc..d95673a4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -544,6 +544,8 @@ public final class ArmorUtils { * * @param inputStream input stream * @return BufferedInputStreamExt + * + * @throws IOException in case of an IO error */ @Nonnull public static InputStream getDecoderStream(@Nonnull InputStream inputStream) From 4aaa242d64f98cc57e85ef7072832abb4ebfd736 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Apr 2022 10:40:57 +0200 Subject: [PATCH 0147/1204] Add javadoc to SignatureSubpacketsUtil --- .../signature/subpackets/SignatureSubpacketsUtil.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index 7d3b4622..5a839bcb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -438,7 +438,7 @@ public final class SignatureSubpacketsUtil { /** * Return the notation data subpackets from the signatures unhashed area. * - * @param signature signture + * @param signature signature * @return unhashed notations */ public static List getUnhashedNotationData(PGPSignature signature) { @@ -638,6 +638,12 @@ public final class SignatureSubpacketsUtil { } } + /** + * Make sure that a key of the given {@link PublicKeyAlgorithm} is able to carry the given key flags. + * + * @param algorithm key algorithm + * @param flags key flags + */ public static void assureKeyCanCarryFlags(PublicKeyAlgorithm algorithm, KeyFlag... flags) { final int mask = KeyFlag.toBitmask(flags); From bfbe03f9e08b62b2767e5ea7ff7b919891dd312c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Apr 2022 12:19:07 +0200 Subject: [PATCH 0148/1204] Document SelectUserIds --- .../util/selection/userid/SelectUserId.java | 125 ++++++++++++++++-- 1 file changed, 113 insertions(+), 12 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java index 25ee5a33..e86e59f3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java @@ -9,19 +9,44 @@ import java.util.List; import org.bouncycastle.openpgp.PGPKeyRing; import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; import javax.annotation.Nonnull; +import javax.annotation.Nullable; +/** + * Filter for selecting user-ids from keys and from lists. + */ public abstract class SelectUserId { + /** + * Return true, if the given user-id is accepted by this particular filter, false otherwise. + * + * @param userId user-id + * @return acceptance of the filter + */ protected abstract boolean accept(String userId); - public List selectUserIds(PGPKeyRing keyRing) { + /** + * Select all currently valid user-ids of the given key ring. + * + * @param keyRing public or secret key ring + * @return valid user-ids + */ + @Nonnull + public List selectUserIds(@Nonnull PGPKeyRing keyRing) { List userIds = PGPainless.inspectKeyRing(keyRing).getValidUserIds(); return selectUserIds(userIds); } - public List selectUserIds(List userIds) { + /** + * Select all acceptable (see {@link #accept(String)}) from the given list of user-ids. + * + * @param userIds list of user-ids + * @return sub-list of acceptable user-ids + */ + @Nonnull + public List selectUserIds(@Nonnull List userIds) { List selected = new ArrayList<>(); for (String userId : userIds) { if (accept(userId)) { @@ -31,11 +56,25 @@ public abstract class SelectUserId { return selected; } + /** + * Return the first valid, acceptable user-id from the given public or secret key ring. + * + * @param keyRing public or secret key ring + * @return first matching valid user-id or null + */ + @Nullable public String firstMatch(PGPKeyRing keyRing) { return firstMatch(selectUserIds(keyRing)); } - public String firstMatch(List userIds) { + /** + * Return the first valid, acceptable user-id from the list of user-ids. + * + * @param userIds list of user-ids + * @return first matching valid user-id or null + */ + @Nullable + public String firstMatch(@Nonnull List userIds) { for (String userId : userIds) { if (accept(userId)) { return userId; @@ -44,6 +83,12 @@ public abstract class SelectUserId { return null; } + /** + * Filter that filters for user-ids which contain the given
query
as a substring. + * + * @param query query + * @return filter + */ public static SelectUserId containsSubstring(@Nonnull CharSequence query) { return new SelectUserId() { @Override @@ -53,6 +98,12 @@ public abstract class SelectUserId { }; } + /** + * Filter that filters for user-ids which match the given
query
exactly. + * + * @param query query + * @return filter + */ public static SelectUserId exactMatch(@Nonnull CharSequence query) { return new SelectUserId() { @Override @@ -62,6 +113,12 @@ public abstract class SelectUserId { }; } + /** + * Filter that filters for user-ids which start with the given
substring
. + * + * @param substring substring + * @return filter + */ public static SelectUserId startsWith(@Nonnull CharSequence substring) { String string = substring.toString(); return new SelectUserId() { @@ -72,55 +129,99 @@ public abstract class SelectUserId { }; } + /** + * Filter that filters for user-ids which contain the given
email
address. + * Note: This only accepts user-ids which properly have the email address surrounded by angle brackets. + * + * The argument
email
can both be a plain email address (
"foo@bar.baz"
), + * or surrounded by angle brackets (
""
, the result of the filter will be the same. + * + * @param email email address + * @return filter + */ public static SelectUserId containsEmailAddress(@Nonnull CharSequence email) { String string = email.toString(); return containsSubstring(string.matches("^<.+>$") ? string : '<' + string + '>'); } + /** + * Filter that filters for valid user-ids on the given
keyRing
only. + * + * @param keyRing public / secret keys + * @return filter + */ public static SelectUserId validUserId(PGPKeyRing keyRing) { + final KeyRingInfo info = PGPainless.inspectKeyRing(keyRing); + return new SelectUserId() { @Override protected boolean accept(String userId) { - return PGPainless.inspectKeyRing(keyRing).isUserIdValid(userId); + return info.isUserIdValid(userId); } }; } - public static SelectUserId and(SelectUserId... strategies) { + /** + * Filter that filters for user-ids which pass all the given
filters
. + * + * @param filters filters + * @return filter + */ + public static SelectUserId and(SelectUserId... filters) { return new SelectUserId() { @Override protected boolean accept(String userId) { boolean accept = true; - for (SelectUserId strategy : strategies) { - accept &= strategy.accept(userId); + for (SelectUserId filter : filters) { + accept &= filter.accept(userId); } return accept; } }; } - public static SelectUserId or(SelectUserId... strategies) { + /** + * Filter that filters for user-ids which pass at least one of the given
filters
>. + * + * @param filters filters + * @return filter + */ + public static SelectUserId or(SelectUserId... filters) { return new SelectUserId() { @Override protected boolean accept(String userId) { boolean accept = false; - for (SelectUserId strategy : strategies) { - accept |= strategy.accept(userId); + for (SelectUserId filter : filters) { + accept |= filter.accept(userId); } return accept; } }; } - public static SelectUserId not(SelectUserId strategy) { + /** + * Filter that inverts the result of the given
filter
. + * + * @param filter filter + * @return inverting filter + */ + public static SelectUserId not(SelectUserId filter) { return new SelectUserId() { @Override protected boolean accept(String userId) { - return !strategy.accept(userId); + return !filter.accept(userId); } }; } + /** + * Filter that selects user-ids by the given
email
address. + * It returns user-ids which either contain the given
email
address as angle-bracketed string, + * or which equal the given
email
string exactly. + * + * @param email email + * @return filter + */ public static SelectUserId byEmail(CharSequence email) { return SelectUserId.or( SelectUserId.exactMatch(email), From 7ca9934cbe08ae0648f73d34e31b0a6f532577ce Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Apr 2022 12:32:37 +0200 Subject: [PATCH 0149/1204] Document KeyRingSelectionStrategy --- .../keyring/KeyRingSelectionStrategy.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/KeyRingSelectionStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/KeyRingSelectionStrategy.java index a9f842e3..fb57e338 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/KeyRingSelectionStrategy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/KeyRingSelectionStrategy.java @@ -8,11 +8,41 @@ import java.util.Set; import org.pgpainless.util.MultiMap; +/** + * + * @param Type of {@link org.bouncycastle.openpgp.PGPKeyRing} ({@link org.bouncycastle.openpgp.PGPSecretKeyRing} + * or {@link org.bouncycastle.openpgp.PGPPublicKeyRing}). + * @param Type of key ring collection (e.g. {@link org.bouncycastle.openpgp.PGPSecretKeyRingCollection} + * or {@link org.bouncycastle.openpgp.PGPPublicKeyRingCollection}). + * @param Type of key identifier + */ public interface KeyRingSelectionStrategy { + /** + * Return true, if the filter accepts the given
keyRing
based on the given
identifier
. + * + * @param identifier identifier + * @param keyRing key ring + * @return acceptance + */ boolean accept(O identifier, R keyRing); + /** + * Iterate of the given
keyRingCollection
and return a {@link Set} of all acceptable + * keyRings in the collection, based on the given
identifier
. + * + * @param identifier identifier + * @param keyRingCollection collection + * @return set of acceptable key rings + */ Set selectKeyRingsFromCollection(O identifier, C keyRingCollection); + /** + * Iterate over all keyRings in the given {@link MultiMap} of keyRingCollections and return a new {@link MultiMap} + * which for every identifier (key of the map) contains all acceptable keyRings based on that identifier. + * + * @param keyRingCollections MultiMap of identifiers and keyRingCollections. + * @return MultiMap of identifiers and acceptable keyRings. + */ MultiMap selectKeyRingsFromCollections(MultiMap keyRingCollections); } From 2c86d8dfe45089d1c02b9b8383c90afaa4c51fe5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Apr 2022 12:49:00 +0200 Subject: [PATCH 0150/1204] Document various KeyRingSelectionStrategies --- .../PublicKeyRingSelectionStrategy.java | 5 +++++ .../SecretKeyRingSelectionStrategy.java | 6 +++++ .../selection/keyring/impl/ExactUserId.java | 12 ++++++++++ .../selection/keyring/impl/Whitelist.java | 22 +++++++++++++++++++ .../util/selection/keyring/impl/Wildcard.java | 3 +++ .../util/selection/keyring/impl/XMPP.java | 16 ++++++++++++++ 6 files changed, 64 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java index 038549bf..68c9d946 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java @@ -13,6 +13,11 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.pgpainless.util.MultiMap; +/** + * Abstract {@link KeyRingSelectionStrategy} for {@link PGPPublicKeyRing PGPPublicKeyRings}. + * + * @param Type of identifier + */ public abstract class PublicKeyRingSelectionStrategy implements KeyRingSelectionStrategy { @Override diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java index c54f81a5..943f2d82 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java @@ -9,10 +9,16 @@ import java.util.HashSet; import java.util.Iterator; import java.util.Set; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.pgpainless.util.MultiMap; +/** + * Abstract {@link KeyRingSelectionStrategy} for {@link PGPSecretKeyRing PGPSecretKeyRings}. + * + * @param Type of identifier + */ public abstract class SecretKeyRingSelectionStrategy implements KeyRingSelectionStrategy { @Override public Set selectKeyRingsFromCollection(O identifier, @Nonnull PGPSecretKeyRingCollection keyRingCollection) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/ExactUserId.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/ExactUserId.java index 5a05dc56..29c43c41 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/ExactUserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/ExactUserId.java @@ -11,12 +11,20 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.pgpainless.util.selection.keyring.PublicKeyRingSelectionStrategy; import org.pgpainless.util.selection.keyring.SecretKeyRingSelectionStrategy; +/** + * Implementations of {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which select key rings + * based on the exact user-id. + */ public final class ExactUserId { private ExactUserId() { } + /** + * {@link PublicKeyRingSelectionStrategy} which accepts {@link PGPPublicKeyRing PGPPublicKeyRings} if those + * have a user-id which exactly matches the given
identifier
. + */ public static class PubRingSelectionStrategy extends PublicKeyRingSelectionStrategy { @Override @@ -29,6 +37,10 @@ public final class ExactUserId { } } + /** + * {@link SecretKeyRingSelectionStrategy} which accepts {@link PGPSecretKeyRing PGPSecretKeyRings} if those + * have a user-id which exactly matches the given
identifier
. + */ public static class SecRingSelectionStrategy extends SecretKeyRingSelectionStrategy { @Override diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Whitelist.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Whitelist.java index f296d8f1..934f5577 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Whitelist.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Whitelist.java @@ -13,12 +13,25 @@ import org.pgpainless.util.selection.keyring.PublicKeyRingSelectionStrategy; import org.pgpainless.util.selection.keyring.SecretKeyRingSelectionStrategy; import org.pgpainless.util.MultiMap; +/** + * Implementations of {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accept PGP KeyRings + * based on a whitelist of acceptable keyIds. + */ public final class Whitelist { private Whitelist() { } + /** + * {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accepts + * {@link PGPPublicKeyRing PGPPublicKeyRings} if the
whitelist
contains their primary key id. + * + * If the whitelist contains 123L for "alice@pgpainless.org", the key with primary key id 123L is + * acceptable for "alice@pgpainless.org". + * + * @param Type of identifier for {@link org.bouncycastle.openpgp.PGPPublicKeyRingCollection PGPPublicKeyRingCollections}. + */ public static class PubRingSelectionStrategy extends PublicKeyRingSelectionStrategy { private final MultiMap whitelist; @@ -43,6 +56,15 @@ public final class Whitelist { } } + /** + * {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accepts + * {@link PGPSecretKeyRing PGPSecretKeyRings} if the
whitelist
contains their primary key id. + * + * If the whitelist contains 123L for "alice@pgpainless.org", the key with primary key id 123L is + * acceptable for "alice@pgpainless.org". + * + * @param Type of identifier for {@link org.bouncycastle.openpgp.PGPSecretKeyRingCollection PGPSecretKeyRingCollections}. + */ public static class SecRingSelectionStrategy extends SecretKeyRingSelectionStrategy { private final MultiMap whitelist; diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Wildcard.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Wildcard.java index f7bab777..d1929028 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Wildcard.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Wildcard.java @@ -9,6 +9,9 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.pgpainless.util.selection.keyring.PublicKeyRingSelectionStrategy; import org.pgpainless.util.selection.keyring.SecretKeyRingSelectionStrategy; +/** + * Implementations of {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accept all keyRings. + */ public final class Wildcard { private Wildcard() { diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/XMPP.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/XMPP.java index 11c3c8ff..edc38006 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/XMPP.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/XMPP.java @@ -7,12 +7,22 @@ package org.pgpainless.util.selection.keyring.impl; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +/** + * Implementations of {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accept KeyRings + * containing a given XMPP address of the format "xmpp:alice@pgpainless.org". + */ public final class XMPP { private XMPP() { } + /** + * {@link org.pgpainless.util.selection.keyring.PublicKeyRingSelectionStrategy} which accepts a given + * {@link PGPPublicKeyRing} if its primary key has a user-id that matches the given
jid
. + * + * The argument
jid
can either contain the prefix "xmpp:", or not, the result will be the same. + */ public static class PubRingSelectionStrategy extends ExactUserId.PubRingSelectionStrategy { @Override @@ -24,6 +34,12 @@ public final class XMPP { } } + /** + * {@link org.pgpainless.util.selection.keyring.SecretKeyRingSelectionStrategy} which accepts a given + * {@link PGPSecretKeyRing} if its primary key has a user-id that matches the given
jid
. + * + * The argument
jid
can either contain the prefix "xmpp:", or not, the result will be the same. + */ public static class SecRingSelectionStrategy extends ExactUserId.SecRingSelectionStrategy { @Override From c8a1ca5b29d3f9e8520ddd2d5e7065daec8200fc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Apr 2022 12:53:47 +0200 Subject: [PATCH 0151/1204] Make use of DateUtil.now() in test --- .../RevokeKeyWithoutPreferredAlgorithmsOnPrimaryKey.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithoutPreferredAlgorithmsOnPrimaryKey.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithoutPreferredAlgorithmsOnPrimaryKey.java index 606b69f4..2cbbbe87 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithoutPreferredAlgorithmsOnPrimaryKey.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithoutPreferredAlgorithmsOnPrimaryKey.java @@ -100,7 +100,7 @@ public class RevokeKeyWithoutPreferredAlgorithmsOnPrimaryKey { @ExtendWith(TestAllImplementations.class) public void testChangingExpirationTimeWithKeyWithoutPrefAlgos() throws IOException, PGPException { - Date expirationDate = DateUtil.toSecondsPrecision(new Date()); + Date expirationDate = DateUtil.now(); PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); SecretKeyRingProtector protector = new UnprotectedKeysProtector(); From 2065b4e4edf8b09c7ba7e1ddb321e140922ae6fc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Apr 2022 13:08:24 +0200 Subject: [PATCH 0152/1204] Document planned removal of BCUtil.constantTimeAreEquals(char[], char[]) --- .../src/main/java/org/pgpainless/util/BCUtil.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java index 78e604c8..bb811cea 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java @@ -15,6 +15,11 @@ public final class BCUtil { * test will fail. For best results always pass the expected value * as the first parameter. * + * TODO: This method was proposed as a patch to BC: + * https://github.com/bcgit/bc-java/pull/1141 + * Replace usage of this method with upstream eventually. + * Remove once BC 172 gets released, given it contains the patch. + * * @param expected first array * @param supplied second array * @return true if arrays equal, false otherwise. From e601f8dbdae2505bd8d0c38a68d95277cbdda1ac Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Apr 2022 19:49:28 +0200 Subject: [PATCH 0153/1204] In Encrypt example: Read keys from string --- .../java/org/pgpainless/example/Encrypt.java | 118 +++++++++++++++--- 1 file changed, 101 insertions(+), 17 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java index 385198e9..ed57b90b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java @@ -11,8 +11,6 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; @@ -29,11 +27,102 @@ import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.util.Passphrase; public class Encrypt { + private static final String ALICE_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 12E3 4F04 C66D 2B70 D16C 960D ACF2 16F0 F93D DD20\n" + + "Comment: alice@pgpainless.org\n" + + "\n" + + "lFgEYksu1hYJKwYBBAHaRw8BAQdAIhUpRrs6zFTBI1pK40jCkzY/DQ/t4fUgNtlS\n" + + "mXOt1cIAAP4wM0LQD/Wj9w6/QujM/erj/TodDZzmp2ZwblrvDQri0RJ/tBRhbGlj\n" + + "ZUBwZ3BhaW5sZXNzLm9yZ4iPBBMWCgBBBQJiSy7WCRCs8hbw+T3dIBYhBBLjTwTG\n" + + "bStw0WyWDazyFvD5Pd0gAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAOOTAQDf\n" + + "UsRQSAs0d/Nm4YIrq+gU7gOdTJuf33f/u/u1nGM1fAD/RY7I3gQoZ0lWbvXVkRAL\n" + + "Cu9cUJdvL7kpW1oYtYg21QucXQRiSy7WEgorBgEEAZdVAQUBAQdA60F84k6MY/Uy\n" + + "BCZe4/WP8JDw/Efu5/Gyk8hcd3HzHFsDAQgHAAD/aC8DOOkK0XNVz2hkSVczmNoJ\n" + + "Umog0PfQLRujpOTqonAQKIh1BBgWCgAdBQJiSy7WAp4BApsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsACgkQrPIW8Pk93SCd6AD/Y3LF2RvgbEaOBtAvH6w0ZBPorB3rk6dx+Ae0\n" + + "GvW4E8wA+QHmgNo0pdkDxTl0BN1KC7BV1iRFqe9Vo7fW2LLfhlEEnFgEYksu1hYJ\n" + + "KwYBBAHaRw8BAQdAPtqap21/zmVzxOHk++891/EZSNikwWkq9t0pmYjhtJ8AAP9N\n" + + "m/G6nbiEB8mu/TkNnb7vdhSmLddL9kdKh0LzWD95LBF0iNUEGBYKAH0FAmJLLtYC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJiSy7WAAoJEOEz2Vo79Yyl\n" + + "zN0A/iZAVklSJsfQslshR6/zMBufwCK1S05jg/5Ydaksv3QcAQC4gsxdFFne+H4M\n" + + "mos4atad6hMhlqr0/Zyc71ZdO5I/CAAKCRCs8hbw+T3dIGhqAQCIdVtCus336cDe\n" + + "Nug+E9v1PEM3F/dt6GAqSG8LJqdAGgEA8cUXdUBooOo/QBkDnpteke8Z3IhIGyGe\n" + + "dc8OwJyVFwc=\n" + + "=ARAi\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + private static final String ALICE_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 12E3 4F04 C66D 2B70 D16C 960D ACF2 16F0 F93D DD20\n" + + "Comment: alice@pgpainless.org\n" + + "\n" + + "mDMEYksu1hYJKwYBBAHaRw8BAQdAIhUpRrs6zFTBI1pK40jCkzY/DQ/t4fUgNtlS\n" + + "mXOt1cK0FGFsaWNlQHBncGFpbmxlc3Mub3JniI8EExYKAEEFAmJLLtYJEKzyFvD5\n" + + "Pd0gFiEEEuNPBMZtK3DRbJYNrPIW8Pk93SACngECmwEFFgIDAQAECwkIBwUVCgkI\n" + + "CwKZAQAA45MBAN9SxFBICzR382bhgiur6BTuA51Mm5/fd/+7+7WcYzV8AP9Fjsje\n" + + "BChnSVZu9dWREAsK71xQl28vuSlbWhi1iDbVC7g4BGJLLtYSCisGAQQBl1UBBQEB\n" + + "B0DrQXziToxj9TIEJl7j9Y/wkPD8R+7n8bKTyFx3cfMcWwMBCAeIdQQYFgoAHQUC\n" + + "Yksu1gKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEKzyFvD5Pd0gnegA/2Nyxdkb\n" + + "4GxGjgbQLx+sNGQT6Kwd65OncfgHtBr1uBPMAPkB5oDaNKXZA8U5dATdSguwVdYk\n" + + "RanvVaO31tiy34ZRBLgzBGJLLtYWCSsGAQQB2kcPAQEHQD7amqdtf85lc8Th5Pvv\n" + + "PdfxGUjYpMFpKvbdKZmI4bSfiNUEGBYKAH0FAmJLLtYCngECmwIFFgIDAQAECwkI\n" + + "BwUVCgkIC18gBBkWCgAGBQJiSy7WAAoJEOEz2Vo79YylzN0A/iZAVklSJsfQslsh\n" + + "R6/zMBufwCK1S05jg/5Ydaksv3QcAQC4gsxdFFne+H4Mmos4atad6hMhlqr0/Zyc\n" + + "71ZdO5I/CAAKCRCs8hbw+T3dIGhqAQCIdVtCus336cDeNug+E9v1PEM3F/dt6GAq\n" + + "SG8LJqdAGgEA8cUXdUBooOo/QBkDnpteke8Z3IhIGyGedc8OwJyVFwc=\n" + + "=GUhm\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + private static final String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: A0D2 F316 0F6B 2CE5 7A50 FF32 261E 5081 9736 C493\n" + + "Comment: bob@pgpainless.org\n" + + "\n" + + "lFgEYksu1hYJKwYBBAHaRw8BAQdAXTBT1OKN1GAvGC+fzuy/k34BK+d5Saa87Glb\n" + + "iQgIxg8AAPwMI5DGqADFfl6H3Nxj3NxEZLasiFDpwEszluLVRy0jihGbtBJib2JA\n" + + "cGdwYWlubGVzcy5vcmeIjwQTFgoAQQUCYksu1gkQJh5QgZc2xJMWIQSg0vMWD2ss\n" + + "5XpQ/zImHlCBlzbEkwKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAADvrAD/cWBW\n" + + "mRkSfoCbEl22s59FXE7NPENrsJK8jxmWsWX3jbEA/AyXMCjwH6IhDgdgO7wH2z1r\n" + + "cUb/hokiCcCaJs6hjKcInF0EYksu1hIKKwYBBAGXVQEFAQEHQCeURSBi9brhisUH\n" + + "Dz0xN1NCgU5yeirx53xrQDFFx+d6AwEIBwAA/1GHX9+4Rg0ePsXGm1QIWL+C4rdf\n" + + "AReCTYoS3EBiZVdADoyIdQQYFgoAHQUCYksu1gKeAQKbDAUWAgMBAAQLCQgHBRUK\n" + + "CQgLAAoJECYeUIGXNsST8c0A/1dEIO9gsFB15UWDlTzN3S0TXQNN8wVzIMdW7XP2\n" + + "7c6bAQCB5ChqQA9AB1020DLr28BAbSjI7mPdIWg2PpE7B1EXC5xYBGJLLtYWCSsG\n" + + "AQQB2kcPAQEHQKP5NxT0ZhmRbrl3S6uwrUN248g1TEUR0DCVuLgyGSLpAAEA6bMa\n" + + "GaUf3S55rkFDjFC4Cv72zc8E5ex2RKgbpxXxqhYQN4jVBBgWCgB9BQJiSy7WAp4B\n" + + "ApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUCYksu1gAKCRDJLjPCA2NIfylD\n" + + "AP4tNFV23FBlrC57iesHVc+TTfNJ8rd+U7mbJvUgykcSNAEAy64tKPuVj+aA1bpm\n" + + "gHxfqdEJCOko8UhVVP6ltiDUcAoACgkQJh5QgZc2xJP9TQEA1DNgFno3di+xGDEN\n" + + "pwe9lmz8d/RWy/kuBT9S/3CMJjQBAKNBhHPuFfvk7RFbsmMrHsSqDFqIuUfGqq39\n" + + "VzmiMp8N\n" + + "=LpkJ\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + private static final String BOB_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: A0D2 F316 0F6B 2CE5 7A50 FF32 261E 5081 9736 C493\n" + + "Comment: bob@pgpainless.org\n" + + "\n" + + "mDMEYksu1hYJKwYBBAHaRw8BAQdAXTBT1OKN1GAvGC+fzuy/k34BK+d5Saa87Glb\n" + + "iQgIxg+0EmJvYkBwZ3BhaW5sZXNzLm9yZ4iPBBMWCgBBBQJiSy7WCRAmHlCBlzbE\n" + + "kxYhBKDS8xYPayzlelD/MiYeUIGXNsSTAp4BApsBBRYCAwEABAsJCAcFFQoJCAsC\n" + + "mQEAAO+sAP9xYFaZGRJ+gJsSXbazn0VcTs08Q2uwkryPGZaxZfeNsQD8DJcwKPAf\n" + + "oiEOB2A7vAfbPWtxRv+GiSIJwJomzqGMpwi4OARiSy7WEgorBgEEAZdVAQUBAQdA\n" + + "J5RFIGL1uuGKxQcPPTE3U0KBTnJ6KvHnfGtAMUXH53oDAQgHiHUEGBYKAB0FAmJL\n" + + "LtYCngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRAmHlCBlzbEk/HNAP9XRCDvYLBQ\n" + + "deVFg5U8zd0tE10DTfMFcyDHVu1z9u3OmwEAgeQoakAPQAddNtAy69vAQG0oyO5j\n" + + "3SFoNj6ROwdRFwu4MwRiSy7WFgkrBgEEAdpHDwEBB0Cj+TcU9GYZkW65d0ursK1D\n" + + "duPINUxFEdAwlbi4Mhki6YjVBBgWCgB9BQJiSy7WAp4BApsCBRYCAwEABAsJCAcF\n" + + "FQoJCAtfIAQZFgoABgUCYksu1gAKCRDJLjPCA2NIfylDAP4tNFV23FBlrC57iesH\n" + + "Vc+TTfNJ8rd+U7mbJvUgykcSNAEAy64tKPuVj+aA1bpmgHxfqdEJCOko8UhVVP6l\n" + + "tiDUcAoACgkQJh5QgZc2xJP9TQEA1DNgFno3di+xGDENpwe9lmz8d/RWy/kuBT9S\n" + + "/3CMJjQBAKNBhHPuFfvk7RFbsmMrHsSqDFqIuUfGqq39VzmiMp8N\n" + + "=1MqZ\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + /** * In this example, Alice is sending a signed and encrypted message to Bob. * She signs the message using her key and then encrypts the message to both bobs certificate and her own. @@ -42,16 +131,14 @@ public class Encrypt { * her certificate. */ @Test - public void encryptAndSignMessage() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void encryptAndSignMessage() throws PGPException, IOException { // Prepare keys - PGPSecretKeyRing keyAlice = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org", null); - PGPPublicKeyRing certificateAlice = KeyRingUtils.publicKeyRingFrom(keyAlice); + PGPSecretKeyRing keyAlice = PGPainless.readKeyRing().secretKeyRing(ALICE_KEY); + PGPPublicKeyRing certificateAlice = PGPainless.readKeyRing().publicKeyRing(ALICE_CERT); SecretKeyRingProtector protectorAlice = SecretKeyRingProtector.unprotectedKeys(); - PGPSecretKeyRing keyBob = PGPainless.generateKeyRing() - .modernKeyRing("bob@pgpainless.org", null); - PGPPublicKeyRing certificateBob = KeyRingUtils.publicKeyRingFrom(keyBob); + PGPSecretKeyRing keyBob = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); + PGPPublicKeyRing certificateBob = PGPainless.readKeyRing().publicKeyRing(BOB_CERT); SecretKeyRingProtector protectorBob = SecretKeyRingProtector.unprotectedKeys(); // plaintext message to encrypt @@ -138,15 +225,12 @@ public class Encrypt { * Bob subsequently decrypts the message using his key. */ @Test - public void encryptWithCommentHeader() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void encryptWithCommentHeader() throws PGPException, IOException { // Prepare keys - PGPSecretKeyRing keyAlice = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org", null); - PGPPublicKeyRing certificateAlice = KeyRingUtils.publicKeyRingFrom(keyAlice); + PGPPublicKeyRing certificateAlice = PGPainless.readKeyRing().publicKeyRing(ALICE_CERT); - PGPSecretKeyRing keyBob = PGPainless.generateKeyRing() - .modernKeyRing("bob@pgpainless.org", null); - PGPPublicKeyRing certificateBob = KeyRingUtils.publicKeyRingFrom(keyBob); + PGPSecretKeyRing keyBob = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); + PGPPublicKeyRing certificateBob = PGPainless.readKeyRing().publicKeyRing(BOB_CERT); SecretKeyRingProtector protectorBob = SecretKeyRingProtector.unprotectedKeys(); // plaintext message to encrypt From d0b070f0f3d45833b556babea37afff0770fda48 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Apr 2022 20:17:57 +0200 Subject: [PATCH 0154/1204] Fix javadoc --- .../util/selection/keyring/KeyRingSelectionStrategy.java | 1 + .../util/selection/keyring/SecretKeyRingSelectionStrategy.java | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/KeyRingSelectionStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/KeyRingSelectionStrategy.java index fb57e338..26180214 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/KeyRingSelectionStrategy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/KeyRingSelectionStrategy.java @@ -9,6 +9,7 @@ import java.util.Set; import org.pgpainless.util.MultiMap; /** + * Filter for selecting public / secret key rings based on identifiers (e.g. user-ids). * * @param Type of {@link org.bouncycastle.openpgp.PGPKeyRing} ({@link org.bouncycastle.openpgp.PGPSecretKeyRing} * or {@link org.bouncycastle.openpgp.PGPPublicKeyRing}). diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java index 943f2d82..ac5e8065 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java @@ -4,12 +4,11 @@ package org.pgpainless.util.selection.keyring; -import javax.annotation.Nonnull; import java.util.HashSet; import java.util.Iterator; import java.util.Set; +import javax.annotation.Nonnull; -import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.pgpainless.util.MultiMap; From 0bce68d6ee3c147d707b627a5766c01f76a107ae Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Apr 2022 20:18:15 +0200 Subject: [PATCH 0155/1204] Add shortcut SigningOptions.addSignature() method --- .../encryption_signing/SigningOptions.java | 15 +++++++++++++++ .../test/java/org/pgpainless/example/Sign.java | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java index 0608b22e..52d21641 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -98,6 +98,21 @@ public final class SigningOptions { return new SigningOptions(); } + /** + * Sign the message using an inline signature made by the provided signing key. + * + * @param signingKeyProtector protector to unlock the signing key + * @param signingKey key ring containing the signing key + * @return this + * + * @throws PGPException if the key cannot be unlocked or a signing method cannot be created + */ + public SigningOptions addSignature(SecretKeyRingProtector signingKeyProtector, + PGPSecretKeyRing signingKey) + throws PGPException { + return addInlineSignature(signingKeyProtector, signingKey, DocumentSignatureType.BINARY_DOCUMENT); + } + /** * Add inline signatures with all secret key rings in the provided secret key ring collection. * diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java b/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java index 13185e37..77db571d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java @@ -55,7 +55,7 @@ public class Sign { EncryptionStream signingStream = PGPainless.encryptAndOrSign() .onOutputStream(signedOut) .withOptions(ProducerOptions.sign(SigningOptions.get() - .addInlineSignature(protector, secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) + .addSignature(protector, secretKey)) ); Streams.pipeAll(messageIn, signingStream); From f6c6b9aded3bbb38d1cf5b5e7496f39944a24005 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 5 Apr 2022 14:10:04 +0200 Subject: [PATCH 0156/1204] Do not attempt to verify signatures made by external keys using primary key. This aims at fixing #266 in combination with #267. --- .../org/pgpainless/key/KeyRingValidator.java | 8 ++++++++ .../signature/consumer/CertificateValidator.java | 16 ++++++++++++++++ .../signature/consumer/SignaturePicker.java | 5 +++++ .../signature/consumer/SignatureVerifier.java | 4 ++++ 4 files changed, 33 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/KeyRingValidator.java b/pgpainless-core/src/main/java/org/pgpainless/key/KeyRingValidator.java index f52c1408..c3355ff8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/KeyRingValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/KeyRingValidator.java @@ -94,6 +94,10 @@ public final class KeyRingValidator { List signatures = CollectionUtils.iteratorToList(userIdSigs); Collections.sort(signatures, new SignatureCreationDateComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); for (PGPSignature signature : signatures) { + if (signature.getKeyID() != primaryKey.getKeyID()) { + // Signature was not made by primary key + continue; + } try { if (SignatureType.valueOf(signature.getSignatureType()) == SignatureType.CERTIFICATION_REVOCATION) { if (SignatureVerifier.verifyUserIdRevocation(userId, signature, primaryKey, policy, validationDate)) { @@ -116,6 +120,10 @@ public final class KeyRingValidator { Iterator userAttributeSignatureIterator = primaryKey.getSignaturesForUserAttribute(userAttribute); while (userAttributeSignatureIterator.hasNext()) { PGPSignature signature = userAttributeSignatureIterator.next(); + if (signature.getKeyID() != primaryKey.getKeyID()) { + // Signature was not made by primary key + continue; + } try { if (SignatureType.valueOf(signature.getSignatureType()) == SignatureType.CERTIFICATION_REVOCATION) { if (SignatureVerifier.verifyUserAttributesRevocation(userAttribute, signature, primaryKey, policy, validationDate)) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java index 65a1a41d..809ea003 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java @@ -72,6 +72,10 @@ public final class CertificateValidator { Iterator primaryKeyRevocationIterator = primaryKey.getSignaturesOfType(SignatureType.KEY_REVOCATION.getCode()); while (primaryKeyRevocationIterator.hasNext()) { PGPSignature revocation = primaryKeyRevocationIterator.next(); + if (revocation.getKeyID() != primaryKey.getKeyID()) { + // Revocation was not made by primary key, skip + // TODO: What about external revocation keys? + } try { if (SignatureVerifier.verifyKeyRevocationSignature(revocation, primaryKey, policy, signature.getCreationTime())) { directKeySignatures.add(revocation); @@ -86,6 +90,10 @@ public final class CertificateValidator { Iterator keySignatures = primaryKey.getSignaturesOfType(SignatureType.DIRECT_KEY.getCode()); while (keySignatures.hasNext()) { PGPSignature keySignature = keySignatures.next(); + if (keySignature.getKeyID() != primaryKey.getKeyID()) { + // Signature was not made by primary key, skip + continue; + } try { if (SignatureVerifier.verifyDirectKeySignature(keySignature, primaryKey, policy, signature.getCreationTime())) { directKeySignatures.add(keySignature); @@ -112,6 +120,10 @@ public final class CertificateValidator { Iterator userIdSigs = primaryKey.getSignaturesForID(userId); while (userIdSigs.hasNext()) { PGPSignature userIdSig = userIdSigs.next(); + if (userIdSig.getKeyID() != primaryKey.getKeyID()) { + // Sig was made by external key, skip + continue; + } try { if (SignatureVerifier.verifySignatureOverUserId(userId, userIdSig, primaryKey, policy, signature.getCreationTime())) { signaturesOnUserId.add(userIdSig); @@ -168,6 +180,10 @@ public final class CertificateValidator { Iterator bindingRevocations = signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.getCode()); while (bindingRevocations.hasNext()) { PGPSignature revocation = bindingRevocations.next(); + if (revocation.getKeyID() != primaryKey.getKeyID()) { + // Subkey Revocation was not made by primary key, skip + continue; + } try { if (SignatureVerifier.verifySubkeyBindingRevocation(revocation, primaryKey, signingSubkey, policy, signature.getCreationTime())) { subkeySigs.add(revocation); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java index 74fff1e9..e6f2f755 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java @@ -209,10 +209,15 @@ public final class SignaturePicker { Iterator userIdSigIterator = primaryKey.getSignaturesForID(userId); List signatures = CollectionUtils.iteratorToList(userIdSigIterator); + Collections.sort(signatures, new SignatureCreationDateComparator()); PGPSignature mostRecentUserIdCertification = null; for (PGPSignature signature : signatures) { + if (primaryKey.getKeyID() != signature.getKeyID()) { + // Signature not made by primary key + continue; + } try { SignatureVerifier.verifyUserIdCertification(userId, signature, primaryKey, policy, validationDate); } catch (SignatureValidationException e) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java index 1cfff7db..d4a61271 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java @@ -89,6 +89,7 @@ public final class SignatureVerifier { */ public static boolean verifyUserIdCertification(String userId, PGPSignature signature, PGPPublicKey signingKey, PGPPublicKey keyWithUserId, Policy policy, Date validationDate) throws SignatureValidationException { + SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); SignatureValidator.signatureIsCertification().verify(signature); SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); SignatureValidator.signatureIsEffective(validationDate).verify(signature); @@ -129,6 +130,7 @@ public final class SignatureVerifier { */ public static boolean verifyUserIdRevocation(String userId, PGPSignature signature, PGPPublicKey signingKey, PGPPublicKey keyWithUserId, Policy policy, Date validationDate) throws SignatureValidationException { + SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); SignatureValidator.signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature); SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); SignatureValidator.signatureIsEffective(validationDate).verify(signature); @@ -174,6 +176,7 @@ public final class SignatureVerifier { PGPPublicKey keyWithUserAttributes, Policy policy, Date validationDate) throws SignatureValidationException { + SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); SignatureValidator.signatureIsCertification().verify(signature); SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); SignatureValidator.signatureIsEffective(validationDate).verify(signature); @@ -219,6 +222,7 @@ public final class SignatureVerifier { PGPPublicKey keyWithUserAttributes, Policy policy, Date validationDate) throws SignatureValidationException { + SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); SignatureValidator.signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature); SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); SignatureValidator.signatureIsEffective(validationDate).verify(signature); From 8c6813ce565567ed84d5b1e0a5ee32a14634a984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Barab=C3=A1s?= Date: Tue, 5 Apr 2022 10:38:19 +0200 Subject: [PATCH 0157/1204] #266 Handle ClassCastException in signature.init calls --- .../signature/consumer/SignatureValidator.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java index 3572fff0..51bfa7c3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java @@ -461,7 +461,7 @@ public abstract class SignatureValidator { if (!valid) { throw new SignatureValidationException("Signature is not correct."); } - } catch (PGPException e) { + } catch (PGPException | ClassCastException e) { throw new SignatureValidationException("Cannot verify subkey binding signature correctness", e); } } @@ -485,7 +485,7 @@ public abstract class SignatureValidator { if (!valid) { throw new SignatureValidationException("Primary Key Binding Signature is not correct."); } - } catch (PGPException e) { + } catch (PGPException | ClassCastException e) { throw new SignatureValidationException("Cannot verify primary key binding signature correctness", e); } } @@ -514,7 +514,7 @@ public abstract class SignatureValidator { if (!valid) { throw new SignatureValidationException("Signature is not correct."); } - } catch (PGPException e) { + } catch (PGPException | ClassCastException e) { throw new SignatureValidationException("Cannot verify direct-key signature correctness", e); } } @@ -577,7 +577,7 @@ public abstract class SignatureValidator { if (!valid) { throw new SignatureValidationException("Signature over user-id '" + userId + "' is not correct."); } - } catch (PGPException e) { + } catch (PGPException | ClassCastException e) { throw new SignatureValidationException("Cannot verify signature over user-id '" + userId + "'.", e); } } @@ -602,7 +602,7 @@ public abstract class SignatureValidator { if (!valid) { throw new SignatureValidationException("Signature over user-attribute vector is not correct."); } - } catch (PGPException e) { + } catch (PGPException | ClassCastException e) { throw new SignatureValidationException("Cannot verify signature over user-attribute vector.", e); } } From 30c9ea254adefafcd5caae084e11378ed2a7a958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Barab=C3=A1s?= Date: Tue, 5 Apr 2022 14:35:01 +0200 Subject: [PATCH 0158/1204] Fix XML comment --- .../java/org/pgpainless/util/selection/userid/SelectUserId.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java index e86e59f3..65ac568b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java @@ -181,7 +181,7 @@ public abstract class SelectUserId { } /** - * Filter that filters for user-ids which pass at least one of the given
filters
>. + * Filter that filters for user-ids which pass at least one of the given
filters
. * * @param filters filters * @return filter From 3245dff73101a7720b789a6ac85fe70e412b1a5f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 5 Apr 2022 14:43:14 +0200 Subject: [PATCH 0159/1204] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44686127..41838790 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ SPDX-License-Identifier: CC0-1.0 - `ProducerOptions.setEncoding()`: The encoding is henceforth only considered metadata and will no longer trigger CRLF encoding. - This fixes broken signature generation for mismatching (`StreamEncoding`,`DocumentSignatureType`) tuples. - Applications that rely on CRLF-encoding can request PGPainless to apply this encoding by calling `ProducerOptions.applyCRLFEncoding(true)`. +- Rename `KeyRingUtils.removeSecretKey()` to `stripSecretKey()`. +- Add handy `SignatureOptions.addSignature()` method. +- Fix `ClassCastException` when evaluating a certificate with third party signatures. Thanks @p-barabas for the initial report and bug fix! ## 1.1.4 - Add utility method `KeyRingUtils.removeSecretKey()` to remove secret key part from key ring From a7d56e3461d2b0fb6f53d0ca0d067034b5092104 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 5 Apr 2022 14:48:44 +0200 Subject: [PATCH 0160/1204] PGPainless 1.1.5 --- CHANGELOG.md | 2 +- README.md | 2 +- .../org/pgpainless/util/selection/userid/SelectUserId.java | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41838790..8134c0cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.1.5-SNAPSHOT +## 1.1.5 - SOP encrypt: match signature type when using `encrypt --as=` option - `ProducerOptions.setEncoding()`: The encoding is henceforth only considered metadata and will no longer trigger CRLF encoding. - This fixes broken signature generation for mismatching (`StreamEncoding`,`DocumentSignatureType`) tuples. diff --git a/README.md b/README.md index 514dbd84..58e59401 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.1.4' + implementation 'org.pgpainless:pgpainless-core:1.1.5' } ``` diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java index 65ac568b..5c1611af 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java @@ -134,7 +134,7 @@ public abstract class SelectUserId { * Note: This only accepts user-ids which properly have the email address surrounded by angle brackets. * * The argument
email
can both be a plain email address (
"foo@bar.baz"
), - * or surrounded by angle brackets (
""
, the result of the filter will be the same. + * or surrounded by angle brackets ({@code
""
}), the result of the filter will be the same. * * @param email email address * @return filter diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 9ec6c8cd..bb4af342 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.1.4" + implementation "org.pgpainless:pgpainless-sop:1.1.5" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.1.4 + 1.1.5 ... diff --git a/version.gradle b/version.gradle index 3b94fb87..384e25a3 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.1.5' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From 8e45a2a7f6bef5cfafd5e9753424c038f2bfd0e3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 5 Apr 2022 14:51:26 +0200 Subject: [PATCH 0161/1204] PGPainless-1.1.6-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 384e25a3..3b633561 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.1.5' - isSnapshot = false + shortVersion = '1.1.6' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.70' From 636fc63bc1f03bd4cb3e752dac0f984181cfe426 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 5 Apr 2022 14:57:43 +0200 Subject: [PATCH 0162/1204] Add local.properties to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 84123d97..f12ea5f8 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ libs/ *.log *.jar +local.properties + gradle.properties !gradle-wrapper.jar From 6e6789542892c23cebfaefc896cb6a4859d646c1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 5 Apr 2022 17:01:15 +0200 Subject: [PATCH 0163/1204] Add ECOSYSTEM.md --- ECOSYSTEM.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 ECOSYSTEM.md diff --git a/ECOSYSTEM.md b/ECOSYSTEM.md new file mode 100644 index 00000000..ead27565 --- /dev/null +++ b/ECOSYSTEM.md @@ -0,0 +1,47 @@ + + +# Ecosystem + +PGPainless consists of an ecosystem of different libraries and projects. + +## [PGPainless](https://github.com/pgpainless/pgpainless) + +The main repository contains the following components: + +* `pgpainless-core` - core implementation - powerful, yet easy to use OpenPGP API +* `pgpainless-sop` - super simple OpenPGP implementation. Drop-in for `sop-java` +* `pgpainless-cli` - SOP CLI implementation using PGPainless + +## [SOP-Java](https://github.com/pgpainless/sop-java) + +An API definition and CLI implementation of the [Stateless OpenPGP Protocol](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-03.html). + +* `sop-java` - generic OpenPGP API definition +* `sop-java-picocli` - Abstract CLI implementation for `sop-java` + +## [WKD-Java](https://github.com/pgpainless/wkd-java) + +Implementation of the [Web Key Directory](https://www.ietf.org/archive/id/draft-koch-openpgp-webkey-service-13.html). + +* `wkd-java` - abstract WKD discovery implementation +* `wkd-java-cli` - CLI application implementing WKD discovery using PGPainless +* `wkd-test-suite` - Generator for test vectors for testing WKD implementations + +## [Cert-D-Java](https://github.com/pgpainless/cert-d-java) + +Implementations of the [Shared OpenPGP Certificate Directory specification](https://sequoia-pgp.gitlab.io/pgp-cert-d/). + +* `pgp-certificate-store` - abstract definitions of OpenPGP certificate stores +* `pgp-cert-d-java` - implementation of `pgp-certificate-store` following the PGP-CERT-D spec. +* `pgp-cert-d-java-jdbc-sqlite-lookup` - subkey lookup using sqlite database + +## [Cert-D-PGPainless](https://github.com/pgpainless/cert-d-pgpainless) + +Implementation of the [Shared OpenPGP Certificate Directory specification](https://sequoia-pgp.gitlab.io/pgp-cert-d/) using PGPainless. + +* `pgpainless-cert-d` - PGPainless-based implementation of `pgp-cert-d-java`. +* `pgpainless-cert-d-cli` - CLI frontend for `pgpainless-cert-d`. \ No newline at end of file From 02d6d19aac4202efb30eb60794cbae882d6785cc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Apr 2022 11:37:31 +0200 Subject: [PATCH 0164/1204] Update ECOSYSTEM --- ECOSYSTEM.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ECOSYSTEM.md b/ECOSYSTEM.md index ead27565..0652bdef 100644 --- a/ECOSYSTEM.md +++ b/ECOSYSTEM.md @@ -31,6 +31,12 @@ Implementation of the [Web Key Directory](https://www.ietf.org/archive/id/draft- * `wkd-java-cli` - CLI application implementing WKD discovery using PGPainless * `wkd-test-suite` - Generator for test vectors for testing WKD implementations +## [VKS-Java](https://github.com/pgpainless/vks-java) + +Client-side API for communicating with Verifying Key Servers, such as https://keys.openpgp.org/. + +* `vks-java` - VKS client implementation + ## [Cert-D-Java](https://github.com/pgpainless/cert-d-java) Implementations of the [Shared OpenPGP Certificate Directory specification](https://sequoia-pgp.gitlab.io/pgp-cert-d/). From 53017d2d38dba66d1e4347ec953a79f769f691c2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Apr 2022 19:40:39 +0200 Subject: [PATCH 0165/1204] Bump BC to 1.71 --- pgpainless-core/build.gradle | 5 +++-- version.gradle | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index e6947458..6cb6faa5 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -15,8 +15,9 @@ dependencies { api "org.slf4j:slf4j-api:$slf4jVersion" testImplementation "ch.qos.logback:logback-classic:$logbackVersion" - api "org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion" - api "org.bouncycastle:bcpg-jdk15on:$bouncyCastleVersion" + // Bouncy Castle + api "org.bouncycastle:bcprov-jdk15to18:$bouncyCastleVersion" + api "org.bouncycastle:bcpg-jdk15to18:$bouncyCastleVersion" // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' diff --git a/version.gradle b/version.gradle index 3b633561..2d2d8941 100644 --- a/version.gradle +++ b/version.gradle @@ -8,6 +8,6 @@ allprojects { isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 - bouncyCastleVersion = '1.70' + bouncyCastleVersion = '1.71' } } From 6b3f37796c6e107498bedb122c64152cfa611d24 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Apr 2022 19:40:56 +0200 Subject: [PATCH 0166/1204] Restructure dependencies and version.gradle --- build.gradle | 4 ---- pgpainless-core/build.gradle | 5 +++-- version.gradle | 4 ++++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index b0881e63..33e493f0 100644 --- a/build.gradle +++ b/build.gradle @@ -71,10 +71,6 @@ allprojects { } project.ext { - slf4jVersion = '1.7.32' - logbackVersion = '1.2.9' - junitVersion = '5.8.2' - sopJavaVersion = '1.2.0' rootConfigDir = new File(rootDir, 'config') gitCommit = getGitCommit() isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI')) diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 6cb6faa5..50cbb700 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -7,6 +7,7 @@ plugins { } dependencies { + // JUnit testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" @@ -19,6 +20,6 @@ dependencies { api "org.bouncycastle:bcprov-jdk15to18:$bouncyCastleVersion" api "org.bouncycastle:bcpg-jdk15to18:$bouncyCastleVersion" - // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 - implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' + // @Nullable, @Nonnull annotations + implementation "com.google.code.findbugs:jsr305:3.0.2" } diff --git a/version.gradle b/version.gradle index 2d2d8941..9406a66b 100644 --- a/version.gradle +++ b/version.gradle @@ -9,5 +9,9 @@ allprojects { pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' + slf4jVersion = '1.7.32' + logbackVersion = '1.2.9' + junitVersion = '5.8.2' + sopJavaVersion = '1.2.0' } } From a22336a79582382264fac468bfd28e79dd306613 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Mar 2022 14:04:59 +0100 Subject: [PATCH 0167/1204] Create dedicated KeyException class for key-related exceptions. --- .../encryption_signing/EncryptionOptions.java | 9 +- .../encryption_signing/SigningOptions.java | 48 ++++--- .../exception/KeyCannotSignException.java | 13 -- .../pgpainless/exception/KeyException.java | 118 ++++++++++++++++++ .../exception/KeyIntegrityException.java | 4 + .../exception/KeyValidationError.java | 14 --- .../org/pgpainless/key/info/KeyRingInfo.java | 9 +- .../EncryptDecryptTest.java | 3 +- .../EncryptionOptionsTest.java | 10 +- .../encryption_signing/SigningTest.java | 17 ++- .../signature/CertificateExpirationTest.java | 3 +- ...ncryptCommsStorageFlagsDifferentiated.java | 3 +- .../java/org/pgpainless/sop/SignImpl.java | 3 +- 13 files changed, 186 insertions(+), 68 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/KeyCannotSignException.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/KeyValidationError.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index af557703..ca7b8ddb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -23,6 +23,7 @@ import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.exception.KeyException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.SubkeyIdentifier; @@ -154,7 +155,7 @@ public class EncryptionOptions { List encryptionSubkeys = encryptionKeySelectionStrategy .selectEncryptionSubkeys(info.getEncryptionSubkeys(userId, purpose)); if (encryptionSubkeys.isEmpty()) { - throw new IllegalArgumentException("Key has no suitable encryption subkeys."); + throw new KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)); } for (PGPPublicKey encryptionSubkey : encryptionSubkeys) { @@ -193,16 +194,16 @@ public class EncryptionOptions { try { primaryKeyExpiration = info.getPrimaryKeyExpirationDate(); } catch (NoSuchElementException e) { - throw new IllegalArgumentException("Provided key " + OpenPgpFingerprint.of(key) + " does not have a valid/acceptable signature carrying a primary key expiration date."); + throw new KeyException.UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key)); } if (primaryKeyExpiration != null && primaryKeyExpiration.before(evaluationDate)) { - throw new IllegalArgumentException("Provided key " + OpenPgpFingerprint.of(key) + " is expired: " + primaryKeyExpiration); + throw new KeyException.ExpiredKeyException(OpenPgpFingerprint.of(key), primaryKeyExpiration); } List encryptionSubkeys = encryptionKeySelectionStrategy .selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose)); if (encryptionSubkeys.isEmpty()) { - throw new IllegalArgumentException("Key " + OpenPgpFingerprint.of(key) + " has no suitable encryption subkeys."); + throw new KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)); } for (PGPPublicKey encryptionSubkey : encryptionSubkeys) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java index 52d21641..f81bf5aa 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -24,8 +24,7 @@ import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator; -import org.pgpainless.exception.KeyCannotSignException; -import org.pgpainless.exception.KeyValidationError; +import org.pgpainless.exception.KeyException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.SubkeyIdentifier; @@ -120,13 +119,13 @@ public final class SigningOptions { * @param signingKeys collection of signing keys * @param signatureType type of signature (binary, canonical text) * @return this - * @throws KeyValidationError if something is wrong with any of the keys + * @throws KeyException if something is wrong with any of the keys * @throws PGPException if any of the keys cannot be unlocked or a signing method cannot be created */ public SigningOptions addInlineSignatures(SecretKeyRingProtector secrectKeyDecryptor, Iterable signingKeys, DocumentSignatureType signatureType) - throws KeyValidationError, PGPException { + throws KeyException, PGPException { for (PGPSecretKeyRing signingKey : signingKeys) { addInlineSignature(secrectKeyDecryptor, signingKey, signatureType); } @@ -141,14 +140,14 @@ public final class SigningOptions { * @param secretKeyDecryptor decryptor to unlock the signing secret key * @param secretKey signing key * @param signatureType type of signature (binary, canonical text) - * @throws KeyValidationError if something is wrong with the key + * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be unlocked or the signing method cannot be created * @return this */ public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, PGPSecretKeyRing secretKey, DocumentSignatureType signatureType) - throws KeyValidationError, PGPException { + throws KeyException, PGPException { return addInlineSignature(secretKeyDecryptor, secretKey, null, signatureType); } @@ -164,14 +163,14 @@ public final class SigningOptions { * @param userId user-id of the signer * @param signatureType signature type (binary, canonical text) * @return this - * @throws KeyValidationError if the key is invalid + * @throws KeyException if the key is invalid * @throws PGPException if the key cannot be unlocked or the signing method cannot be created */ public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, PGPSecretKeyRing secretKey, String userId, DocumentSignatureType signatureType) - throws KeyValidationError, PGPException { + throws KeyException, PGPException { return addInlineSignature(secretKeyDecryptor, secretKey, userId, signatureType, null); } @@ -188,7 +187,8 @@ public final class SigningOptions { * @param signatureType signature type (binary, canonical text) * @param subpacketsCallback callback to modify the hashed and unhashed subpackets of the signature * @return this - * @throws KeyValidationError if the key is invalid + * @throws KeyException + * if the key is invalid * @throws PGPException if the key cannot be unlocked or the signing method cannot be created */ public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, @@ -196,19 +196,27 @@ public final class SigningOptions { String userId, DocumentSignatureType signatureType, @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) - throws KeyValidationError, PGPException { + throws KeyException, PGPException { KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); if (userId != null && !keyRingInfo.isUserIdValid(userId)) { - throw new KeyValidationError(userId, keyRingInfo.getLatestUserIdCertification(userId), keyRingInfo.getUserIdRevocation(userId)); + throw new KeyException.UnboundUserIdException( + OpenPgpFingerprint.of(secretKey), + userId, + keyRingInfo.getLatestUserIdCertification(userId), + keyRingInfo.getUserIdRevocation(userId) + ); } List signingPubKeys = keyRingInfo.getSigningSubkeys(); if (signingPubKeys.isEmpty()) { - throw new KeyCannotSignException("Key " + OpenPgpFingerprint.of(secretKey) + " has no valid signing key."); + throw new KeyException.UnacceptableSigningKeyException(OpenPgpFingerprint.of(secretKey)); } for (PGPPublicKey signingPubKey : signingPubKeys) { PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID()); + if (signingSecKey == null) { + throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), signingPubKey.getKeyID()); + } PGPPrivateKey signingSubkey = UnlockSecretKey.unlockSecretKey(signingSecKey, secretKeyDecryptor); Set hashAlgorithms = userId != null ? keyRingInfo.getPreferredHashAlgorithms(userId) : keyRingInfo.getPreferredHashAlgorithms(signingPubKey.getKeyID()); @@ -304,18 +312,23 @@ public final class SigningOptions { throws PGPException { KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); if (userId != null && !keyRingInfo.isUserIdValid(userId)) { - throw new KeyValidationError(userId, keyRingInfo.getLatestUserIdCertification(userId), keyRingInfo.getUserIdRevocation(userId)); + throw new KeyException.UnboundUserIdException( + OpenPgpFingerprint.of(secretKey), + userId, + keyRingInfo.getLatestUserIdCertification(userId), + keyRingInfo.getUserIdRevocation(userId) + ); } List signingPubKeys = keyRingInfo.getSigningSubkeys(); if (signingPubKeys.isEmpty()) { - throw new KeyCannotSignException("Key has no valid signing key."); + throw new KeyException.UnacceptableSigningKeyException(OpenPgpFingerprint.of(secretKey)); } for (PGPPublicKey signingPubKey : signingPubKeys) { PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID()); if (signingSecKey == null) { - throw new PGPException("Missing secret key for signing key " + Long.toHexString(signingPubKey.getKeyID())); + throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), signingPubKey.getKeyID()); } PGPPrivateKey signingSubkey = signingSecKey.extractPrivateKey( secretKeyDecryptor.getDecryptor(signingPubKey.getKeyID())); @@ -340,8 +353,9 @@ public final class SigningOptions { PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(signingSecretKey.getPublicKey().getAlgorithm()); int bitStrength = secretKey.getPublicKey().getBitStrength(); if (!PGPainless.getPolicy().getPublicKeyAlgorithmPolicy().isAcceptable(publicKeyAlgorithm, bitStrength)) { - throw new IllegalArgumentException("Public key algorithm policy violation: " + - publicKeyAlgorithm + " with bit strength " + bitStrength + " is not acceptable."); + throw new KeyException.UnacceptableSigningKeyException( + new KeyException.PublicKeyAlgorithmPolicyException( + OpenPgpFingerprint.of(secretKey), signingSecretKey.getKeyID(), publicKeyAlgorithm, bitStrength)); } PGPSignatureGenerator generator = createSignatureGenerator(signingSubkey, hashAlgorithm, signatureType); diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyCannotSignException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyCannotSignException.java deleted file mode 100644 index ee869fa9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyCannotSignException.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import org.bouncycastle.openpgp.PGPException; - -public class KeyCannotSignException extends PGPException { - public KeyCannotSignException(String message) { - super(message); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java new file mode 100644 index 00000000..7ffc66ee --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception; + +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.util.DateUtil; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Date; + +public abstract class KeyException extends RuntimeException { + + private final OpenPgpFingerprint fingerprint; + + protected KeyException(@Nonnull String message, @Nonnull OpenPgpFingerprint fingerprint) { + super(message); + this.fingerprint = fingerprint; + } + + protected KeyException(@Nonnull String message, @Nonnull OpenPgpFingerprint fingerprint, @Nonnull Throwable underlying) { + super(message, underlying); + this.fingerprint = fingerprint; + } + + public OpenPgpFingerprint getFingerprint() { + return fingerprint; + } + + public static class ExpiredKeyException extends KeyException { + + public ExpiredKeyException(@Nonnull OpenPgpFingerprint fingerprint, @Nonnull Date expirationDate) { + super("Key " + fingerprint + " is expired. Expiration date: " + DateUtil.formatUTCDate(expirationDate), fingerprint); + } + } + + public static class UnacceptableEncryptionKeyException extends KeyException { + + public UnacceptableEncryptionKeyException(@Nonnull OpenPgpFingerprint fingerprint) { + super("Key " + fingerprint + " has no acceptable encryption key.", fingerprint); + } + + public UnacceptableEncryptionKeyException(@Nonnull PublicKeyAlgorithmPolicyException reason) { + super("Key " + reason.getFingerprint() + " has no acceptable encryption key.", reason.getFingerprint(), reason); + } + } + + public static class UnacceptableSigningKeyException extends KeyException { + + public UnacceptableSigningKeyException(@Nonnull OpenPgpFingerprint fingerprint) { + super("Key " + fingerprint + " has no acceptable signing key.", fingerprint); + } + + public UnacceptableSigningKeyException(@Nonnull PublicKeyAlgorithmPolicyException reason) { + super("Key " + reason.getFingerprint() + " has no acceptable signing key.", reason.getFingerprint(), reason); + } + } + + public static class UnacceptableSelfSignatureException extends KeyException { + + public UnacceptableSelfSignatureException(@Nonnull OpenPgpFingerprint fingerprint) { + super("Key " + fingerprint + " does not have a valid/acceptable signature to derive an expiration date from.", fingerprint); + } + } + + public static class MissingSecretKeyException extends KeyException { + + private final long missingSecretKeyId; + + public MissingSecretKeyException(@Nonnull OpenPgpFingerprint fingerprint, long keyId) { + super("Key " + fingerprint + " does not contain a secret key for public key " + Long.toHexString(keyId), fingerprint); + this.missingSecretKeyId = keyId; + } + + public long getMissingSecretKeyId() { + return missingSecretKeyId; + } + } + + public static class PublicKeyAlgorithmPolicyException extends KeyException { + + private final long violatingSubkeyId; + + public PublicKeyAlgorithmPolicyException(@Nonnull OpenPgpFingerprint fingerprint, long keyId, @Nonnull PublicKeyAlgorithm algorithm, int bitSize) { + super("Subkey " + Long.toHexString(keyId) + " of key " + fingerprint + " is violating the Public Key Algorithm Policy:\n" + + algorithm + " of size " + bitSize + " is not acceptable.", fingerprint); + this.violatingSubkeyId = keyId; + } + + public long getViolatingSubkeyId() { + return violatingSubkeyId; + } + } + + public static class UnboundUserIdException extends KeyException { + + public UnboundUserIdException(@Nonnull OpenPgpFingerprint fingerprint, @Nonnull String userId, + @Nullable PGPSignature userIdSignature, @Nullable PGPSignature userIdRevocation) { + super(errorMessage(fingerprint, userId, userIdSignature, userIdRevocation), fingerprint); + } + + private static String errorMessage(@Nonnull OpenPgpFingerprint fingerprint, @Nonnull String userId, + @Nullable PGPSignature userIdSignature, @Nullable PGPSignature userIdRevocation) { + String errorMessage = "UserID '" + userId + "' is not valid for key " + fingerprint + ": "; + if (userIdSignature == null) { + return errorMessage + "Missing binding signature."; + } + if (userIdRevocation != null) { + return errorMessage + "UserID is revoked."; + } + return errorMessage + "Unacceptable binding signature."; + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyIntegrityException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyIntegrityException.java index 65ed3dea..b7a87ab7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyIntegrityException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyIntegrityException.java @@ -4,6 +4,10 @@ package org.pgpainless.exception; +/** + * This exception gets thrown, when the integrity of an OpenPGP key is broken. + * That could happen on accident, or during an active attack, so take this exception seriously. + */ public class KeyIntegrityException extends AssertionError { public KeyIntegrityException() { diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyValidationError.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyValidationError.java deleted file mode 100644 index 8296d6c9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyValidationError.java +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import org.bouncycastle.openpgp.PGPSignature; - -public class KeyValidationError extends AssertionError { - - public KeyValidationError(String userId, PGPSignature userIdSig, PGPSignature userIdRevocation) { - super("User-ID '" + userId + "' is not valid: Sig: " + userIdSig + " Rev: " + userIdRevocation); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index f1611aff..3ea2f424 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -36,7 +36,7 @@ import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.exception.KeyValidationError; +import org.pgpainless.exception.KeyException; import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.util.RevocationAttributes; @@ -949,7 +949,12 @@ public class KeyRingInfo { */ public @Nonnull List getEncryptionSubkeys(String userId, EncryptionPurpose purpose) { if (userId != null && !isUserIdValid(userId)) { - throw new KeyValidationError(userId, getLatestUserIdCertification(userId), getUserIdRevocation(userId)); + throw new KeyException.UnboundUserIdException( + OpenPgpFingerprint.of(keys), + userId, + getLatestUserIdCertification(userId), + getUserIdRevocation(userId) + ); } return getEncryptionSubkeys(purpose); diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java index 966e273f..88a2c38b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java @@ -34,6 +34,7 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.exception.KeyException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; @@ -326,7 +327,7 @@ public class EncryptDecryptTest { PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); - assertThrows(IllegalArgumentException.class, () -> + assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> EncryptionOptions.encryptCommunications() .addRecipient(publicKeys)); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 0c5f6489..488e5918 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -28,7 +28,7 @@ import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.exception.KeyValidationError; +import org.pgpainless.exception.KeyException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; @@ -132,14 +132,14 @@ public class EncryptionOptionsTest { .build(); PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys); - assertThrows(IllegalArgumentException.class, () -> options.addRecipient(publicKeys)); + assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys)); } @Test public void testEncryptionKeySelectionStrategyEmpty_ThrowsAssertionError() { EncryptionOptions options = new EncryptionOptions(); - assertThrows(IllegalArgumentException.class, + assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, new EncryptionOptions.EncryptionKeySelector() { @Override public List selectEncryptionSubkeys(List encryptionCapableKeys) { @@ -147,7 +147,7 @@ public class EncryptionOptionsTest { } })); - assertThrows(IllegalArgumentException.class, + assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, "test@pgpainless.org", new EncryptionOptions.EncryptionKeySelector() { @Override public List selectEncryptionSubkeys(List encryptionCapableKeys) { @@ -180,6 +180,6 @@ public class EncryptionOptionsTest { @Test public void testAddRecipient_withInvalidUserId() { EncryptionOptions options = new EncryptionOptions(); - assertThrows(KeyValidationError.class, () -> options.addRecipient(publicKeys, "invalid@user.id")); + assertThrows(KeyException.UnboundUserIdException.class, () -> options.addRecipient(publicKeys, "invalid@user.id")); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/SigningTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/SigningTest.java index 593938ad..156e6b57 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/SigningTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/SigningTest.java @@ -35,8 +35,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.exception.KeyCannotSignException; -import org.pgpainless.exception.KeyValidationError; +import org.pgpainless.exception.KeyException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; @@ -45,9 +44,9 @@ import org.pgpainless.key.generation.type.eddsa.EdDSACurve; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.MultiMap; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.TestAllImplementations; public class SigningTest { @@ -125,7 +124,7 @@ public class SigningTest { SigningOptions opts = new SigningOptions(); // "bob" is not a valid user-id - assertThrows(KeyValidationError.class, + assertThrows(KeyException.UnboundUserIdException.class, () -> opts.addInlineSignature(protector, secretKeys, "bob", DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)); } @@ -146,7 +145,7 @@ public class SigningTest { SigningOptions opts = new SigningOptions(); // "alice" has been revoked - assertThrows(KeyValidationError.class, + assertThrows(KeyException.UnboundUserIdException.class, () -> opts.addInlineSignature(protector, fSecretKeys, "alice", DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)); } @@ -253,9 +252,9 @@ public class SigningTest { .build(); SigningOptions options = new SigningOptions(); - assertThrows(KeyCannotSignException.class, () -> options.addDetachedSignature( + assertThrows(KeyException.UnacceptableSigningKeyException.class, () -> options.addDetachedSignature( SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT)); - assertThrows(KeyCannotSignException.class, () -> options.addInlineSignature( + assertThrows(KeyException.UnacceptableSigningKeyException.class, () -> options.addInlineSignature( SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT)); } @@ -270,10 +269,10 @@ public class SigningTest { .build(); SigningOptions options = new SigningOptions(); - assertThrows(KeyValidationError.class, () -> + assertThrows(KeyException.UnboundUserIdException.class, () -> options.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, "Bob", DocumentSignatureType.BINARY_DOCUMENT)); - assertThrows(KeyValidationError.class, () -> + assertThrows(KeyException.UnboundUserIdException.class, () -> options.addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, "Bob", DocumentSignatureType.BINARY_DOCUMENT)); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateExpirationTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateExpirationTest.java index c87bd2aa..f8c6471e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateExpirationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateExpirationTest.java @@ -21,6 +21,7 @@ import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.exception.KeyException; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.SubkeyIdentifier; @@ -124,7 +125,7 @@ public class CertificateExpirationTest { "-----END PGP PUBLIC KEY BLOCK-----\n"; PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(CERT); - assertThrows(IllegalArgumentException.class, () -> encrypt(cert)); + assertThrows(KeyException.ExpiredKeyException.class, () -> encrypt(cert)); } private EncryptionResult encrypt(PGPPublicKeyRing certificate) throws PGPException, IOException { diff --git a/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestEncryptCommsStorageFlagsDifferentiated.java b/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestEncryptCommsStorageFlagsDifferentiated.java index 079f1062..b96d95e5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestEncryptCommsStorageFlagsDifferentiated.java +++ b/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestEncryptCommsStorageFlagsDifferentiated.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.exception.KeyException; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.rsa.RsaLength; @@ -38,7 +39,7 @@ public class TestEncryptCommsStorageFlagsDifferentiated { PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys); - assertThrows(IllegalArgumentException.class, () -> EncryptionOptions.encryptCommunications() + assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> EncryptionOptions.encryptCommunications() .addRecipient(publicKeys)); } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java index b3d18043..286c5262 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java @@ -22,6 +22,7 @@ import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.exception.KeyException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -63,7 +64,7 @@ public class SignImpl implements Sign { } signingOptions.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key, modeToSigType(mode)); } - } catch (PGPException e) { + } catch (PGPException | KeyException e) { throw new SOPGPException.BadData(e); } return this; From bb8ecaa1c13094797f00f8228f0cb3cb7436458e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Mar 2022 15:50:08 +0100 Subject: [PATCH 0168/1204] PGPainless-1.2.0-SNAPSHOT --- CHANGELOG.md | 5 +++++ version.gradle | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8134c0cc..e7b0c522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog + +## 1.2.0-SNAPSHOT +- Improve exception hierarchy for key-related exceptions + - See [PR](https://github.com/pgpainless/pgpainless/pull/261) for more information on how to migrate. + ## 1.1.5 - SOP encrypt: match signature type when using `encrypt --as=` option - `ProducerOptions.setEncoding()`: The encoding is henceforth only considered metadata and will no longer trigger CRLF encoding. diff --git a/version.gradle b/version.gradle index 9406a66b..2f1bd17d 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '1.1.6' + shortVersion = '1.2.0' isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 From 864bfad80ce0a2d6f6d45de95e8164516e902127 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Mar 2022 16:52:56 +0100 Subject: [PATCH 0169/1204] Add test for encryption / decryption, signing with missing secret subkey --- .../CertificateWithMissingSecretKeyTest.java | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java new file mode 100644 index 00000000..13359830 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.exception.KeyException; +import org.pgpainless.exception.MissingDecryptionMethodException; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.util.KeyRingUtils; + +public class CertificateWithMissingSecretKeyTest { + + private static final String MISSING_SIGNING_SECKEY = "" + + "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: E97B 15E6 52FA 8BAE 2311 DDCB A5BD 9DC4 4415 C987\n" + + "Comment: Missing Signing Subkey \n" + + "\n" + + "lFgEYjCuERYJKwYBBAHaRw8BAQdAaqeTdbyb/D+UXd2aXsP58+k+tvt22DnL6bC0\n" + + "7p2tJacAAP0fEmwUY7rSPugQakzsA8nV4Nv3PYlKa6meqEePT+8s8BFitC9NaXNz\n" + + "aW5nIFNpZ25pbmcgU3Via2V5IDxtaXNzaW5nQHNpZ25pbmcuc3Via2V5PoiPBBMW\n" + + "CgBBBQJiMK4RCRClvZ3ERBXJhxYhBOl7FeZS+ouuIxHdy6W9ncREFcmHAp4BApsB\n" + + "BRYCAwEABAsJCAcFFQoJCAsCmQEAAN0HAPkB7IphgTM94s/VpyV5+hvYbxesnji5\n" + + "RNzqs3nRhS8DBgEA/+gCpAkgznB3T/uNtWIoTf7Kuib5mIJ+SW0l+htuEgacXQRi\n" + + "MK4REgorBgEEAZdVAQUBAQdAlaQH44c7PdKkjaVVXvg86i+thKV121C/nH75Krhh\n" + + "QxYDAQgHAAD/aWJt9M85Al+57lPqS5ppzrIoCoTZ6JCwuJUSNEAg4BgQ6Ih1BBgW\n" + + "CgAdBQJiMK4RAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQpb2dxEQVyYdzuAD9\n" + + "GEkU7NfugHw8alQT7IJbUobVyZzeXQyzPqSKUw/Vp54BAJXZj8NzQrrM4Q5C3+Mf\n" + + "uznN+ryRovDXhf8T5PUXHloDuDMEYjCuERYJKwYBBAHaRw8BAQdAVeBpPurrwAU3\n" + + "ns+1C2c6wJ8iTZ1eWEP2qgBAlokx5N+I1QQYFgoAfQUCYjCuEQKeAQKbAgUWAgMB\n" + + "AAQLCQgHBRUKCQgLXyAEGRYKAAYFAmIwrhEACgkQld4KwYO6xR4YEwEA942iduoW\n" + + "1ANEmwCwnYwMAa3HlXsMs5bdIUGnxuo7MBEA/1YYeAu45O2Z8kTdrDZM/1emoxt1\n" + + "j6EzybnaJ+2XGX4AAAoJEKW9ncREFcmHLXsBAITCIwGtaCvZdWCQlJeYak1NTuBp\n" + + "bmOEFga0sLmRI/zYAP97U2oc8dqV55S1b4yNkfENK2MD6Ow0nv8CL6+S0UaCBw==\n" + + "=eTh7\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final long signingSubkeyId = -7647663290973502178L; + private static PGPSecretKeyRing missingSigningSecKey; + + private static long encryptionSubkeyId; + private static PGPSecretKeyRing missingDecryptionSecKey; + + private static final SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + + @BeforeAll + public static void prepare() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + // missing signing sec key we read from bytes + missingSigningSecKey = PGPainless.readKeyRing().secretKeyRing(MISSING_SIGNING_SECKEY); + + // missing encryption sec key we generate on the fly + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Missing Decryption Key ", null); + encryptionSubkeyId = PGPainless.inspectKeyRing(secretKeys) + .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID(); + // remove the encryption/decryption secret key + missingDecryptionSecKey = KeyRingUtils.removeSecretKey(secretKeys, encryptionSubkeyId); + } + + @Test + public void assureMissingSigningSecKeyOnlyContainSigningPubKey() { + assertNotNull(missingSigningSecKey.getPublicKey(signingSubkeyId)); + assertNull(missingSigningSecKey.getSecretKey(signingSubkeyId)); + + KeyRingInfo info = PGPainless.inspectKeyRing(missingSigningSecKey); + assertFalse(info.getSigningSubkeys().isEmpty()); // This method only tests for pub keys. + } + + @Test + public void assureMissingDecryptionSecKeyOnlyContainsEncryptionPubKey() { + assertNotNull(missingDecryptionSecKey.getPublicKey(encryptionSubkeyId)); + assertNull(missingDecryptionSecKey.getSecretKey(encryptionSubkeyId)); + + KeyRingInfo info = PGPainless.inspectKeyRing(missingDecryptionSecKey); + assertFalse(info.getEncryptionSubkeys(EncryptionPurpose.ANY).isEmpty()); // pub key is still there + } + + @Test + public void testSignWithMissingSigningSecKey() { + SigningOptions signingOptions = SigningOptions.get(); + + assertThrows(KeyException.MissingSecretKeyException.class, () -> + signingOptions.addInlineSignature(protector, missingSigningSecKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)); + assertThrows(KeyException.MissingSecretKeyException.class, () -> + signingOptions.addDetachedSignature(protector, missingSigningSecKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)); + } + + @Test + public void testEncryptDecryptWithMissingDecryptionKey() throws PGPException, IOException { + ByteArrayInputStream in = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + PGPPublicKeyRing certificate = PGPainless.extractCertificate(missingDecryptionSecKey); + ProducerOptions producerOptions = ProducerOptions.encrypt( + EncryptionOptions.encryptCommunications() + .addRecipient(certificate)); // we can still encrypt, since the pub key is still there + + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(producerOptions); + + Streams.pipeAll(in, encryptionStream); + encryptionStream.close(); + + assertTrue(encryptionStream.getResult().isEncryptedFor(certificate)); + + // Test decryption + ByteArrayInputStream cipherIn = new ByteArrayInputStream(out.toByteArray()); + + ConsumerOptions consumerOptions = new ConsumerOptions() + .addDecryptionKey(missingDecryptionSecKey); + + assertThrows(MissingDecryptionMethodException.class, () -> + PGPainless.decryptAndOrVerify() + .onInputStream(cipherIn) + .withOptions(consumerOptions)); // <- cannot find decryption key + } +} From 73fa46895e4ee8e524a5c43bf8106de1286603a9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Apr 2022 19:51:42 +0200 Subject: [PATCH 0170/1204] Implement merging of certificates Fixes #211 --- .../src/main/java/org/pgpainless/PGPainless.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 46dd35c2..827085ce 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.util.Date; import javax.annotation.Nonnull; +import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; @@ -66,6 +67,21 @@ public final class PGPainless { return KeyRingUtils.publicKeyRingFrom(secretKey); } + /** + * Merge two copies of the same certificate (e.g. an old copy, and one retrieved from a key server) together. + * + * @param originalCopy local, older copy of the cert + * @param updatedCopy updated, newer copy of the cert + * @return merged certificate + * @throws PGPException in case of an error + */ + public static PGPPublicKeyRing mergeCertificate( + @Nonnull PGPPublicKeyRing originalCopy, + @Nonnull PGPPublicKeyRing updatedCopy) + throws PGPException { + return PGPPublicKeyRing.join(originalCopy, updatedCopy); + } + /** * Wrap a key or certificate in ASCII armor. * From 361d2376f5273404ba595c5c7027835a3975446d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Apr 2022 20:21:07 +0200 Subject: [PATCH 0171/1204] Update documentation on curve oid workaround --- .../src/main/java/org/pgpainless/key/info/KeyInfo.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java index d1a5f748..f85c5c8a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java @@ -96,7 +96,9 @@ public class KeyInfo { // Workaround for ECUtil not recognizing ed25519 // see https://github.com/bcgit/bc-java/issues/1087 - // TODO: Remove once BC 1.71 gets released and contains a fix + // UPDATE: Apparently 1087 is not fixed properly with BC 1.71 + // See https://github.com/bcgit/bc-java/issues/1142 + // TODO: Remove when BC 1.72 comes out with a fix. if (identifier.equals(GNUObjectIdentifiers.Ed25519)) { return EdDSACurve._Ed25519.getName(); } From d0544e690ea71473a70235f930c8540fef4a97ff Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Apr 2022 20:24:36 +0200 Subject: [PATCH 0172/1204] Fix KeyRingUtils.keysPlusPublicKey() --- .../java/org/pgpainless/key/util/KeyRingUtils.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 375e3527..a7a2b0b9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -355,7 +355,6 @@ public final class KeyRingUtils { * Inject a {@link PGPPublicKey} into the given key ring. * * Note: Right now this method is broken and will throw a {@link NotYetImplementedException}. - * TODO: Fix with BC 171 * * @param keyRing key ring * @param publicKey public key @@ -365,10 +364,6 @@ public final class KeyRingUtils { @Nonnull public static T keysPlusPublicKey(@Nonnull T keyRing, @Nonnull PGPPublicKey publicKey) { - if (true) - // Is currently broken beyond repair - throw new NotYetImplementedException(); - PGPSecretKeyRing secretKeys = null; PGPPublicKeyRing publicKeys; if (keyRing instanceof PGPSecretKeyRing) { @@ -382,11 +377,7 @@ public final class KeyRingUtils { if (secretKeys == null) { return (T) publicKeys; } else { - // TODO: Replace with PGPSecretKeyRing.insertOrReplacePublicKey() once available - // Right now replacePublicKeys looses extra public keys. - // See https://github.com/bcgit/bc-java/pull/1068 for a possible fix - // Fix once BC 171 gets released. - secretKeys = PGPSecretKeyRing.replacePublicKeys(secretKeys, publicKeys); + secretKeys = PGPSecretKeyRing.insertOrReplacePublicKey(secretKeys, publicKey); return (T) secretKeys; } } From 5f65ca443764087f83fe3a2b2ac274eeea7c2699 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Apr 2022 20:28:45 +0200 Subject: [PATCH 0173/1204] Remove workaround for BC not properly parsing RevocationKey subpacket --- .../signature/subpackets/SignatureSubpacketsUtil.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index 5a839bcb..9ebc03e8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -596,15 +596,6 @@ public final class SignatureSubpacketsUtil { } org.bouncycastle.bcpg.SignatureSubpacket last = allPackets[allPackets.length - 1]; - - if (type == SignatureSubpacket.revocationKey) { - // RevocationKey subpackets are not castable for some reason - // See https://github.com/bcgit/bc-java/pull/1085 for an upstreamed fix - // We need to manually construct the new object for now. - // TODO: Remove workaround when BC 1.71 is released (and has our fix) - return (P) new RevocationKey(last.isCritical(), last.isLongLength(), last.getData()); - } - return (P) last; } From d4c56f655ff0a732217ef0644ebdd79a03ea2791 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Apr 2022 20:41:21 +0200 Subject: [PATCH 0174/1204] Add support for PolicyURI subpackets (fixes #248) --- .../subpackets/BaseSignatureSubpackets.java | 8 +++++++ .../subpackets/SignatureSubpackets.java | 23 +++++++++++++++++++ .../subpackets/SignatureSubpacketsHelper.java | 7 +++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java index 182917a0..c6d68c6a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java @@ -5,6 +5,7 @@ package org.pgpainless.signature.subpackets; import java.io.IOException; +import java.net.URL; import java.util.Date; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -15,6 +16,7 @@ import org.bouncycastle.bcpg.sig.IntendedRecipientFingerprint; import org.bouncycastle.bcpg.sig.IssuerFingerprint; import org.bouncycastle.bcpg.sig.IssuerKeyID; import org.bouncycastle.bcpg.sig.NotationData; +import org.bouncycastle.bcpg.sig.PolicyURI; import org.bouncycastle.bcpg.sig.Revocable; import org.bouncycastle.bcpg.sig.SignatureCreationTime; import org.bouncycastle.bcpg.sig.SignatureExpirationTime; @@ -88,6 +90,12 @@ public interface BaseSignatureSubpackets { BaseSignatureSubpackets setExportable(@Nullable Exportable exportable); + BaseSignatureSubpackets setPolicyUrl(@Nullable URL policyUrl); + + BaseSignatureSubpackets setPolicyUrl(boolean isCritical, @Nonnull URL policyUrl); + + BaseSignatureSubpackets setPolicyUrl(@Nullable PolicyURI policyUrl); + BaseSignatureSubpackets setRevocable(boolean revocable); BaseSignatureSubpackets setRevocable(boolean isCritical, boolean isRevocable); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java index 6c0740c7..742ab831 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java @@ -5,6 +5,7 @@ package org.pgpainless.signature.subpackets; import java.io.IOException; +import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; @@ -26,6 +27,7 @@ import org.bouncycastle.bcpg.sig.IssuerKeyID; import org.bouncycastle.bcpg.sig.KeyExpirationTime; import org.bouncycastle.bcpg.sig.KeyFlags; import org.bouncycastle.bcpg.sig.NotationData; +import org.bouncycastle.bcpg.sig.PolicyURI; import org.bouncycastle.bcpg.sig.PreferredAlgorithms; import org.bouncycastle.bcpg.sig.PrimaryUserID; import org.bouncycastle.bcpg.sig.Revocable; @@ -68,6 +70,7 @@ public class SignatureSubpackets private final List embeddedSignatureList = new ArrayList<>(); private SignerUserID signerUserId; private KeyExpirationTime keyExpirationTime; + private PolicyURI policyURI; private PrimaryUserID primaryUserId; private Revocable revocable; private RevocationReason revocationReason; @@ -485,6 +488,26 @@ public class SignatureSubpackets return exportable; } + @Override + public BaseSignatureSubpackets setPolicyUrl(@Nullable URL policyUrl) { + return policyUrl == null ? setPolicyUrl((PolicyURI) null) : setPolicyUrl(false, policyUrl); + } + + @Override + public BaseSignatureSubpackets setPolicyUrl(boolean isCritical, @Nonnull URL policyUrl) { + return setPolicyUrl(new PolicyURI(isCritical, policyUrl.toString())); + } + + @Override + public BaseSignatureSubpackets setPolicyUrl(@Nullable PolicyURI policyUrl) { + this.policyURI = policyUrl; + return this; + } + + public PolicyURI getPolicyURI() { + return policyURI; + } + @Override public SignatureSubpackets setRevocable(boolean revocable) { return setRevocable(true, revocable); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java index 2118c49c..4bea3036 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java @@ -12,6 +12,7 @@ import org.bouncycastle.bcpg.sig.IntendedRecipientFingerprint; import org.bouncycastle.bcpg.sig.KeyExpirationTime; import org.bouncycastle.bcpg.sig.KeyFlags; import org.bouncycastle.bcpg.sig.NotationData; +import org.bouncycastle.bcpg.sig.PolicyURI; import org.bouncycastle.bcpg.sig.PreferredAlgorithms; import org.bouncycastle.bcpg.sig.PrimaryUserID; import org.bouncycastle.bcpg.sig.Revocable; @@ -114,11 +115,14 @@ public class SignatureSubpacketsHelper { IntendedRecipientFingerprint intendedRecipientFingerprint = (IntendedRecipientFingerprint) subpacket; subpackets.addIntendedRecipientFingerprint(intendedRecipientFingerprint); break; + case policyUrl: + PolicyURI policyURI = (PolicyURI) subpacket; + subpackets.setPolicyUrl(policyURI); + break; case regularExpression: case keyServerPreferences: case preferredKeyServers: - case policyUrl: case placeholder: case preferredAEADAlgorithms: case attestedCertification: @@ -135,6 +139,7 @@ public class SignatureSubpacketsHelper { addSubpacket(generator, subpackets.getSignatureCreationTimeSubpacket()); addSubpacket(generator, subpackets.getSignatureExpirationTimeSubpacket()); addSubpacket(generator, subpackets.getExportableSubpacket()); + addSubpacket(generator, subpackets.getPolicyURI()); for (NotationData notationData : subpackets.getNotationDataSubpackets()) { addSubpacket(generator, notationData); } From 771084545417e839f5d2c492e7c4ee4b3f04839f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Apr 2022 20:46:21 +0200 Subject: [PATCH 0175/1204] Simplify setPolicyUrl implementation --- .../signature/subpackets/BaseSignatureSubpackets.java | 2 +- .../pgpainless/signature/subpackets/SignatureSubpackets.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java index c6d68c6a..7e44b748 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java @@ -90,7 +90,7 @@ public interface BaseSignatureSubpackets { BaseSignatureSubpackets setExportable(@Nullable Exportable exportable); - BaseSignatureSubpackets setPolicyUrl(@Nullable URL policyUrl); + BaseSignatureSubpackets setPolicyUrl(@Nonnull URL policyUrl); BaseSignatureSubpackets setPolicyUrl(boolean isCritical, @Nonnull URL policyUrl); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java index 742ab831..914ff2ff 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java @@ -489,8 +489,8 @@ public class SignatureSubpackets } @Override - public BaseSignatureSubpackets setPolicyUrl(@Nullable URL policyUrl) { - return policyUrl == null ? setPolicyUrl((PolicyURI) null) : setPolicyUrl(false, policyUrl); + public BaseSignatureSubpackets setPolicyUrl(@Nonnull URL policyUrl) { + return setPolicyUrl(false, policyUrl); } @Override From e4bccaf58d70476100f163aaf4b5f437edad4516 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Apr 2022 20:47:47 +0200 Subject: [PATCH 0176/1204] Add support for RegularExpression subpackets (fixes #246) --- .../subpackets/BaseSignatureSubpackets.java | 7 ++++++ .../subpackets/SignatureSubpackets.java | 22 +++++++++++++++++++ .../subpackets/SignatureSubpacketsHelper.java | 7 +++++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java index 7e44b748..28e99b9e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java @@ -17,6 +17,7 @@ import org.bouncycastle.bcpg.sig.IssuerFingerprint; import org.bouncycastle.bcpg.sig.IssuerKeyID; import org.bouncycastle.bcpg.sig.NotationData; import org.bouncycastle.bcpg.sig.PolicyURI; +import org.bouncycastle.bcpg.sig.RegularExpression; import org.bouncycastle.bcpg.sig.Revocable; import org.bouncycastle.bcpg.sig.SignatureCreationTime; import org.bouncycastle.bcpg.sig.SignatureExpirationTime; @@ -96,6 +97,12 @@ public interface BaseSignatureSubpackets { BaseSignatureSubpackets setPolicyUrl(@Nullable PolicyURI policyUrl); + BaseSignatureSubpackets setRegularExpression(@Nonnull String regex); + + BaseSignatureSubpackets setRegularExpression(boolean isCritical, @Nonnull String regex); + + BaseSignatureSubpackets setRegularExpression(@Nullable RegularExpression regex); + BaseSignatureSubpackets setRevocable(boolean revocable); BaseSignatureSubpackets setRevocable(boolean isCritical, boolean isRevocable); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java index 914ff2ff..c622844f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java @@ -30,6 +30,7 @@ import org.bouncycastle.bcpg.sig.NotationData; import org.bouncycastle.bcpg.sig.PolicyURI; import org.bouncycastle.bcpg.sig.PreferredAlgorithms; import org.bouncycastle.bcpg.sig.PrimaryUserID; +import org.bouncycastle.bcpg.sig.RegularExpression; import org.bouncycastle.bcpg.sig.Revocable; import org.bouncycastle.bcpg.sig.RevocationKey; import org.bouncycastle.bcpg.sig.RevocationReason; @@ -72,6 +73,7 @@ public class SignatureSubpackets private KeyExpirationTime keyExpirationTime; private PolicyURI policyURI; private PrimaryUserID primaryUserId; + private RegularExpression regularExpression; private Revocable revocable; private RevocationReason revocationReason; private final List residualSubpackets = new ArrayList<>(); @@ -508,6 +510,26 @@ public class SignatureSubpackets return policyURI; } + @Override + public BaseSignatureSubpackets setRegularExpression(@Nonnull String regex) { + return setRegularExpression(false, regex); + } + + @Override + public BaseSignatureSubpackets setRegularExpression(boolean isCritical, @Nonnull String regex) { + return setRegularExpression(new RegularExpression(isCritical, regex)); + } + + @Override + public BaseSignatureSubpackets setRegularExpression(@Nullable RegularExpression regex) { + this.regularExpression = regex; + return this; + } + + public RegularExpression getRegularExpression() { + return regularExpression; + } + @Override public SignatureSubpackets setRevocable(boolean revocable) { return setRevocable(true, revocable); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java index 4bea3036..2def2af6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java @@ -15,6 +15,7 @@ import org.bouncycastle.bcpg.sig.NotationData; import org.bouncycastle.bcpg.sig.PolicyURI; import org.bouncycastle.bcpg.sig.PreferredAlgorithms; import org.bouncycastle.bcpg.sig.PrimaryUserID; +import org.bouncycastle.bcpg.sig.RegularExpression; import org.bouncycastle.bcpg.sig.Revocable; import org.bouncycastle.bcpg.sig.RevocationKey; import org.bouncycastle.bcpg.sig.RevocationReason; @@ -119,8 +120,11 @@ public class SignatureSubpacketsHelper { PolicyURI policyURI = (PolicyURI) subpacket; subpackets.setPolicyUrl(policyURI); break; - case regularExpression: + RegularExpression regex = (RegularExpression) subpacket; + subpackets.setRegularExpression(regex); + break; + case keyServerPreferences: case preferredKeyServers: case placeholder: @@ -140,6 +144,7 @@ public class SignatureSubpacketsHelper { addSubpacket(generator, subpackets.getSignatureExpirationTimeSubpacket()); addSubpacket(generator, subpackets.getExportableSubpacket()); addSubpacket(generator, subpackets.getPolicyURI()); + addSubpacket(generator, subpackets.getRegularExpression()); for (NotationData notationData : subpackets.getNotationDataSubpackets()) { addSubpacket(generator, notationData); } From 9a012b5bab30f523dcb35f1669620301d58c0ce4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Apr 2022 21:15:43 +0200 Subject: [PATCH 0177/1204] Update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7b0c522..7778d873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ SPDX-License-Identifier: CC0-1.0 ## 1.2.0-SNAPSHOT - Improve exception hierarchy for key-related exceptions - See [PR](https://github.com/pgpainless/pgpainless/pull/261) for more information on how to migrate. +- Bump Bouncy Castle to `1.71` + - Switch from `bcpg-jdk15on:1.70` to `bcpg-jdk15to18:1.71` + - Switch from `bcprov-jdk15on:1.70` to `bcprov-jdk15to18:1.71` +- Implement merging of certificate copies + - can be used to implement updating certificates from key servers +- Fix `KeyRingUtils.keysPlusPublicKey()` +- Add support for adding `PolicyURI` and `RegularExpression` signature subpackets on signatures ## 1.1.5 - SOP encrypt: match signature type when using `encrypt --as=` option From 05022fcbb5f7b39ace18b78bbcf5a4948e0663b9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Apr 2022 21:17:00 +0200 Subject: [PATCH 0178/1204] Fix whitespace error --- .../signature/subpackets/SignatureSubpacketsHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java index 2def2af6..8af60a03 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java @@ -124,7 +124,7 @@ public class SignatureSubpacketsHelper { RegularExpression regex = (RegularExpression) subpacket; subpackets.setRegularExpression(regex); break; - + case keyServerPreferences: case preferredKeyServers: case placeholder: From 9f50946dd7c195df1870fd1a1ffe47302c23974b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Apr 2022 21:20:46 +0200 Subject: [PATCH 0179/1204] PGPainless 1.2.0 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7778d873..25ab7ef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.2.0-SNAPSHOT +## 1.2.0 - Improve exception hierarchy for key-related exceptions - See [PR](https://github.com/pgpainless/pgpainless/pull/261) for more information on how to migrate. - Bump Bouncy Castle to `1.71` diff --git a/README.md b/README.md index 58e59401..503475d9 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.1.5' + implementation 'org.pgpainless:pgpainless-core:1.2.0' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index bb4af342..a656eb4c 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.1.5" + implementation "org.pgpainless:pgpainless-sop:1.2.0" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.1.5 + 1.2.0 ... diff --git a/version.gradle b/version.gradle index 2f1bd17d..38f38e95 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.2.0' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From 5f9ad3396acbf9e8e86463315479769b6916a75f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Apr 2022 21:22:53 +0200 Subject: [PATCH 0180/1204] PGPainless 1.2.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 38f38e95..cf6c38c4 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.2.0' - isSnapshot = false + shortVersion = '1.2.1' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From 9558deab741aa4e24860a8c397f39651ddfde6ca Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 11 Apr 2022 12:11:26 +0200 Subject: [PATCH 0181/1204] Set mainClass name in application section --- pgpainless-cli/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle index 869eedf7..7317524b 100644 --- a/pgpainless-cli/build.gradle +++ b/pgpainless-cli/build.gradle @@ -48,6 +48,10 @@ dependencies { mainClassName = 'org.pgpainless.cli.PGPainlessCLI' +application { + mainClass = mainClassName +} + jar { duplicatesStrategy(DuplicatesStrategy.EXCLUDE) manifest { From 5307402edb112b831b7ff0e6a68730a4d372059a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 11 Apr 2022 14:15:29 +0200 Subject: [PATCH 0182/1204] Bump sop-java to 1.2.2 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index cf6c38c4..c556858b 100644 --- a/version.gradle +++ b/version.gradle @@ -12,6 +12,6 @@ allprojects { slf4jVersion = '1.7.32' logbackVersion = '1.2.9' junitVersion = '5.8.2' - sopJavaVersion = '1.2.0' + sopJavaVersion = '1.2.2' } } From 218d7becae63e4c1d739e8bbc46b245eb09842ce Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 11 Apr 2022 14:15:29 +0200 Subject: [PATCH 0183/1204] Bump sop-java to 1.2.2 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index cf6c38c4..c556858b 100644 --- a/version.gradle +++ b/version.gradle @@ -12,6 +12,6 @@ allprojects { slf4jVersion = '1.7.32' logbackVersion = '1.2.9' junitVersion = '5.8.2' - sopJavaVersion = '1.2.0' + sopJavaVersion = '1.2.2' } } From b64d6e8e55243737bcb0ebef5e2eceea40e2b670 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 16 Apr 2022 00:22:41 +0200 Subject: [PATCH 0184/1204] Stabilize HashAlgorithm.fromName() --- .../main/java/org/pgpainless/algorithm/HashAlgorithm.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java index bcd69cc0..12feb678 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java @@ -80,7 +80,12 @@ public enum HashAlgorithm { */ @Nullable public static HashAlgorithm fromName(String name) { - return NAME_MAP.get(name); + String algorithmName = name.toUpperCase(); + HashAlgorithm algorithm = NAME_MAP.get(algorithmName); + if (algorithm == null) { + algorithm = NAME_MAP.get(algorithmName.replace("-", "")); + } + return algorithm; } private final int algorithmId; From c3dfb254b1cee1be1a15725e3b37b07809c67230 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 16 Apr 2022 00:23:20 +0200 Subject: [PATCH 0185/1204] Experimental implementation of signing of existing hash contexts (MessageDigest instances) --- .../HashContextPGPContentSignerBuilder.java | 237 ++++++++++++++++++ .../signature/builder/HashContextSigner.java | 38 +++ .../signature/HashContextSignerTest.java | 125 +++++++++ 3 files changed, 400 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextPGPContentSignerBuilder.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/signature/HashContextSignerTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextPGPContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextPGPContentSignerBuilder.java new file mode 100644 index 00000000..68c24743 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextPGPContentSignerBuilder.java @@ -0,0 +1,237 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.MessageDigest; + +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.CryptoException; +import org.bouncycastle.crypto.DataLengthException; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.Signer; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters; +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; +import org.bouncycastle.crypto.signers.DSADigestSigner; +import org.bouncycastle.crypto.signers.DSASigner; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.Ed25519Signer; +import org.bouncycastle.crypto.signers.Ed448Signer; +import org.bouncycastle.crypto.signers.RSADigestSigner; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.operator.PGPContentSigner; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter; +import org.bouncycastle.util.Arrays; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; + +/** + * Implementation of {@link PGPContentSignerBuilder} using the BC API, which can be used to sign hash contexts. + * This can come in handy to sign data, which was already processed to calculate the hash context, without the + * need to process it again to calculate the OpenPGP signature. + */ +public class HashContextPGPContentSignerBuilder implements PGPContentSignerBuilder { + + private final BcPGPKeyConverter keyConverter = new BcPGPKeyConverter(); + private final MessageDigest messageDigest; + private final HashAlgorithm hashAlgorithm; + + public HashContextPGPContentSignerBuilder(MessageDigest messageDigest) { + this.messageDigest = messageDigest; + this.hashAlgorithm = HashAlgorithm.fromName(messageDigest.getAlgorithm()); + if (hashAlgorithm == null) { + throw new IllegalArgumentException("Cannot recognize OpenPGP Hash Algorithm: " + messageDigest.getAlgorithm()); + } + } + + @Override + public PGPContentSigner build(int signatureType, PGPPrivateKey privateKey) throws PGPException { + PublicKeyAlgorithm keyAlgorithm = PublicKeyAlgorithm.requireFromId(privateKey.getPublicKeyPacket().getAlgorithm()); + AsymmetricKeyParameter privKeyParam = keyConverter.getPrivateKey(privateKey); + final Signer signer = createSigner(keyAlgorithm, messageDigest, privKeyParam); + signer.init(true, privKeyParam); + + return new PGPContentSigner() { + public int getType() { + return signatureType; + } + + public int getHashAlgorithm() { + return hashAlgorithm.getAlgorithmId(); + } + + public int getKeyAlgorithm() { + return keyAlgorithm.getAlgorithmId(); + } + + public long getKeyID() { + return privateKey.getKeyID(); + } + + public OutputStream getOutputStream() { + return new SignerOutputStream(signer); + } + + public byte[] getSignature() { + try { + return signer.generateSignature(); + } catch (CryptoException e) { + throw new IllegalStateException("unable to create signature"); + } + } + + public byte[] getDigest() { + return messageDigest.digest(); + } + }; + } + + static Signer createSigner( + PublicKeyAlgorithm keyAlgorithm, + MessageDigest messageDigest, + CipherParameters keyParam) + throws PGPException { + ExistingMessageDigest staticDigest = new ExistingMessageDigest(messageDigest); + switch (keyAlgorithm.getAlgorithmId()) { + case PublicKeyAlgorithmTags.RSA_GENERAL: + case PublicKeyAlgorithmTags.RSA_SIGN: + return new RSADigestSigner(staticDigest); + case PublicKeyAlgorithmTags.DSA: + return new DSADigestSigner(new DSASigner(), staticDigest); + case PublicKeyAlgorithmTags.ECDSA: + return new DSADigestSigner(new ECDSASigner(), staticDigest); + case PublicKeyAlgorithmTags.EDDSA: + if (keyParam instanceof Ed25519PrivateKeyParameters || keyParam instanceof Ed25519PublicKeyParameters) { + return new EdDsaSigner(new Ed25519Signer(), staticDigest); + } + return new EdDsaSigner(new Ed448Signer(new byte[0]), staticDigest); + default: + throw new PGPException("cannot recognise keyAlgorithm: " + keyAlgorithm); + } + } + + static class ExistingMessageDigest implements Digest { + + private final MessageDigest digest; + + ExistingMessageDigest(MessageDigest messageDigest) { + this.digest = messageDigest; + } + + @Override + public void update(byte in) { + digest.update(in); + } + + @Override + public void update(byte[] in, int inOff, int len) { + digest.update(in, inOff, len); + } + + @Override + public int doFinal(byte[] out, int outOff) { + byte[] hash = digest.digest(); + System.arraycopy(hash, 0, out, outOff, hash.length); + return getDigestSize(); + } + + @Override + public void reset() { + // Nope! + // We cannot reset, since BCs signer classes are resetting in their init() methods, which would also reset + // the messageDigest, losing its state. This would shatter our intention. + } + + @Override + public String getAlgorithmName() { + return digest.getAlgorithm(); + } + + @Override + public int getDigestSize() { + return digest.getDigestLength(); + } + } + + // Copied from BCs BcImplProvider - required since BCs class is package visible only :/ + private static class EdDsaSigner + implements Signer { + private final Signer signer; + private final Digest digest; + private final byte[] digBuf; + + EdDsaSigner(Signer signer, Digest digest) { + this.signer = signer; + this.digest = digest; + this.digBuf = new byte[digest.getDigestSize()]; + } + + public void init(boolean forSigning, CipherParameters param) { + this.signer.init(forSigning, param); + this.digest.reset(); + } + + public void update(byte b) { + this.digest.update(b); + } + + public void update(byte[] in, int off, int len) { + this.digest.update(in, off, len); + } + + public byte[] generateSignature() + throws CryptoException, DataLengthException { + digest.doFinal(digBuf, 0); + + signer.update(digBuf, 0, digBuf.length); + + return signer.generateSignature(); + } + + public boolean verifySignature(byte[] signature) { + digest.doFinal(digBuf, 0); + + signer.update(digBuf, 0, digBuf.length); + + return signer.verifySignature(signature); + } + + public void reset() { + Arrays.clear(digBuf); + signer.reset(); + digest.reset(); + } + } + + // Copied from BC, required since BCs class is package visible only + static class SignerOutputStream + extends OutputStream { + private Signer sig; + + SignerOutputStream(Signer sig) { + this.sig = sig; + } + + public void write(byte[] bytes, int off, int len) + throws IOException { + sig.update(bytes, off, len); + } + + public void write(byte[] bytes) + throws IOException { + sig.update(bytes, 0, bytes.length); + } + + public void write(int b) + throws IOException { + sig.update((byte) b); + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java new file mode 100644 index 00000000..57d687cc --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +import java.security.MessageDigest; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.pgpainless.algorithm.SignatureType; + +public class HashContextSigner { + + /** + * Create an OpenPGP Signature over the given {@link MessageDigest} hash context. + * + * WARNING: This method does not yet validate the signing key. + * TODO: Change API to receive and evaluate PGPSecretKeyRing + SecretKeyRingProtector instead. + * + * @param hashContext hash context + * @param privateKey signing-capable key + * @return signature + * @throws PGPException in case of an OpenPGP error + */ + public static PGPSignature signHashContext(MessageDigest hashContext, SignatureType signatureType, PGPPrivateKey privateKey) + throws PGPException { + // TODO: Validate signing key + PGPSignatureGenerator sigGen = new PGPSignatureGenerator( + new HashContextPGPContentSignerBuilder(hashContext) + ); + + sigGen.init(signatureType.getCode(), privateKey); + return sigGen.generate(); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/HashContextSignerTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/HashContextSignerTest.java new file mode 100644 index 00000000..baa5a73f --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/HashContextSignerTest.java @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.key.generation.type.rsa.RsaLength; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.signature.builder.HashContextSigner; +import org.pgpainless.util.Passphrase; + +public class HashContextSignerTest { + + private static final String message = "Hello, World!\n"; + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 62D5 CBED 8BD0 7D3F D167 240D 4364 E4C1 C4ED 8F59\n" + + "Comment: Sigfried \n" + + "\n" + + "lFgEYlnOkRYJKwYBBAHaRw8BAQdA7Kxn/sPIXo44xnxLBL81G5ghGzMikFc5ib9/\n" + + "qgJpZSUAAQCZnJN2l/cfWWh4DijBAwFWoVJOCphKhsJEjKxOzWdqMA2DtBVTaWdm\n" + + "cmllZCA8c2lnQGZyaS5lZD6IjwQTFgoAQQUCYlnOkQkQQ2TkwcTtj1kWIQRi1cvt\n" + + "i9B9P9FnJA1DZOTBxO2PWQKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAAd/gEA\n" + + "kiPFDdMGjZV/7Do/3ox46iCH3N1I3BGmA2Jt8PsYwe0BAKe5ahLzCNAXjBQU4iSD\n" + + "A4FGipNaG2ZWgAMkdwVjMLEAnF0EYlnOkRIKKwYBBAGXVQEFAQEHQI3n0cWbBh+7\n" + + "zeuBjMWevsyxLUCExKSj5fxCh/0GuJgAAwEIBwAA/16V22vjZfAvtnUrVtUZQoYZ\n" + + "E8h87Jzj/XxXFy63I6qoER2IdQQYFgoAHQUCYlnOkQKeAQKbDAUWAgMBAAQLCQgH\n" + + "BRUKCQgLAAoJEENk5MHE7Y9ZzhsA+gPb2FNutetjrYUSY7BEsk+PPkCXF9W6JZmb\n" + + "W/zyRxgpAP9zNzpWrO7kKQ0PwMMd3R1F4Rg6GH+vjXsIbd6jT25UBJxYBGJZzpEW\n" + + "CSsGAQQB2kcPAQEHQPOZhITstSj3cPfsTiBEPhtCrc184fkAjl4s+gSB9ttRAAD/\n" + + "RVpdc9BhJ/ZXtqQaCBL65h7Uym7i+HExQphHOiuB3iwQOIjVBBgWCgB9BQJiWc6R\n" + + "Ap4BApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUCYlnOkQAKCRDXXcvYX8Ym\n" + + "crh9AP99WWietGWYs2//FYi0bEAWp6D0HmHP42rvC3qsqyMa8wD8D1Pi2atKwQTP\n" + + "JAxQFa06cUIw2POE3llaB0MKQXdTVgQACgkQQ2TkwcTtj1mF+gD+OHo68KeGFUi0\n" + + "VcVV/dx/6ES9GAIf1TI6jEsaU8TPBcMBAOHG+5MMVvyNiVKLA0JgJPF3JXOOEU+5\n" + + "qiHwlVoGncUM\n" + + "=431t\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @Test + public void signContextWithEdDSAKeys() throws PGPException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + signWithKeys(secretKeys); + } + + @Test + public void signContextWithRSAKeys() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleRsaKeyRing("Sigfried", RsaLength._3072); + signWithKeys(secretKeys); + } + + @Test + public void signContextWithEcKeys() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("Sigfried"); + signWithKeys(secretKeys); + } + + private void signWithKeys(PGPSecretKeyRing secretKeys) throws PGPException, NoSuchAlgorithmException, IOException { + for (HashAlgorithm hashAlgorithm : new HashAlgorithm[] { + HashAlgorithm.SHA256, HashAlgorithm.SHA384, HashAlgorithm.SHA512 + }) { + signFromContext(secretKeys, hashAlgorithm); + } + } + + private void signFromContext(PGPSecretKeyRing secretKeys, HashAlgorithm hashAlgorithm) throws PGPException, NoSuchAlgorithmException, IOException { + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + long signingKeyId = PGPainless.inspectKeyRing(certificate).getSigningSubkeys().get(0).getKeyID(); + PGPPrivateKey signingKey = UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(signingKeyId), Passphrase.emptyPassphrase()); + + byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); + ByteArrayInputStream messageIn = new ByteArrayInputStream(messageBytes); + + PGPSignature signature = signMessage(messageBytes, hashAlgorithm, signingKey); + assertEquals(hashAlgorithm.getAlgorithmId(), signature.getHashAlgorithm()); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(messageIn) + .withOptions(new ConsumerOptions() + .addVerificationCert(certificate) + .addVerificationOfDetachedSignature(signature)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + OpenPgpMetadata metadata = decryptionStream.getResult(); + assertTrue(metadata.isVerified()); + } + + private PGPSignature signMessage(byte[] message, HashAlgorithm hashAlgorithm, PGPPrivateKey signingKey) + throws NoSuchAlgorithmException, PGPException { + // Prepare the hash context + // This would be done by the caller application + MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm.getAlgorithmName(), new BouncyCastleProvider()); + messageDigest.update(message); + + return HashContextSigner.signHashContext(messageDigest, SignatureType.BINARY_DOCUMENT, signingKey); + } +} From 73b7f1b9bb0e2d8adf2dae79c95051eecfe22efa Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 19 Apr 2022 21:07:46 +0200 Subject: [PATCH 0186/1204] Refactoring --- .../BcHashContextSigner.java | 66 ++++++++++++++ ...BcPGPHashContextContentSignerBuilder.java} | 76 +--------------- .../PGPHashContextContentSignerBuilder.java | 86 +++++++++++++++++++ .../signature/builder/HashContextSigner.java | 38 -------- .../BcHashContextSignerTest.java} | 20 ++--- 5 files changed, 164 insertions(+), 122 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java rename pgpainless-core/src/main/java/org/pgpainless/{signature/builder/HashContextPGPContentSignerBuilder.java => encryption_signing/BcPGPHashContextContentSignerBuilder.java} (74%) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java rename pgpainless-core/src/test/java/org/pgpainless/{signature/HashContextSignerTest.java => encryption_signing/BcHashContextSignerTest.java} (87%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java new file mode 100644 index 00000000..54c6df9e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import java.security.MessageDigest; +import java.util.List; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; + +import javax.annotation.Nonnull; + +public class BcHashContextSigner { + + public static PGPSignature signHashContext(@Nonnull MessageDigest hashContext, + @Nonnull SignatureType signatureType, + @Nonnull PGPSecretKeyRing secretKeys, + @Nonnull SecretKeyRingProtector protector) + throws PGPException { + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + List signingSubkeyCandidates = info.getSigningSubkeys(); + PGPSecretKey signingKey = null; + for (PGPPublicKey signingKeyCandidate : signingSubkeyCandidates) { + signingKey = secretKeys.getSecretKey(signingKeyCandidate.getKeyID()); + if (signingKey != null) { + break; + } + } + if (signingKey == null) { + throw new PGPException("Key does not contain suitable signing subkey."); + } + + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(signingKey, protector); + return signHashContext(hashContext, signatureType, privateKey); + } + + /** + * Create an OpenPGP Signature over the given {@link MessageDigest} hash context. + * + * @param hashContext hash context + * @param privateKey signing-capable key + * @return signature + * @throws PGPException in case of an OpenPGP error + */ + static PGPSignature signHashContext(MessageDigest hashContext, SignatureType signatureType, PGPPrivateKey privateKey) + throws PGPException { + PGPSignatureGenerator sigGen = new PGPSignatureGenerator( + new BcPGPHashContextContentSignerBuilder(hashContext) + ); + + sigGen.init(signatureType.getCode(), privateKey); + return sigGen.generate(); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextPGPContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java similarity index 74% rename from pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextPGPContentSignerBuilder.java rename to pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java index 68c24743..474ee9ef 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextPGPContentSignerBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java @@ -2,9 +2,8 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.signature.builder; +package org.pgpainless.encryption_signing; -import java.io.IOException; import java.io.OutputStream; import java.security.MessageDigest; @@ -37,13 +36,13 @@ import org.pgpainless.algorithm.PublicKeyAlgorithm; * This can come in handy to sign data, which was already processed to calculate the hash context, without the * need to process it again to calculate the OpenPGP signature. */ -public class HashContextPGPContentSignerBuilder implements PGPContentSignerBuilder { +class BcPGPHashContextContentSignerBuilder extends PGPHashContextContentSignerBuilder { private final BcPGPKeyConverter keyConverter = new BcPGPKeyConverter(); private final MessageDigest messageDigest; private final HashAlgorithm hashAlgorithm; - public HashContextPGPContentSignerBuilder(MessageDigest messageDigest) { + public BcPGPHashContextContentSignerBuilder(MessageDigest messageDigest) { this.messageDigest = messageDigest; this.hashAlgorithm = HashAlgorithm.fromName(messageDigest.getAlgorithm()); if (hashAlgorithm == null) { @@ -76,7 +75,7 @@ public class HashContextPGPContentSignerBuilder implements PGPContentSignerBuild } public OutputStream getOutputStream() { - return new SignerOutputStream(signer); + return new PGPHashContextContentSignerBuilder.SignerOutputStream(signer); } public byte[] getSignature() { @@ -117,49 +116,6 @@ public class HashContextPGPContentSignerBuilder implements PGPContentSignerBuild } } - static class ExistingMessageDigest implements Digest { - - private final MessageDigest digest; - - ExistingMessageDigest(MessageDigest messageDigest) { - this.digest = messageDigest; - } - - @Override - public void update(byte in) { - digest.update(in); - } - - @Override - public void update(byte[] in, int inOff, int len) { - digest.update(in, inOff, len); - } - - @Override - public int doFinal(byte[] out, int outOff) { - byte[] hash = digest.digest(); - System.arraycopy(hash, 0, out, outOff, hash.length); - return getDigestSize(); - } - - @Override - public void reset() { - // Nope! - // We cannot reset, since BCs signer classes are resetting in their init() methods, which would also reset - // the messageDigest, losing its state. This would shatter our intention. - } - - @Override - public String getAlgorithmName() { - return digest.getAlgorithm(); - } - - @Override - public int getDigestSize() { - return digest.getDigestLength(); - } - } - // Copied from BCs BcImplProvider - required since BCs class is package visible only :/ private static class EdDsaSigner implements Signer { @@ -210,28 +166,4 @@ public class HashContextPGPContentSignerBuilder implements PGPContentSignerBuild } } - // Copied from BC, required since BCs class is package visible only - static class SignerOutputStream - extends OutputStream { - private Signer sig; - - SignerOutputStream(Signer sig) { - this.sig = sig; - } - - public void write(byte[] bytes, int off, int len) - throws IOException { - sig.update(bytes, off, len); - } - - public void write(byte[] bytes) - throws IOException { - sig.update(bytes, 0, bytes.length); - } - - public void write(int b) - throws IOException { - sig.update((byte) b); - } - } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java new file mode 100644 index 00000000..2ea36206 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.MessageDigest; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.Signer; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; + +abstract class PGPHashContextContentSignerBuilder implements PGPContentSignerBuilder { + + // Copied from BC, required since BCs class is package visible only + static class SignerOutputStream + extends OutputStream { + private Signer sig; + + SignerOutputStream(Signer sig) { + this.sig = sig; + } + + public void write(byte[] bytes, int off, int len) + throws IOException { + sig.update(bytes, off, len); + } + + public void write(byte[] bytes) + throws IOException { + sig.update(bytes, 0, bytes.length); + } + + public void write(int b) + throws IOException { + sig.update((byte) b); + } + } + + + static class ExistingMessageDigest implements Digest { + + private final MessageDigest digest; + + ExistingMessageDigest(MessageDigest messageDigest) { + this.digest = messageDigest; + } + + @Override + public void update(byte in) { + digest.update(in); + } + + @Override + public void update(byte[] in, int inOff, int len) { + digest.update(in, inOff, len); + } + + @Override + public int doFinal(byte[] out, int outOff) { + byte[] hash = digest.digest(); + System.arraycopy(hash, 0, out, outOff, hash.length); + return getDigestSize(); + } + + @Override + public void reset() { + // Nope! + // We cannot reset, since BCs signer classes are resetting in their init() methods, which would also reset + // the messageDigest, losing its state. This would shatter our intention. + } + + @Override + public String getAlgorithmName() { + return digest.getAlgorithm(); + } + + @Override + public int getDigestSize() { + return digest.getDigestLength(); + } + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java deleted file mode 100644 index 57d687cc..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/HashContextSigner.java +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import java.security.MessageDigest; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.pgpainless.algorithm.SignatureType; - -public class HashContextSigner { - - /** - * Create an OpenPGP Signature over the given {@link MessageDigest} hash context. - * - * WARNING: This method does not yet validate the signing key. - * TODO: Change API to receive and evaluate PGPSecretKeyRing + SecretKeyRingProtector instead. - * - * @param hashContext hash context - * @param privateKey signing-capable key - * @return signature - * @throws PGPException in case of an OpenPGP error - */ - public static PGPSignature signHashContext(MessageDigest hashContext, SignatureType signatureType, PGPPrivateKey privateKey) - throws PGPException { - // TODO: Validate signing key - PGPSignatureGenerator sigGen = new PGPSignatureGenerator( - new HashContextPGPContentSignerBuilder(hashContext) - ); - - sigGen.init(signatureType.getCode(), privateKey); - return sigGen.generate(); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/HashContextSignerTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java similarity index 87% rename from pgpainless-core/src/test/java/org/pgpainless/signature/HashContextSignerTest.java rename to pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java index baa5a73f..50b7cbf7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/HashContextSignerTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.signature; +package org.pgpainless.encryption_signing; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -17,7 +17,6 @@ import java.security.NoSuchAlgorithmException; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; @@ -30,11 +29,9 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.signature.builder.HashContextSigner; -import org.pgpainless.util.Passphrase; +import org.pgpainless.key.protection.SecretKeyRingProtector; -public class HashContextSignerTest { +public class BcHashContextSignerTest { private static final String message = "Hello, World!\n"; private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -88,15 +85,14 @@ public class HashContextSignerTest { } } - private void signFromContext(PGPSecretKeyRing secretKeys, HashAlgorithm hashAlgorithm) throws PGPException, NoSuchAlgorithmException, IOException { + private void signFromContext(PGPSecretKeyRing secretKeys, HashAlgorithm hashAlgorithm) + throws PGPException, NoSuchAlgorithmException, IOException { PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); - long signingKeyId = PGPainless.inspectKeyRing(certificate).getSigningSubkeys().get(0).getKeyID(); - PGPPrivateKey signingKey = UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(signingKeyId), Passphrase.emptyPassphrase()); byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); ByteArrayInputStream messageIn = new ByteArrayInputStream(messageBytes); - PGPSignature signature = signMessage(messageBytes, hashAlgorithm, signingKey); + PGPSignature signature = signMessage(messageBytes, hashAlgorithm, secretKeys); assertEquals(hashAlgorithm.getAlgorithmId(), signature.getHashAlgorithm()); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() @@ -113,13 +109,13 @@ public class HashContextSignerTest { assertTrue(metadata.isVerified()); } - private PGPSignature signMessage(byte[] message, HashAlgorithm hashAlgorithm, PGPPrivateKey signingKey) + private PGPSignature signMessage(byte[] message, HashAlgorithm hashAlgorithm, PGPSecretKeyRing secretKeys) throws NoSuchAlgorithmException, PGPException { // Prepare the hash context // This would be done by the caller application MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm.getAlgorithmName(), new BouncyCastleProvider()); messageDigest.update(message); - return HashContextSigner.signHashContext(messageDigest, SignatureType.BINARY_DOCUMENT, signingKey); + return BcHashContextSigner.signHashContext(messageDigest, SignatureType.BINARY_DOCUMENT, secretKeys, SecretKeyRingProtector.unprotectedKeys()); } } From 46f69b9fa522a21a394a0bdcf4f2588567883c06 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 22 Apr 2022 20:53:44 +0200 Subject: [PATCH 0187/1204] Introduce OpenPgpInputStream to distinguish between armored, binary and non-OpenPGP data --- .../DecryptionStreamFactory.java | 80 +- .../OpenPgpInputStream.java | 144 ++++ .../java/org/pgpainless/util/ArmorUtils.java | 21 +- .../org/pgpainless/util/PGPUtilWrapper.java | 40 - .../org/bouncycastle/PGPUtilWrapperTest.java | 52 -- .../OpenPgpInputStreamTest.java | 704 ++++++++++++++++++ 6 files changed, 884 insertions(+), 157 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/PGPUtilWrapper.java delete mode 100644 pgpainless-core/src/test/java/org/bouncycastle/PGPUtilWrapperTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index d552ead8..7f8e7f0d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -4,8 +4,6 @@ package org.pgpainless.decryption_verification; -import java.io.BufferedInputStream; -import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -66,8 +64,6 @@ import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.consumer.DetachedSignatureCheck; import org.pgpainless.signature.consumer.OnePassSignatureCheck; import org.pgpainless.util.ArmoredInputStreamFactory; -import org.pgpainless.util.CRCingArmoredInputStreamWrapper; -import org.pgpainless.util.PGPUtilWrapper; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; import org.pgpainless.util.Tuple; @@ -81,9 +77,6 @@ public final class DecryptionStreamFactory { // Maximum nesting depth of packets (e.g. compression, encryption...) private static final int MAX_PACKET_NESTING_DEPTH = 16; - // Buffer Size for BufferedInputStreams - public static int BUFFER_SIZE = 4096; - private final ConsumerOptions options; private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); private final List onePassSignatureChecks = new ArrayList<>(); @@ -99,8 +92,8 @@ public final class DecryptionStreamFactory { @Nonnull ConsumerOptions options) throws PGPException, IOException { DecryptionStreamFactory factory = new DecryptionStreamFactory(options); - BufferedInputStream bufferedIn = new BufferedInputStream(inputStream, BUFFER_SIZE); - return factory.parseOpenPGPDataAndCreateDecryptionStream(bufferedIn); + OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); + return factory.parseOpenPGPDataAndCreateDecryptionStream(openPgpIn); } public DecryptionStreamFactory(ConsumerOptions options) { @@ -133,69 +126,52 @@ public final class DecryptionStreamFactory { } } - private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(BufferedInputStream bufferedIn) + private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(OpenPgpInputStream openPgpIn) throws IOException, PGPException { + InputStream pgpInStream; InputStream outerDecodingStream; PGPObjectFactory objectFactory; - try { - outerDecodingStream = PGPUtilWrapper.getDecoderStream(bufferedIn); - outerDecodingStream = CRCingArmoredInputStreamWrapper.possiblyWrap(outerDecodingStream); - - if (outerDecodingStream instanceof ArmoredInputStream) { - ArmoredInputStream armor = (ArmoredInputStream) outerDecodingStream; - - // Cleartext Signed Message - // Throw a WrongConsumingMethodException to delegate preparation (extraction of signatures) - // to the CleartextSignatureProcessor which will call us again (see comment above) - if (armor.isClearText()) { - bufferedIn.reset(); - return parseCleartextSignedMessage(bufferedIn); - } - } + // Non-OpenPGP data. We are probably verifying detached signatures + if (openPgpIn.isNonOpenPgp()) { + outerDecodingStream = openPgpIn; + pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null); + return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); + } + if (openPgpIn.isBinaryOpenPgp()) { + outerDecodingStream = openPgpIn; objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); // Parse OpenPGP message pgpInStream = processPGPPackets(objectFactory, 1); return new DecryptionStream(pgpInStream, - resultBuilder, integrityProtectedEncryptedInputStream, - (outerDecodingStream instanceof ArmoredInputStream) ? outerDecodingStream : null); - } catch (EOFException | FinalIOException e) { - // Broken message or invalid decryption session key - throw e; - } catch (MissingLiteralDataException e) { - // Not an OpenPGP message. - // Reset the buffered stream to parse the message as arbitrary binary data - // to allow for detached signature verification. - LOGGER.debug("The message appears to not be an OpenPGP message. This is probably data signed with detached signatures?"); - bufferedIn.reset(); - outerDecodingStream = bufferedIn; - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); - pgpInStream = wrapInVerifySignatureStream(bufferedIn, objectFactory); - } catch (IOException e) { - if (e.getMessage().contains("invalid armor") || e.getMessage().contains("invalid header encountered")) { - // We falsely assumed the data to be armored. - LOGGER.debug("The message is apparently not armored."); - bufferedIn.reset(); - outerDecodingStream = CRCingArmoredInputStreamWrapper.possiblyWrap(bufferedIn); - pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null); + resultBuilder, integrityProtectedEncryptedInputStream, null); + } + + if (openPgpIn.isAsciiArmored()) { + ArmoredInputStream armoredInputStream = ArmoredInputStreamFactory.get(openPgpIn); + if (armoredInputStream.isClearText()) { + return parseCleartextSignedMessage(armoredInputStream); } else { - throw new FinalIOException(e); + outerDecodingStream = armoredInputStream; + objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); + // Parse OpenPGP message + pgpInStream = processPGPPackets(objectFactory, 1); + return new DecryptionStream(pgpInStream, + resultBuilder, integrityProtectedEncryptedInputStream, + outerDecodingStream); } } - return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, - (outerDecodingStream instanceof ArmoredInputStream) ? outerDecodingStream : null); + throw new PGPException("Not sure how to handle the input stream."); } - private DecryptionStream parseCleartextSignedMessage(BufferedInputStream in) + private DecryptionStream parseCleartextSignedMessage(ArmoredInputStream armorIn) throws IOException, PGPException { resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) .setFileEncoding(StreamEncoding.TEXT); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(in); - MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream()); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java new file mode 100644 index 00000000..c3668daa --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.pgpainless.implementation.ImplementationFactory; + +public class OpenPgpInputStream extends BufferedInputStream { + + private static final byte[] ARMOR_HEADER = "-----BEGIN PGP ".getBytes(Charset.forName("UTF8")); + + // Buffer beginning bytes of the data + public static final int MAX_BUFFER_SIZE = 8192; + + private final byte[] buffer; + private final int bufferLen; + + private boolean containsArmorHeader; + private boolean containsOpenPgpPackets; + + public OpenPgpInputStream(InputStream in) throws IOException { + super(in, MAX_BUFFER_SIZE); + + mark(MAX_BUFFER_SIZE); + buffer = new byte[MAX_BUFFER_SIZE]; + bufferLen = read(buffer); + reset(); + + inspectBuffer(); + } + + private void inspectBuffer() { + if (determineIsArmored()) { + return; + } + + determineIsBinaryOpenPgp(); + } + + private boolean determineIsArmored() { + if (startsWithIgnoringWhitespace(buffer, ARMOR_HEADER, bufferLen)) { + containsArmorHeader = true; + return true; + } + return false; + } + + private void determineIsBinaryOpenPgp() { + if (bufferLen == -1) { + // Empty data + return; + } + + try { + ByteArrayInputStream bufferIn = new ByteArrayInputStream(buffer, 0, bufferLen); + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(bufferIn); + while (objectFactory.nextObject() != null) { + // read all packets in buffer + } + containsOpenPgpPackets = true; + } catch (IOException e) { + if (e.getMessage().contains("premature end of stream in PartialInputStream")) { + // We *probably* hit valid, but large OpenPGP data + // This is not an optimal way of determining the nature of data, but probably the best + // we can get from BC. + containsOpenPgpPackets = true; + } + // else: seemingly random, non-OpenPGP data + } + } + + private boolean startsWith(byte[] bytes, byte[] subsequence, int bufferLen) { + return indexOfSubsequence(bytes, subsequence, bufferLen) == 0; + } + + private int indexOfSubsequence(byte[] bytes, byte[] subsequence, int bufferLen) { + if (bufferLen == -1) { + return -1; + } + // Naive implementation + // TODO: Could be improved by using e.g. Knuth-Morris-Pratt algorithm. + for (int i = 0; i < bufferLen; i++) { + if ((i + subsequence.length) <= bytes.length) { + boolean found = true; + for (int j = 0; j < subsequence.length; j++) { + if (bytes[i + j] != subsequence[j]) { + found = false; + break; + } + } + + if (found) { + return i; + } + } + } + return -1; + } + + private boolean startsWithIgnoringWhitespace(byte[] bytes, byte[] subsequence, int bufferLen) { + if (bufferLen == -1) { + return false; + } + + for (int i = 0; i < bufferLen; i++) { + // Working on bytes is not trivial with unicode data, but its good enough here + if (Character.isWhitespace(bytes[i])) { + continue; + } + + if ((i + subsequence.length) > bytes.length) { + return false; + } + + for (int j = 0; j < subsequence.length; j++) { + if (bytes[i + j] != subsequence[j]) { + return false; + } + } + return true; + } + return false; + } + + public boolean isAsciiArmored() { + return containsArmorHeader; + } + + public boolean isBinaryOpenPgp() { + return containsOpenPgpPackets; + } + + public boolean isNonOpenPgp() { + return !isAsciiArmored() && !isBinaryOpenPgp(); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index d95673a4..e862652d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -4,7 +4,6 @@ package org.pgpainless.util; -import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -14,6 +13,8 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.regex.Pattern; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.ArmoredOutputStream; @@ -29,11 +30,9 @@ import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.bouncycastle.util.io.Streams; import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.decryption_verification.OpenPgpInputStream; import org.pgpainless.key.OpenPgpFingerprint; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - public final class ArmorUtils { // MessageIDs are 32 printable characters @@ -550,16 +549,12 @@ public final class ArmorUtils { @Nonnull public static InputStream getDecoderStream(@Nonnull InputStream inputStream) throws IOException { - BufferedInputStream buf = new BufferedInputStream(inputStream, 512); - InputStream decoderStream = PGPUtilWrapper.getDecoderStream(buf); - // Data is not armored -> return - if (decoderStream instanceof BufferedInputStream) { - return decoderStream; + OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); + if (openPgpIn.isAsciiArmored()) { + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); + return PGPUtil.getDecoderStream(armorIn); } - // Wrap armored input stream with fix for #159 - decoderStream = CRCingArmoredInputStreamWrapper.possiblyWrap(decoderStream); - decoderStream = PGPUtil.getDecoderStream(decoderStream); - return decoderStream; + return openPgpIn; } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/PGPUtilWrapper.java b/pgpainless-core/src/main/java/org/pgpainless/util/PGPUtilWrapper.java deleted file mode 100644 index dd95a3bc..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/PGPUtilWrapper.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.bouncycastle.openpgp.PGPUtil; - -public final class PGPUtilWrapper { - - private PGPUtilWrapper() { - - } - - /** - * {@link PGPUtil#getDecoderStream(InputStream)} sometimes mistakens non-base64 data for base64 encoded data. - * - * This method expects a {@link BufferedInputStream} which is being reset in case an {@link IOException} is encountered. - * Therefore, we can properly handle non-base64 encoded data. - * - * @param buf buffered input stream - * @return input stream - * @throws IOException in case of an io error which is unrelated to base64 encoding - */ - public static InputStream getDecoderStream(BufferedInputStream buf) throws IOException { - try { - return PGPUtil.getDecoderStream(buf); - } catch (IOException e) { - if (e.getMessage().contains("invalid characters encountered at end of base64 data")) { - buf.reset(); - return buf; - } - throw e; - } - } -} diff --git a/pgpainless-core/src/test/java/org/bouncycastle/PGPUtilWrapperTest.java b/pgpainless-core/src/test/java/org/bouncycastle/PGPUtilWrapperTest.java deleted file mode 100644 index 1c0093f3..00000000 --- a/pgpainless-core/src/test/java/org/bouncycastle/PGPUtilWrapperTest.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.bouncycastle; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.util.Date; - -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPLiteralDataGenerator; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.util.TestAllImplementations; -import org.pgpainless.util.PGPUtilWrapper; - -public class PGPUtilWrapperTest { - - @TestTemplate - @ExtendWith(TestAllImplementations.class) - public void testGetDecoderStream() throws IOException { - - ByteArrayInputStream msg = new ByteArrayInputStream("Foo\nBar".getBytes(StandardCharsets.UTF_8)); - PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator(); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - OutputStream litOut = literalDataGenerator.open(out, PGPLiteralDataGenerator.TEXT, "", new Date(), new byte[1 << 9]); - Streams.pipeAll(msg, litOut); - literalDataGenerator.close(); - - InputStream in = new ByteArrayInputStream(out.toByteArray()); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(in); - PGPLiteralData literalData = (PGPLiteralData) objectFactory.nextObject(); - InputStream litIn = literalData.getDataStream(); - BufferedInputStream bufIn = new BufferedInputStream(litIn); - InputStream decoderStream = PGPUtilWrapper.getDecoderStream(bufIn); - ByteArrayOutputStream result = new ByteArrayOutputStream(); - Streams.pipeAll(decoderStream, result); - assertEquals("Foo\nBar", result.toString()); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java new file mode 100644 index 00000000..f416ea7f --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java @@ -0,0 +1,704 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Random; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; + +public class OpenPgpInputStreamTest { + + private static final Random RANDOM = new Random(); + + @Test + public void randomBytesDoNotContainOpenPgpData() throws IOException { + byte[] randomBytes = new byte[1000000]; + RANDOM.nextBytes(randomBytes); + ByteArrayInputStream randomIn = new ByteArrayInputStream(randomBytes); + + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(randomIn); + assertFalse(openPgpInputStream.isAsciiArmored()); + assertFalse(openPgpInputStream.isBinaryOpenPgp()); + assertTrue(openPgpInputStream.isNonOpenPgp()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(openPgpInputStream, out); + + assertArrayEquals(randomBytes, out.toByteArray()); + } + + @Test + public void shortAsciiArmoredMessageIsAsciiArmored() throws IOException { + String asciiArmoredMessage = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMAwAAAAAAAAAAAQv9FBhYmbkqLBVrhUUPouXTiXJ/ElyDknSW0xTDgofFbIZ5\n" + + "9ABYrYHaDEUAupwYzh5H8xNiL70/RdI0cMv7k2Rqlug/W4f0Mz+wYJ4xN24NzRQ5\n" + + "BqlsTIlXwJI0N4Rj7KSBfVhSHYEm0EtA4qx8ylL3vJfAH1AH7bBLjSzkDYE7dvu8\n" + + "2/PigN2c0tQ+AG4O+QV8zgJpc0tE2bh0h1eiXarhOZZSNjJKqmYZ4PlhgdiQBRs7\n" + + "a7EgkdNYMUTCbBiEpyQiiorDIxqmiaQVJjoCmSiSMCxvae9ozue6x1FvFyZWEPdV\n" + + "Lp8pSnuZwQt7jAw/Qm3u1ogyNdQaoXF/pDuwJEf0ufYwMsI7wDUVUJiRL23BGDOB\n" + + "h2YbFu7TWz63wkwjTs8bfeQ8JPmWXTG75Z95sjaiMloGhKwhYem8XPWAmh6xLWfF\n" + + "TgYU/AgKTgBvb/WugSLpi1zSOjkET3IY00vjvCzfwxxojJd/vfaSdOQX2EbADwgm\n" + + "KAmdO0Q9+BRuBDNPAEH/0j8BuiicOrrHRd0c9T4ku9u1vvxGJCMwiKPj9TGlxxpw\n" + + "C5uUVzvOSzGKfZ5ZH4SToaMhbYW37UXtA7URW1zF86c=\n" + + "=Yz3x\n" + + "-----END PGP MESSAGE-----"; + + ByteArrayInputStream asciiIn = new ByteArrayInputStream(asciiArmoredMessage.getBytes(StandardCharsets.UTF_8)); + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(asciiIn); + + assertTrue(openPgpInputStream.isAsciiArmored()); + assertFalse(openPgpInputStream.isNonOpenPgp()); + assertFalse(openPgpInputStream.isBinaryOpenPgp()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(openPgpInputStream, out); + + assertArrayEquals(asciiArmoredMessage.getBytes(StandardCharsets.UTF_8), out.toByteArray()); + } + + String longAsciiArmoredMessage = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: 7F91 16FE A90A 5983 936C 7CFA A027 DB2F 3E1E 118A\n" + + "Comment: Paul Schaub \n" + + "Comment: Paul Schaub \n" + + "Comment: Paul Schaub \n" + + "\n" + + "xsFNBFfz1ucBEADXSvUjnOWSzgW5hXki1xUpGv7vacT8XqqGbO9Z32P3eFxa4E9J\n" + + "vveJmx+voxRWpleZ/L6XCYYmCKnagjF0fMxFD1Zxicp5tzbruC1cm/Els0IJVjFV\n" + + "RLke3SegTHxHncA8+BYn2k/VnTKwDXzP0ZLyc7mUbDl8CCtWGGUkXpaa7WyZIA/q\n" + + "mvUqh7671Vr4vJlq0kFbUibsFblZjk9uydHvvqaVpmBzbr/gWDyirHXwPl5lCnWp\n" + + "ORjT7tc8hjyt+dxpmnGdqlDIcqUjdCWoN6NxffLtKz/XpJ+dBvA8rXT/QaPSaVCG\n" + + "o0DbgybvRF1HvX30udx4FF9fFsVAbYP1mvZx4fHy+Z1rJJhODZv1YpH7YY1bmG02\n" + + "vfFkwpW4AyAdsONA+n/XdMCsA006/pljNd3GxjcqB5D6BhpdUvcgUslkuELsVYWb\n" + + "EyhxKzzJvZNjQ/iHsaThooy9SFHc71PgYdyEL/WzoGr421GwpCL6BuE0rlumgaTm\n" + + "joU/9ydLO6zpbV4RYDgtsaGQxOxVc0y1Lj8CWTi/XYIVRnmqrjGmubRV7q8pTxrg\n" + + "oyk2zwQ+twyxp/8ZRHzl5ISiDLKSDlcMK1oa7NqyL+MCwiswpaObk56HxgF2ZwEb\n" + + "JZYCwetxyTK7HX4/WV0V6TaPzS7dHAsb6t1Aq8IS1JdGjWKRPkjkhR95nQARAQAB\n" + + "zSNQYXVsIFNjaGF1YiA8dmFuaXRhc3ZpdGFlQGZzZmUub3JnPsLEAgQTAQoCrAIb\n" + + "AwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4ACGQEWIQR/kRb+qQpZg5NsfPqgJ9sv\n" + + "Ph4RigUCYAwbLDUUgAAAAAASABpwcm9vZkBtZXRhY29kZS5iaXpkbnM6amFiYmVy\n" + + "aGVhZC50az90eXBlPVRYVD4UgAAAAAASACNwcm9vZkBtZXRhY29kZS5iaXpodHRw\n" + + "czovL2Zvc3N0b2Rvbi5vcmcvQHZhbml0YXN2aXRhZZAUgAAAAAASAHVwcm9vZkBt\n" + + "ZXRhY29kZS5iaXp4bXBwOnZhbml0YXN2aXRhZUBqYWJiZXJoZWFkLnRrP29tZW1v\n" + + "LXNpZC0yMDkzNjgxNTQ1PTYyODlhYTNiZDhhNTAxYTM2MzIyYTBmODk0ZjhkMWQ5\n" + + "NzE4ZGVkMDM2MTYwMzlmMWNmNDhiMmE0MWVlMzU5MjCPFIAAAAAAEgB0cHJvb2ZA\n" + + "bWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVt\n" + + "by1zaWQtMTk5MTQxODIwPWY0YThmZjg0MDA0MzkzYTg3ZjcwMTNjNjAwNjViZGM4\n" + + "OWIxMTY5ZWJjZmI4MDYwYzRmOTY2OWI0M2JhMGM4MTSQFIAAAAAAEgB1cHJvb2ZA\n" + + "bWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVt\n" + + "by1zaWQtMTQyOTY3NzEyNT1lOGE3YjEyMzZiODUwYjQ2N2E1MDkyYzBiZGZlYTg2\n" + + "YTUzZTM2ODQyODNhMWQ1ZmUyNmVmNTg3MmRkMGFlZjQxSBSAAAAAABIALXByb29m\n" + + "QG1ldGFjb2RlLmJpemh0dHBzOi8vY29kZWJlcmcub3JnL3Zhbml0YXN2aXRhZS9n\n" + + "aXRlYV9wcm9vZgAKCRCgJ9svPh4RivdTEADC3xMcrcDR/+4JlDl5fblecfJHr3/E\n" + + "0fzkPWJJBL+TIn3ON2sSKIfLn9M7NYWIGT0QLI4LnqT+SZ3Ont1h8irM4O8LuTwZ\n" + + "kqjLkytGhgCErSdGzJ3oIcdXcnzX/p6fmxer1Qg/bpFy8mRrpSQ5tI0TYUXfD0qs\n" + + "BEbUhB3Tsg8AYaDRcdPx8gf1METZDxx/E6RQNzVIfyCK8hszzU1pRFr15DYDCjl5\n" + + "RZjTxXqxJFKUz85LvQToaFo5SXgH/fWf0EeoD+YNqyhROYr8iWMLCLiHqvqkEXny\n" + + "lm7qNlFxFGFSu8Mcj6HSet5qvRj2wn6XssOWm2pOalDJx+L/biETr5vEnBwfw7p2\n" + + "1Pmrg/jhK9yasKsdYKRlJdJWOtpEi9amcQ4sGA9OD74weJ/zEEPgLKbvkWFuUy8a\n" + + "69AEeKAbB3RH3r7+PRnPVvxC3MpEmLsRsjVdP21xGhtnqAzJFkMRXf5lpC6czJiH\n" + + "gd/sao0mJPrkWUHDn0k9rgoZI9gRRENk3tXefjwQ2A5aEcAagmb2l0DjugYAb7dU\n" + + "ip9bJNUhBgjiaWYBj9uZOzYdQ7kFcFWp7iCGvkoeBMQf29rXZOZsxQmKLgEPZuCl\n" + + "YmIO4PS6sERoPT+FUGl85YAkEIBII0TCQdVQd/Vx6JRLc/f/cFCoKBv2+9LKVPIp\n" + + "wNNL5J+0m/H1dMLDzAQTAQoCdgIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AC\n" + + "GQEWIQR/kRb+qQpZg5NsfPqgJ9svPh4RigUCX8gjtUgUgAAAAAASAC1wcm9vZkBt\n" + + "ZXRhY29kZS5iaXpodHRwczovL2NvZGViZXJnLm9yZy92YW5pdGFzdml0YWUvZ2l0\n" + + "ZWFfcHJvb2aQFIAAAAAAEgB1cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2YW5pdGFz\n" + + "dml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMTQyOTY3NzEyNT1lOGE3YjEy\n" + + "MzZiODUwYjQ2N2E1MDkyYzBiZGZlYTg2YTUzZTM2ODQyODNhMWQ1ZmUyNmVmNTg3\n" + + "MmRkMGFlZjQxjxSAAAAAABIAdHByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRh\n" + + "c3ZpdGFlQGphYmJlcmhlYWQudGs/b21lbW8tc2lkLTE5OTE0MTgyMD1mNGE4ZmY4\n" + + "NDAwNDM5M2E4N2Y3MDEzYzYwMDY1YmRjODliMTE2OWViY2ZiODA2MGM0Zjk2Njli\n" + + "NDNiYTBjODE0kBSAAAAAABIAdXByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRh\n" + + "c3ZpdGFlQGphYmJlcmhlYWQudGs/b21lbW8tc2lkLTIwOTM2ODE1NDU9NjI4OWFh\n" + + "M2JkOGE1MDFhMzYzMjJhMGY4OTRmOGQxZDk3MThkZWQwMzYxNjAzOWYxY2Y0OGIy\n" + + "YTQxZWUzNTkyMD4UgAAAAAASACNwcm9vZkBtZXRhY29kZS5iaXpodHRwczovL2Zv\n" + + "c3N0b2Rvbi5vcmcvQHZhbml0YXN2aXRhZQAKCRCgJ9svPh4RiiRwD/47o9xzTDXB\n" + + "thNwd/T1UWKSNtLoPX6V4V2hUW/z1SZulba9i041fM04yaauqOFrKfoFJjovdZis\n" + + "UZeYs0Bfjf87JoJwN6TgX/7bQjSncBKHmKDXI7SLuY9dtYvqGCUOlVPTr4lxm1Ht\n" + + "CK5XJWzMjE/mUaPwUeP8agG2lRko46K2O4msUGvnZt/m6ggtyn7WhdxHAMEiBxmk\n" + + "j0lTIj5Q78hMxlWCI7D9bSNkRSHKN+5AQ0OIQCQnvbh1Gz85DO+VJdtr529L5pz+\n" + + "WEsrApGbjhi3UYfIS5fBTMfIcOZ8gs7fty79LOBuweAKKWnLt6jrRlBZ16D8LuM+\n" + + "1nrPUzTIanuqFLiysBhKBrX16UCKsW+kRvWLRG4AnEdWVlJr79kSzbzVYPHwKBqb\n" + + "41fagZdQdxt0xZcA2wGdV7UKLbY+rNew4PC9Lt+nS6pnItT0hlSVdPOBKoieoLR0\n" + + "XQAPM+Cr1qGlCFWNbMq6Q5ssS3kbTULd7UTKZuD9Wp+7h8zHqB8GoffaIT0Vvl0x\n" + + "t2TPM9+GJIkS3K+JQOGpPMrT2qRt9sL8J8u2usk/KOiD2uqu0QH3I+0qkvakFc24\n" + + "sGnj1XmIg46vYEF1N+E8kjzkIKkxoX/1sTKd5EHnw2ivOxLQM3B2PGNAn2N4S9eF\n" + + "qN+60sNMNXmlptdlVuOxdeJBSeF0vXFZ2cLDgwQTAQoCLQIbAwYLCQgHAwIGFQgC\n" + + "CQoLBBYCAwECHgECF4ACGQEWIQR/kRb+qQpZg5NsfPqgJ9svPh4RigUCX8giLj4U\n" + + "gAAAAAASACNwcm9vZkBtZXRhY29kZS5iaXpodHRwczovL2Zvc3N0b2Rvbi5vcmcv\n" + + "QHZhbml0YXN2aXRhZZAUgAAAAAASAHVwcm9vZkBtZXRhY29kZS5iaXp4bXBwOnZh\n" + + "bml0YXN2aXRhZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0yMDkzNjgxNTQ1PTYy\n" + + "ODlhYTNiZDhhNTAxYTM2MzIyYTBmODk0ZjhkMWQ5NzE4ZGVkMDM2MTYwMzlmMWNm\n" + + "NDhiMmE0MWVlMzU5MjCPFIAAAAAAEgB0cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2\n" + + "YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMTk5MTQxODIwPWY0\n" + + "YThmZjg0MDA0MzkzYTg3ZjcwMTNjNjAwNjViZGM4OWIxMTY5ZWJjZmI4MDYwYzRm\n" + + "OTY2OWI0M2JhMGM4MTSQFIAAAAAAEgB1cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2\n" + + "YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMTQyOTY3NzEyNT1l\n" + + "OGE3YjEyMzZiODUwYjQ2N2E1MDkyYzBiZGZlYTg2YTUzZTM2ODQyODNhMWQ1ZmUy\n" + + "NmVmNTg3MmRkMGFlZjQxAAoJEKAn2y8+HhGKcCkP+gPiUroUSbVfJzFyWej0EPF1\n" + + "773h5aVoKgZ4gtVYSupM4rudP0oP/tH8sjSFebetpgyKEfZqau3lGbiWaIjXgNRW\n" + + "+9Tyi201tJbg/sAMczhK9ikGM0RtzI0oA1YK5DFYA8ImCfxkv7ZDi3/AiUzPei/6\n" + + "ja4g417ueNw8kp12Jh3jErWWHpeideHpcKg9vbbXO9GJ/nNWKXLwBAGhTKNAulby\n" + + "CYMfXqG1xKiWchDI9BylNF5bSPz5Yoxz91QBAR7X5x77rhSmg0zWkMIbla8VMrzX\n" + + "ZvfypFMeQeju3qRzLmAsSUr8JCg0q7q9tePQynn/wvcRoPGPxLLEsHdcOM2j5e3G\n" + + "+jU+gDsOVCpyEYP70OGsF8duR/iNCJ+pso1JPu2I+5NSGeIYfejuoa0AoHUt6yHs\n" + + "+K2bGh3hEFz8jyxp27GvcQvwAYDDaZ+RQRdAo4DKXb9Y/mqxvrm8GsbB+puzrIxw\n" + + "be3/iAw47ANJG0RbuDVlycBEwGImAKhQ24fM1/QFhs3YyRPg2jqOujOrcgYVC599\n" + + "XSGMwcdpS/dka0l77rkMK2WKk1R0+cfwM/XItMti/dVgfMPstfjO3xc8E5LAxZIv\n" + + "n9yfLIdS87jqgw1mUKF9PSFC53v7cQppYlt6tztFjo8HWisiP7LRkSR+wR+HKjSL\n" + + "Ek3f6fF97SSUREcxN2cfwsNEBBMBCgHuAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIe\n" + + "AQIXgJAUgAAAAAASAHVwcm9vZkBtZXRhY29kZS5iaXp4bXBwOnZhbml0YXN2aXRh\n" + + "ZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0xNDI5Njc3MTI1PWU4YTdiMTIzNmI4\n" + + "NTBiNDY3YTUwOTJjMGJkZmVhODZhNTNlMzY4NDI4M2ExZDVmZTI2ZWY1ODcyZGQw\n" + + "YWVmNDGPFIAAAAAAEgB0cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0\n" + + "YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMTk5MTQxODIwPWY0YThmZjg0MDA0\n" + + "MzkzYTg3ZjcwMTNjNjAwNjViZGM4OWIxMTY5ZWJjZmI4MDYwYzRmOTY2OWI0M2Jh\n" + + "MGM4MTSQFIAAAAAAEgB1cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0\n" + + "YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMjA5MzY4MTU0NT02Mjg5YWEzYmQ4\n" + + "YTUwMWEzNjMyMmEwZjg5NGY4ZDFkOTcxOGRlZDAzNjE2MDM5ZjFjZjQ4YjJhNDFl\n" + + "ZTM1OTIwFiEEf5EW/qkKWYOTbHz6oCfbLz4eEYoFAl/IHp4CGQEACgkQoCfbLz4e\n" + + "EYqTOA/+OubamE0ivV15sXOLbVTYoPYgy21lJilGXnV7JBcSixRDEupTIaWqZwB4\n" + + "YVtA8hbyXOMgA96VT0SJ93rN7WDQYCiPjF+oQD2yo24rHxj831SNjPQBjjQiCVtA\n" + + "aYOvqfgE9peUgAmGxB0JZ9CDCjQFxzV0lAhsb1KlWNNCqTNYqWWlwRdziKeKoUEH\n" + + "//fiQvWRK7NZbbnNj6rKKo4CnfXKuVCzKDNIeq3vf877k+EIwyNXVlgghFaqTjP8\n" + + "kUVD0clmtS6fBwZ+LbQydo3yEQ66/mbkjYJ1lpO3hn2hvHXn/kZE7qRmWe/frIMU\n" + + "Z6niuKaAoPErYQyMTuQ/dFRbsqT6cXHw1mGkuoqiLp6wccb5JrfaszVbUF3MIdZF\n" + + "041uQqYJvaATgCsM236cgRCpfxlc/8YC2C5PK0oMyYTiHe910PB0aYY1v2IEOnpq\n" + + "LP+0hdOET0bzTBVwsq9fD4YxNclw4mYHZ439TezI+Fnr47OuIS/BrWWOxBrFdTnL\n" + + "eHBL42/5+i46jbdE6RKU+Kpb0byWr/jYkm9AZVp1/zHBU31u/TpEFXE/Imn0bauH\n" + + "ubiBC9L+8Oy4SMrCLdcclfG4Sk3JaBDgetAZLslzxSXEMl9C2tHFSgyO8Xx+5KNK\n" + + "TZx5n04SWFFUgNZIYATCV70QpVAgagkSrNwrpV2QcfcsFbACiDzCw0EEEwEKAesC\n" + + "GwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAFiEEf5EW/qkKWYOTbHz6oCfbLz4e\n" + + "EYoFAl/IG5qQFIAAAAAAEgB1cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2YW5pdGFz\n" + + "dml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMTQyOTY3NzEyNT1lOGE3YjEy\n" + + "MzZiODUwYjQ2N2E1MDkyYzBiZGZlYTg2YTUzZTM2ODQyODNhMWQ1ZmUyNmVmNTg3\n" + + "MmRkMGFlZjQxjxSAAAAAABIAdHByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRh\n" + + "c3ZpdGFlQGphYmJlcmhlYWQudGs/b21lbW8tc2lkLTE5OTE0MTgyMD1mNGE4ZmY4\n" + + "NDAwNDM5M2E4N2Y3MDEzYzYwMDY1YmRjODliMTE2OWViY2ZiODA2MGM0Zjk2Njli\n" + + "NDNiYTBjODE0kBSAAAAAABIAdXByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRh\n" + + "c3ZpdGFlQGphYmJlcmhlYWQudGs/b21lbW8tc2lkLTIwOTM2ODE1NDU9NjI4OWFh\n" + + "M2JkOGE1MDFhMzYzMjJhMGY4OTRmOGQxZDk3MThkZWQwMzYxNjAzOWYxY2Y0OGIy\n" + + "YTQxZWUzNTkyMAAKCRCgJ9svPh4RioY0EAC2URLKbRQTP97apJ7qctk9dWOKgx+m\n" + + "xLqCmo0d4uH7phxx6VAXLXCJRwPhrvOekUL4xRC2qYyO0Zit4yXM7HbxqC8lScMX\n" + + "z98siF7aAXWjJ+2UpIaoP75jpUPs0t0Ude1gQ6UqPqLJI/yQLWVtAaa8IqEFBvFR\n" + + "sOg4T2MuZdUo75r/PApL1npZcHhNUSwagHYOY6lCKAlpMxitEhxPR07Ji5llIKGV\n" + + "d1wyOxzP09IXsfeKME2zfrTSf9ybHEVAUQdjybXcEaB+carkw/gzKrxEgrACnEOe\n" + + "CvWwd0Fq84F9U+MTnikIhdcSTUDAVRbFQQnxdd+G6QocYc15jtdOFnnZvXI9FuWf\n" + + "asEKlCdqT/bv6a/Nvba0AE9aAIkbCwr3I4Dx93alVPPMBiFpvG6p7mMQLQAL4IsA\n" + + "m+OinigQy2BtsSd/fwP851QFJNc8aUf9dvu0zq8f/rFNk+V58SEKVXEOtSYK7tcI\n" + + "A+mrkL/jwkwFas2Uh3ZirkqVq4KFtJA2jlW3m14TTyuk6IGxP5SB5RjTY2nlJbJw\n" + + "+jO1AkAONomoy0uzuAxlJAFxUjhEpHNU5MuNCYPIlmplkSoCdEv1pc7c1ZnB2zjP\n" + + "FUqsx69gorhoIEGeMfi1XGxuHy2I3GbmFtA681Vgzl5FppV2v7X9jicL4GruAkwH\n" + + "zUz1S6VeXnelcMLBjwQTAQIAIgUCV/PW5wIbAwYLCQgHAwIGFQgCCQoLBBYCAwEC\n" + + "HgECF4AAIQkQoCfbLz4eEYoWIQR/kRb+qQpZg5NsfPqgJ9svPh4RikX2EACFH0OF\n" + + "okyqKs4hJTeIW5i3nMYID1F3vfusmDFfpcltue+2LdEvrj1rhOXfvOpNSWLWUzJa\n" + + "O46tH813WSBncMwSlo+6zAkojcOnf0fC08RlDSimioXG4dOcs9pd3TPKxEMOTQYs\n" + + "kGbyRUrvg6Hl+zv7eXRyyMFMQYAwOQJ9pIf5AGp5ObJ2RU87IOxKH/jTjAV6yDvr\n" + + "RrBii8NhVr4ouj7c/UflLKLgZ/8RJxcUL5yFInTfbaEMBnQv20AMsAqFR+1VTQ5M\n" + + "flLfa7eK+g2lPpCXaZaNrzZkdWk6GggAg4A/6Ighx/VxaPY8PI5K0j7C/PUiKSxQ\n" + + "pHHIwuEOZG4Uy33iOjT6n9oiHSMF3iNbf4zvs1Gv5IJOgv1xgU+ppfLF3o322NTh\n" + + "t5YXLnbMXPGSh6SvxLlBUxI8gjQdjfaJol0oz31UDedF+CElD7SJbJIPKJq4NBqe\n" + + "kQjNUFuHNRouXWNjpX5jlTGx8VM4jUzKISo5I1UvGbUZRxteyWWyFJgbr7VCH2+e\n" + + "aENvN215GHWi63EE8Qkp/euTBqA2U69E6vHxwhw+5NA9zE4J0C9yn1JsqBqjPgpt\n" + + "emn14QJeJw+yms+BXzAASZY4CL/OGHS40BJgpV7n9GNF8OrZuEZZM+dfzgVd9r4S\n" + + "Ogq+ogmrA7DvTpM4OA9Cu+wVVXQRL/BNndEdjc0mUGF1bCBTY2hhdWIgPHZhbml0\n" + + "YXN2aXRhZUBtYWlsYm94Lm9yZz7Cw/4EEwEKAqgCGwMFCwkIBwMFFQoJCAsFFgID\n" + + "AQACHgECF4AWIQR/kRb+qQpZg5NsfPqgJ9svPh4RigUCYAwbOTUUgAAAAAASABpw\n" + + "cm9vZkBtZXRhY29kZS5iaXpkbnM6amFiYmVyaGVhZC50az90eXBlPVRYVD4UgAAA\n" + + "AAASACNwcm9vZkBtZXRhY29kZS5iaXpodHRwczovL2Zvc3N0b2Rvbi5vcmcvQHZh\n" + + "bml0YXN2aXRhZZAUgAAAAAASAHVwcm9vZkBtZXRhY29kZS5iaXp4bXBwOnZhbml0\n" + + "YXN2aXRhZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0yMDkzNjgxNTQ1PTYyODlh\n" + + "YTNiZDhhNTAxYTM2MzIyYTBmODk0ZjhkMWQ5NzE4ZGVkMDM2MTYwMzlmMWNmNDhi\n" + + "MmE0MWVlMzU5MjCPFIAAAAAAEgB0cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2YW5p\n" + + "dGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMTk5MTQxODIwPWY0YThm\n" + + "Zjg0MDA0MzkzYTg3ZjcwMTNjNjAwNjViZGM4OWIxMTY5ZWJjZmI4MDYwYzRmOTY2\n" + + "OWI0M2JhMGM4MTSQFIAAAAAAEgB1cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2YW5p\n" + + "dGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQtMTQyOTY3NzEyNT1lOGE3\n" + + "YjEyMzZiODUwYjQ2N2E1MDkyYzBiZGZlYTg2YTUzZTM2ODQyODNhMWQ1ZmUyNmVm\n" + + "NTg3MmRkMGFlZjQxSBSAAAAAABIALXByb29mQG1ldGFjb2RlLmJpemh0dHBzOi8v\n" + + "Y29kZWJlcmcub3JnL3Zhbml0YXN2aXRhZS9naXRlYV9wcm9vZgAKCRCgJ9svPh4R\n" + + "ivVhD/46gD755fsVTqanw0VUq9HCWEmSGu5jIU6USs8ZD71Jb1uivXjjKVM4Ir8a\n" + + "BZW7+HNrz+XoRfztExxnwh90GVTWYkdrM44x3dOBxQ33etW41yqkmdHHbDnJ45Oj\n" + + "23RBp7zSEHmG5TZyvSU5aWUVw+QEqV6uzt43XYL5z3Nnt9RKs9CEAXcrKxOi9FLs\n" + + "V/g9xARlfsNw5J4LxoTYV856qPabb4VZy/6TRKxWMJXFQg55xODKgMm+Us2C97db\n" + + "6d4rrGH+XFE5rwKNbJH8m3bsHxEwdleIWX270cwtd769FeAydtjte9kTNNJ+9JGG\n" + + "Pj2LbhRkf8gnnvQxzyOdiMQ59cAz4rrgVviB0wXOEqhgjxxmIg3e3Y3pncnXRzZm\n" + + "v2ShxzpUw7UWK25S3TDBVcHRE0IpOm0eOMQq5kWGy+pEUm1IbJz+kPb0cI9x+VhZ\n" + + "k4nnni4yrhAooBcxn5gkKlQc3FFiM8gqw6duj68ugheL/CtJYuYFdJoKtSajzKSD\n" + + "vn/64t+rvPY1eywmOgaQ7ljZXEYO3KrgILaKZp5quTY4HY644OMSFboOphLQ2yMm\n" + + "ZNUMeYKyHNu5Nw6qyrhcpCLEQ8D5RK63YLuvyDIn+psseOCjjNQhjSRTyYfV4cfW\n" + + "C7Bgs9j14xh7t77CY7OtOjWof2mHSzAerMIr5F698BeqMx9DHsLDyAQTAQoCcgIb\n" + + "AwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgBYhBH+RFv6pClmDk2x8+qAn2y8+HhGK\n" + + "BQJfyCO2SBSAAAAAABIALXByb29mQG1ldGFjb2RlLmJpemh0dHBzOi8vY29kZWJl\n" + + "cmcub3JnL3Zhbml0YXN2aXRhZS9naXRlYV9wcm9vZpAUgAAAAAASAHVwcm9vZkBt\n" + + "ZXRhY29kZS5iaXp4bXBwOnZhbml0YXN2aXRhZUBqYWJiZXJoZWFkLnRrP29tZW1v\n" + + "LXNpZC0xNDI5Njc3MTI1PWU4YTdiMTIzNmI4NTBiNDY3YTUwOTJjMGJkZmVhODZh\n" + + "NTNlMzY4NDI4M2ExZDVmZTI2ZWY1ODcyZGQwYWVmNDGPFIAAAAAAEgB0cHJvb2ZA\n" + + "bWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVt\n" + + "by1zaWQtMTk5MTQxODIwPWY0YThmZjg0MDA0MzkzYTg3ZjcwMTNjNjAwNjViZGM4\n" + + "OWIxMTY5ZWJjZmI4MDYwYzRmOTY2OWI0M2JhMGM4MTSQFIAAAAAAEgB1cHJvb2ZA\n" + + "bWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVt\n" + + "by1zaWQtMjA5MzY4MTU0NT02Mjg5YWEzYmQ4YTUwMWEzNjMyMmEwZjg5NGY4ZDFk\n" + + "OTcxOGRlZDAzNjE2MDM5ZjFjZjQ4YjJhNDFlZTM1OTIwPhSAAAAAABIAI3Byb29m\n" + + "QG1ldGFjb2RlLmJpemh0dHBzOi8vZm9zc3RvZG9uLm9yZy9AdmFuaXRhc3ZpdGFl\n" + + "AAoJEKAn2y8+HhGK7R4P/RtmQN/Q39Jj+v4pPWxetHRAqFasLoZnFCj1rYgHE7z1\n" + + "hWqhCFMaCgeM3r63knwNNQhbZ2KTGhw1tjC/yfWnvDrhQkm1Idr6Zpn9v/D3KIXM\n" + + "s4bdPMlRUpRXOE/AM+RS08/bouE7CqIwv0oAj3VOMiMazRYLwXAfkJtUzgWNqlwX\n" + + "pujDtAJB6M11XM/Q6qeM4j1pjvXJs/faUFHXyku1zH4rcR0go79qyAbZ1vS67Ps/\n" + + "Wg5QYpklc80XarpHRtFVFWagGEtM0mkazkyYBgySZRm8miDGEuwm2HzDru0x+Clp\n" + + "H7uSDy6uiOjJO6+ApbJxkWDH/POuwpd0fCLwI9C4UAEnZLkCE3iXNbTKguXEc1Rb\n" + + "t8nxrMVlhQO35+1AVo9rpr/8r+FZRlYfZYEB4sUtxjbbIpFV0YZkOBiAW3r6Tp0X\n" + + "YJU8wi/fChJvF4j81grwckQavRDbsuQEEbnYzwjucpw2D+Ug/6U+Dhjj1qeYuxJG\n" + + "VfF+S07d3k2h84IzElcPwoP5uxpe2MdIOQY+EK0D3mpfedmrlkv8wnImMKp9dU2N\n" + + "tG4YqAikdAy4akbU03nk6GVFrGo3gLDKXBA9GVNbnjW9qd0S3OI64Ci+8mBg3NBN\n" + + "yIX5uLPsN1+PrwwzQuOBw/gSeWs9JhJA4C8emlzwb+sT8mw+h3ZZ8EI81LD+0h09\n" + + "wsN/BBMBCgIpAhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAFiEEf5EW/qkKWYOT\n" + + "bHz6oCfbLz4eEYoFAl/IIjU+FIAAAAAAEgAjcHJvb2ZAbWV0YWNvZGUuYml6aHR0\n" + + "cHM6Ly9mb3NzdG9kb24ub3JnL0B2YW5pdGFzdml0YWWQFIAAAAAAEgB1cHJvb2ZA\n" + + "bWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVt\n" + + "by1zaWQtMjA5MzY4MTU0NT02Mjg5YWEzYmQ4YTUwMWEzNjMyMmEwZjg5NGY4ZDFk\n" + + "OTcxOGRlZDAzNjE2MDM5ZjFjZjQ4YjJhNDFlZTM1OTIwjxSAAAAAABIAdHByb29m\n" + + "QG1ldGFjb2RlLmJpenhtcHA6dmFuaXRhc3ZpdGFlQGphYmJlcmhlYWQudGs/b21l\n" + + "bW8tc2lkLTE5OTE0MTgyMD1mNGE4ZmY4NDAwNDM5M2E4N2Y3MDEzYzYwMDY1YmRj\n" + + "ODliMTE2OWViY2ZiODA2MGM0Zjk2NjliNDNiYTBjODE0kBSAAAAAABIAdXByb29m\n" + + "QG1ldGFjb2RlLmJpenhtcHA6dmFuaXRhc3ZpdGFlQGphYmJlcmhlYWQudGs/b21l\n" + + "bW8tc2lkLTE0Mjk2NzcxMjU9ZThhN2IxMjM2Yjg1MGI0NjdhNTA5MmMwYmRmZWE4\n" + + "NmE1M2UzNjg0MjgzYTFkNWZlMjZlZjU4NzJkZDBhZWY0MQAKCRCgJ9svPh4Riuaw\n" + + "D/9zil7na4utYS7e87CDlnUZT1JmWFRB/fglMG6B3dV1I+wIqsCIYWEkkobJlBI4\n" + + "YLYqx3UrYn/TGEca6y6pzlhbRk7YaY+z31XSWZj+fuRBZLLx2WTRgH1L3brQn5+k\n" + + "AHkUx2cS1R1usTxqFqWp+APbdDGDpzvHp8omtaYqecAaOhJp3AN96kdsyXCR/SeY\n" + + "Kc8aghCBqQx1uhXjyATO3OE+nD/DtWU7z/wqR2LrIvIzrUIQW76FgaqPMSf922p8\n" + + "1GxFvAHIa81SGptYDPq7kNXqG1LVF/NJBJAqxCZhu/yIrx+jus7+g3XaoEbuGtO/\n" + + "SPxpdDcKiuRwRer1MznX0cbzE2DoaI21t9kJ3y9l8QBl7xLHSCXYxF+hxBy3w7Nq\n" + + "TeGpdclC2uMV05H43vKk4Ecrax96g8Bwt6J+jpDPw0LbOBbwGKs5P5ggugtlSFFG\n" + + "jMmuIfd+s89lhXzTkBirkM8rEcLrORXww1meaxlhZ8gqHP/amWvNIG/Rpoa+oMs2\n" + + "ArA4BJpSeK58pPKH+kL+uZzbfIHZORM54hnuyDOYiMAjdjETETK4QJuNdHkniEU2\n" + + "FkHZVYmmdP2Vtjx9XWoFoWjAg2V4XPo87p4GUzwLQ12YS8tNkZMdtUOf0sOKE45E\n" + + "EhAb4jxjIcWFQdX99YrtG9Pb8H8KlJeMunQwyLGUcbmt7cLDQAQTAQoB6gIbAwUL\n" + + "CQgHAwUVCgkICwUWAgMBAAIeAQIXgBYhBH+RFv6pClmDk2x8+qAn2y8+HhGKBQJf\n" + + "yBuakBSAAAAAABIAdXByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRhc3ZpdGFl\n" + + "QGphYmJlcmhlYWQudGs/b21lbW8tc2lkLTE0Mjk2NzcxMjU9ZThhN2IxMjM2Yjg1\n" + + "MGI0NjdhNTA5MmMwYmRmZWE4NmE1M2UzNjg0MjgzYTFkNWZlMjZlZjU4NzJkZDBh\n" + + "ZWY0MY8UgAAAAAASAHRwcm9vZkBtZXRhY29kZS5iaXp4bXBwOnZhbml0YXN2aXRh\n" + + "ZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0xOTkxNDE4MjA9ZjRhOGZmODQwMDQz\n" + + "OTNhODdmNzAxM2M2MDA2NWJkYzg5YjExNjllYmNmYjgwNjBjNGY5NjY5YjQzYmEw\n" + + "YzgxNJAUgAAAAAASAHVwcm9vZkBtZXRhY29kZS5iaXp4bXBwOnZhbml0YXN2aXRh\n" + + "ZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0yMDkzNjgxNTQ1PTYyODlhYTNiZDhh\n" + + "NTAxYTM2MzIyYTBmODk0ZjhkMWQ5NzE4ZGVkMDM2MTYwMzlmMWNmNDhiMmE0MWVl\n" + + "MzU5MjAACgkQoCfbLz4eEYq+WhAAxE+FWFauoqKvk7m9XfV9m1v8o9jzialXMo92\n" + + "pbyH0TZl2L8H8zUxxJIgvwdgHxvlqLnK95mDNKkRi2qCLhtLVAy04W4n0h7D+//D\n" + + "5pCbvMokU4LKYWNL8Rtv0cBIFxpUI2xdVdAG5E3pLimdcpE5/IpHAj+ImkF+8rNk\n" + + "yKUHwTUZ24PKgugdzI5zp0UUZ3QrLe4PxOrZif3UURhzej2751+5GSZixZQN+eWl\n" + + "L+CldUaWTG4I6e93FpepX3gCpPJo5zMbTlDZG9dQFFMY/jfxNf84MlfDOp5EuIYO\n" + + "v4QrG1EdYn9xMBdDilK5lWzAh2flQx3Oi2y5jFIGYX8enUJeMrsbtchbcWhS6O/u\n" + + "fefSAAAriw3r4CLQrJ1eyH5DHK2nh6leNP8hXmiV7c1TzK/KMI8uiDQ13Wp2Utoq\n" + + "hLVs1tXfM1EMzGoPXQIMDdbOqtJCtjFVlRsBDu/pp1+IppTpq9+ftqHXoB3+nMrh\n" + + "mV7r2/BMyR+q88PfJGahxQc0w82YZjaMufWfaDIixDpVtRFNSzbWmz7AA+ylOSOv\n" + + "lJKHpJVHo7YP7h23jhqOc25vZ+JQS1YQ00IYFMg86T/7Xq0gttSYLf2deZHnKF8E\n" + + "mEoZL0UY2tqOZfXl+Ge+w4QsV01WrXmzcBLydGneACdJ6Luk40kwWO70VEkK+Ed5\n" + + "u64eyejCwY4EEwEKADgWIQR/kRb+qQpZg5NsfPqgJ9svPh4RigUCXJkXLAIbAwUL\n" + + "CQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRCgJ9svPh4RipPtD/0duXEGR8m82Pbj\n" + + "zivuW0HCyLIxsbhvWYyBlbENo2qvX+zWl2n4Q24n6nfTOh+6WNLc9MHworhO3laC\n" + + "9syN4CLgv14cbSCAdTsLaDpOpLBTkwFhEI2gFEiKGaNRnRrf6oGci9q5O4DTkYtk\n" + + "ARZHq9e0tWA/rYYcsQQrRbj+eG50Lirwn39CwvPlMx5Gag50jThyUb2qbyOXJAkb\n" + + "7R6UxRvHOKJxjZqW0qp8F5GPBjqRhqcVQ6BypAHsvnhiOtZPiagQSovf6U1gHMU5\n" + + "kysuybtPMoxesa/U2ZtOs6xvDv2JF+Lscbg/wB1nIe1VwIuzrN80fXB1IGn+Dxl8\n" + + "hYTFUn7iJuVhPgAkmN4m6+hD6EQcOB+SLO+rJKFNTaVAL4w79onDgVQGJR/FspBI\n" + + "aHTPUaC3zV8G+91SUFPV37e64+FgPFEGu15UcXJdt3/m1dO/nDu/YU8xC0TMyPk/\n" + + "llIc+vNl/IxhT0Y8FEHL+WJWZQ9FyBxXBILlP5THuUwnedCuhnlO46GDmZSxHxh7\n" + + "CoMF19QxMQ6Qf0uDnnr0vMfnYQuwdEcushJHam1XwWe7kvPao0irq1r8tab2BFhP\n" + + "AnnY+nl4e9S23IkkVbbmCXaRM5QCmwgrLY/XggfcVxqb82qBp8irYHRiIjVAEElB\n" + + "HJrJWeCQnhVM18nrAbG3ic6sAeB/8s0lUGF1bCBTY2hhdWIgPHZhbml0YXN2aXRh\n" + + "ZUByaXNldXAubmV0PsLD/gQTAQoCqAIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIX\n" + + "gBYhBH+RFv6pClmDk2x8+qAn2y8+HhGKBQJgDBs5NRSAAAAAABIAGnByb29mQG1l\n" + + "dGFjb2RlLmJpemRuczpqYWJiZXJoZWFkLnRrP3R5cGU9VFhUPhSAAAAAABIAI3By\n" + + "b29mQG1ldGFjb2RlLmJpemh0dHBzOi8vZm9zc3RvZG9uLm9yZy9AdmFuaXRhc3Zp\n" + + "dGFlkBSAAAAAABIAdXByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRhc3ZpdGFl\n" + + "QGphYmJlcmhlYWQudGs/b21lbW8tc2lkLTIwOTM2ODE1NDU9NjI4OWFhM2JkOGE1\n" + + "MDFhMzYzMjJhMGY4OTRmOGQxZDk3MThkZWQwMzYxNjAzOWYxY2Y0OGIyYTQxZWUz\n" + + "NTkyMI8UgAAAAAASAHRwcm9vZkBtZXRhY29kZS5iaXp4bXBwOnZhbml0YXN2aXRh\n" + + "ZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0xOTkxNDE4MjA9ZjRhOGZmODQwMDQz\n" + + "OTNhODdmNzAxM2M2MDA2NWJkYzg5YjExNjllYmNmYjgwNjBjNGY5NjY5YjQzYmEw\n" + + "YzgxNJAUgAAAAAASAHVwcm9vZkBtZXRhY29kZS5iaXp4bXBwOnZhbml0YXN2aXRh\n" + + "ZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0xNDI5Njc3MTI1PWU4YTdiMTIzNmI4\n" + + "NTBiNDY3YTUwOTJjMGJkZmVhODZhNTNlMzY4NDI4M2ExZDVmZTI2ZWY1ODcyZGQw\n" + + "YWVmNDFIFIAAAAAAEgAtcHJvb2ZAbWV0YWNvZGUuYml6aHR0cHM6Ly9jb2RlYmVy\n" + + "Zy5vcmcvdmFuaXRhc3ZpdGFlL2dpdGVhX3Byb29mAAoJEKAn2y8+HhGKwsUP/1o5\n" + + "+7BMfta1gsVSEBvaqmCZDK0jL7Mo3g2Sayiw+aOVyFUIYy//YLd4QZGIjn7Wq015\n" + + "pjA/sSwAEtZ3rUE74ACbi29YMqSqgfMBvuD6O3u2TvV0y5I6ozGUkwP2cicNlXxn\n" + + "cONKBpfDRGa1VDIg4ghGM7/Al4AaBMIhNAQOJS1FiofXZ7qJ7jKK57BY8e1uUfg0\n" + + "KChPv/xu21wrhKy8DusBz7PSt8S8KBtisst8Mq+ew8rLRFbZ0F/l5VgvdudVSaR1\n" + + "mmSToRvmKgi2RHjIs7hlEEwRr+dWGO9SaW0oxNbVygMlP/pLEn1R9U94tAxDLXgm\n" + + "aDYL2NNXwyka5uBKLsy1dHXqXukKPS8py2PZhu2FJMLU0+ml+s2kTbA2Bze7slRO\n" + + "uiGPJg9WzovCQYVDam8eafGDMC6Q393HXH+gxq29LRg2Lulf4NJtosO4JVbOyzee\n" + + "Rd5FlZkUiJ7vbiVqIzGN8jel8Mr/NNKCcockwmry1u3JArwgNSqR+Uv+CeH446bm\n" + + "lfZ6JrwKWQRcuKVRfrXGuT46YmoFSaJjjlTATUVxcUuQNkFlQ6bibmdzEmFaKpVS\n" + + "QUf8gXnEjLh78K7kdx81c9cmIU4GrulK2uzGQULt3UgKytyrYf5EOwqnbrDDhAYR\n" + + "FwclbYRjvPUZSlTWoCo4u72gOuxdRWDgya9Ic0YnwsPIBBMBCgJyAhsDBQsJCAcD\n" + + "BRUKCQgLBRYCAwEAAh4BAheAFiEEf5EW/qkKWYOTbHz6oCfbLz4eEYoFAl/II7ZI\n" + + "FIAAAAAAEgAtcHJvb2ZAbWV0YWNvZGUuYml6aHR0cHM6Ly9jb2RlYmVyZy5vcmcv\n" + + "dmFuaXRhc3ZpdGFlL2dpdGVhX3Byb29mkBSAAAAAABIAdXByb29mQG1ldGFjb2Rl\n" + + "LmJpenhtcHA6dmFuaXRhc3ZpdGFlQGphYmJlcmhlYWQudGs/b21lbW8tc2lkLTE0\n" + + "Mjk2NzcxMjU9ZThhN2IxMjM2Yjg1MGI0NjdhNTA5MmMwYmRmZWE4NmE1M2UzNjg0\n" + + "MjgzYTFkNWZlMjZlZjU4NzJkZDBhZWY0MY8UgAAAAAASAHRwcm9vZkBtZXRhY29k\n" + + "ZS5iaXp4bXBwOnZhbml0YXN2aXRhZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0x\n" + + "OTkxNDE4MjA9ZjRhOGZmODQwMDQzOTNhODdmNzAxM2M2MDA2NWJkYzg5YjExNjll\n" + + "YmNmYjgwNjBjNGY5NjY5YjQzYmEwYzgxNJAUgAAAAAASAHVwcm9vZkBtZXRhY29k\n" + + "ZS5iaXp4bXBwOnZhbml0YXN2aXRhZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0y\n" + + "MDkzNjgxNTQ1PTYyODlhYTNiZDhhNTAxYTM2MzIyYTBmODk0ZjhkMWQ5NzE4ZGVk\n" + + "MDM2MTYwMzlmMWNmNDhiMmE0MWVlMzU5MjA+FIAAAAAAEgAjcHJvb2ZAbWV0YWNv\n" + + "ZGUuYml6aHR0cHM6Ly9mb3NzdG9kb24ub3JnL0B2YW5pdGFzdml0YWUACgkQoCfb\n" + + "Lz4eEYpSJw/+MXSg/xXIpdIVQ3NWeWB3p05op3/ilfb8GuF09XGqck4DeUq6aj93\n" + + "LD997vFmvL98ypGoyIpe3ds3DoUXzSFVjPLttFcHPsNm2CmkK6L9M1MY/2JzIRPh\n" + + "9GO1fUe5ZxspXgsf3rTZmkYXRUi/22DrEODm6H6fSK57D9J90ppRe8Rm8rqCV29J\n" + + "ht1LLgiaCwbz/DKQBBWv5ePaesnyYGTePWqLeHCLsa25mX46NS2HlBSFrcmyR+58\n" + + "wxnhkXn8SAbm5JCu/FpY5KX0PSY71QDfPUN2BaOoHRHRT5mSxqsgInJHnRVDf66L\n" + + "Lxh65LnfpdjCjTUsT6WPu7DgO9F8ObYnkno+YiaP7b9Uz7qzV3eK8SwmWTLiGE/x\n" + + "E1wFGGSvJuvCFAsMGvnc6lVZGA/F3jJCOdBy/QwyfVU54bGjPyUmodZAKoxn6Og2\n" + + "jylRUL3a9zdzt6sRxeCtuY+eqbq0ZcasP7b3PjYyMOdNQz2k9G0Fz7SW4PPQiu/m\n" + + "JFuCV33O6X5boaqoO/HTa9ZLJqCA8DjbF4i4r2phVzlv0veskqY1Nl9myGV5Mfq/\n" + + "El2dc/WfjlZAaw5Hs5qz9vdeFgqu14tSZVdLGENtg4F4TFdLobTE/ElqAVYxyA6e\n" + + "PsbOVHdIrwnZQmQvDJEoZEumZbXFmDhAm0Rb/9J8kqAd4KH0wIZq/VDCw38EEwEK\n" + + "AikCGwMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AWIQR/kRb+qQpZg5NsfPqgJ9sv\n" + + "Ph4RigUCX8giNT4UgAAAAAASACNwcm9vZkBtZXRhY29kZS5iaXpodHRwczovL2Zv\n" + + "c3N0b2Rvbi5vcmcvQHZhbml0YXN2aXRhZZAUgAAAAAASAHVwcm9vZkBtZXRhY29k\n" + + "ZS5iaXp4bXBwOnZhbml0YXN2aXRhZUBqYWJiZXJoZWFkLnRrP29tZW1vLXNpZC0y\n" + + "MDkzNjgxNTQ1PTYyODlhYTNiZDhhNTAxYTM2MzIyYTBmODk0ZjhkMWQ5NzE4ZGVk\n" + + "MDM2MTYwMzlmMWNmNDhiMmE0MWVlMzU5MjCPFIAAAAAAEgB0cHJvb2ZAbWV0YWNv\n" + + "ZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQt\n" + + "MTk5MTQxODIwPWY0YThmZjg0MDA0MzkzYTg3ZjcwMTNjNjAwNjViZGM4OWIxMTY5\n" + + "ZWJjZmI4MDYwYzRmOTY2OWI0M2JhMGM4MTSQFIAAAAAAEgB1cHJvb2ZAbWV0YWNv\n" + + "ZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVyaGVhZC50az9vbWVtby1zaWQt\n" + + "MTQyOTY3NzEyNT1lOGE3YjEyMzZiODUwYjQ2N2E1MDkyYzBiZGZlYTg2YTUzZTM2\n" + + "ODQyODNhMWQ1ZmUyNmVmNTg3MmRkMGFlZjQxAAoJEKAn2y8+HhGKNEoP/j/1WwVN\n" + + "9h17ZpRvz9ZD3e9WN8iwYZnGTvjuAuCJPpCznfEOszP4Gcy/ixPRQXrnAYaqCoFL\n" + + "08a+dambbFPhGduVgoSkItwNcl6KyPv0Q6dDykXfKXBHSTAdvMmhhL51/f3J0Sxa\n" + + "xzJ8ev2OzOqJIUzkXtRHwYrdrclJrX/iLankL1lBZzDXJwf+IpAPczBe2a/S/sYz\n" + + "NAeiH/OvipNpchQQG6lkQF0duzdx1OudbIGQYuWzVtep8uokIJcrVxMleVZLEvfF\n" + + "ZkR5woOSloTbMfB/Kb1MEL2w9R8trJ5F81ZvXolL/DDIEqLTbZEpP9pUSYLe+eWO\n" + + "6cszMA2jBwIPYgamg3JKXS/zdDdqnV++rF5/IztLAv1T8mqClOUstU88LuYGBchu\n" + + "N4hLgeIB+/q0EmCTWIM/ewnMh/KEZHVXJI+ljoMjwS2dZSkV8KcmTVtU1JccR0Ud\n" + + "HxpYtrcwZaUgzFkPJf3WvFidt7rDs32DJCZiM/NKhzIdukyvG6DAFzabveR8XTlz\n" + + "evZDNn6gVw04v+jtX++YbasBOI4t8wj2mtqCs3f2bLTRx1fuBu+DyNjAbJT+925G\n" + + "kbT4gBkb/8aJOOvwWsT4ljXqngXykk39eAVSmRNlsa2Wnv5v/5Fh4gGY5ecfdLH0\n" + + "z14X/fFoVPkp+g+FOD9lpEP96swULamszCG4wsNABBMBCgHqAhsDBQsJCAcDBRUK\n" + + "CQgLBRYCAwEAAh4BAheAFiEEf5EW/qkKWYOTbHz6oCfbLz4eEYoFAl/IG5qQFIAA\n" + + "AAAAEgB1cHJvb2ZAbWV0YWNvZGUuYml6eG1wcDp2YW5pdGFzdml0YWVAamFiYmVy\n" + + "aGVhZC50az9vbWVtby1zaWQtMTQyOTY3NzEyNT1lOGE3YjEyMzZiODUwYjQ2N2E1\n" + + "MDkyYzBiZGZlYTg2YTUzZTM2ODQyODNhMWQ1ZmUyNmVmNTg3MmRkMGFlZjQxjxSA\n" + + "AAAAABIAdHByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRhc3ZpdGFlQGphYmJl\n" + + "cmhlYWQudGs/b21lbW8tc2lkLTE5OTE0MTgyMD1mNGE4ZmY4NDAwNDM5M2E4N2Y3\n" + + "MDEzYzYwMDY1YmRjODliMTE2OWViY2ZiODA2MGM0Zjk2NjliNDNiYTBjODE0kBSA\n" + + "AAAAABIAdXByb29mQG1ldGFjb2RlLmJpenhtcHA6dmFuaXRhc3ZpdGFlQGphYmJl\n" + + "cmhlYWQudGs/b21lbW8tc2lkLTIwOTM2ODE1NDU9NjI4OWFhM2JkOGE1MDFhMzYz\n" + + "MjJhMGY4OTRmOGQxZDk3MThkZWQwMzYxNjAzOWYxY2Y0OGIyYTQxZWUzNTkyMAAK\n" + + "CRCgJ9svPh4Rijb9D/9LGdGSSD7DhHEd9vMKHe7PL+pysg2K/aTm+XMHKozCOkaf\n" + + "hnF6ltSW5vjCXOaEnMtKpH5vnb/RL4tKuWLj/CVC0L1rGAa0MQ0b4AG4fWlbctw1\n" + + "I7PAEES+fUFLftrnMgxYF97gM/yGp9a74IfIKHcZ+sVs7dw9Sa8kDCtg3KBCFG4h\n" + + "Y5PqUDVlQjWDU0E17y7Vx+0yT9Gfw6esDoao1vCGJhe+AZRZdr5fasdkejUhnZEf\n" + + "We1NhGbpfQSh92blSu8YxDhM1N0JFL2WOpZ0JVi/N5rYBRsh9gxHSRhsk1xu9EMU\n" + + "OWORX80bBrvN0md8N4F2SWtuzOz/CpJejrxqvx07lJSW+2nRA9TESg1vdPQxhlBz\n" + + "NL4HgixHxMhURjPYcvNa8ZPMC40aqukAwt7s/JVMpGwAqOPrCZX3afsbp/OOX3Jp\n" + + "F5B0V15GNuoDww/yIGAl+7x8QA3L6eDfjgEHtVYKKJEWN/SHak6QN6M4/ku3zsxk\n" + + "gguhr+hZ+BPXh3+Pk1NGiAKBpo+nnUKBcUpXWV4ie1E8DsNhLGgn2Gci2aTt+CW8\n" + + "LboPCZJGokhnQhimElUbgZ/8ggsSC6fYqA6qe1EONjw0TSerMJETZqeH/fwJURWN\n" + + "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\n" + + "=UC83\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + @Test + public void longAsciiArmoredMessageIsAsciiArmored() throws IOException { + byte[] asciiArmoredBytes = longAsciiArmoredMessage.getBytes(StandardCharsets.UTF_8); + assertTrue(asciiArmoredBytes.length > OpenPgpInputStream.MAX_BUFFER_SIZE); + ByteArrayInputStream asciiIn = new ByteArrayInputStream(asciiArmoredBytes); + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(asciiIn); + + assertTrue(openPgpInputStream.isAsciiArmored()); + assertFalse(openPgpInputStream.isNonOpenPgp()); + assertFalse(openPgpInputStream.isBinaryOpenPgp()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(openPgpInputStream, out); + + assertArrayEquals(asciiArmoredBytes, out.toByteArray()); + } + + @Test + public void shortBinaryOpenPgpMessageIsBinary() throws IOException { + String asciiArmored = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wV4DR2b2udXyHrYSAQdA8GwHRf0XsR9FsPL36oNvdoBZPXddygb2iYdGBJko9X0w\n" + + "VQqhsjX54WCiMBQx4ma0om49rAWHCk4h4IAq5+WsdN+xCklAUXsbIA7BZUaXfzEB\n" + + "0j8BpWiU6SJ9YB23OtZSWl/5bu8hx1bnKd5ZM0D5VP2QF772Ci/oAGywSuOA+C6b\n" + + "G4Bkf1xlQ9vctnBpMix3xUA=\n" + + "=95Eb\n" + + "-----END PGP MESSAGE-----\n"; + // Dearmor the data to get binary openpgp data + ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(asciiArmored.getBytes(StandardCharsets.UTF_8))); + ByteArrayOutputStream binaryOut = new ByteArrayOutputStream(); + Streams.pipeAll(armoredInputStream, binaryOut); + + byte[] binaryBytes = binaryOut.toByteArray(); + ByteArrayInputStream binaryIn = new ByteArrayInputStream(binaryBytes); + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(binaryIn); + + assertTrue(openPgpInputStream.isBinaryOpenPgp()); + assertFalse(openPgpInputStream.isAsciiArmored()); + assertFalse(openPgpInputStream.isNonOpenPgp()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(openPgpInputStream, out); + assertArrayEquals(binaryBytes, out.toByteArray()); + } + + @Test + public void longBinaryOpenPgpMessageIsBinary() throws IOException { + // Dearmor the data to get binary openpgp data + ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(longAsciiArmoredMessage.getBytes(StandardCharsets.UTF_8))); + ByteArrayOutputStream binaryOut = new ByteArrayOutputStream(); + Streams.pipeAll(armoredInputStream, binaryOut); + + byte[] binaryBytes = binaryOut.toByteArray(); + ByteArrayInputStream binaryIn = new ByteArrayInputStream(binaryBytes); + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(binaryIn); + + assertTrue(openPgpInputStream.isBinaryOpenPgp()); + assertFalse(openPgpInputStream.isAsciiArmored()); + assertFalse(openPgpInputStream.isNonOpenPgp()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(openPgpInputStream, out); + assertArrayEquals(binaryBytes, out.toByteArray()); + } + + @Test + public void emptyStreamTest() throws IOException { + ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]); + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(in); + + assertFalse(openPgpInputStream.isBinaryOpenPgp()); + assertFalse(openPgpInputStream.isAsciiArmored()); + assertTrue(openPgpInputStream.isNonOpenPgp()); + } +} From 8172aa108305068d9d31e2d6b2e8dc519b2e0b4c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 22 Apr 2022 20:56:02 +0200 Subject: [PATCH 0188/1204] Update documentation of #96 workaround --- .../src/main/java/org/pgpainless/util/ArmorUtils.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index e862652d..48bdc0cc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -536,11 +536,9 @@ public final class ArmorUtils { * to read all PGPKeyRings properly, we apparently have to make sure that the {@link InputStream} that is given * as constructor argument is a PGPUtil.BufferedInputStreamExt. * Since {@link PGPUtil#getDecoderStream(InputStream)} will return an {@link org.bouncycastle.bcpg.ArmoredInputStream} - * if the underlying input stream contains armored data, we have to nest two method calls to make sure that the + * if the underlying input stream contains armored data, we first dearmor the data ourselves to make sure that the * end-result is a PGPUtil.BufferedInputStreamExt. * - * This is a hacky solution. - * * @param inputStream input stream * @return BufferedInputStreamExt * From 230725f6ff81db5207daaf3a8a25ede11c66431a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 22 Apr 2022 21:33:13 +0200 Subject: [PATCH 0189/1204] Add option to force handling of data as non-openpgp --- .../ConsumerOptions.java | 16 ++++++ .../DecryptionStreamFactory.java | 2 +- .../OpenPgpInputStream.java | 57 +++++++------------ 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index c23ede3d..7b8d1e70 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -36,6 +36,7 @@ public class ConsumerOptions { private boolean ignoreMDCErrors = false; + private boolean forceNonOpenPgpData = false; private Date verifyNotBefore = null; private Date verifyNotAfter = new Date(); @@ -296,6 +297,21 @@ public class ConsumerOptions { return ignoreMDCErrors; } + /** + * Force PGPainless to handle the data provided by the {@link InputStream} as non-OpenPGP data. + * This workaround might come in handy if PGPainless accidentally mistakes the data for binary OpenPGP data. + * + * @return options + */ + public ConsumerOptions forceNonOpenPgpData() { + this.forceNonOpenPgpData = true; + return this; + } + + boolean isForceNonOpenPgpData() { + return forceNonOpenPgpData; + } + /** * Specify the {@link MissingKeyPassphraseStrategy}. * This strategy defines, how missing passphrases for unlocking secret keys are handled. diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 7f8e7f0d..a8847c33 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -134,7 +134,7 @@ public final class DecryptionStreamFactory { PGPObjectFactory objectFactory; // Non-OpenPGP data. We are probably verifying detached signatures - if (openPgpIn.isNonOpenPgp()) { + if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) { outerDecodingStream = openPgpIn; pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null); return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java index c3668daa..d15f2ffb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java @@ -18,7 +18,7 @@ public class OpenPgpInputStream extends BufferedInputStream { private static final byte[] ARMOR_HEADER = "-----BEGIN PGP ".getBytes(Charset.forName("UTF8")); // Buffer beginning bytes of the data - public static final int MAX_BUFFER_SIZE = 8192; + public static final int MAX_BUFFER_SIZE = 8192 * 2; private final byte[] buffer; private final int bufferLen; @@ -53,6 +53,14 @@ public class OpenPgpInputStream extends BufferedInputStream { return false; } + /** + * This method is still brittle. + * Basically we try to parse OpenPGP packets from the buffer. + * If we run into exceptions, then we know that the data is non-OpenPGP'ish. + * + * This breaks down though if we read plausible garbage where the data accidentally makes sense, + * or valid, yet incomplete packets (remember, we are still only working on a portion of the data). + */ private void determineIsBinaryOpenPgp() { if (bufferLen == -1) { // Empty data @@ -62,47 +70,24 @@ public class OpenPgpInputStream extends BufferedInputStream { try { ByteArrayInputStream bufferIn = new ByteArrayInputStream(buffer, 0, bufferLen); PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(bufferIn); + + boolean containsPackets = false; while (objectFactory.nextObject() != null) { - // read all packets in buffer + containsPackets = true; + // read all packets in buffer - hope to confirm invalid data via thrown IOExceptions } - containsOpenPgpPackets = true; + containsOpenPgpPackets = containsPackets; + } catch (IOException e) { - if (e.getMessage().contains("premature end of stream in PartialInputStream")) { - // We *probably* hit valid, but large OpenPGP data - // This is not an optimal way of determining the nature of data, but probably the best - // we can get from BC. - containsOpenPgpPackets = true; - } - // else: seemingly random, non-OpenPGP data - } - } + String msg = e.getMessage(); - private boolean startsWith(byte[] bytes, byte[] subsequence, int bufferLen) { - return indexOfSubsequence(bytes, subsequence, bufferLen) == 0; - } + // If true, we *probably* hit valid, but large OpenPGP data (not sure though) :/ + // Otherwise we hit garbage and can be sure that this is no OpenPGP data \o/ + containsOpenPgpPackets = (msg != null && msg.contains("premature end of stream in PartialInputStream")); - private int indexOfSubsequence(byte[] bytes, byte[] subsequence, int bufferLen) { - if (bufferLen == -1) { - return -1; + // This is not an optimal way of determining the nature of data, but probably the best + // we can do :/ } - // Naive implementation - // TODO: Could be improved by using e.g. Knuth-Morris-Pratt algorithm. - for (int i = 0; i < bufferLen; i++) { - if ((i + subsequence.length) <= bytes.length) { - boolean found = true; - for (int j = 0; j < subsequence.length; j++) { - if (bytes[i + j] != subsequence[j]) { - found = false; - break; - } - } - - if (found) { - return i; - } - } - } - return -1; } private boolean startsWithIgnoringWhitespace(byte[] bytes, byte[] subsequence, int bufferLen) { From 4764202ac9468da1449fe38748175c0a60d0e872 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 22 Apr 2022 22:43:19 +0200 Subject: [PATCH 0190/1204] Change visibility of BcPGPHashContextContentSignerBuilder constructor --- .../BcPGPHashContextContentSignerBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java index 474ee9ef..df6f6ca3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java @@ -42,7 +42,7 @@ class BcPGPHashContextContentSignerBuilder extends PGPHashContextContentSignerBu private final MessageDigest messageDigest; private final HashAlgorithm hashAlgorithm; - public BcPGPHashContextContentSignerBuilder(MessageDigest messageDigest) { + BcPGPHashContextContentSignerBuilder(MessageDigest messageDigest) { this.messageDigest = messageDigest; this.hashAlgorithm = HashAlgorithm.fromName(messageDigest.getAlgorithm()); if (hashAlgorithm == null) { From 6c983d66e0dcf4f87258a7a35f6b78d748bb972a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 22 Apr 2022 22:45:39 +0200 Subject: [PATCH 0191/1204] Take hash algorithm usage date into account when checking algorithm acceptance --- .../java/org/pgpainless/policy/Policy.java | 151 +++++++++++++++++- .../consumer/SignatureValidator.java | 5 +- .../org/pgpainless/policy/PolicyTest.java | 31 +++- 3 files changed, 175 insertions(+), 12 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java index e64899c0..17804e5e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java @@ -6,7 +6,9 @@ package org.pgpainless.policy; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.EnumMap; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -18,6 +20,7 @@ import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.DateUtil; import org.pgpainless.util.NotationRegistry; /** @@ -301,11 +304,39 @@ public final class Policy { public static final class HashAlgorithmPolicy { private final HashAlgorithm defaultHashAlgorithm; - private final List acceptableHashAlgorithms; + private final Map acceptableHashAlgorithmsAndTerminationDates; - public HashAlgorithmPolicy(HashAlgorithm defaultHashAlgorithm, List acceptableHashAlgorithms) { + /** + * Create a {@link HashAlgorithmPolicy} which accepts all {@link HashAlgorithm HashAlgorithms} from the + * given map, if the queried usage date is BEFORE the respective termination date. + * A termination date value of
null
means no termination, resulting in the algorithm being + * acceptable, regardless of usage date. + * + * @param defaultHashAlgorithm default hash algorithm + * @param algorithmTerminationDates map of acceptable algorithms and their termination dates + */ + public HashAlgorithmPolicy(@Nonnull HashAlgorithm defaultHashAlgorithm, @Nonnull Map algorithmTerminationDates) { this.defaultHashAlgorithm = defaultHashAlgorithm; - this.acceptableHashAlgorithms = Collections.unmodifiableList(acceptableHashAlgorithms); + this.acceptableHashAlgorithmsAndTerminationDates = algorithmTerminationDates; + } + + /** + * Create a {@link HashAlgorithmPolicy} which accepts all {@link HashAlgorithm HashAlgorithms} listed in + * the given list, regardless of usage date. + * + * @param defaultHashAlgorithm default hash algorithm (e.g. used as fallback if negotiation fails) + * @param acceptableHashAlgorithms list of acceptable hash algorithms + */ + public HashAlgorithmPolicy(@Nonnull HashAlgorithm defaultHashAlgorithm, @Nonnull List acceptableHashAlgorithms) { + this(defaultHashAlgorithm, Collections.unmodifiableMap(listToMap(acceptableHashAlgorithms))); + } + + private static Map listToMap(@Nonnull List algorithms) { + Map algorithmsAndTerminationDates = new HashMap<>(); + for (HashAlgorithm algorithm : algorithms) { + algorithmsAndTerminationDates.put(algorithm, null); + } + return algorithmsAndTerminationDates; } /** @@ -319,17 +350,17 @@ public final class Policy { } /** - * Return true if the given hash algorithm is acceptable by this policy. + * Return true if the given hash algorithm is currently acceptable by this policy. * * @param hashAlgorithm hash algorithm * @return true if the hash algorithm is acceptable, false otherwise */ - public boolean isAcceptable(HashAlgorithm hashAlgorithm) { - return acceptableHashAlgorithms.contains(hashAlgorithm); + public boolean isAcceptable(@Nonnull HashAlgorithm hashAlgorithm) { + return isAcceptable(hashAlgorithm, new Date()); } /** - * Return true if the given hash algorithm is acceptable by this policy. + * Return true if the given hash algorithm is currently acceptable by this policy. * * @param algorithmId hash algorithm * @return true if the hash algorithm is acceptable, false otherwise @@ -344,6 +375,39 @@ public final class Policy { } } + /** + * Return true, if the given algorithm is acceptable for the given usage date. + * + * @param hashAlgorithm algorithm + * @param usageDate usage date (e.g. signature creation time) + * + * @return acceptance + */ + public boolean isAcceptable(@Nonnull HashAlgorithm hashAlgorithm, @Nonnull Date usageDate) { + if (!acceptableHashAlgorithmsAndTerminationDates.containsKey(hashAlgorithm)) { + return false; + } + + // Check termination date + Date terminationDate = acceptableHashAlgorithmsAndTerminationDates.get(hashAlgorithm); + if (terminationDate == null) { + return true; + } + + // Reject if usage date is past termination date + return terminationDate.after(usageDate); + } + + public boolean isAcceptable(int algorithmId, @Nonnull Date usageDate) { + try { + HashAlgorithm algorithm = HashAlgorithm.requireFromId(algorithmId); + return isAcceptable(algorithm, usageDate); + } catch (NoSuchElementException e) { + // Unknown algorithm is not acceptable + return false; + } + } + /** * The default signature hash algorithm policy of PGPainless. * Note that this policy is only used for non-revocation signatures. @@ -352,6 +416,44 @@ public final class Policy { * @return default signature hash algorithm policy */ public static HashAlgorithmPolicy defaultSignatureAlgorithmPolicy() { + return smartSignatureHashAlgorithmPolicy(); + } + + /** + * {@link HashAlgorithmPolicy} which takes the date of the algorithm usage into consideration. + * If the policy has a termination date for a given algorithm, and the usage date is after that termination + * date, the algorithm is rejected. + * + * This policy is inspired by Sequoia-PGP's collision resistant algorithm policy. + * + * @see + * Sequoia-PGP's Collision Resistant Algorithm Policy + * + * @return smart signature algorithm policy + */ + public static HashAlgorithmPolicy smartSignatureHashAlgorithmPolicy() { + Map algorithmDateMap = new HashMap<>(); + + algorithmDateMap.put(HashAlgorithm.MD5, DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")); + algorithmDateMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); + algorithmDateMap.put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); + algorithmDateMap.put(HashAlgorithm.SHA224, null); + algorithmDateMap.put(HashAlgorithm.SHA256, null); + algorithmDateMap.put(HashAlgorithm.SHA384, null); + algorithmDateMap.put(HashAlgorithm.SHA512, null); + + return new HashAlgorithmPolicy(HashAlgorithm.SHA512, algorithmDateMap); + } + + /** + * {@link HashAlgorithmPolicy} which only accepts signatures made using algorithms which are acceptable + * according to 2022 standards. + * + * Particularly this policy only accepts algorithms from the SHA2 family. + * + * @return static signature algorithm policy + */ + public static HashAlgorithmPolicy static2022SignatureAlgorithmPolicy() { return new HashAlgorithmPolicy(HashAlgorithm.SHA512, Arrays.asList( HashAlgorithm.SHA224, HashAlgorithm.SHA256, @@ -366,6 +468,41 @@ public final class Policy { * @return default revocation signature hash algorithm policy */ public static HashAlgorithmPolicy defaultRevocationSignatureHashAlgorithmPolicy() { + return smartRevocationSignatureHashAlgorithmPolicy(); + } + + /** + * Revocation Signature {@link HashAlgorithmPolicy} which takes the date of the algorithm usage + * into consideration. + * If the policy has a termination date for a given algorithm, and the usage date is after that termination + * date, the algorithm is rejected. + * + * This policy is inspired by Sequoia-PGP's collision resistant algorithm policy. + * + * @see + * Sequoia-PGP's Collision Resistant Algorithm Policy + * + * @return smart signature revocation algorithm policy + */ + public static HashAlgorithmPolicy smartRevocationSignatureHashAlgorithmPolicy() { + Map algorithmDateMap = new HashMap<>(); + + algorithmDateMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); + algorithmDateMap.put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); + algorithmDateMap.put(HashAlgorithm.SHA224, null); + algorithmDateMap.put(HashAlgorithm.SHA256, null); + algorithmDateMap.put(HashAlgorithm.SHA384, null); + algorithmDateMap.put(HashAlgorithm.SHA512, null); + + return new HashAlgorithmPolicy(HashAlgorithm.SHA512, algorithmDateMap); + } + + /** + * Hash algorithm policy for revocation signatures, which accepts SHA1 & SHA2 algorithms, as well as RIPEMD160. + * + * @return static revocation signature hash algorithm policy + */ + public static HashAlgorithmPolicy static2022RevocationSignatureHashAlgorithmPolicy() { return new HashAlgorithmPolicy(HashAlgorithm.SHA512, Arrays.asList( HashAlgorithm.RIPEMD160, HashAlgorithm.SHA1, diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java index 51bfa7c3..6a10e1f3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java @@ -195,8 +195,9 @@ public abstract class SignatureValidator { try { HashAlgorithm hashAlgorithm = HashAlgorithm.requireFromId(signature.getHashAlgorithm()); Policy.HashAlgorithmPolicy hashAlgorithmPolicy = getHashAlgorithmPolicyForSignature(signature, policy); - if (!hashAlgorithmPolicy.isAcceptable(signature.getHashAlgorithm())) { - throw new SignatureValidationException("Signature uses unacceptable hash algorithm " + hashAlgorithm); + if (!hashAlgorithmPolicy.isAcceptable(signature.getHashAlgorithm(), signature.getCreationTime())) { + throw new SignatureValidationException("Signature uses unacceptable hash algorithm " + hashAlgorithm + + " (Signature creation time: " + DateUtil.formatUTCDate(signature.getCreationTime()) + ")"); } } catch (NoSuchElementException e) { throw new SignatureValidationException("Signature uses unknown hash algorithm " + signature.getHashAlgorithm()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java index 6d86f8d9..9959be68 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java @@ -9,6 +9,9 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -16,6 +19,7 @@ import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.DateUtil; public class PolicyTest { @@ -33,11 +37,23 @@ public class PolicyTest { policy.setSymmetricKeyDecryptionAlgorithmPolicy(new Policy.SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_256, Arrays.asList(SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.AES_192, SymmetricKeyAlgorithm.AES_128, SymmetricKeyAlgorithm.BLOWFISH))); - policy.setSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, - Arrays.asList(HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256))); + Map sigHashAlgoMap = new HashMap<>(); + sigHashAlgoMap.put(HashAlgorithm.SHA512, null); + sigHashAlgoMap.put(HashAlgorithm.SHA384, null); + sigHashAlgoMap.put(HashAlgorithm.SHA256, null); + sigHashAlgoMap.put(HashAlgorithm.SHA224, null); + sigHashAlgoMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); + policy.setSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, sigHashAlgoMap)); + Map revHashAlgoMap = new HashMap<>(); + revHashAlgoMap.put(HashAlgorithm.SHA512, null); + revHashAlgoMap.put(HashAlgorithm.SHA384, null); + revHashAlgoMap.put(HashAlgorithm.SHA256, null); + revHashAlgoMap.put(HashAlgorithm.SHA224, null); + revHashAlgoMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); + revHashAlgoMap.put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); policy.setRevocationSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, - Arrays.asList(HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256, HashAlgorithm.SHA224, HashAlgorithm.SHA1))); + revHashAlgoMap)); policy.setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); } @@ -92,12 +108,17 @@ public class PolicyTest { public void testAcceptableSignatureHashAlgorithm() { assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512)); assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512.getAlgorithmId())); + // Usage date before termination date -> acceptable + assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); + assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); } @Test public void testUnacceptableSignatureHashAlgorithm() { assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1)); assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId())); + assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); + assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); } @Test @@ -109,12 +130,16 @@ public class PolicyTest { public void testAcceptableRevocationSignatureHashAlgorithm() { assertTrue(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA384)); assertTrue(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA384.getAlgorithmId())); + assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); + assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); } @Test public void testUnacceptableRevocationSignatureHashAlgorithm() { assertFalse(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.RIPEMD160)); assertFalse(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.RIPEMD160.getAlgorithmId())); + assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); + assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); } @Test From 9b8cf37dd1e7286634b5bd3d9664d7bdc8ed96f2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 22 Apr 2022 23:06:40 +0200 Subject: [PATCH 0192/1204] Use smart hash algorithm policy as default revocation hash policy --- .../java/org/pgpainless/policy/Policy.java | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java index 17804e5e..36582bf0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java @@ -468,33 +468,7 @@ public final class Policy { * @return default revocation signature hash algorithm policy */ public static HashAlgorithmPolicy defaultRevocationSignatureHashAlgorithmPolicy() { - return smartRevocationSignatureHashAlgorithmPolicy(); - } - - /** - * Revocation Signature {@link HashAlgorithmPolicy} which takes the date of the algorithm usage - * into consideration. - * If the policy has a termination date for a given algorithm, and the usage date is after that termination - * date, the algorithm is rejected. - * - * This policy is inspired by Sequoia-PGP's collision resistant algorithm policy. - * - * @see - * Sequoia-PGP's Collision Resistant Algorithm Policy - * - * @return smart signature revocation algorithm policy - */ - public static HashAlgorithmPolicy smartRevocationSignatureHashAlgorithmPolicy() { - Map algorithmDateMap = new HashMap<>(); - - algorithmDateMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); - algorithmDateMap.put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); - algorithmDateMap.put(HashAlgorithm.SHA224, null); - algorithmDateMap.put(HashAlgorithm.SHA256, null); - algorithmDateMap.put(HashAlgorithm.SHA384, null); - algorithmDateMap.put(HashAlgorithm.SHA512, null); - - return new HashAlgorithmPolicy(HashAlgorithm.SHA512, algorithmDateMap); + return smartSignatureHashAlgorithmPolicy(); } /** From 9b11b943545997615126d6b71debdc8255197a92 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 22 Apr 2022 23:06:46 +0200 Subject: [PATCH 0193/1204] Update CHANGELOG --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25ab7ef0..ca3c8c29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.2.1-SNAPSHOT +- Bump `sop-java` dependency to `1.2.2` +- Add experimental support for creating signatures over pre-calculated `MessageDigest` objects. + - `BcHashContextSigner.signHashContext()` can be used to create OpenPGP signatures over manually hashed data. + This allows applications to do the hashing themselves. +- Harden detection of binary/ascii armored/non-OpenPGP data +- Add `ConsumerOptions.forceNonOpenPgpData()` to force PGPainless to handle data as non-OpenPGP data + - This is a workaround for when PGPainless accidentally mistakes non-OpenPGP data for binary OpenPGP data +- Implement "smart" hash algorithm policies, which take the 'usage-date' for algorithms into account + - This allows for fine-grained signature hash algorithm policing with usage termination dates +- Switch to smart signature hash algorithm policies by default + - PGPainless now accepts SHA-1 signatures if they were made before 2013-02-01 + - We also now accept RIPEMD160 signatures if they were made before 2013-02-01 + - We further accept MD5 signatures made prior to 1997-02-01 + ## 1.2.0 - Improve exception hierarchy for key-related exceptions From 4698b6801551279bdcc65b16913e415256dcbcf3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 23 Apr 2022 01:47:44 +0200 Subject: [PATCH 0194/1204] Fix javadoc generation --- pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java index 36582bf0..83b89791 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java @@ -472,7 +472,7 @@ public final class Policy { } /** - * Hash algorithm policy for revocation signatures, which accepts SHA1 & SHA2 algorithms, as well as RIPEMD160. + * Hash algorithm policy for revocation signatures, which accepts SHA1 and SHA2 algorithms, as well as RIPEMD160. * * @return static revocation signature hash algorithm policy */ From 6bf1649cb729c37a0a08614c151196ab2f37e2f1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 26 Apr 2022 00:39:25 +0200 Subject: [PATCH 0195/1204] Bump slf4j to 1.7.36 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index c556858b..928903b4 100644 --- a/version.gradle +++ b/version.gradle @@ -9,7 +9,7 @@ allprojects { pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' - slf4jVersion = '1.7.32' + slf4jVersion = '1.7.36' logbackVersion = '1.2.9' junitVersion = '5.8.2' sopJavaVersion = '1.2.2' From 249cab6eab8c82576e6b48f901406395c904181e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 26 Apr 2022 00:39:40 +0200 Subject: [PATCH 0196/1204] Bump logback to 1.2.11 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 928903b4..27856c41 100644 --- a/version.gradle +++ b/version.gradle @@ -10,7 +10,7 @@ allprojects { javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' slf4jVersion = '1.7.36' - logbackVersion = '1.2.9' + logbackVersion = '1.2.11' junitVersion = '5.8.2' sopJavaVersion = '1.2.2' } From 009ef616995e52777b6975398ee995f274c32912 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 26 Apr 2022 00:41:39 +0200 Subject: [PATCH 0197/1204] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca3c8c29..f19eb0d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ SPDX-License-Identifier: CC0-1.0 ## 1.2.1-SNAPSHOT - Bump `sop-java` dependency to `1.2.2` +- Bump `slf4j` dependency to `1.7.36` +- Bump `logback` dependency to `1.2.11` - Add experimental support for creating signatures over pre-calculated `MessageDigest` objects. - `BcHashContextSigner.signHashContext()` can be used to create OpenPGP signatures over manually hashed data. This allows applications to do the hashing themselves. From 71d5007edcddbf52a06df1b5b4232fb24cee8e44 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 26 Apr 2022 02:11:53 +0200 Subject: [PATCH 0198/1204] Add dependency diagram --- ECOSYSTEM.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/ECOSYSTEM.md b/ECOSYSTEM.md index 0652bdef..b0b4125b 100644 --- a/ECOSYSTEM.md +++ b/ECOSYSTEM.md @@ -8,6 +8,45 @@ SPDX-License-Identifier: Apache-2.0 PGPainless consists of an ecosystem of different libraries and projects. +```mermaid +flowchart TB + subgraph SOP-JAVA + sop-java-picocli-->sop-java + end + subgraph PGPAINLESS + pgpainless-sop-->pgpainless-core + pgpainless-sop-->sop-java + pgpainless-cli-->pgpainless-sop + pgpainless-cli-->sop-java-picocli + end + subgraph WKD-JAVA + wkd-java-cli-->wkd-java + wkd-test-suite-->wkd-java + wkd-test-suite-->pgpainless-core + end + subgraph CERT-D-JAVA + pgp-cert-d-java-->pgp-certificate-store + pgp-cert-d-java-jdbc-sqlite-lookup-->pgp-cert-d-java + end + subgraph CERT-D-PGPAINLESS + pgpainless-cert-d-->pgpainless-core + pgpainless-cert-d-->pgp-cert-d-java + pgpainless-cert-d-cli-->pgpainless-cert-d + pgpainless-cert-d-cli-->pgp-cert-d-java-jdbc-sqlite-lookup + end + subgraph VKS-JAVA + vks-java-cli-->vks-java + end + subgraph PGPEASY + pgpeasy-->pgpainless-cli + pgpeasy-->wkd-java-cli + pgpeasy-->vks-java-cli + pgpeasy-->pgpainless-cert-d-cli + end + wkd-java-cli-->pgpainless-cert-d + wkd-java-->pgp-certificate-store +``` + ## [PGPainless](https://github.com/pgpainless/pgpainless) The main repository contains the following components: From a983f99644b02bee631d9b24cdc7303718ec151f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 29 Apr 2022 17:01:11 +0200 Subject: [PATCH 0199/1204] Bump sop-java to 1.2.3 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 27856c41..b3bd537e 100644 --- a/version.gradle +++ b/version.gradle @@ -12,6 +12,6 @@ allprojects { slf4jVersion = '1.7.36' logbackVersion = '1.2.11' junitVersion = '5.8.2' - sopJavaVersion = '1.2.2' + sopJavaVersion = '1.2.3' } } From 3b00eb3334ca7e6d6708abff06b5349aa136bd66 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 29 Apr 2022 17:01:22 +0200 Subject: [PATCH 0200/1204] Update changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f19eb0d1..e44d2d43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,8 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.2.1-SNAPSHOT -- Bump `sop-java` dependency to `1.2.2` +## 1.2.1 +- Bump `sop-java` dependency to `1.2.3` - Bump `slf4j` dependency to `1.7.36` - Bump `logback` dependency to `1.2.11` - Add experimental support for creating signatures over pre-calculated `MessageDigest` objects. From c3f6ca2ab8d592186d85fac5beea1f451f8e56f5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 29 Apr 2022 17:02:11 +0200 Subject: [PATCH 0201/1204] PGPainless 1.2.1 --- README.md | 2 +- build.gradle | 1 + pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 503475d9..e27a0c3d 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.2.0' + implementation 'org.pgpainless:pgpainless-core:1.2.1' } ``` diff --git a/build.gradle b/build.gradle index 33e493f0..3c4db676 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,7 @@ allprojects { repositories { mavenCentral() + mavenLocal() } // Reproducible Builds diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index a656eb4c..fd635594 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.2.0" + implementation "org.pgpainless:pgpainless-sop:1.2.1" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.2.0 + 1.2.1 ... diff --git a/version.gradle b/version.gradle index b3bd537e..d1efa621 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.2.1' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From 51cd75533bb0a468ed6e3e2f74b03918836f0405 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 29 Apr 2022 17:05:59 +0200 Subject: [PATCH 0202/1204] PGPainless 1.2.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index d1efa621..452291ef 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.2.1' - isSnapshot = false + shortVersion = '1.2.2' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From b980fcd7b1fff3c12e07cdc8b5f8d47ab0091a27 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 29 Apr 2022 22:49:45 +0200 Subject: [PATCH 0203/1204] EncryptionOptions.addRecipients(collection): Disallow empty collections Fixes #281 --- .../pgpainless/encryption_signing/EncryptionOptions.java | 6 ++++++ .../encryption_signing/EncryptionOptionsTest.java | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index ca7b8ddb..99059c0a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -107,6 +107,9 @@ public class EncryptionOptions { * @return this */ public EncryptionOptions addRecipients(Iterable keys) { + if (!keys.iterator().hasNext()) { + throw new IllegalArgumentException("Set of recipient keys cannot be empty."); + } for (PGPPublicKeyRing key : keys) { addRecipient(key); } @@ -122,6 +125,9 @@ public class EncryptionOptions { * @return this */ public EncryptionOptions addRecipients(@Nonnull Iterable keys, @Nonnull EncryptionKeySelector selector) { + if (!keys.iterator().hasNext()) { + throw new IllegalArgumentException("Set of recipient keys cannot be empty."); + } for (PGPPublicKeyRing key : keys) { addRecipient(key, selector); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 488e5918..40a336e8 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -116,6 +116,14 @@ public class EncryptionOptionsTest { assertTrue(encryptionKeys.contains(encryptStorage)); } + @Test + public void testAddEmptyRecipientsFails() { + EncryptionOptions options = new EncryptionOptions(); + assertThrows(IllegalArgumentException.class, () -> options.addRecipients(Collections.emptyList())); + assertThrows(IllegalArgumentException.class, () -> options.addRecipients(Collections.emptyList(), + encryptionCapableKeys -> encryptionCapableKeys)); + } + @Test public void testAddEmptyPassphraseFails() { EncryptionOptions options = new EncryptionOptions(); From 2b37c4c9cb7ca3c52c9c8327a5d6cabe3d2a7418 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 3 May 2022 11:23:40 +0200 Subject: [PATCH 0204/1204] Deprecate Policy.*.default*Policy() methods in favor of methods with more expressive names You cannot tell, what defaultHashAlgorithmPolicy() really means. Therefore the default methods were deprecated in favor for more expressive methods --- .../java/org/pgpainless/policy/Policy.java | 73 +++++++++++++++++-- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java index 83b89791..d1eca6ba 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java @@ -31,17 +31,17 @@ public final class Policy { private static Policy INSTANCE; private HashAlgorithmPolicy signatureHashAlgorithmPolicy = - HashAlgorithmPolicy.defaultSignatureAlgorithmPolicy(); + HashAlgorithmPolicy.smartSignatureHashAlgorithmPolicy(); private HashAlgorithmPolicy revocationSignatureHashAlgorithmPolicy = - HashAlgorithmPolicy.defaultRevocationSignatureHashAlgorithmPolicy(); + HashAlgorithmPolicy.smartSignatureHashAlgorithmPolicy(); private SymmetricKeyAlgorithmPolicy symmetricKeyEncryptionAlgorithmPolicy = - SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyEncryptionAlgorithmPolicy(); + SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022(); private SymmetricKeyAlgorithmPolicy symmetricKeyDecryptionAlgorithmPolicy = - SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyDecryptionAlgorithmPolicy(); + SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022(); private CompressionAlgorithmPolicy compressionAlgorithmPolicy = - CompressionAlgorithmPolicy.defaultCompressionAlgorithmPolicy(); + CompressionAlgorithmPolicy.anyCompressionAlgorithmPolicy(); private PublicKeyAlgorithmPolicy publicKeyAlgorithmPolicy = - PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy(); + PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy(); private final NotationRegistry notationRegistry = new NotationRegistry(); private AlgorithmSuite keyGenerationAlgorithmSuite = AlgorithmSuite.getDefaultAlgorithmSuite(); @@ -249,8 +249,20 @@ public final class Policy { * The default symmetric encryption algorithm policy of PGPainless. * * @return default symmetric encryption algorithm policy + * @deprecated not expressive - will be removed in a future release */ + @Deprecated public static SymmetricKeyAlgorithmPolicy defaultSymmetricKeyEncryptionAlgorithmPolicy() { + return symmetricKeyEncryptionPolicy2022(); + } + + /** + * Policy for symmetric encryption algorithms in the context of message production (encryption). + * This suite contains algorithms that are deemed safe to use in 2022. + * + * @return 2022 symmetric key encryption algorithm policy + */ + public static SymmetricKeyAlgorithmPolicy symmetricKeyEncryptionPolicy2022() { return new SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_256, Arrays.asList( // Reject: Unencrypted, IDEA, TripleDES, CAST5, Blowfish SymmetricKeyAlgorithm.AES_256, @@ -267,8 +279,20 @@ public final class Policy { * The default symmetric decryption algorithm policy of PGPainless. * * @return default symmetric decryption algorithm policy + * @deprecated not expressive - will be removed in a future update */ + @Deprecated public static SymmetricKeyAlgorithmPolicy defaultSymmetricKeyDecryptionAlgorithmPolicy() { + return symmetricKeyDecryptionPolicy2022(); + } + + /** + * Policy for symmetric key encryption algorithms in the context of message consumption (decryption). + * This suite contains algorithms that are deemed safe to use in 2022. + * + * @return 2022 symmetric key decryption algorithm policy + */ + public static SymmetricKeyAlgorithmPolicy symmetricKeyDecryptionPolicy2022() { return new SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_256, Arrays.asList( // Reject: Unencrypted, IDEA, TripleDES, Blowfish SymmetricKeyAlgorithm.CAST5, @@ -414,7 +438,9 @@ public final class Policy { * For revocation signatures {@link #defaultRevocationSignatureHashAlgorithmPolicy()} is used instead. * * @return default signature hash algorithm policy + * @deprecated not expressive - will be removed in an upcoming release */ + @Deprecated public static HashAlgorithmPolicy defaultSignatureAlgorithmPolicy() { return smartSignatureHashAlgorithmPolicy(); } @@ -453,7 +479,7 @@ public final class Policy { * * @return static signature algorithm policy */ - public static HashAlgorithmPolicy static2022SignatureAlgorithmPolicy() { + public static HashAlgorithmPolicy static2022SignatureHashAlgorithmPolicy() { return new HashAlgorithmPolicy(HashAlgorithm.SHA512, Arrays.asList( HashAlgorithm.SHA224, HashAlgorithm.SHA256, @@ -466,7 +492,9 @@ public final class Policy { * The default revocation signature hash algorithm policy of PGPainless. * * @return default revocation signature hash algorithm policy + * @deprecated not expressive - will be removed in an upcoming release */ + @Deprecated public static HashAlgorithmPolicy defaultRevocationSignatureHashAlgorithmPolicy() { return smartSignatureHashAlgorithmPolicy(); } @@ -517,7 +545,25 @@ public final class Policy { return acceptableCompressionAlgorithms.contains(compressionAlgorithm); } + /** + * Default {@link CompressionAlgorithmPolicy} of PGPainless. + * The default compression algorithm policy accepts any compression algorithm. + * + * @return default algorithm policy + * @deprecated not expressive - might be removed in a future release + */ + @Deprecated public static CompressionAlgorithmPolicy defaultCompressionAlgorithmPolicy() { + return anyCompressionAlgorithmPolicy(); + } + + /** + * Policy that accepts any known compression algorithm and offers {@link CompressionAlgorithm#ZIP} as + * default algorithm. + * + * @return compression algorithm policy + */ + public static CompressionAlgorithmPolicy anyCompressionAlgorithmPolicy() { return new CompressionAlgorithmPolicy(CompressionAlgorithm.ZIP, Arrays.asList( CompressionAlgorithm.UNCOMPRESSED, CompressionAlgorithm.ZIP, @@ -556,6 +602,17 @@ public final class Policy { /** * Return PGPainless' default public key algorithm policy. + * This policy is based upon recommendations made by the German Federal Office for Information Security (BSI). + * + * @return default algorithm policy + * @deprecated not expressive - might be removed in a future release + */ + @Deprecated + public static PublicKeyAlgorithmPolicy defaultPublicKeyAlgorithmPolicy() { + return bsi2021PublicKeyAlgorithmPolicy(); + } + + /** * This policy is based upon recommendations made by the German Federal Office for Information Security (BSI). * * Basically this policy requires keys based on elliptic curves to have a bit strength of at least 250, @@ -567,7 +624,7 @@ public final class Policy { * * @return default algorithm policy */ - public static PublicKeyAlgorithmPolicy defaultPublicKeyAlgorithmPolicy() { + public static PublicKeyAlgorithmPolicy bsi2021PublicKeyAlgorithmPolicy() { Map minimalBitStrengths = new EnumMap<>(PublicKeyAlgorithm.class); // §5.4.1 minimalBitStrengths.put(PublicKeyAlgorithm.RSA_GENERAL, 2000); From 288f1b414b2375f38754d36d859f9beb9397cf5e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 3 May 2022 11:31:19 +0200 Subject: [PATCH 0205/1204] Fix javadoc links --- .../src/main/java/org/pgpainless/policy/Policy.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java index d1eca6ba..ad9f0635 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java @@ -452,8 +452,7 @@ public final class Policy { * * This policy is inspired by Sequoia-PGP's collision resistant algorithm policy. * - * @see - * Sequoia-PGP's Collision Resistant Algorithm Policy + * @see Sequoia-PGP's Collision Resistant Algorithm Policy * * @return smart signature algorithm policy */ @@ -618,8 +617,7 @@ public final class Policy { * Basically this policy requires keys based on elliptic curves to have a bit strength of at least 250, * and keys based on prime number factorization / discrete logarithm problems to have a strength of at least 2000 bits. * - * @see - * BSI - Technical Guideline - Cryptographic Mechanisms: Recommendations and Key Lengths (2021-01) + * @see BSI - Technical Guideline - Cryptographic Mechanisms: Recommendations and Key Lengths (2021-01) * @see BlueKrypt | Cryptographic Key Length Recommendation * * @return default algorithm policy From 69f84f24b60006c975c77e84a2de3162cf014653 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 4 May 2022 20:55:29 +0200 Subject: [PATCH 0206/1204] Implement heavy duty packet inspection to figure out nature of data --- .../DecryptionStreamFactory.java | 2 +- .../OpenPgpInputStream.java | 320 ++++++++++++++++-- .../OpenPgpInputStreamTest.java | 32 +- 3 files changed, 328 insertions(+), 26 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index a8847c33..2da18c00 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -140,7 +140,7 @@ public final class DecryptionStreamFactory { return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); } - if (openPgpIn.isBinaryOpenPgp()) { + if (openPgpIn.isLikelyOpenPgpMessage()) { outerDecodingStream = openPgpIn; objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); // Parse OpenPGP message diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java index d15f2ffb..fa954f97 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java @@ -4,14 +4,46 @@ package org.pgpainless.decryption_verification; +import static org.bouncycastle.bcpg.PacketTags.COMPRESSED_DATA; +import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_1; +import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_2; +import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_3; +import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_4; +import static org.bouncycastle.bcpg.PacketTags.LITERAL_DATA; +import static org.bouncycastle.bcpg.PacketTags.MARKER; +import static org.bouncycastle.bcpg.PacketTags.MOD_DETECTION_CODE; +import static org.bouncycastle.bcpg.PacketTags.ONE_PASS_SIGNATURE; +import static org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY; +import static org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY_ENC_SESSION; +import static org.bouncycastle.bcpg.PacketTags.PUBLIC_SUBKEY; +import static org.bouncycastle.bcpg.PacketTags.RESERVED; +import static org.bouncycastle.bcpg.PacketTags.SECRET_KEY; +import static org.bouncycastle.bcpg.PacketTags.SECRET_SUBKEY; +import static org.bouncycastle.bcpg.PacketTags.SIGNATURE; +import static org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC; +import static org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC_SESSION; +import static org.bouncycastle.bcpg.PacketTags.SYM_ENC_INTEGRITY_PRO; +import static org.bouncycastle.bcpg.PacketTags.TRUST; +import static org.bouncycastle.bcpg.PacketTags.USER_ATTRIBUTE; +import static org.bouncycastle.bcpg.PacketTags.USER_ID; + import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.pgpainless.implementation.ImplementationFactory; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedData; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; public class OpenPgpInputStream extends BufferedInputStream { @@ -25,8 +57,9 @@ public class OpenPgpInputStream extends BufferedInputStream { private boolean containsArmorHeader; private boolean containsOpenPgpPackets; + private boolean isLikelyOpenPgpMessage; - public OpenPgpInputStream(InputStream in) throws IOException { + public OpenPgpInputStream(InputStream in, boolean check) throws IOException { super(in, MAX_BUFFER_SIZE); mark(MAX_BUFFER_SIZE); @@ -34,10 +67,16 @@ public class OpenPgpInputStream extends BufferedInputStream { bufferLen = read(buffer); reset(); - inspectBuffer(); + if (check) { + inspectBuffer(); + } } - private void inspectBuffer() { + public OpenPgpInputStream(InputStream in) throws IOException { + this(in, true); + } + + private void inspectBuffer() throws IOException { if (determineIsArmored()) { return; } @@ -61,32 +100,250 @@ public class OpenPgpInputStream extends BufferedInputStream { * This breaks down though if we read plausible garbage where the data accidentally makes sense, * or valid, yet incomplete packets (remember, we are still only working on a portion of the data). */ - private void determineIsBinaryOpenPgp() { + private void determineIsBinaryOpenPgp() throws IOException { if (bufferLen == -1) { // Empty data return; } - try { - ByteArrayInputStream bufferIn = new ByteArrayInputStream(buffer, 0, bufferLen); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(bufferIn); + ByteArrayInputStream bufferIn = new ByteArrayInputStream(buffer, 0, bufferLen); + nonExhaustiveParseAndCheckPlausibility(bufferIn); + } - boolean containsPackets = false; - while (objectFactory.nextObject() != null) { - containsPackets = true; - // read all packets in buffer - hope to confirm invalid data via thrown IOExceptions + private void nonExhaustiveParseAndCheckPlausibility(ByteArrayInputStream bufferIn) throws IOException { + int hdr = bufferIn.read(); + if (hdr < 0 || (hdr & 0x80) == 0) { + return; + } + + boolean newPacket = (hdr & 0x40) != 0; + int tag = 0; + int bodyLen = 0; + boolean partial = false; + + if (newPacket) { + tag = hdr & 0x3f; + + int l = bufferIn.read(); + if (l < 192) { + bodyLen = l; + } else if (l <= 223) { + int b = bufferIn.read(); + bodyLen = ((l - 192) << 8) + (b) + 192; + } else if (l == 255) { + bodyLen = (bufferIn.read() << 24) | (bufferIn.read() << 16) | (bufferIn.read() << 8) | bufferIn.read(); + } else { + partial = true; + bodyLen = 1 << (l & 0x1f); } - containsOpenPgpPackets = containsPackets; + } else { + int lengthType = hdr & 0x3; + tag = (hdr & 0x3f) >> 2; + switch (lengthType) { + case 0: + bodyLen = bufferIn.read(); + break; + case 1: + bodyLen = (bufferIn.read() << 8) | bufferIn.read(); + break; + case 2: + bodyLen = (bufferIn.read() << 24) | (bufferIn.read() << 16) | (bufferIn.read() << 8) | bufferIn.read(); + break; + case 3: + partial = true; + break; + default: + return; + } + } - } catch (IOException e) { - String msg = e.getMessage(); + if (bodyLen < 0) { + return; + } - // If true, we *probably* hit valid, but large OpenPGP data (not sure though) :/ - // Otherwise we hit garbage and can be sure that this is no OpenPGP data \o/ - containsOpenPgpPackets = (msg != null && msg.contains("premature end of stream in PartialInputStream")); + BCPGInputStream bcpgIn = new BCPGInputStream(bufferIn); + switch (tag) { + case RESERVED: + // How to handle this? Probably discard as garbage... + return; - // This is not an optimal way of determining the nature of data, but probably the best - // we can do :/ + case PUBLIC_KEY_ENC_SESSION: + int pkeskVersion = bcpgIn.read(); + if (pkeskVersion <= 0 || pkeskVersion > 5) { + return; + } + + // Skip Key-ID + for (int i = 0; i < 8; i++) { + bcpgIn.read(); + } + + int pkeskAlg = bcpgIn.read(); + if (PublicKeyAlgorithm.fromId(pkeskAlg) == null) { + return; + } + + containsOpenPgpPackets = true; + isLikelyOpenPgpMessage = true; + break; + + case SIGNATURE: + int sigVersion = bcpgIn.read(); + int sigType; + if (sigVersion == 2 || sigVersion == 3) { + int l = bcpgIn.read(); + sigType = bcpgIn.read(); + } else if (sigVersion == 4 || sigVersion == 5) { + sigType = bcpgIn.read(); + } else { + return; + } + + try { + SignatureType.valueOf(sigType); + } catch (IllegalArgumentException e) { + return; + } + + containsOpenPgpPackets = true; + isLikelyOpenPgpMessage = true; + break; + + case SYMMETRIC_KEY_ENC_SESSION: + int skeskVersion = bcpgIn.read(); + if (skeskVersion == 4) { + int skeskAlg = bcpgIn.read(); + if (SymmetricKeyAlgorithm.fromId(skeskAlg) == null) { + return; + } + // TODO: Parse S2K? + } else { + return; + } + containsOpenPgpPackets = true; + isLikelyOpenPgpMessage = true; + break; + + case ONE_PASS_SIGNATURE: + int opsVersion = bcpgIn.read(); + if (opsVersion == 3) { + int opsSigType = bcpgIn.read(); + try { + SignatureType.valueOf(opsSigType); + } catch (IllegalArgumentException e) { + return; + } + int opsHashAlg = bcpgIn.read(); + if (HashAlgorithm.fromId(opsHashAlg) == null) { + return; + } + int opsKeyAlg = bcpgIn.read(); + if (PublicKeyAlgorithm.fromId(opsKeyAlg) == null) { + return; + } + } else { + return; + } + + containsOpenPgpPackets = true; + isLikelyOpenPgpMessage = true; + break; + + case SECRET_KEY: + case PUBLIC_KEY: + case SECRET_SUBKEY: + case PUBLIC_SUBKEY: + int keyVersion = bcpgIn.read(); + for (int i = 0; i < 4; i++) { + // Creation time + bcpgIn.read(); + } + if (keyVersion == 3) { + long validDays = (in.read() << 8) | in.read(); + if (validDays < 0) { + return; + } + } else if (keyVersion == 4) { + + } else if (keyVersion == 5) { + + } else { + return; + } + int keyAlg = bcpgIn.read(); + if (PublicKeyAlgorithm.fromId(keyAlg) == null) { + return; + } + + containsOpenPgpPackets = true; + break; + + case COMPRESSED_DATA: + int compAlg = bcpgIn.read(); + if (CompressionAlgorithm.fromId(compAlg) == null) { + return; + } + + containsOpenPgpPackets = true; + isLikelyOpenPgpMessage = true; + break; + + case SYMMETRIC_KEY_ENC: + // No data to compare :( + containsOpenPgpPackets = true; + break; + + case MARKER: + byte[] marker = new byte[3]; + bcpgIn.readFully(marker); + if (marker[0] != 0x50 || marker[1] != 0x47 || marker[2] != 0x50) { + return; + } + + containsOpenPgpPackets = true; + break; + + case LITERAL_DATA: + int format = bcpgIn.read(); + if (StreamEncoding.fromCode(format) == null) { + return; + } + + containsOpenPgpPackets = true; + isLikelyOpenPgpMessage = true; + break; + + case TRUST: + case USER_ID: + case USER_ATTRIBUTE: + // Not much to compare + containsOpenPgpPackets = true; + break; + + case SYM_ENC_INTEGRITY_PRO: + int seipVersion = bcpgIn.read(); + if (seipVersion != 1) { + return; + } + isLikelyOpenPgpMessage = true; + containsOpenPgpPackets = true; + break; + + case MOD_DETECTION_CODE: + byte[] digest = new byte[20]; + bcpgIn.readFully(digest); + + containsOpenPgpPackets = true; + break; + + case EXPERIMENTAL_1: + case EXPERIMENTAL_2: + case EXPERIMENTAL_3: + case EXPERIMENTAL_4: + return; + default: + containsOpenPgpPackets = false; + break; } } @@ -119,10 +376,31 @@ public class OpenPgpInputStream extends BufferedInputStream { return containsArmorHeader; } + /** + * Return true, if the data is possibly binary OpenPGP. + * The criterion for this are less strict than for {@link #isLikelyOpenPgpMessage()}, + * as it also accepts other OpenPGP packets at the beginning of the data stream. + * + * Use with caution. + * + * @return true if data appears to be binary OpenPGP data + */ public boolean isBinaryOpenPgp() { return containsOpenPgpPackets; } + /** + * Returns true, if the underlying data is very likely (more than 99,9%) an OpenPGP message. + * OpenPGP Message means here that it starts with either an {@link PGPEncryptedData}, + * {@link PGPCompressedData}, {@link PGPOnePassSignature} or {@link PGPLiteralData} packet. + * The plausability of these data packets is checked as far as possible. + * + * @return true if likely OpenPGP message + */ + public boolean isLikelyOpenPgpMessage() { + return isLikelyOpenPgpMessage; + } + public boolean isNonOpenPgp() { return !isAsciiArmored() && !isBinaryOpenPgp(); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java index f416ea7f..67719886 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java @@ -11,18 +11,22 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.Random; import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; public class OpenPgpInputStreamTest { private static final Random RANDOM = new Random(); - @Test + @RepeatedTest(10) public void randomBytesDoNotContainOpenPgpData() throws IOException { byte[] randomBytes = new byte[1000000]; RANDOM.nextBytes(randomBytes); @@ -30,13 +34,33 @@ public class OpenPgpInputStreamTest { OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(randomIn); assertFalse(openPgpInputStream.isAsciiArmored()); - assertFalse(openPgpInputStream.isBinaryOpenPgp()); - assertTrue(openPgpInputStream.isNonOpenPgp()); + assertFalse(openPgpInputStream.isLikelyOpenPgpMessage()); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(openPgpInputStream, out); + byte[] outBytes = out.toByteArray(); - assertArrayEquals(randomBytes, out.toByteArray()); + assertArrayEquals(randomBytes, outBytes); + } + + @RepeatedTest(10) + public void largeCompressedDataIsBinaryOpenPgp() throws IOException { + // Since we are compressing RANDOM data, the output will likely be roughly the same size + // So we very likely will end up with data larger than the MAX_BUFFER_SIZE + byte[] randomBytes = new byte[OpenPgpInputStream.MAX_BUFFER_SIZE * 10]; + RANDOM.nextBytes(randomBytes); + + ByteArrayOutputStream compressedDataPacket = new ByteArrayOutputStream(); + PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compressor = compressedDataGenerator.open(compressedDataPacket); + compressor.write(randomBytes); + compressor.close(); + + OpenPgpInputStream inputStream = new OpenPgpInputStream(new ByteArrayInputStream(compressedDataPacket.toByteArray())); + assertFalse(inputStream.isAsciiArmored()); + assertFalse(inputStream.isNonOpenPgp()); + assertTrue(inputStream.isBinaryOpenPgp()); + assertTrue(inputStream.isLikelyOpenPgpMessage()); } @Test From 826331917fd8bcad6c116a9b6a8deefd6ee0a27f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 5 May 2022 11:15:19 +0200 Subject: [PATCH 0207/1204] Add comments to unexhaustive parsing method --- .../decryption_verification/OpenPgpInputStream.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java index fa954f97..cccbe250 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java @@ -111,6 +111,7 @@ public class OpenPgpInputStream extends BufferedInputStream { } private void nonExhaustiveParseAndCheckPlausibility(ByteArrayInputStream bufferIn) throws IOException { + // Read the packet header int hdr = bufferIn.read(); if (hdr < 0 || (hdr & 0x80) == 0) { return; @@ -121,6 +122,7 @@ public class OpenPgpInputStream extends BufferedInputStream { int bodyLen = 0; boolean partial = false; + // Determine the packet length if (newPacket) { tag = hdr & 0x3f; @@ -157,10 +159,12 @@ public class OpenPgpInputStream extends BufferedInputStream { } } + // Negative body length -> garbage if (bodyLen < 0) { return; } + // Try to unexhaustively parse the first packet bit by bit and check for plausibility BCPGInputStream bcpgIn = new BCPGInputStream(bufferIn); switch (tag) { case RESERVED: From 4c72bc2a8ed46d5f314d559d0721dc009f60a174 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 5 May 2022 11:19:23 +0200 Subject: [PATCH 0208/1204] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e44d2d43..f125f2f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.2.2 +- `EncryptionOptions.addRecipients(collection)`: Disallow empty collections to prevent misuse from resulting in unencrypted messages +- Deprecate default policy factory methods in favor of policy factory methods with expressive names +- Another fix for OpenPGP data detection + - We now inspect the first packet of the data stream to figure out, whether it is plausible OpenPGP data, without exhausting the stream + ## 1.2.1 - Bump `sop-java` dependency to `1.2.3` - Bump `slf4j` dependency to `1.7.36` From ae3004a2216d563a38bd9e226d55f3940cd1c430 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 5 May 2022 11:22:59 +0200 Subject: [PATCH 0209/1204] PGPainless 1.2.2 --- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e27a0c3d..ea004600 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.2.1' + implementation 'org.pgpainless:pgpainless-core:1.2.2' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index fd635594..27546b3c 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.2.1" + implementation "org.pgpainless:pgpainless-sop:1.2.2" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.2.1 + 1.2.2 ... diff --git a/version.gradle b/version.gradle index 452291ef..e81f0793 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.2.2' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From 7b7707b3a9e3bc9621cd1516772244d543bf97f8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 5 May 2022 11:25:32 +0200 Subject: [PATCH 0210/1204] PGPainless 1.2.3-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index e81f0793..f890e8d6 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.2.2' - isSnapshot = false + shortVersion = '1.2.3' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From 64a50266f1476127db69ddcd4d1e423b2552d308 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 5 May 2022 12:43:44 +0200 Subject: [PATCH 0211/1204] Test for detection of uncompressed, signed messages, and improve decryption of seip messages --- .../DecryptionStreamFactory.java | 5 ++- .../OpenPgpInputStream.java | 11 +++--- .../OpenPgpInputStreamTest.java | 38 ++++++++++++++++++- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 2da18c00..1913444e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -140,7 +140,10 @@ public final class DecryptionStreamFactory { return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); } - if (openPgpIn.isLikelyOpenPgpMessage()) { + // Data appears to be OpenPGP message, + // or we handle it as such, since user provided a session-key for decryption + if (openPgpIn.isLikelyOpenPgpMessage() || + (openPgpIn.isBinaryOpenPgp() && options.getSessionKey() != null)) { outerDecodingStream = openPgpIn; objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); // Parse OpenPGP message diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java index cccbe250..669c2dec 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java @@ -77,14 +77,14 @@ public class OpenPgpInputStream extends BufferedInputStream { } private void inspectBuffer() throws IOException { - if (determineIsArmored()) { + if (checkForAsciiArmor()) { return; } - determineIsBinaryOpenPgp(); + checkForBinaryOpenPgp(); } - private boolean determineIsArmored() { + private boolean checkForAsciiArmor() { if (startsWithIgnoringWhitespace(buffer, ARMOR_HEADER, bufferLen)) { containsArmorHeader = true; return true; @@ -100,7 +100,7 @@ public class OpenPgpInputStream extends BufferedInputStream { * This breaks down though if we read plausible garbage where the data accidentally makes sense, * or valid, yet incomplete packets (remember, we are still only working on a portion of the data). */ - private void determineIsBinaryOpenPgp() throws IOException { + private void checkForBinaryOpenPgp() throws IOException { if (bufferLen == -1) { // Empty data return; @@ -210,7 +210,6 @@ public class OpenPgpInputStream extends BufferedInputStream { } containsOpenPgpPackets = true; - isLikelyOpenPgpMessage = true; break; case SYMMETRIC_KEY_ENC_SESSION: @@ -295,6 +294,8 @@ public class OpenPgpInputStream extends BufferedInputStream { case SYMMETRIC_KEY_ENC: // No data to compare :( containsOpenPgpPackets = true; + // While this is a valid OpenPGP message, enabling the line below would lead to too many false positives + // isLikelyOpenPgpMessage = true; break; case MARKER: diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java index 67719886..c2a24c76 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java @@ -13,20 +13,30 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; import java.util.Random; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.CompressionAlgorithmTags; import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.key.protection.SecretKeyRingProtector; public class OpenPgpInputStreamTest { private static final Random RANDOM = new Random(); - @RepeatedTest(10) + @RepeatedTest(100) public void randomBytesDoNotContainOpenPgpData() throws IOException { byte[] randomBytes = new byte[1000000]; RANDOM.nextBytes(randomBytes); @@ -43,7 +53,7 @@ public class OpenPgpInputStreamTest { assertArrayEquals(randomBytes, outBytes); } - @RepeatedTest(10) + @Test public void largeCompressedDataIsBinaryOpenPgp() throws IOException { // Since we are compressing RANDOM data, the output will likely be roughly the same size // So we very likely will end up with data larger than the MAX_BUFFER_SIZE @@ -725,4 +735,28 @@ public class OpenPgpInputStreamTest { assertFalse(openPgpInputStream.isAsciiArmored()); assertTrue(openPgpInputStream.isNonOpenPgp()); } + + @Test + public void testSignedMessageConsumption() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + ByteArrayInputStream plaintext = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Sigmund ", null); + + ByteArrayOutputStream signedOut = new ByteArrayOutputStream(); + EncryptionStream signer = PGPainless.encryptAndOrSign() + .onOutputStream(signedOut) + .withOptions(ProducerOptions.sign(new SigningOptions() + .addSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys)) + .setAsciiArmor(false) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + + Streams.pipeAll(plaintext, signer); + signer.close(); + + byte[] binary = signedOut.toByteArray(); + + OpenPgpInputStream openPgpIn = new OpenPgpInputStream(new ByteArrayInputStream(binary)); + assertFalse(openPgpIn.isAsciiArmored()); + assertTrue(openPgpIn.isLikelyOpenPgpMessage()); + } } From 3e7e6df3f9ac37633773bd18a17cf9d9c6d9f409 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 7 May 2022 14:11:39 +0200 Subject: [PATCH 0212/1204] Disallow stripping of primary secret keys --- .../src/main/java/org/pgpainless/key/util/KeyRingUtils.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index a7a2b0b9..dd366e46 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -451,6 +451,11 @@ public final class KeyRingUtils { public static PGPSecretKeyRing stripSecretKey(@Nonnull PGPSecretKeyRing secretKeys, long secretKeyId) throws IOException, PGPException { + + if (secretKeys.getPublicKey().getKeyID() == secretKeyId) { + throw new IllegalArgumentException("Bouncy Castle currently cannot deal with stripped secret primary keys."); + } + if (secretKeys.getSecretKey(secretKeyId) == null) { throw new NoSuchElementException("PGPSecretKeyRing does not contain secret key " + Long.toHexString(secretKeyId)); } From 374e6452f0d49384f3540255024e68fe31a6ecdb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 7 May 2022 14:12:18 +0200 Subject: [PATCH 0213/1204] Add RevokedKeyException --- .../main/java/org/pgpainless/exception/KeyException.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java index 7ffc66ee..66c5d2cf 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java @@ -38,6 +38,13 @@ public abstract class KeyException extends RuntimeException { } } + public static class RevokedKeyException extends KeyException { + + public RevokedKeyException(@Nonnull OpenPgpFingerprint fingerprint) { + super("Key " + fingerprint + " appears to be revoked.", fingerprint); + } + } + public static class UnacceptableEncryptionKeyException extends KeyException { public UnacceptableEncryptionKeyException(@Nonnull OpenPgpFingerprint fingerprint) { From d3f412873bd0b8db6e5a5fadf4eab41270082710 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 7 May 2022 21:44:52 +0200 Subject: [PATCH 0214/1204] Fix checkstyle issues --- .../src/main/java/org/pgpainless/key/util/KeyRingUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index dd366e46..8306db34 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -449,9 +449,9 @@ public final class KeyRingUtils { */ @Nonnull public static PGPSecretKeyRing stripSecretKey(@Nonnull PGPSecretKeyRing secretKeys, - long secretKeyId) + long secretKeyId) throws IOException, PGPException { - + if (secretKeys.getPublicKey().getKeyID() == secretKeyId) { throw new IllegalArgumentException("Bouncy Castle currently cannot deal with stripped secret primary keys."); } From 49d65788b48018bf2e194b21ab392b7c7c7c8d55 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 7 May 2022 21:46:03 +0200 Subject: [PATCH 0215/1204] Remove support for processing compressed detached signatures Signatures are indistinguishable from randomness, so there is no point in compressing them, apart from attempting to exploit flaws in compression algorithms. Thanks to @DemiMarie for pointing this out Fixes #286 --- .../pgpainless/signature/SignatureUtils.java | 8 +------- .../signature/SignatureUtilsTest.java | 19 ------------------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java index 1ebbdda4..cfbf4ce4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java @@ -17,7 +17,6 @@ import org.bouncycastle.bcpg.sig.IssuerKeyID; import org.bouncycastle.bcpg.sig.KeyExpirationTime; import org.bouncycastle.bcpg.sig.RevocationReason; import org.bouncycastle.bcpg.sig.SignatureExpirationTime; -import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPPublicKey; @@ -232,7 +231,7 @@ public final class SignatureUtils { /** * Read and return {@link PGPSignature PGPSignatures}. - * This method can deal with signatures that may be armored, compressed and may contain marker packets. + * This method can deal with signatures that may be binary, armored and may contain marker packets. * * @param inputStream input stream * @param maxIterations number of loop iterations until reading is aborted @@ -248,11 +247,6 @@ public final class SignatureUtils { int i = 0; Object nextObject; while (i++ < maxIterations && (nextObject = objectFactory.nextObject()) != null) { - if (nextObject instanceof PGPCompressedData) { - PGPCompressedData compressedData = (PGPCompressedData) nextObject; - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(compressedData.getDataStream()); - } - if (nextObject instanceof PGPSignatureList) { PGPSignatureList signatureList = (PGPSignatureList) nextObject; for (PGPSignature s : signatureList) { diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java index 296f20a8..87dd75c2 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java @@ -12,28 +12,9 @@ import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSignature; import org.junit.jupiter.api.Test; -import org.pgpainless.key.util.KeyIdUtil; public class SignatureUtilsTest { - @Test - public void readSignaturesFromCompressedData() throws PGPException, IOException { - String compressed = "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "owHrKGVhEOZiYGNlSoxcsJtBkVMg3OzZZKnz5jxiiiz+aTG+h46kcR9zinOECZ/o\n" + - "YmTYsKve/opb3v/o8J0qq1/MFFBhP9jfEq+/avK6qPMrlh70Zfinu96c+cncX9GK\n" + - "B4ui3fUfbUo8tFrVTIRn7kROq69H77hd6cCw9susVdls1as1gNYunnp5V8Qp+wX3\n" + - "+jUnwoRB1p4SfPk412lb/cSmShb211fOX07h0JxVH1JXsc/vi2mi5ieG/2Xxb5tk\n" + - "LE+r7WwruxSaeXLuLsOmXTPZD0/VtvlqO89RYjsA\n" + - "=yZ18\n" + - "-----END PGP MESSAGE-----"; - List signatures = SignatureUtils.readSignatures(compressed); - assertEquals(2, signatures.size()); - assertEquals(KeyIdUtil.fromLongKeyId("5736E6931ACF370C"), signatures.get(0).getKeyID()); - assertEquals(KeyIdUtil.fromLongKeyId("F49AAA6B067BAB28"), signatures.get(1).getKeyID()); - } - @Test public void noIssuerResultsInKeyId0() throws PGPException, IOException { String sig = "-----BEGIN PGP SIGNATURE-----\n" + From be6c16079e041adfa705b5bec02d983961d831ba Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 7 May 2022 22:15:13 +0200 Subject: [PATCH 0216/1204] Switch to version agnostic SOP spec URL --- pgpainless-cli/README.md | 2 +- pgpainless-sop/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-cli/README.md b/pgpainless-cli/README.md index d1d72afc..f3cf2861 100644 --- a/pgpainless-cli/README.md +++ b/pgpainless-cli/README.md @@ -8,7 +8,7 @@ SPDX-License-Identifier: Apache-2.0 [![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-cli/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-cli) -PGPainless-CLI is an implementation of the [Stateless OpenPGP Command Line Interface](https://tools.ietf.org/html/draft-dkg-openpgp-stateless-cli-01) specification based on PGPainless. +PGPainless-CLI is an implementation of the [Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) specification based on PGPainless. It plugs `pgpainless-sop` into `sop-java-picocli`. diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 27546b3c..0ef67686 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless-SOP -[![Spec Revision: 3](https://img.shields.io/badge/Spec%20Revision-3-blue)](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03) +[![Spec Revision: 3](https://img.shields.io/badge/Spec%20Revision-3-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) [![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-sop)](https://search.maven.org/artifact/org.pgpainless/pgpainless-sop) [![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-sop/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-sop) From ba767cc7ed298098cd8ca57c88ac93bcdd45eded Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 7 May 2022 22:29:17 +0200 Subject: [PATCH 0217/1204] Add NGI and Flowcrypt logo svgs --- assets/NGIAssure_tag.svg | 1 + assets/flowcrypt-logo.svg | 1 + 2 files changed, 2 insertions(+) create mode 100644 assets/NGIAssure_tag.svg create mode 100644 assets/flowcrypt-logo.svg diff --git a/assets/NGIAssure_tag.svg b/assets/NGIAssure_tag.svg new file mode 100644 index 00000000..62b7c127 --- /dev/null +++ b/assets/NGIAssure_tag.svg @@ -0,0 +1 @@ +Logo-NGIAssure-tag diff --git a/assets/flowcrypt-logo.svg b/assets/flowcrypt-logo.svg new file mode 100644 index 00000000..a6bf4919 --- /dev/null +++ b/assets/flowcrypt-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file From 08ec140b63437c55af6f53e581a38bf2b0a514e8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 7 May 2022 22:49:22 +0200 Subject: [PATCH 0218/1204] Add Logos for FlowCrypt and NGI and add NGI info --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index ea004600..3dbb435f 100644 --- a/README.md +++ b/README.md @@ -231,5 +231,10 @@ Please follow the [code of conduct](CODE_OF_CONDUCT.md) if you want to be part o ## Acknowledgements Development on PGPainless is generously sponsored by [FlowCrypt.com](https://flowcrypt.com). Thank you very very very much! +[![FlowCrypt Logo](https://raw.githubusercontent.com/pgpainless/pgpainless/main/assets/flowcrypt-logo.svg)](https://flowcrypt.com) + +Parts of PGPainless development ([project page](https://nlnet.nl/project/PGPainless/)) will be funded by [NGI Assure](https://nlnet.nl/assure/) through [NLNet](https://nlnet.nl). +NGI Assure is made possible with financial support from the [European Commission](https://ec.europa.eu/)'s [Next Generation Internet](https://ngi.eu/) programme, under the aegis of [DG Communications Networks, Content and Technology](https://ec.europa.eu/info/departments/communications-networks-content-and-technology_en). +[![NGI Assure Logo](https://raw.githubusercontent.com/pgpainless/pgpainless/main/assets/NGIAssure_tag.svg)](https://nlnet.nl/assure/) Continuous Integration is kindly provided by [Travis-CI.com](https://travis-ci.com/). From 12e62d381c38a43aec442dbd187cc1f42fe22eae Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 8 May 2022 11:24:34 +0200 Subject: [PATCH 0219/1204] Make readSignatures skip over compressed data packets without decompression. --- .../org/pgpainless/signature/SignatureUtils.java | 7 +++++++ .../pgpainless/signature/SignatureUtilsTest.java | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java index cfbf4ce4..5d68e8b9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java @@ -17,6 +17,7 @@ import org.bouncycastle.bcpg.sig.IssuerKeyID; import org.bouncycastle.bcpg.sig.KeyExpirationTime; import org.bouncycastle.bcpg.sig.RevocationReason; import org.bouncycastle.bcpg.sig.SignatureExpirationTime; +import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPPublicKey; @@ -26,6 +27,7 @@ import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; import org.bouncycastle.util.encoders.Hex; +import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SignatureType; @@ -247,6 +249,11 @@ public final class SignatureUtils { int i = 0; Object nextObject; while (i++ < maxIterations && (nextObject = objectFactory.nextObject()) != null) { + if (nextObject instanceof PGPCompressedData) { + PGPCompressedData compressedData = (PGPCompressedData) nextObject; + Streams.drain(compressedData.getInputStream()); // Skip packet without decompressing + } + if (nextObject instanceof PGPSignatureList) { PGPSignatureList signatureList = (PGPSignatureList) nextObject; for (PGPSignature s : signatureList) { diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java index 87dd75c2..757f28e6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java @@ -15,6 +15,22 @@ import org.junit.jupiter.api.Test; public class SignatureUtilsTest { + @Test + public void readSignaturesFromCompressedDataDoesNotAttemptDecompression() throws PGPException, IOException { + String compressed = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "owHrKGVhEOZiYGNlSoxcsJtBkVMg3OzZZKnz5jxiiiz+aTG+h46kcR9zinOECZ/o\n" + + "YmTYsKve/opb3v/o8J0qq1/MFFBhP9jfEq+/avK6qPMrlh70Zfinu96c+cncX9GK\n" + + "B4ui3fUfbUo8tFrVTIRn7kROq69H77hd6cCw9susVdls1as1gNYunnp5V8Qp+wX3\n" + + "+jUnwoRB1p4SfPk412lb/cSmShb211fOX07h0JxVH1JXsc/vi2mi5ieG/2Xxb5tk\n" + + "LE+r7WwruxSaeXLuLsOmXTPZD0/VtvlqO89RYjsA\n" + + "=yZ18\n" + + "-----END PGP MESSAGE-----"; + List signatures = SignatureUtils.readSignatures(compressed); + assertEquals(0, signatures.size()); + } + @Test public void noIssuerResultsInKeyId0() throws PGPException, IOException { String sig = "-----BEGIN PGP SIGNATURE-----\n" + From 8fd67da973f3b51717536698483fb5d033ae1079 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 8 May 2022 11:34:56 +0200 Subject: [PATCH 0220/1204] Add comment about readSignatures skipping compressed data packets --- .../main/java/org/pgpainless/signature/SignatureUtils.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java index 5d68e8b9..f002d5cd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java @@ -249,8 +249,13 @@ public final class SignatureUtils { int i = 0; Object nextObject; while (i++ < maxIterations && (nextObject = objectFactory.nextObject()) != null) { + + // Since signatures are indistinguishable from randomness, there is no point in having them compressed, + // except for an attacker who is trying to exploit flaws in the decompression algorithm. + // Therefore, we ignore compressed data packets without attempting decompression. if (nextObject instanceof PGPCompressedData) { PGPCompressedData compressedData = (PGPCompressedData) nextObject; + // getInputStream() does not do decompression, contrary to getDataStream(). Streams.drain(compressedData.getInputStream()); // Skip packet without decompressing } From c510551f1624b9cdc240b920976ba02ecc53caef Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 12 May 2022 18:16:44 +0200 Subject: [PATCH 0221/1204] Move Flowcrypt and NGI logos to external host --- README.md | 4 ++-- assets/NGIAssure_tag.svg | 1 - assets/flowcrypt-logo.svg | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 assets/NGIAssure_tag.svg delete mode 100644 assets/flowcrypt-logo.svg diff --git a/README.md b/README.md index 3dbb435f..edd6a245 100644 --- a/README.md +++ b/README.md @@ -231,10 +231,10 @@ Please follow the [code of conduct](CODE_OF_CONDUCT.md) if you want to be part o ## Acknowledgements Development on PGPainless is generously sponsored by [FlowCrypt.com](https://flowcrypt.com). Thank you very very very much! -[![FlowCrypt Logo](https://raw.githubusercontent.com/pgpainless/pgpainless/main/assets/flowcrypt-logo.svg)](https://flowcrypt.com) +[![FlowCrypt Logo](https://blog.jabberhead.tk/wp-content/uploads/2022/05/flowcrypt-logo.svg)](https://flowcrypt.com) Parts of PGPainless development ([project page](https://nlnet.nl/project/PGPainless/)) will be funded by [NGI Assure](https://nlnet.nl/assure/) through [NLNet](https://nlnet.nl). NGI Assure is made possible with financial support from the [European Commission](https://ec.europa.eu/)'s [Next Generation Internet](https://ngi.eu/) programme, under the aegis of [DG Communications Networks, Content and Technology](https://ec.europa.eu/info/departments/communications-networks-content-and-technology_en). -[![NGI Assure Logo](https://raw.githubusercontent.com/pgpainless/pgpainless/main/assets/NGIAssure_tag.svg)](https://nlnet.nl/assure/) +[![NGI Assure Logo](https://blog.jabberhead.tk/wp-content/uploads/2022/05/NGIAssure_tag.svg)](https://nlnet.nl/assure/) Continuous Integration is kindly provided by [Travis-CI.com](https://travis-ci.com/). diff --git a/assets/NGIAssure_tag.svg b/assets/NGIAssure_tag.svg deleted file mode 100644 index 62b7c127..00000000 --- a/assets/NGIAssure_tag.svg +++ /dev/null @@ -1 +0,0 @@ -Logo-NGIAssure-tag diff --git a/assets/flowcrypt-logo.svg b/assets/flowcrypt-logo.svg deleted file mode 100644 index a6bf4919..00000000 --- a/assets/flowcrypt-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 51baa0e5cbad25cd6e952f35737e12657b13d487 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 May 2022 18:12:37 +0200 Subject: [PATCH 0222/1204] Add modernKeyRing(userId) shortcut method --- .../key/generation/KeyRingTemplates.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java index 0d663ff9..41d39a1a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java @@ -188,6 +188,21 @@ public final class KeyRingTemplates { return builder.build(); } + /** + * Generate a modern PGP key ring consisting of an ed25519 EdDSA primary key which is used to certify + * an X25519 XDH encryption subkey and an ed25519 EdDSA signing key. + * + * @param userId primary user id + * @return key ring + * + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error + */ + public PGPSecretKeyRing modernKeyRing(String userId) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + return modernKeyRing(userId, null); + } + /** * Generate a modern PGP key ring consisting of an ed25519 EdDSA primary key which is used to certify * an X25519 XDH encryption subkey and an ed25519 EdDSA signing key. From 77d010ec941c8ac56010ff09608683db6f49e00d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 May 2022 18:15:54 +0200 Subject: [PATCH 0223/1204] Add CollectionUtils.addAll(iterator, collection) --- .../java/org/pgpainless/util/CollectionUtils.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java index e4414b31..e901eecc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java @@ -6,6 +6,7 @@ package org.pgpainless.util; import java.lang.reflect.Array; import java.util.ArrayList; +import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -60,4 +61,17 @@ public final class CollectionUtils { } return false; } + + /** + * Add all items from the iterator to the collection. + * + * @param type of item + * @param iterator iterator to gather items from + * @param collection collection to add items to + */ + public static void addAll(Iterator iterator, Collection collection) { + while (iterator.hasNext()) { + collection.add(iterator.next()); + } + } } From 1a37058c66fd4832743a95a42b2999f3ae038309 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 May 2022 18:31:10 +0200 Subject: [PATCH 0224/1204] Add SignatureUtils.getSignaturesForUserIdBy(key, userId, keyId) --- .../pgpainless/signature/SignatureUtils.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java index f002d5cd..9b86f575 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java @@ -9,7 +9,9 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; +import java.util.Iterator; import java.util.List; import java.util.Set; @@ -40,6 +42,8 @@ import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; import org.pgpainless.util.ArmorUtils; +import javax.annotation.Nonnull; + /** * Utility methods related to signatures. */ @@ -333,4 +337,36 @@ public final class SignatureUtils { return fp.equals(issuerFp); } + + /** + * Extract all signatures from the given
key
which were issued by
issuerKeyId
+ * over
userId
. + * + * @param key public key + * @param userId user-id + * @param issuerKeyId issuer key-id + * @return (potentially empty) list of signatures + */ + public static @Nonnull List getSignaturesOverUserIdBy( + @Nonnull PGPPublicKey key, + @Nonnull String userId, + long issuerKeyId) { + List signaturesByKeyId = new ArrayList<>(); + Iterator userIdSignatures = key.getSignaturesForID(userId); + + // getSignaturesForID() is nullable -.- + if (userIdSignatures == null) { + return signaturesByKeyId; + } + + // filter for signatures by key-id + while (userIdSignatures.hasNext()) { + PGPSignature signature = userIdSignatures.next(); + if (signature.getKeyID() == issuerKeyId) { + signaturesByKeyId.add(signature); + } + } + + return Collections.unmodifiableList(signaturesByKeyId); + } } From 3a9bfd57ac371a84a59563ebf19c0deccbc96f18 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 May 2022 18:32:18 +0200 Subject: [PATCH 0225/1204] Add test for SignatureUtils.getSignaturesForUserIdBy() --- .../signature/SignatureUtilsTest.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java index 757f28e6..ac67d2de 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java @@ -10,8 +10,11 @@ import java.io.IOException; import java.util.List; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; public class SignatureUtilsTest { @@ -83,4 +86,56 @@ public class SignatureUtilsTest { List signatures = SignatureUtils.readSignatures(sigs); assertEquals(1, signatures.size()); // first sig gets skipped } + + @Test + public void testGetSignaturesOverUserIdBy() throws IOException { + String alice = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9CA2 8D6D DBA6 BCF1 23A4 2775 0EB5 08CD 1714 B46A\n" + + "Comment: Alice \n" + + "Comment: 1 further identity\n" + + "\n" + + "mDMEYoPLwhYJKwYBBAHaRw8BAQdAnuduN87Gu2qvsfdRxLP83strq+doPNP8Hx2J\n" + + "esvaN0+0GUFsaWNlIDxhbGljZUBleG1hcGxlLmNvbT6IjwQTFgoAQQUCYoPLwgkQ\n" + + "DrUIzRcUtGoWIQScoo1t26a88SOkJ3UOtQjNFxS0agKeAQKbAQUWAgMBAAQLCQgH\n" + + "BRUKCQgLApkBAABRRwD+II62grSOGKDyBYMLTfCNQejcazQYWoSVyJiD308CRxgA\n" + + "/2H6kTXaV+Lk2+te/yZ3aeAd1wFBDe2HRelrMy4074gMiHUEEBYKACcFAmKDy8IJ\n" + + "EE3a6g4UHIzBFiEE0VukWebIQb/PImHfTdrqDhQcjMEAAOjCAQCcCQySwr/8VgW8\n" + + "Ww+pKM21gWWSGMazMqAcDwqnCrebtAEAiU2PtfWGFZc6VVdsMI1GOcRp++fz+AJ5\n" + + "fqzWZ+QBBgK0LUFsaWNlIEV4YW1wbGUgPGFsaWNlQGV4YW1wbGUuY29tPiBbZnJv\n" + + "bSB3b3JrXYh1BBAWCgAnBQJig8vCCRC2GO3iDTVMtxYhBKl1XHhzEUcOxqNPwLYY\n" + + "7eINNUy3AADMFQD+Pcfk5nT7P4KDBxYiLs8Jct3dWLoOMR7dY9jn43d4Q6IBANWy\n" + + "DqBF1IsqTeqRaKUVKw8sWrEIZcgFt7SpgcsLTHMOuDgEYoPLwhIKKwYBBAGXVQEF\n" + + "AQEHQKY2huLPeGlqnLi4ITEgbtYp/C4ofZjmh6/rKUirtopIAwEIB4h1BBgWCgAd\n" + + "BQJig8vCAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQDrUIzRcUtGp9qQD+KuK+\n" + + "lWnlioN8gEyh1Rl2b4ABH6hOBdfW6zjUggnvVHwBAN6r6MJdu47c9xsLKypzyhwB\n" + + "0RbnyH5NMS6jwsK5zmoOuDMEYoPLwhYJKwYBBAHaRw8BAQdAxst2EY4/drt/MeTU\n" + + "RkzQdB8AO1Wc2gnlXavk2a+0DpyI1QQYFgoAfQUCYoPLwgKeAQKbAgUWAgMBAAQL\n" + + "CQgHBRUKCQgLXyAEGRYKAAYFAmKDy8IACgkQchAyuqB7Hn2yOAD/cPA01NO5YJPg\n" + + "KUuSDLnk872y+e419bvFizrM4LKYbeoA/0aw12mcpi1smQJ3mm9T/oGidatBQJ74\n" + + "JIPqTtwHSTIHAAoJEA61CM0XFLRqzj4A+QGjS6ay2AioirHJ9SCA8Eq6L2f/N3RB\n" + + "YBOlV32f3zxyAP9fwXlz0hRbBDnnie2O5eXT9ZurnAKGXPwCtlsqrmeTBg==\n" + + "=uC3F\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + String aliceId = "Alice "; + String charliesPetNameForAlice = "Alice Example [from work]"; + + long aliceKeyId = 1059762964264170602L; + long bobKeyId = 5610053632031231169L; + long charlieKeyId = -5325245004225622857L; + + PGPPublicKeyRing aliceCert = PGPainless.readKeyRing().publicKeyRing(alice); + PGPPublicKey aliceKey = aliceCert.getPublicKey(); + + // alice self-signed her user-id + assertEquals(1, SignatureUtils.getSignaturesOverUserIdBy(aliceKey, aliceId, aliceKeyId).size()); + // Bob signed alices user-id + assertEquals(1, SignatureUtils.getSignaturesOverUserIdBy(aliceKey, aliceId, bobKeyId).size()); + // charlie gave alice a pet name + assertEquals(1, SignatureUtils.getSignaturesOverUserIdBy(aliceKey, charliesPetNameForAlice, charlieKeyId).size()); + + // Alice did not certify the petname charlie gave her + assertEquals(0, SignatureUtils.getSignaturesOverUserIdBy(aliceKey, charliesPetNameForAlice, aliceKeyId).size()); + } } From 9921fc6ff6fbde7783df7989b5a992f3a4c7f0cd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 May 2022 14:19:08 +0200 Subject: [PATCH 0226/1204] Add and test OpenPgpFingerprint.parseFromBinary(bytes) --- .../pgpainless/key/OpenPgpFingerprint.java | 11 +++++ .../key/OpenPgpV4FingerprintTest.java | 39 ++++++++++++++++++ .../key/OpenPgpV5FingerprintTest.java | 40 +++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java index d5a1daad..6abe207b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java @@ -67,6 +67,17 @@ public abstract class OpenPgpFingerprint implements CharSequence, Comparable OpenPgpFingerprint.parseFromBinary(binary)); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java index fffccfe0..2e657d72 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java @@ -4,9 +4,11 @@ package org.pgpainless.key; +import org.bouncycastle.util.encoders.Hex; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; public class OpenPgpV5FingerprintTest { @@ -33,4 +35,42 @@ public class OpenPgpV5FingerprintTest { OpenPgpV5Fingerprint v5fp = (OpenPgpV5Fingerprint) parsed; assertEquals(prettyPrint, v5fp.prettyPrint()); } + + @Test + public void testParseFromBinary() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_leadingZeros() { + String hex = "000000000000000001AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_trailingZeros() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA100000000000000000"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_wrongLength() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA012345"; // missing 2 digits + byte[] binary = Hex.decode(hex); + + assertThrows(IllegalArgumentException.class, () -> OpenPgpFingerprint.parseFromBinary(binary)); + } } From 70a861611cf7804ed71f0f1e34e766906dd59200 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 May 2022 14:21:22 +0200 Subject: [PATCH 0227/1204] Improve SignatureUtils.wasIssuedBy() by adding support for v5 fingerprints --- .../pgpainless/signature/SignatureUtils.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java index 9b86f575..2b69a832 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java @@ -14,6 +14,7 @@ import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Set; +import javax.annotation.Nonnull; import org.bouncycastle.bcpg.sig.IssuerKeyID; import org.bouncycastle.bcpg.sig.KeyExpirationTime; @@ -36,14 +37,11 @@ import org.pgpainless.algorithm.SignatureType; import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.util.OpenPgpKeyAttributeUtil; import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; import org.pgpainless.util.ArmorUtils; -import javax.annotation.Nonnull; - /** * Utility methods related to signatures. */ @@ -325,17 +323,17 @@ public final class SignatureUtils { } public static boolean wasIssuedBy(byte[] fingerprint, PGPSignature signature) { - if (fingerprint.length != 20) { + try { + OpenPgpFingerprint fp = OpenPgpFingerprint.parseFromBinary(fingerprint); + OpenPgpFingerprint issuerFp = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature); + if (issuerFp == null) { + return fp.getKeyId() == signature.getKeyID(); + } + return fp.equals(issuerFp); + } catch (IllegalArgumentException e) { // Unknown fingerprint length return false; } - OpenPgpV4Fingerprint fp = new OpenPgpV4Fingerprint(fingerprint); - OpenPgpFingerprint issuerFp = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature); - if (issuerFp == null) { - return fp.getKeyId() == signature.getKeyID(); - } - - return fp.equals(issuerFp); } /** @@ -354,7 +352,7 @@ public final class SignatureUtils { List signaturesByKeyId = new ArrayList<>(); Iterator userIdSignatures = key.getSignaturesForID(userId); - // getSignaturesForID() is nullable -.- + // getSignaturesForID() is nullable for some reason -.- if (userIdSignatures == null) { return signaturesByKeyId; } From 44c32d0620d86e8a0af9e4d5d9a0702a08fba262 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 1 Jun 2022 13:36:00 +0200 Subject: [PATCH 0228/1204] When setting expiration dates: Prevent integer overflow --- .../subpackets/SignatureSubpackets.java | 17 +++++++---- .../modification/ChangeExpirationTest.java | 30 ++++++++++++++++++- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java index c622844f..df0269a3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java @@ -212,9 +212,7 @@ public class SignatureSubpackets @Override public SignatureSubpackets setSignatureExpirationTime(boolean isCritical, long seconds) { - if (seconds < 0) { - throw new IllegalArgumentException("Expiration time cannot be negative."); - } + enforceBounds(seconds); return setSignatureExpirationTime(new SignatureExpirationTime(isCritical, seconds)); } @@ -285,12 +283,19 @@ public class SignatureSubpackets @Override public SignatureSubpackets setKeyExpirationTime(boolean isCritical, long secondsFromCreationToExpiration) { - if (secondsFromCreationToExpiration < 0) { - throw new IllegalArgumentException("Seconds from key creation to expiration cannot be less than 0."); - } + enforceBounds(secondsFromCreationToExpiration); return setKeyExpirationTime(new KeyExpirationTime(isCritical, secondsFromCreationToExpiration)); } + private void enforceBounds(long secondsFromCreationToExpiration) { + if (secondsFromCreationToExpiration < 0) { + throw new IllegalArgumentException("Seconds from creation to expiration cannot be less than 0."); + } + if (secondsFromCreationToExpiration > 0xffffffffL) { + throw new IllegalArgumentException("Integer overflow. Seconds from creation to expiration cannot be larger than 0xffffffff"); + } + } + @Override public SignatureSubpackets setKeyExpirationTime(@Nullable KeyExpirationTime keyExpirationTime) { this.keyExpirationTime = keyExpirationTime; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java index 6a1c2f71..7aaf71e4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java @@ -7,6 +7,7 @@ package org.pgpainless.key.modification; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; import java.util.Calendar; @@ -14,13 +15,14 @@ import java.util.Date; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.JUtils; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.JUtils; import org.pgpainless.PGPainless; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.TestKeys; import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.util.DateUtil; import org.pgpainless.util.TestAllImplementations; @@ -93,4 +95,30 @@ public class ChangeExpirationTest { sInfo = PGPainless.inspectKeyRing(secretKeys); assertNull(sInfo.getPrimaryKeyExpirationDate()); } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testExtremeExpirationDates() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + // seconds from 2021 to 2199 will overflow 32bit integers + Date farAwayExpiration = DateUtil.parseUTCDate("2199-01-01 00:00:00 UTC"); + + final PGPSecretKeyRing finalKeys = secretKeys; + assertThrows(IllegalArgumentException.class, () -> + PGPainless.modifyKeyRing(finalKeys) + .setExpirationDate(farAwayExpiration, protector) + .done()); + + Date notSoFarAwayExpiration = DateUtil.parseUTCDate("2100-01-01 00:00:00 UTC"); + + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .setExpirationDate(notSoFarAwayExpiration, protector) + .done(); + + Date actualExpiration = PGPainless.inspectKeyRing(secretKeys) + .getPrimaryKeyExpirationDate(); + JUtils.assertDateEquals(notSoFarAwayExpiration, actualExpiration); + } } From 444ec6d5939295205d6f30b04684f30acf3b7862 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 1 Jun 2022 13:40:07 +0200 Subject: [PATCH 0229/1204] Add documentation to enforceBounds() --- .../signature/subpackets/SignatureSubpackets.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java index df0269a3..a2eb1f7d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java @@ -287,6 +287,13 @@ public class SignatureSubpackets return setKeyExpirationTime(new KeyExpirationTime(isCritical, secondsFromCreationToExpiration)); } + /** + * Enforce that
secondsFromCreationToExpiration
is within bounds of an unsigned 32bit number. + * Values less than 0 are illegal, as well as values greater 0xffffffff. + * + * @param secondsFromCreationToExpiration number to check + * @throws IllegalArgumentException in case of an under- or overflow + */ private void enforceBounds(long secondsFromCreationToExpiration) { if (secondsFromCreationToExpiration < 0) { throw new IllegalArgumentException("Seconds from creation to expiration cannot be less than 0."); From 03be9b8bae2bc41a838a6d9d13857b5eb63d476a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Jun 2022 18:39:56 +0200 Subject: [PATCH 0230/1204] Update README --- README.md | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index edd6a245..390e8771 100644 --- a/README.md +++ b/README.md @@ -192,17 +192,9 @@ dependencies { } ``` -## About -PGPainless is a by-product of my [Summer of Code 2018 project](https://blog.jabberhead.tk/summer-of-code-2018/) -implementing OpenPGP support for the XMPP client library [Smack](https://github.com/igniterealtime/Smack). -For that project I was in need of a simple-to-use OpenPGP library. - -Originally I was going to use [Bouncy-GPG](https://github.com/neuhalje/bouncy-gpg) for my project, -but ultimately I decided to create my own OpenPGP library which better fits my needs. - -However, PGPainless was heavily influenced by Bouncy-GPG. - -To reach out to the development team, feel free to send a mail: info@pgpainless.org +## Professional Support +Do you need a custom feature? Are you unsure of what's the best way to integrate PGPainless into your product? +We offer paid professional services. Don't hesitate to send an inquiry to [info@pgpainless.org](mailto:info@pgpainless.org). ## Development PGPainless is developed in - and accepts contributions from - the following places: From c967cbb9f0b58ac446cd7b5a146f379b23b02578 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 16 Jun 2022 11:05:28 +0200 Subject: [PATCH 0231/1204] SOP: Properly throw CannotDecrypt --- .../src/main/java/org/pgpainless/sop/DecryptImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 38656c9b..413136da 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -22,6 +22,7 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -128,6 +129,8 @@ public class DecryptImpl implements Decrypt { decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(ciphertext) .withOptions(consumerOptions); + } catch (MissingDecryptionMethodException e) { + throw new SOPGPException.CannotDecrypt(); } catch (PGPException | IOException e) { throw new SOPGPException.BadData(e); } From 57fbb469ea86aaf507cdd328244d5fd6bb970e31 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Jun 2022 23:13:53 +0200 Subject: [PATCH 0232/1204] Fix performance issue of encrypt and sign operations by buffering --- .../pgpainless/encryption_signing/CRLFGeneratorStream.java | 6 ++++++ .../pgpainless/encryption_signing/EncryptionStream.java | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java index 9743f6f9..4cab8be3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java @@ -46,4 +46,10 @@ public class CRLFGeneratorStream extends OutputStream { } crlfOut.close(); } + + @Override + public void flush() throws IOException { + super.flush(); + crlfOut.flush(); + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 3b737702..0cba8093 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -4,6 +4,7 @@ package org.pgpainless.encryption_signing; +import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; @@ -182,7 +183,11 @@ public final class EncryptionStream extends OutputStream { } public void prepareInputEncoding() { - CRLFGeneratorStream crlfGeneratorStream = new CRLFGeneratorStream(outermostStream, + // By buffering here, we drastically improve performance + // Reason is that CRLFGeneratorStream only implements write(int), so we need BufferedOutputStream to + // "convert" to write(buf) calls again + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outermostStream); + CRLFGeneratorStream crlfGeneratorStream = new CRLFGeneratorStream(bufferedOutputStream, options.isApplyCRLFEncoding() ? StreamEncoding.UTF8 : StreamEncoding.BINARY); outermostStream = crlfGeneratorStream; } From 9cdea63ec4227017cb9ae89a94e03ba40d6330f7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Jun 2022 23:14:09 +0200 Subject: [PATCH 0233/1204] Fix performance issues of sop armor and dearmor operations --- .../src/main/java/org/pgpainless/sop/ArmorImpl.java | 6 +++++- .../src/main/java/org/pgpainless/sop/DearmorImpl.java | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java index 5c92f9b9..71bc3e6b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java @@ -4,6 +4,7 @@ package org.pgpainless.sop; +import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -28,8 +29,11 @@ public class ArmorImpl implements Armor { return new Ready() { @Override public void writeTo(OutputStream outputStream) throws IOException { - ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(outputStream); + // By buffering the output stream, we can improve performance drastically + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); + ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(bufferedOutputStream); Streams.pipeAll(data, armor); + bufferedOutputStream.flush(); armor.close(); } }; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java index 73bd65d5..1cebec6e 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java @@ -4,6 +4,7 @@ package org.pgpainless.sop; +import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -22,7 +23,9 @@ public class DearmorImpl implements Dearmor { @Override public void writeTo(OutputStream outputStream) throws IOException { - Streams.pipeAll(decoder, outputStream); + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); + Streams.pipeAll(decoder, bufferedOutputStream); + bufferedOutputStream.flush(); decoder.close(); } }; From 9a545a29360678cfec793bef26ec6b899f91ea5a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 7 Jun 2022 08:55:10 +0200 Subject: [PATCH 0234/1204] Wip: SOP 4 --- .../key/generation/KeyRingTemplates.java | 12 +- pgpainless-sop/build.gradle | 2 + .../java/org/pgpainless/sop/DecryptImpl.java | 37 ++++-- .../{SignImpl.java => DetachedSignImpl.java} | 15 ++- ...erifyImpl.java => DetachedVerifyImpl.java} | 12 +- .../java/org/pgpainless/sop/EncryptImpl.java | 50 +++++--- .../org/pgpainless/sop/GenerateKeyImpl.java | 10 +- ...MessageImpl.java => InlineDetachImpl.java} | 6 +- .../org/pgpainless/sop/InlineSignImpl.java | 42 +++++++ .../org/pgpainless/sop/InlineVerifyImpl.java | 37 ++++++ .../MatchMakingSecretKeyRingProtector.java | 101 +++++++++++++++ .../main/java/org/pgpainless/sop/SOPImpl.java | 40 ++++-- .../java/org/pgpainless/sop/ArmorTest.java | 2 +- .../DetachInbandSignatureAndMessageTest.java | 2 +- .../sop/EncryptDecryptRoundTripTest.java | 117 +++++++++++++++++- version.gradle | 2 +- 16 files changed, 429 insertions(+), 58 deletions(-) rename pgpainless-sop/src/main/java/org/pgpainless/sop/{SignImpl.java => DetachedSignImpl.java} (92%) rename pgpainless-sop/src/main/java/org/pgpainless/sop/{VerifyImpl.java => DetachedVerifyImpl.java} (86%) rename pgpainless-sop/src/main/java/org/pgpainless/sop/{DetachInbandSignatureAndMessageImpl.java => InlineDetachImpl.java} (92%) create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java index 41d39a1a..e2cf7190 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java @@ -200,7 +200,7 @@ public final class KeyRingTemplates { * @throws PGPException in case of an OpenPGP related error */ public PGPSecretKeyRing modernKeyRing(String userId) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - return modernKeyRing(userId, null); + return modernKeyRing(userId, (Passphrase) null); } /** @@ -217,13 +217,19 @@ public final class KeyRingTemplates { */ public PGPSecretKeyRing modernKeyRing(String userId, String password) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + Passphrase passphrase = (password != null ? Passphrase.fromPassword(password) : null); + return modernKeyRing(userId, passphrase); + } + + public PGPSecretKeyRing modernKeyRing(String userId, Passphrase passphrase) + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { KeyRingBuilder builder = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) .addUserId(userId); - if (!isNullOrEmpty(password)) { - builder.setPassphrase(Passphrase.fromPassword(password)); + if (passphrase != null && !passphrase.isEmpty()) { + builder.setPassphrase(passphrase); } return builder.build(); } diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index d6b97e64..4eca8a7f 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -10,9 +10,11 @@ group 'org.pgpainless' repositories { mavenCentral() + mavenLocal() } dependencies { + implementation 'org.jetbrains:annotations:20.1.0' testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 413136da..89eb29db 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -7,9 +7,12 @@ package org.pgpainless.sop; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; @@ -23,9 +26,8 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.exception.MissingDecryptionMethodException; +import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.Passphrase; import sop.DecryptionResult; import sop.ReadyWithResult; @@ -37,6 +39,8 @@ import sop.operation.Decrypt; public class DecryptImpl implements Decrypt { private final ConsumerOptions consumerOptions = new ConsumerOptions(); + private final Set keys = new HashSet<>(); + private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); @Override public DecryptImpl verifyNotBefore(Date timestamp) throws SOPGPException.UnsupportedOption { @@ -96,29 +100,34 @@ public class DecryptImpl implements Decrypt { } @Override - public DecryptImpl withKey(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo { + public DecryptImpl withKey(InputStream keyIn) throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo { try { - PGPSecretKeyRingCollection secretKeys = PGPainless.readKeyRing() + PGPSecretKeyRingCollection secretKeyCollection = PGPainless.readKeyRing() .secretKeyRingCollection(keyIn); - - for (PGPSecretKeyRing secretKey : secretKeys) { - KeyRingInfo info = new KeyRingInfo(secretKey); - if (!info.isFullyDecrypted()) { - throw new SOPGPException.KeyIsProtected(); - } + for (PGPSecretKeyRing key : secretKeyCollection) { + keys.add(key); } - - consumerOptions.addDecryptionKeys(secretKeys, SecretKeyRingProtector.unprotectedKeys()); } catch (IOException | PGPException e) { throw new SOPGPException.BadData(e); } return this; } + @Override + public Decrypt withKeyPassword(byte[] password) { + String string = new String(password, Charset.forName("UTF8")); + protector.addPassphrase(Passphrase.fromPassword(string)); + return this; + } + @Override public ReadyWithResult ciphertext(InputStream ciphertext) throws SOPGPException.BadData, SOPGPException.MissingArg { + for (PGPSecretKeyRing key : keys) { + protector.addSecretKey(key); + consumerOptions.addDecryptionKey(key, protector); + } if (consumerOptions.getDecryptionKeys().isEmpty() && consumerOptions.getDecryptionPassphrases().isEmpty() && consumerOptions.getSessionKey() == null) { throw new SOPGPException.MissingArg("Missing decryption key, passphrase or session key."); @@ -131,8 +140,12 @@ public class DecryptImpl implements Decrypt { .withOptions(consumerOptions); } catch (MissingDecryptionMethodException e) { throw new SOPGPException.CannotDecrypt(); + } catch (WrongPassphraseException e) { + throw new SOPGPException.KeyIsProtected(); } catch (PGPException | IOException e) { throw new SOPGPException.BadData(e); + } finally { + protector.clear(); } return new ReadyWithResult() { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java similarity index 92% rename from pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java rename to pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java index 286c5262..3341712a 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java @@ -32,28 +32,28 @@ import sop.ReadyWithResult; import sop.SigningResult; import sop.enums.SignAs; import sop.exception.SOPGPException; -import sop.operation.Sign; +import sop.operation.DetachedSign; -public class SignImpl implements Sign { +public class DetachedSignImpl implements DetachedSign { private boolean armor = true; private SignAs mode = SignAs.Binary; private final SigningOptions signingOptions = new SigningOptions(); @Override - public Sign noArmor() { + public DetachedSign noArmor() { armor = false; return this; } @Override - public Sign mode(SignAs mode) { + public DetachedSign mode(SignAs mode) { this.mode = mode; return this; } @Override - public Sign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { + public DetachedSign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { try { PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); @@ -70,6 +70,11 @@ public class SignImpl implements Sign { return this; } + @Override + public DetachedSign withKeyPassword(byte[] password) { + return null; + } + @Override public ReadyWithResult data(InputStream data) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java similarity index 86% rename from pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java rename to pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index c874b452..d4db494f 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -21,26 +21,26 @@ import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.key.SubkeyIdentifier; import sop.Verification; import sop.exception.SOPGPException; -import sop.operation.Verify; +import sop.operation.DetachedVerify; -public class VerifyImpl implements Verify { +public class DetachedVerifyImpl implements DetachedVerify { private final ConsumerOptions options = new ConsumerOptions(); @Override - public Verify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { + public DetachedVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { options.verifyNotBefore(timestamp); return this; } @Override - public Verify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { + public DetachedVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { options.verifyNotAfter(timestamp); return this; } @Override - public Verify cert(InputStream cert) throws SOPGPException.BadData { + public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData { PGPPublicKeyRingCollection certificates; try { certificates = PGPainless.readKeyRing().publicKeyRingCollection(cert); @@ -52,7 +52,7 @@ public class VerifyImpl implements Verify { } @Override - public VerifyImpl signatures(InputStream signatures) throws SOPGPException.BadData { + public DetachedVerifyImpl signatures(InputStream signatures) throws SOPGPException.BadData { try { options.addVerificationOfDetachedSignatures(signatures); } catch (IOException | PGPException e) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index d6bd7709..bfa80f47 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -7,9 +7,13 @@ package org.pgpainless.sop; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.HashSet; +import java.util.Set; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; @@ -20,7 +24,6 @@ import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.Passphrase; import sop.util.ProxyOutputStream; import sop.Ready; @@ -33,6 +36,9 @@ public class EncryptImpl implements Encrypt { EncryptionOptions encryptionOptions = new EncryptionOptions(); SigningOptions signingOptions = null; + Set signingKeys = new HashSet<>(); + MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + private EncryptAs encryptAs = EncryptAs.Binary; boolean armor = true; @@ -49,7 +55,7 @@ public class EncryptImpl implements Encrypt { } @Override - public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { + public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { try { PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); if (keys.size() != 1) { @@ -59,23 +65,20 @@ public class EncryptImpl implements Encrypt { if (signingOptions == null) { signingOptions = SigningOptions.get(); } - try { - signingOptions.addInlineSignatures( - SecretKeyRingProtector.unprotectedKeys(), - keys, - (encryptAs == EncryptAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) - ); - } catch (IllegalArgumentException e) { - throw new SOPGPException.KeyCannotSign(); - } catch (WrongPassphraseException e) { - throw new SOPGPException.KeyIsProtected(); - } + signingKeys.add(keys.getKeyRings().next()); } catch (IOException | PGPException e) { throw new SOPGPException.BadData(e); } return this; } + @Override + public Encrypt withKeyPassword(byte[] password) { + String passphrase = new String(password, Charset.forName("UTF8")); + protector.addPassphrase(Passphrase.fromPassword(passphrase)); + return this; + } + @Override public Encrypt withPassword(String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { encryptionOptions.addPassphrase(Passphrase.fromPassword(password)); @@ -97,6 +100,26 @@ public class EncryptImpl implements Encrypt { @Override public Ready plaintext(InputStream plaintext) throws IOException { + for (PGPSecretKeyRing signingKey : signingKeys) { + protector.addSecretKey(signingKey); + } + + if (signingOptions != null) { + try { + signingOptions.addInlineSignatures( + protector, + signingKeys, + (encryptAs == EncryptAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) + ); + } catch (IllegalArgumentException e) { + throw new SOPGPException.KeyCannotSign(); + } catch (WrongPassphraseException e) { + throw new SOPGPException.KeyIsProtected(); + } catch (PGPException e) { + throw new SOPGPException.BadData(e); + } + } + ProducerOptions producerOptions = signingOptions != null ? ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions) : ProducerOptions.encrypt(encryptionOptions); @@ -125,7 +148,6 @@ public class EncryptImpl implements Encrypt { private static StreamEncoding encryptAsToStreamEncoding(EncryptAs encryptAs) { switch (encryptAs) { case Binary: - case MIME: return StreamEncoding.BINARY; case Text: return StreamEncoding.UTF8; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index 6a2c09f9..893c8dd8 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -19,6 +19,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmorUtils; +import org.pgpainless.util.Passphrase; import sop.Ready; import sop.exception.SOPGPException; import sop.operation.GenerateKey; @@ -27,6 +28,7 @@ public class GenerateKeyImpl implements GenerateKey { private boolean armor = true; private final Set userIds = new LinkedHashSet<>(); + private Passphrase passphrase; @Override public GenerateKey noArmor() { @@ -40,6 +42,12 @@ public class GenerateKeyImpl implements GenerateKey { return this; } + @Override + public GenerateKey withKeyPassword(String password) { + this.passphrase = Passphrase.fromPassword(password); + return this; + } + @Override public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { Iterator userIdIterator = userIds.iterator(); @@ -50,7 +58,7 @@ public class GenerateKeyImpl implements GenerateKey { PGPSecretKeyRing key; try { key = PGPainless.generateKeyRing() - .modernKeyRing(userIdIterator.next(), null); + .modernKeyRing(userIdIterator.next(), passphrase); if (userIdIterator.hasNext()) { SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(key); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachInbandSignatureAndMessageImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java similarity index 92% rename from pgpainless-sop/src/main/java/org/pgpainless/sop/DetachInbandSignatureAndMessageImpl.java rename to pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java index 34d2f618..f23434c2 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachInbandSignatureAndMessageImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java @@ -20,14 +20,14 @@ import org.pgpainless.util.ArmoredOutputStreamFactory; import sop.ReadyWithResult; import sop.Signatures; import sop.exception.SOPGPException; -import sop.operation.DetachInbandSignatureAndMessage; +import sop.operation.InlineDetach; -public class DetachInbandSignatureAndMessageImpl implements DetachInbandSignatureAndMessage { +public class InlineDetachImpl implements InlineDetach { private boolean armor = true; @Override - public DetachInbandSignatureAndMessage noArmor() { + public InlineDetach noArmor() { this.armor = false; return this; } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java new file mode 100644 index 00000000..7eaeaa6f --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import sop.ReadyWithResult; +import sop.SigningResult; +import sop.enums.InlineSignAs; +import sop.exception.SOPGPException; +import sop.operation.DetachedSign; +import sop.operation.InlineSign; + +import java.io.IOException; +import java.io.InputStream; + +public class InlineSignImpl implements InlineSign { + @Override + public DetachedSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption { + return null; + } + + @Override + public DetachedSign noArmor() { + return null; + } + + @Override + public InlineSign key(InputStream key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { + return null; + } + + @Override + public InlineSign withKeyPassword(byte[] password) { + return null; + } + + @Override + public ReadyWithResult data(InputStream data) throws IOException, SOPGPException.ExpectedText { + return null; + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java new file mode 100644 index 00000000..a230e0fd --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import sop.ReadyWithResult; +import sop.Verification; +import sop.exception.SOPGPException; +import sop.operation.InlineVerify; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Date; +import java.util.List; + +public class InlineVerifyImpl implements InlineVerify { + @Override + public ReadyWithResult> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { + return null; + } + + @Override + public InlineVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { + return null; + } + + @Override + public InlineVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { + return null; + } + + @Override + public InlineVerify cert(InputStream cert) throws SOPGPException.BadData { + return null; + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java new file mode 100644 index 00000000..df54583e --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import java.util.HashSet; +import java.util.Set; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.jetbrains.annotations.Nullable; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.info.KeyInfo; +import org.pgpainless.key.protection.CachingSecretKeyRingProtector; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; + +public class MatchMakingSecretKeyRingProtector implements SecretKeyRingProtector { + + private final Set passphrases = new HashSet<>(); + private final Set keys = new HashSet<>(); + private final CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(); + + public void addPassphrase(Passphrase passphrase) { + if (passphrase.isEmpty()) { + return; + } + + if (!passphrases.add(passphrase)) { + return; + } + + for (PGPSecretKeyRing key : keys) { + for (PGPSecretKey subkey : key) { + if (protector.hasPassphrase(subkey.getKeyID())) { + continue; + } + + testPassphrase(passphrase, subkey); + } + } + } + + public void addSecretKey(PGPSecretKeyRing key) { + if (!keys.add(key)) { + return; + } + + for (PGPSecretKey subkey : key) { + if (KeyInfo.isDecrypted(subkey)) { + protector.addPassphrase(subkey.getKeyID(), Passphrase.emptyPassphrase()); + } else { + for (Passphrase passphrase : passphrases) { + testPassphrase(passphrase, subkey); + } + } + } + } + + private void testPassphrase(Passphrase passphrase, PGPSecretKey subkey) { + try { + PBESecretKeyDecryptor decryptor = ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); + UnlockSecretKey.unlockSecretKey(subkey, decryptor); + protector.addPassphrase(subkey.getKeyID(), passphrase); + } catch (PGPException e) { + // wrong password + } + } + + @Override + public boolean hasPassphraseFor(Long keyId) { + return protector.hasPassphrase(keyId); + } + + @Nullable + @Override + public PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException { + return protector.getDecryptor(keyId); + } + + @Nullable + @Override + public PBESecretKeyEncryptor getEncryptor(Long keyId) throws PGPException { + return protector.getEncryptor(keyId); + } + + public void clear() { + for (Passphrase passphrase : passphrases) { + passphrase.clear(); + } + + for (PGPSecretKeyRing key : keys) { + protector.forgetPassphrase(key); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java index cfa426a5..35ff8994 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -8,12 +8,14 @@ import sop.SOP; import sop.operation.Armor; import sop.operation.Dearmor; import sop.operation.Decrypt; -import sop.operation.DetachInbandSignatureAndMessage; +import sop.operation.DetachedSign; +import sop.operation.DetachedVerify; +import sop.operation.InlineDetach; import sop.operation.Encrypt; import sop.operation.ExtractCert; import sop.operation.GenerateKey; -import sop.operation.Sign; -import sop.operation.Verify; +import sop.operation.InlineSign; +import sop.operation.InlineVerify; import sop.operation.Version; public class SOPImpl implements SOP { @@ -34,13 +36,33 @@ public class SOPImpl implements SOP { } @Override - public Sign sign() { - return new SignImpl(); + public DetachedSign sign() { + return detachedSign(); } @Override - public Verify verify() { - return new VerifyImpl(); + public DetachedSign detachedSign() { + return new DetachedSignImpl(); + } + + @Override + public InlineSign inlineSign() { + return new InlineSignImpl(); + } + + @Override + public DetachedVerify verify() { + return detachedVerify(); + } + + @Override + public DetachedVerify detachedVerify() { + return new DetachedVerifyImpl(); + } + + @Override + public InlineVerify inlineVerify() { + return new InlineVerifyImpl(); } @Override @@ -64,7 +86,7 @@ public class SOPImpl implements SOP { } @Override - public DetachInbandSignatureAndMessage detachInbandSignatureAndMessage() { - return new DetachInbandSignatureAndMessageImpl(); + public InlineDetach inlineDetach() { + return new InlineDetachImpl(); } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java index ad6da440..95129dfa 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java @@ -28,7 +28,7 @@ public class ArmorTest { @Test public void armor() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice", null).getEncoded(); + byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice").getEncoded(); byte[] knownGoodArmor = ArmorUtils.toAsciiArmoredString(data).getBytes(StandardCharsets.UTF_8); byte[] armored = new SOPImpl() .armor() diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachInbandSignatureAndMessageTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachInbandSignatureAndMessageTest.java index 06c11226..b76f069c 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachInbandSignatureAndMessageTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachInbandSignatureAndMessageTest.java @@ -58,7 +58,7 @@ public class DetachInbandSignatureAndMessageTest { signingStream.close(); // actually detach the message - ByteArrayAndResult detachedMsg = sop.detachInbandSignatureAndMessage() + ByteArrayAndResult detachedMsg = sop.inlineDetach() .message(out.toByteArray()) .toByteArrayAndResult(); diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java index d0699ae7..45a26586 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import org.bouncycastle.util.io.Streams; @@ -24,12 +25,13 @@ import sop.exception.SOPGPException; public class EncryptDecryptRoundTripTest { + private static final Charset utf8 = Charset.forName("UTF8"); private static SOP sop; private static byte[] aliceKey; private static byte[] aliceCert; private static byte[] bobKey; private static byte[] bobCert; - private static byte[] message = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); + private static byte[] message = "Hello, World!\n".getBytes(utf8); @BeforeAll public static void setup() throws IOException { @@ -218,8 +220,119 @@ public class EncryptDecryptRoundTripTest { "=MUYS\n" + "-----END PGP PRIVATE KEY BLOCK-----"; + String msg = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4Doj0CaB2GRvISAQdAhV5sjUCxanM68jG9qaq2rep1KKQx2o+9yrK0Rsrtqkww\n" + + "mb4uVv/SD3ixDztUSgUset0jeUeZHZAWfTB9cWawX4fiB2BdbcxhxFqQR8VPJ2SZ\n" + + "0jcB+wH1gq05AkMaCfoEIio3o3QcZq2In8tqj69U3AFRQApoH/p+ZLDz2pcnFBn+\n" + + "x1Y+C6wNg/3g\n" + + "=6vge\n" + + "-----END PGP MESSAGE-----"; + assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.decrypt() - .withKey(passwordProtectedKey.getBytes(StandardCharsets.UTF_8))); + .withKey(passwordProtectedKey.getBytes(StandardCharsets.UTF_8)) + .ciphertext(msg.getBytes(utf8))); + } + + @Test + public void encryptDecryptRoundTripWithProtectedKey() throws IOException { + byte[] passphrase = "sw0rdf1sh".getBytes(utf8); + + byte[] key = sop.generateKey() + .userId("Alice ") + .withKeyPassword(passphrase) + .generate().getBytes(); + + byte[] cert = sop.extractCert() + .key(key) + .getBytes(); + + byte[] plaintext = "Hello, World!\n".getBytes(utf8); + + byte[] ciphertext = sop.encrypt() + .withCert(cert) + .plaintext(plaintext) + .getBytes(); + + byte[] decrypted = sop.decrypt() + .withKeyPassword(passphrase) + .withKey(key) + .ciphertext(ciphertext) + .toByteArrayAndResult() + .getBytes(); + + assertArrayEquals(plaintext, decrypted); + } + + @Test + public void encryptDecryptRoundTripWithTwoProtectedKeysAndOnePassphrase() throws IOException { + byte[] passphrase1 = "sw0rdf1sh".getBytes(utf8); + + byte[] key1 = sop.generateKey() + .userId("Alice ") + .withKeyPassword(passphrase1) + .generate().getBytes(); + + byte[] cert1 = sop.extractCert() + .key(key1) + .getBytes(); + + byte[] passphrase2 = "fooBar".getBytes(utf8); + + byte[] key2 = sop.generateKey() + .userId("Bob ") + .withKeyPassword(passphrase2) + .generate().getBytes(); + + byte[] cert2 = sop.extractCert() + .key(key2) + .getBytes(); + + byte[] plaintext = "Hello, World!\n".getBytes(utf8); + + byte[] ciphertext = sop.encrypt() + .withCert(cert1) + .withCert(cert2) + .plaintext(plaintext) + .getBytes(); + + byte[] decrypted = sop.decrypt() + .withKey(key1) + .withKey(key2) + .withKeyPassword(passphrase2) + .ciphertext(ciphertext) + .toByteArrayAndResult() + .getBytes(); + + assertArrayEquals(plaintext, decrypted); + } + + @Test + public void encryptDecryptRoundTripFailsWithProtectedKeyAndWrongPassphrase() throws IOException { + byte[] passphrase = "sw0rdf1sh".getBytes(utf8); + + byte[] key = sop.generateKey() + .userId("Alice ") + .withKeyPassword(passphrase) + .generate().getBytes(); + + byte[] cert = sop.extractCert() + .key(key) + .getBytes(); + + byte[] plaintext = "Hello, World!\n".getBytes(utf8); + + byte[] ciphertext = sop.encrypt() + .withCert(cert) + .plaintext(plaintext) + .getBytes(); + + assertThrows(SOPGPException.KeyIsProtected.class, + () -> sop.decrypt() + .withKeyPassword("foobar") + .withKey(key) + .ciphertext(ciphertext)); } @Test diff --git a/version.gradle b/version.gradle index f890e8d6..ddb6de7d 100644 --- a/version.gradle +++ b/version.gradle @@ -12,6 +12,6 @@ allprojects { slf4jVersion = '1.7.36' logbackVersion = '1.2.11' junitVersion = '5.8.2' - sopJavaVersion = '1.2.3' + sopJavaVersion = '1.2.4-SNAPSHOT' } } From dd26b5230d2b30513a124270930df78339d95561 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 9 Jun 2022 00:42:06 +0200 Subject: [PATCH 0235/1204] Use newly introduced modernKeyRing(userId) method --- .../CertificateWithMissingSecretKeyTest.java | 2 +- .../CleartextSignatureVerificationTest.java | 2 +- .../OpenPgpInputStreamTest.java | 2 +- .../VerifyWithMissingPublicKeyCallback.java | 2 +- .../EncryptionOptionsTest.java | 2 +- .../encryption_signing/FileInformationTest.java | 2 +- .../java/org/pgpainless/example/ConvertKeys.java | 2 +- .../test/java/org/pgpainless/example/Sign.java | 2 +- .../org/pgpainless/key/KeyRingValidatorTest.java | 2 +- .../generation/KeyGenerationSubpacketsTest.java | 4 ++-- .../org/pgpainless/key/info/KeyRingInfoTest.java | 2 +- ...eyWithModifiedBindingSignatureSubpackets.java | 2 +- .../key/modification/AddUserIdTest.java | 2 +- ...hangePrimaryUserIdAndExpirationDatesTest.java | 10 +++++----- .../modification/RefuseToAddWeakSubkeyTest.java | 4 ++-- .../key/modification/RevokeSubKeyTest.java | 4 ++-- .../key/modification/RevokeUserIdsTest.java | 6 +++--- .../key/parsing/KeyRingCollectionReaderTest.java | 4 ++-- .../key/parsing/KeyRingReaderTest.java | 16 ++++++++-------- .../org/pgpainless/key/util/KeyRingUtilTest.java | 2 +- .../OnePassSignatureBracketingTest.java | 4 ++-- .../signature/SignatureSubpacketsUtilTest.java | 2 +- .../builder/DirectKeySignatureBuilderTest.java | 2 +- ...rdPartyCertificationSignatureBuilderTest.java | 6 +++--- 24 files changed, 44 insertions(+), 44 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java index 13359830..a53999a6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java @@ -79,7 +79,7 @@ public class CertificateWithMissingSecretKeyTest { // missing encryption sec key we generate on the fly PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Missing Decryption Key ", null); + .modernKeyRing("Missing Decryption Key "); encryptionSubkeyId = PGPainless.inspectKeyRing(secretKeys) .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID(); // remove the encryption/decryption secret key diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java index bcf1321e..12596988 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java @@ -214,7 +214,7 @@ public class CleartextSignatureVerificationTest { throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { String message = randomString(28, 4000); - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice", null); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); ByteArrayOutputStream out = new ByteArrayOutputStream(); EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() .onOutputStream(out) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java index c2a24c76..a39ee70f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java @@ -740,7 +740,7 @@ public class OpenPgpInputStreamTest { public void testSignedMessageConsumption() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { ByteArrayInputStream plaintext = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Sigmund ", null); + .modernKeyRing("Sigmund "); ByteArrayOutputStream signedOut = new ByteArrayOutputStream(); EncryptionStream signer = PGPainless.encryptAndOrSign() diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java index 9c786807..e4e9427b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java @@ -42,7 +42,7 @@ public class VerifyWithMissingPublicKeyCallback { @Test public void testMissingPublicKeyCallback() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing signingSecKeys = PGPainless.generateKeyRing().modernKeyRing("alice", null); + PGPSecretKeyRing signingSecKeys = PGPainless.generateKeyRing().modernKeyRing("alice"); PGPPublicKey signingKey = new KeyRingInfo(signingSecKeys).getSigningSubkeys().get(0); PGPPublicKeyRing signingPubKeys = KeyRingUtils.publicKeyRingFrom(signingSecKeys); PGPPublicKeyRing unrelatedKeys = TestKeys.getJulietPublicKeyRing(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 40a336e8..3e7ccb4d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -167,7 +167,7 @@ public class EncryptionOptionsTest { @Test public void testAddRecipients_PGPPublicKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPPublicKeyRing secondKeyRing = KeyRingUtils.publicKeyRingFrom( - PGPainless.generateKeyRing().modernKeyRing("other@pgpainless.org", null)); + PGPainless.generateKeyRing().modernKeyRing("other@pgpainless.org")); PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection( Arrays.asList(publicKeys, secondKeyRing)); diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java index c5a4d28c..662971e4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java @@ -38,7 +38,7 @@ public class FileInformationTest { @BeforeAll public static void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - secretKey = PGPainless.generateKeyRing().modernKeyRing("alice@wonderland.lit", null); + secretKey = PGPainless.generateKeyRing().modernKeyRing("alice@wonderland.lit"); certificate = PGPainless.extractCertificate(secretKey); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ConvertKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/ConvertKeys.java index 2c96112c..fc99fa44 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ConvertKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ConvertKeys.java @@ -26,7 +26,7 @@ public class ConvertKeys { public void secretKeyToCertificate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { String userId = "alice@wonderland.lit"; PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing(userId, null); + .modernKeyRing(userId); // Extract certificate (public key) from secret key PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java b/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java index 77db571d..06228c75 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java @@ -39,7 +39,7 @@ public class Sign { @BeforeAll public static void prepare() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - secretKey = PGPainless.generateKeyRing().modernKeyRing("Emilia Example ", null); + secretKey = PGPainless.generateKeyRing().modernKeyRing("Emilia Example "); protector = SecretKeyRingProtector.unprotectedKeys(); // no password } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/KeyRingValidatorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/KeyRingValidatorTest.java index 970cea9c..7d5c0520 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/KeyRingValidatorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/KeyRingValidatorTest.java @@ -296,7 +296,7 @@ public class KeyRingValidatorTest { @Test public void testKeyWithUserAttributes() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice ", null); + .modernKeyRing("Alice "); PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); PGPPublicKey publicKey = secretKeys.getPublicKey(); PGPSecretKey secretKey = secretKeys.getSecretKey(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java index 9a866d34..cce27a80 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java @@ -41,7 +41,7 @@ public class KeyGenerationSubpacketsTest { @Test public void verifyDefaultSubpacketsForUserIdSignatures() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InterruptedException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice", null); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); PGPSignature userIdSig = info.getLatestUserIdCertification("Alice"); @@ -108,7 +108,7 @@ public class KeyGenerationSubpacketsTest { @Test public void verifyDefaultSubpacketsForSubkeyBindingSignatures() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice", null); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); List keysBefore = info.getPublicKeys(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java index bfececee..43273315 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java @@ -515,7 +515,7 @@ public class KeyRingInfoTest { @Test public void getSecretKeyTest() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice", null); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(secretKeys); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java index e0fa2a01..77143b9f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java @@ -42,7 +42,7 @@ public class AddSubkeyWithModifiedBindingSignatureSubpackets { public void bindEncryptionSubkeyAndModifyBindingSignatureHashedSubpackets() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice ", null); + .modernKeyRing("Alice "); KeyRingInfo before = PGPainless.inspectKeyRing(secretKeys); PGPKeyPair secretSubkey = KeyRingBuilder.generateKeyPair( diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java index 4580b00c..19f84930 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java @@ -114,7 +114,7 @@ public class AddUserIdTest { @Test public void addNewPrimaryUserIdTest() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice", null); + .modernKeyRing("Alice"); UserId bob = UserId.newBuilder().withName("Bob").noEmail().noComment().build(); assertNotEquals("Bob", PGPainless.inspectKeyRing(secretKeys).getPrimaryUserId()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangePrimaryUserIdAndExpirationDatesTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangePrimaryUserIdAndExpirationDatesTest.java index 26618b7a..db60934f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangePrimaryUserIdAndExpirationDatesTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangePrimaryUserIdAndExpirationDatesTest.java @@ -27,7 +27,7 @@ public class ChangePrimaryUserIdAndExpirationDatesTest { public void generateA_primaryB_revokeA_cantSecondaryA() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InterruptedException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("A", null); + .modernKeyRing("A"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); @@ -70,7 +70,7 @@ public class ChangePrimaryUserIdAndExpirationDatesTest { public void generateA_primaryExpire_isExpired() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InterruptedException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("A", null); + .modernKeyRing("A"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); @@ -92,7 +92,7 @@ public class ChangePrimaryUserIdAndExpirationDatesTest { public void generateA_primaryB_primaryExpire_bIsStillPrimary() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InterruptedException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("A", null); + .modernKeyRing("A"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); @@ -130,7 +130,7 @@ public class ChangePrimaryUserIdAndExpirationDatesTest { @Test public void generateA_expire_certify() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InterruptedException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("A", null); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("A"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); secretKeys = PGPainless.modifyKeyRing(secretKeys) @@ -151,7 +151,7 @@ public class ChangePrimaryUserIdAndExpirationDatesTest { @Test public void generateA_expire_primaryB_expire_isPrimaryB() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InterruptedException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("A", null); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("A"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); Thread.sleep(1000); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java index 72dfac71..2570837f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java @@ -37,7 +37,7 @@ public class RefuseToAddWeakSubkeyTest { PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice", null); + .modernKeyRing("Alice"); SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); KeySpec spec = KeySpec.getBuilder(KeyType.RSA(RsaLength._1024), KeyFlag.ENCRYPT_COMMS).build(); @@ -49,7 +49,7 @@ public class RefuseToAddWeakSubkeyTest { public void testEditorAllowsToAddWeakSubkeyIfCompliesToPublicKeyAlgorithmPolicy() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice", null); + .modernKeyRing("Alice"); // set weak policy Map minimalBitStrengths = new EnumMap<>(PublicKeyAlgorithm.class); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeSubKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeSubKeyTest.java index 81e6287f..6a044199 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeSubKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeSubKeyTest.java @@ -128,7 +128,7 @@ public class RevokeSubKeyTest { @Test public void inspectSubpacketsOnDefaultRevocationSignature() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice", null); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); PGPPublicKey encryptionSubkey = PGPainless.inspectKeyRing(secretKeys) .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); @@ -153,7 +153,7 @@ public class RevokeSubKeyTest { @Test public void inspectSubpacketsOnModifiedRevocationSignature() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice", null); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); PGPPublicKey encryptionSubkey = PGPainless.inspectKeyRing(secretKeys) .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeUserIdsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeUserIdsTest.java index a8c732b4..9dc968ed 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeUserIdsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeUserIdsTest.java @@ -27,7 +27,7 @@ public class RevokeUserIdsTest { @Test public void revokeWithSelectUserId() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice ", null); + .modernKeyRing("Alice "); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); secretKeys = PGPainless.modifyKeyRing(secretKeys) @@ -58,7 +58,7 @@ public class RevokeUserIdsTest { @Test public void removeUserId() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice ", null); + .modernKeyRing("Alice "); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); secretKeys = PGPainless.modifyKeyRing(secretKeys) @@ -89,7 +89,7 @@ public class RevokeUserIdsTest { @Test public void emptySelectionYieldsNoSuchElementException() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice ", null); + .modernKeyRing("Alice "); assertThrows(NoSuchElementException.class, () -> PGPainless.modifyKeyRing(secretKeys).revokeUserIds( diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingCollectionReaderTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingCollectionReaderTest.java index 5c117652..552ae25f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingCollectionReaderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingCollectionReaderTest.java @@ -27,8 +27,8 @@ public class KeyRingCollectionReaderTest { @Test public void writeAndParseKeyRingCollections() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { // secret keys - PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice ", null); - PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("Bob ", null); + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice "); + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("Bob "); PGPSecretKeyRingCollection collection = KeyRingUtils.keyRingsToKeyRingCollection(alice, bob); String ascii = ArmorUtils.toAsciiArmoredString(collection); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java index 68233c9b..9349a864 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java @@ -455,8 +455,8 @@ class KeyRingReaderTest { @Test public void testReadSecretKeysIgnoresMultipleMarkers() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org", null); - PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("bob@pgpainless.org", null); + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org"); + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("bob@pgpainless.org"); MarkerPacket marker = TestUtils.getMarkerPacket(); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); @@ -489,7 +489,7 @@ class KeyRingReaderTest { @Test public void testReadingSecretKeysExceedsIterationLimit() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org", null); + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org"); MarkerPacket marker = TestUtils.getMarkerPacket(); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); @@ -508,8 +508,8 @@ class KeyRingReaderTest { @Test public void testReadingSecretKeyCollectionExceedsIterationLimit() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org", null); - PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("bob@pgpainless.org", null); + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org"); + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("bob@pgpainless.org"); MarkerPacket marker = TestUtils.getMarkerPacket(); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); @@ -530,7 +530,7 @@ class KeyRingReaderTest { @Test public void testReadingPublicKeysExceedsIterationLimit() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org", null); + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org"); PGPPublicKeyRing alice = PGPainless.extractCertificate(secretKeys); MarkerPacket marker = TestUtils.getMarkerPacket(); @@ -550,8 +550,8 @@ class KeyRingReaderTest { @Test public void testReadingPublicKeyCollectionExceedsIterationLimit() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing sec1 = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org", null); - PGPSecretKeyRing sec2 = PGPainless.generateKeyRing().modernKeyRing("bob@pgpainless.org", null); + PGPSecretKeyRing sec1 = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org"); + PGPSecretKeyRing sec2 = PGPainless.generateKeyRing().modernKeyRing("bob@pgpainless.org"); PGPPublicKeyRing alice = PGPainless.extractCertificate(sec1); PGPPublicKeyRing bob = PGPainless.extractCertificate(sec2); MarkerPacket marker = TestUtils.getMarkerPacket(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyRingUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyRingUtilTest.java index 45552664..b75969fc 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyRingUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyRingUtilTest.java @@ -33,7 +33,7 @@ public class KeyRingUtilTest { @Test public void testInjectCertification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice", null); + .modernKeyRing("Alice"); // test preconditions assertFalse(secretKeys.getPublicKey().getUserAttributes().hasNext()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java index 19f8c5ac..0042d14b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java @@ -56,8 +56,8 @@ public class OnePassSignatureBracketingTest { public void onePassSignaturePacketsAndSignaturesAreBracketedTest() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing key1 = PGPainless.generateKeyRing().modernKeyRing("Alice", null); - PGPSecretKeyRing key2 = PGPainless.generateKeyRing().modernKeyRing("Bob", null); + PGPSecretKeyRing key1 = PGPainless.generateKeyRing().modernKeyRing("Alice"); + PGPSecretKeyRing key2 = PGPainless.generateKeyRing().modernKeyRing("Bob"); PGPPublicKeyRing cert1 = PGPainless.extractCertificate(key1); ByteArrayOutputStream out = new ByteArrayOutputStream(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java index d990c4b1..d5cfb4f5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java @@ -54,7 +54,7 @@ public class SignatureSubpacketsUtilTest { @Test public void testGetKeyExpirationTimeAsDate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Expire", null); + .modernKeyRing("Expire"); Date expiration = Date.from(new Date().toInstant().plus(365, ChronoUnit.DAYS)); secretKeys = PGPainless.modifyKeyRing(secretKeys) .setExpirationDate(expiration, SecretKeyRingProtector.unprotectedKeys()) diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java index 0ab09eb0..945a06a5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java @@ -34,7 +34,7 @@ public class DirectKeySignatureBuilderTest { @Test public void testDirectKeySignatureBuilding() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InterruptedException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice", null); + .modernKeyRing("Alice"); DirectKeySignatureBuilder dsb = new DirectKeySignatureBuilder( secretKeys.getSecretKey(), diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java index bfc83df4..ced6a466 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java @@ -30,7 +30,7 @@ public class ThirdPartyCertificationSignatureBuilderTest { @Test public void testInvalidSignatureTypeThrows() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice", null); + .modernKeyRing("Alice"); assertThrows(IllegalArgumentException.class, () -> new ThirdPartyCertificationSignatureBuilder( SignatureType.BINARY_DOCUMENT, // invalid type @@ -41,10 +41,10 @@ public class ThirdPartyCertificationSignatureBuilderTest { @Test public void testUserIdCertification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice", null); + .modernKeyRing("Alice"); PGPPublicKeyRing bobsPublicKeys = PGPainless.extractCertificate( - PGPainless.generateKeyRing().modernKeyRing("Bob", null)); + PGPainless.generateKeyRing().modernKeyRing("Bob")); ThirdPartyCertificationSignatureBuilder signatureBuilder = new ThirdPartyCertificationSignatureBuilder( secretKeys.getSecretKey(), From 0b69e18715ae14220e054bebc30a635eef5f5e50 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 9 Jun 2022 00:44:09 +0200 Subject: [PATCH 0236/1204] Experimental support for inline-sign, inline-verify --- .../pgpainless/cli/commands/ArmorTest.java | 4 +- .../pgpainless/cli/commands/DearmorTest.java | 4 +- .../DetachInbandSignatureAndMessageTest.java | 6 +- .../cli/commands/SignVerifyTest.java | 4 +- .../misc/SignUsingPublicKeyBehaviorTest.java | 2 +- .../java/org/pgpainless/sop/DecryptImpl.java | 11 +- .../org/pgpainless/sop/DetachedSignImpl.java | 19 +-- .../java/org/pgpainless/sop/EncryptImpl.java | 51 ++++--- .../org/pgpainless/sop/InlineSignImpl.java | 124 ++++++++++++++++-- .../org/pgpainless/sop/InlineVerifyImpl.java | 71 +++++++++- .../java/org/pgpainless/sop/SignTest.java | 13 -- 11 files changed, 222 insertions(+), 87 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java index 409d4040..c4644319 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java @@ -43,7 +43,7 @@ public class ArmorTest { @FailOnSystemExit public void armorSecretKey() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org", null); + .modernKeyRing("alice@pgpainless.org"); byte[] bytes = secretKey.getEncoded(); System.setIn(new ByteArrayInputStream(bytes)); @@ -59,7 +59,7 @@ public class ArmorTest { @FailOnSystemExit public void armorPublicKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org", null); + .modernKeyRing("alice@pgpainless.org"); PGPPublicKeyRing publicKey = PGPainless.extractCertificate(secretKey); byte[] bytes = publicKey.getEncoded(); diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorTest.java index ab5e2c7a..4dc43822 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorTest.java @@ -43,7 +43,7 @@ public class DearmorTest { @FailOnSystemExit public void dearmorSecretKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org", null); + .modernKeyRing("alice@pgpainless.org"); String armored = PGPainless.asciiArmor(secretKey); System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8))); @@ -59,7 +59,7 @@ public class DearmorTest { @FailOnSystemExit public void dearmorCertificate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org", null); + .modernKeyRing("alice@pgpainless.org"); PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); String armored = PGPainless.asciiArmor(certificate); diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DetachInbandSignatureAndMessageTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DetachInbandSignatureAndMessageTest.java index 15cf2546..dd7a77d2 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DetachInbandSignatureAndMessageTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DetachInbandSignatureAndMessageTest.java @@ -115,7 +115,7 @@ public class DetachInbandSignatureAndMessageTest { // Detach File tempSigFile = new File(tempDir, "sig.out"); - PGPainlessCLI.main(new String[] {"detach-inband-signature-and-message", "--signatures-out=" + tempSigFile.getAbsolutePath()}); + PGPainlessCLI.main(new String[] {"inline-detach", "--signatures-out=" + tempSigFile.getAbsolutePath()}); // Test equality with expected values assertEquals(CLEAR_SIGNED_BODY, msgOut.toString()); @@ -150,7 +150,7 @@ public class DetachInbandSignatureAndMessageTest { // Detach File tempSigFile = new File(tempDir, "sig.asc"); - PGPainlessCLI.main(new String[] {"detach-inband-signature-and-message", "--signatures-out=" + tempSigFile.getAbsolutePath(), "--no-armor"}); + PGPainlessCLI.main(new String[] {"inline-detach", "--signatures-out=" + tempSigFile.getAbsolutePath(), "--no-armor"}); // Test equality with expected values assertEquals(CLEAR_SIGNED_BODY, msgOut.toString()); @@ -187,7 +187,7 @@ public class DetachInbandSignatureAndMessageTest { // Detach File existingSigFile = new File(tempDir, "sig.existing"); assertTrue(existingSigFile.createNewFile()); - PGPainlessCLI.main(new String[] {"detach-inband-signature-and-message", "--signatures-out=" + existingSigFile.getAbsolutePath()}); + PGPainlessCLI.main(new String[] {"inline-detach", "--signatures-out=" + existingSigFile.getAbsolutePath()}); } } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java index 525e3e1d..c207db4e 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java @@ -59,7 +59,7 @@ public class SignVerifyTest { File aliceKeyFile = new File(tempDir, "alice.key"); assertTrue(aliceKeyFile.createNewFile()); PGPSecretKeyRing aliceKeys = PGPainless.generateKeyRing() - .modernKeyRing("alice", null); + .modernKeyRing("alice"); OutputStream aliceKeyOut = new FileOutputStream(aliceKeyFile); Streams.pipeAll(new ByteArrayInputStream(aliceKeys.getEncoded()), aliceKeyOut); aliceKeyOut.close(); @@ -108,7 +108,7 @@ public class SignVerifyTest { String[] split = verification.split(" "); OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(aliceKeys); OpenPgpV4Fingerprint signingKeyFingerprint = new OpenPgpV4Fingerprint(new KeyRingInfo(alicePub, new Date()).getSigningSubkeys().get(0)); - assertEquals(signingKeyFingerprint.toString(), split[1].trim()); + assertEquals(signingKeyFingerprint.toString(), split[1].trim(), verification); assertEquals(primaryKeyFingerprint.toString(), split[2].trim()); // Test micalg output diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java index 0c6eac2b..81562d28 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java @@ -134,7 +134,7 @@ public class SignUsingPublicKeyBehaviorTest { assertTrue(sigFile.createNewFile()); FileOutputStream sigOut = new FileOutputStream(sigFile); System.setOut(new PrintStream(sigOut)); - PGPainlessCLI.execute("sign", "--armor", aliceKeyFile.getAbsolutePath()); + PGPainlessCLI.main(new String[] {"sign", "--armor", aliceKeyFile.getAbsolutePath()}); System.setIn(originalIn); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 89eb29db..4c813efe 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -10,9 +10,7 @@ import java.io.OutputStream; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Date; -import java.util.HashSet; import java.util.List; -import java.util.Set; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; @@ -39,7 +37,6 @@ import sop.operation.Decrypt; public class DecryptImpl implements Decrypt { private final ConsumerOptions consumerOptions = new ConsumerOptions(); - private final Set keys = new HashSet<>(); private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); @Override @@ -105,7 +102,8 @@ public class DecryptImpl implements Decrypt { PGPSecretKeyRingCollection secretKeyCollection = PGPainless.readKeyRing() .secretKeyRingCollection(keyIn); for (PGPSecretKeyRing key : secretKeyCollection) { - keys.add(key); + protector.addSecretKey(key); + consumerOptions.addDecryptionKey(key, protector); } } catch (IOException | PGPException e) { throw new SOPGPException.BadData(e); @@ -124,10 +122,6 @@ public class DecryptImpl implements Decrypt { public ReadyWithResult ciphertext(InputStream ciphertext) throws SOPGPException.BadData, SOPGPException.MissingArg { - for (PGPSecretKeyRing key : keys) { - protector.addSecretKey(key); - consumerOptions.addDecryptionKey(key, protector); - } if (consumerOptions.getDecryptionKeys().isEmpty() && consumerOptions.getDecryptionPassphrases().isEmpty() && consumerOptions.getSessionKey() == null) { throw new SOPGPException.MissingArg("Missing decryption key, passphrase or session key."); @@ -145,6 +139,7 @@ public class DecryptImpl implements Decrypt { } catch (PGPException | IOException e) { throw new SOPGPException.BadData(e); } finally { + // Forget passphrases after decryption protector.clear(); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java index 3341712a..3b985de6 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java @@ -8,6 +8,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; @@ -24,9 +25,8 @@ import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.KeyException; import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmoredOutputStreamFactory; +import org.pgpainless.util.Passphrase; import sop.MicAlg; import sop.ReadyWithResult; import sop.SigningResult; @@ -39,6 +39,7 @@ public class DetachedSignImpl implements DetachedSign { private boolean armor = true; private SignAs mode = SignAs.Binary; private final SigningOptions signingOptions = new SigningOptions(); + private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); @Override public DetachedSign noArmor() { @@ -58,11 +59,8 @@ public class DetachedSignImpl implements DetachedSign { PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); for (PGPSecretKeyRing key : keys) { - KeyRingInfo info = new KeyRingInfo(key); - if (!info.isFullyDecrypted()) { - throw new SOPGPException.KeyIsProtected(); - } - signingOptions.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key, modeToSigType(mode)); + protector.addSecretKey(key); + signingOptions.addDetachedSignature(protector, key, modeToSigType(mode)); } } catch (PGPException | KeyException e) { throw new SOPGPException.BadData(e); @@ -72,7 +70,9 @@ public class DetachedSignImpl implements DetachedSign { @Override public DetachedSign withKeyPassword(byte[] password) { - return null; + String string = new String(password, Charset.forName("UTF8")); + protector.addPassphrase(Passphrase.fromPassword(string)); + return this; } @Override @@ -96,6 +96,9 @@ public class DetachedSignImpl implements DetachedSign { signingStream.close(); EncryptionResult encryptionResult = signingStream.getResult(); + // forget passphrases + protector.clear(); + List signatures = new ArrayList<>(); for (SubkeyIdentifier key : encryptionResult.getDetachedSignatures().keySet()) { signatures.addAll(encryptionResult.getDetachedSignatures().get(key)); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index bfa80f47..635ab82f 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -8,8 +8,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; -import java.util.HashSet; -import java.util.Set; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; @@ -25,18 +23,16 @@ import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.util.Passphrase; -import sop.util.ProxyOutputStream; import sop.Ready; import sop.enums.EncryptAs; import sop.exception.SOPGPException; import sop.operation.Encrypt; +import sop.util.ProxyOutputStream; public class EncryptImpl implements Encrypt { EncryptionOptions encryptionOptions = new EncryptionOptions(); SigningOptions signingOptions = null; - - Set signingKeys = new HashSet<>(); MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); private EncryptAs encryptAs = EncryptAs.Binary; @@ -55,17 +51,34 @@ public class EncryptImpl implements Encrypt { } @Override - public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { + public Encrypt signWith(InputStream keyIn) + throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { + if (signingOptions == null) { + signingOptions = SigningOptions.get(); + } + try { PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); if (keys.size() != 1) { throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size())); } - if (signingOptions == null) { - signingOptions = SigningOptions.get(); + PGPSecretKeyRing signingKey = keys.iterator().next(); + protector.addSecretKey(signingKey); + + try { + signingOptions.addInlineSignature( + protector, + signingKey, + (encryptAs == EncryptAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) + ); + } catch (IllegalArgumentException e) { + throw new SOPGPException.KeyCannotSign(); + } catch (WrongPassphraseException e) { + throw new SOPGPException.KeyIsProtected(); + } catch (PGPException e) { + throw new SOPGPException.BadData(e); } - signingKeys.add(keys.getKeyRings().next()); } catch (IOException | PGPException e) { throw new SOPGPException.BadData(e); } @@ -100,26 +113,6 @@ public class EncryptImpl implements Encrypt { @Override public Ready plaintext(InputStream plaintext) throws IOException { - for (PGPSecretKeyRing signingKey : signingKeys) { - protector.addSecretKey(signingKey); - } - - if (signingOptions != null) { - try { - signingOptions.addInlineSignatures( - protector, - signingKeys, - (encryptAs == EncryptAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) - ); - } catch (IllegalArgumentException e) { - throw new SOPGPException.KeyCannotSign(); - } catch (WrongPassphraseException e) { - throw new SOPGPException.KeyIsProtected(); - } catch (PGPException e) { - throw new SOPGPException.BadData(e); - } - } - ProducerOptions producerOptions = signingOptions != null ? ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions) : ProducerOptions.encrypt(encryptionOptions); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index 7eaeaa6f..d7b0c161 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -4,39 +4,139 @@ package org.pgpainless.sop; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.exception.KeyException; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.util.Passphrase; +import sop.MicAlg; import sop.ReadyWithResult; import sop.SigningResult; import sop.enums.InlineSignAs; import sop.exception.SOPGPException; -import sop.operation.DetachedSign; import sop.operation.InlineSign; -import java.io.IOException; -import java.io.InputStream; - public class InlineSignImpl implements InlineSign { + + private boolean armor = true; + private InlineSignAs mode = InlineSignAs.Binary; + private final SigningOptions signingOptions = new SigningOptions(); + private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + @Override - public DetachedSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption { - return null; + public InlineSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption { + this.mode = mode; + return this; } @Override - public DetachedSign noArmor() { - return null; + public InlineSign noArmor() { + this.armor = false; + return this; } @Override - public InlineSign key(InputStream key) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { - return null; + public InlineSign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { + try { + PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); + + for (PGPSecretKeyRing key : keys) { + protector.addSecretKey(key); + if (mode == InlineSignAs.CleartextSigned) { + signingOptions.addDetachedSignature(protector, key, DocumentSignatureType.BINARY_DOCUMENT); + } else { + signingOptions.addInlineSignature(protector, key, modeToSigType(mode)); + } + } + } catch (PGPException | KeyException e) { + throw new SOPGPException.BadData(e); + } + return this; } @Override public InlineSign withKeyPassword(byte[] password) { - return null; + String string = new String(password, Charset.forName("UTF8")); + protector.addPassphrase(Passphrase.fromPassword(string)); + return this; } @Override public ReadyWithResult data(InputStream data) throws IOException, SOPGPException.ExpectedText { - return null; + + ProducerOptions producerOptions = ProducerOptions.sign(signingOptions); + if (mode == InlineSignAs.CleartextSigned) { + producerOptions.setCleartextSigned(); + producerOptions.setAsciiArmor(true); + } else { + producerOptions.setAsciiArmor(armor); + } + + return new ReadyWithResult() { + @Override + public SigningResult writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { + try { + EncryptionStream signingStream = PGPainless.encryptAndOrSign() + .onOutputStream(outputStream) + .withOptions(producerOptions); + + if (signingStream.isClosed()) { + throw new IllegalStateException("EncryptionStream is already closed."); + } + + Streams.pipeAll(data, signingStream); + signingStream.close(); + EncryptionResult encryptionResult = signingStream.getResult(); + + // forget passphrases + protector.clear(); + + List signatures = new ArrayList<>(); + for (SubkeyIdentifier key : encryptionResult.getDetachedSignatures().keySet()) { + signatures.addAll(encryptionResult.getDetachedSignatures().get(key)); + } + + return SigningResult.builder() + .setMicAlg(micAlgFromSignatures(signatures)) + .build(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + }; + } + + private MicAlg micAlgFromSignatures(Iterable signatures) { + int algorithmId = 0; + for (PGPSignature signature : signatures) { + int sigAlg = signature.getHashAlgorithm(); + if (algorithmId == 0 || algorithmId == sigAlg) { + algorithmId = sigAlg; + } else { + return MicAlg.empty(); + } + } + return algorithmId == 0 ? MicAlg.empty() : MicAlg.fromHashAlgorithmId(algorithmId); + } + + private static DocumentSignatureType modeToSigType(InlineSignAs mode) { + return mode == InlineSignAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT + : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT; } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index a230e0fd..7c1f7ce2 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -4,6 +4,15 @@ package org.pgpainless.sop; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.key.SubkeyIdentifier; import sop.ReadyWithResult; import sop.Verification; import sop.exception.SOPGPException; @@ -11,27 +20,75 @@ import sop.operation.InlineVerify; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; import java.util.Date; import java.util.List; public class InlineVerifyImpl implements InlineVerify { - @Override - public ReadyWithResult> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { - return null; - } + + private final ConsumerOptions options = new ConsumerOptions(); @Override public InlineVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { - return null; + options.verifyNotBefore(timestamp); + return this; } @Override public InlineVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { - return null; + options.verifyNotAfter(timestamp); + return this; } @Override public InlineVerify cert(InputStream cert) throws SOPGPException.BadData { - return null; + PGPPublicKeyRingCollection certificates; + try { + certificates = PGPainless.readKeyRing().publicKeyRingCollection(cert); + } catch (IOException | PGPException e) { + throw new SOPGPException.BadData(e); + } + options.addVerificationCerts(certificates); + return this; + } + + @Override + public ReadyWithResult> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { + return new ReadyWithResult>() { + @Override + public List writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { + DecryptionStream decryptionStream; + try { + decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(data) + .withOptions(options); + + Streams.pipeAll(decryptionStream, outputStream); + decryptionStream.close(); + + OpenPgpMetadata metadata = decryptionStream.getResult(); + List verificationList = new ArrayList<>(); + + for (SubkeyIdentifier verifiedSigningKey : metadata.getVerifiedSignatures().keySet()) { + PGPSignature signature = metadata.getVerifiedSignatures().get(verifiedSigningKey); + verificationList.add(new Verification( + signature.getCreationTime(), + verifiedSigningKey.getSubkeyFingerprint().toString(), + verifiedSigningKey.getPrimaryKeyFingerprint().toString())); + } + + if (!options.getCertificates().isEmpty()) { + if (verificationList.isEmpty()) { + throw new SOPGPException.NoSignature(); + } + } + + return verificationList; + } catch (PGPException e) { + throw new SOPGPException.BadData(e); + } + } + }; } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java index 4736002f..7e2d55dd 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java @@ -11,17 +11,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.List; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.signature.SignatureUtils; import sop.SOP; @@ -128,13 +124,4 @@ public class SignTest { assertEquals(SignatureType.CANONICAL_TEXT_DOCUMENT.getCode(), sig.getSignatureType()); } - @Test - public void rejectEncryptedKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing key = PGPainless.generateKeyRing() - .modernKeyRing("Alice", "passphrase"); - byte[] bytes = key.getEncoded(); - - assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.sign().key(bytes)); - } - } From 53df487e59df397220759e52e9f62648e042883b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 10 Jun 2022 17:43:51 +0200 Subject: [PATCH 0237/1204] Adopt changes from SOP-Java and add test for using incapable keys --- .../misc/SignUsingPublicKeyBehaviorTest.java | 2 +- .../org/pgpainless/key/info/KeyRingInfo.java | 27 ++++++++ .../java/org/pgpainless/sop/DearmorImpl.java | 8 ++- .../org/pgpainless/sop/DetachedSignImpl.java | 7 +- .../java/org/pgpainless/sop/EncryptImpl.java | 3 + .../org/pgpainless/sop/InlineSignImpl.java | 64 +++++++----------- .../org/pgpainless/sop/IncapableKeysTest.java | 67 +++++++++++++++++++ 7 files changed, 137 insertions(+), 41 deletions(-) create mode 100644 pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java index 81562d28..affe621e 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java @@ -99,7 +99,7 @@ public class SignUsingPublicKeyBehaviorTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) + @ExpectSystemExitWithStatus(SOPGPException.KeyCannotSign.EXIT_CODE) public void testSignatureCreationAndVerification() throws IOException { originalSout = System.out; InputStream originalIn = System.in; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 3ea2f424..b73087b4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -21,6 +21,7 @@ import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.bouncycastle.bcpg.S2K; import org.bouncycastle.bcpg.sig.PrimaryUserID; import org.bouncycastle.bcpg.sig.RevocationReason; import org.bouncycastle.openpgp.PGPKeyRing; @@ -1039,6 +1040,32 @@ public class KeyRingInfo { return !getEncryptionSubkeys(purpose).isEmpty(); } + public boolean isUsableForSigning() { + List signingKeys = getSigningSubkeys(); + for (PGPPublicKey pk : signingKeys) { + PGPSecretKey sk = getSecretKey(pk.getKeyID()); + if (sk == null) { + // Missing secret key + continue; + } + S2K s2K = sk.getS2K(); + // Unencrypted key + if (s2K == null) { + return true; + } + + // Secret key on smart-card + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + continue; + } + // protected secret key + return true; + } + // No usable secret key found + return false; + } + private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) { if (getPublicKey(keyID) == null) { throw new NoSuchElementException("No subkey with key id " + Long.toHexString(keyID) + " found on this key."); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java index 1cebec6e..ac53d415 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java @@ -12,13 +12,19 @@ import java.io.OutputStream; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.util.io.Streams; import sop.Ready; +import sop.exception.SOPGPException; import sop.operation.Dearmor; public class DearmorImpl implements Dearmor { @Override public Ready data(InputStream data) throws IOException { - InputStream decoder = PGPUtil.getDecoderStream(data); + InputStream decoder; + try { + decoder = PGPUtil.getDecoderStream(data); + } catch (IOException e) { + throw new SOPGPException.BadData(e); + } return new Ready() { @Override diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java index 3b985de6..704b5af5 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java @@ -25,6 +25,7 @@ import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.KeyException; import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.ArmoredOutputStreamFactory; import org.pgpainless.util.Passphrase; import sop.MicAlg; @@ -54,11 +55,15 @@ public class DetachedSignImpl implements DetachedSign { } @Override - public DetachedSign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { + public DetachedSign key(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { try { PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); for (PGPSecretKeyRing key : keys) { + KeyRingInfo info = PGPainless.inspectKeyRing(key); + if (!info.isUsableForSigning()) { + throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys."); + } protector.addSecretKey(key); signingOptions.addDetachedSignature(protector, key, modeToSigType(mode)); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 635ab82f..3c5f8e8a 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -21,6 +21,7 @@ import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.exception.KeyException; import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.util.Passphrase; import sop.Ready; @@ -105,6 +106,8 @@ public class EncryptImpl implements Encrypt { .keyRingCollection(cert, false) .getPgpPublicKeyRingCollection(); encryptionOptions.addRecipients(certificates); + } catch (KeyException.UnacceptableEncryptionKeyException e) { + throw new SOPGPException.CertCannotEncrypt(e.getMessage(), e); } catch (IOException | PGPException e) { throw new SOPGPException.BadData(e); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index d7b0c161..639d8c6e 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -14,20 +14,17 @@ import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.KeyException; -import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.Passphrase; -import sop.MicAlg; -import sop.ReadyWithResult; -import sop.SigningResult; +import sop.Ready; import sop.enums.InlineSignAs; import sop.exception.SOPGPException; import sop.operation.InlineSign; @@ -38,6 +35,7 @@ public class InlineSignImpl implements InlineSign { private InlineSignAs mode = InlineSignAs.Binary; private final SigningOptions signingOptions = new SigningOptions(); private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + private final List signingKeys = new ArrayList<>(); @Override public InlineSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption { @@ -52,17 +50,17 @@ public class InlineSignImpl implements InlineSign { } @Override - public InlineSign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException { + public InlineSign key(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { try { PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); for (PGPSecretKeyRing key : keys) { - protector.addSecretKey(key); - if (mode == InlineSignAs.CleartextSigned) { - signingOptions.addDetachedSignature(protector, key, DocumentSignatureType.BINARY_DOCUMENT); - } else { - signingOptions.addInlineSignature(protector, key, modeToSigType(mode)); + KeyRingInfo info = PGPainless.inspectKeyRing(key); + if (!info.isUsableForSigning()) { + throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys."); } + protector.addSecretKey(key); + signingKeys.add(key); } } catch (PGPException | KeyException e) { throw new SOPGPException.BadData(e); @@ -78,7 +76,20 @@ public class InlineSignImpl implements InlineSign { } @Override - public ReadyWithResult data(InputStream data) throws IOException, SOPGPException.ExpectedText { + public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, IOException, SOPGPException.ExpectedText { + for (PGPSecretKeyRing key : signingKeys) { + try { + if (mode == InlineSignAs.CleartextSigned) { + signingOptions.addDetachedSignature(protector, key, DocumentSignatureType.BINARY_DOCUMENT); + } else { + signingOptions.addInlineSignature(protector, key, modeToSigType(mode)); + } + } catch (KeyException.UnacceptableSigningKeyException | KeyException.MissingSecretKeyException e) { + throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(key) + " cannot sign.", e); + } catch (PGPException e) { + throw new SOPGPException.KeyIsProtected("Key " + OpenPgpFingerprint.of(key) + " cannot be unlocked.", e); + } + } ProducerOptions producerOptions = ProducerOptions.sign(signingOptions); if (mode == InlineSignAs.CleartextSigned) { @@ -88,9 +99,9 @@ public class InlineSignImpl implements InlineSign { producerOptions.setAsciiArmor(armor); } - return new ReadyWithResult() { + return new Ready() { @Override - public SigningResult writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { + public void writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { try { EncryptionStream signingStream = PGPainless.encryptAndOrSign() .onOutputStream(outputStream) @@ -102,19 +113,9 @@ public class InlineSignImpl implements InlineSign { Streams.pipeAll(data, signingStream); signingStream.close(); - EncryptionResult encryptionResult = signingStream.getResult(); // forget passphrases protector.clear(); - - List signatures = new ArrayList<>(); - for (SubkeyIdentifier key : encryptionResult.getDetachedSignatures().keySet()) { - signatures.addAll(encryptionResult.getDetachedSignatures().get(key)); - } - - return SigningResult.builder() - .setMicAlg(micAlgFromSignatures(signatures)) - .build(); } catch (PGPException e) { throw new RuntimeException(e); } @@ -122,19 +123,6 @@ public class InlineSignImpl implements InlineSign { }; } - private MicAlg micAlgFromSignatures(Iterable signatures) { - int algorithmId = 0; - for (PGPSignature signature : signatures) { - int sigAlg = signature.getHashAlgorithm(); - if (algorithmId == 0 || algorithmId == sigAlg) { - algorithmId = sigAlg; - } else { - return MicAlg.empty(); - } - } - return algorithmId == 0 ? MicAlg.empty() : MicAlg.fromHashAlgorithmId(algorithmId); - } - private static DocumentSignatureType modeToSigType(InlineSignAs mode) { return mode == InlineSignAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT; diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java new file mode 100644 index 00000000..a50acee1 --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.ecc.EllipticCurve; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.util.ArmorUtils; +import sop.SOP; +import sop.exception.SOPGPException; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class IncapableKeysTest { + + private static byte[] nonSigningKey; + private static byte[] nonEncryptionKey; + private static byte[] nonSigningCert; + private static byte[] nonEncryptionCert; + + private static final SOP sop = new SOPImpl(); + + @BeforeAll + public static void generateKeys() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing key = PGPainless.buildKeyRing() + .addSubkey(KeySpec.getBuilder(KeyType.ECDH(EllipticCurve._P256), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addUserId("Non Signing ") + .build(); + nonSigningKey = ArmorUtils.toAsciiArmoredString(key).getBytes(StandardCharsets.UTF_8); + nonSigningCert = sop.extractCert().key(nonSigningKey).getBytes(); + + key = PGPainless.buildKeyRing() + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addUserId("Non Encryption ") + .build(); + nonEncryptionKey = ArmorUtils.toAsciiArmoredString(key).getBytes(StandardCharsets.UTF_8); + nonEncryptionCert = sop.extractCert().key(nonEncryptionKey).getBytes(); + } + + @Test + public void encryptionToNonEncryptionKeyFails() { + assertThrows(SOPGPException.CertCannotEncrypt.class, () -> sop.encrypt().withCert(nonEncryptionCert)); + } + + @Test + public void signingWithNonSigningKeyFails() { + assertThrows(SOPGPException.KeyCannotSign.class, () -> sop.sign().key(nonSigningKey)); + assertThrows(SOPGPException.KeyCannotSign.class, () -> sop.detachedSign().key(nonSigningKey)); + assertThrows(SOPGPException.KeyCannotSign.class, () -> sop.inlineSign().key(nonSigningKey)); + } +} From a3b2070e76799a28930ee92ee5e93687e30d8e7a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 11 Jun 2022 11:22:56 +0200 Subject: [PATCH 0238/1204] Rename test and reference exit codes directly --- .../src/test/java/org/pgpainless/cli/ExitCodeTest.java | 5 +++-- ...andSignatureAndMessageTest.java => InlineDetachTest.java} | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) rename pgpainless-cli/src/test/java/org/pgpainless/cli/commands/{DetachInbandSignatureAndMessageTest.java => InlineDetachTest.java} (99%) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java index aa3d7d5b..07c9bf68 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java @@ -7,17 +7,18 @@ package org.pgpainless.cli; import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import com.ginsberg.junit.exit.FailOnSystemExit; import org.junit.jupiter.api.Test; +import sop.exception.SOPGPException; public class ExitCodeTest { @Test - @ExpectSystemExitWithStatus(69) + @ExpectSystemExitWithStatus(SOPGPException.UnsupportedSubcommand.EXIT_CODE) public void testUnknownCommand_69() { PGPainlessCLI.main(new String[] {"generate-kex"}); } @Test - @ExpectSystemExitWithStatus(37) + @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void testCommandWithUnknownOption_37() { PGPainlessCLI.main(new String[] {"generate-key", "-k", "\"k is unknown\""}); } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DetachInbandSignatureAndMessageTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachTest.java similarity index 99% rename from pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DetachInbandSignatureAndMessageTest.java rename to pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachTest.java index dd7a77d2..07a212fc 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DetachInbandSignatureAndMessageTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachTest.java @@ -28,7 +28,7 @@ import org.pgpainless.cli.PGPainlessCLI; import org.pgpainless.cli.TestUtils; import sop.exception.SOPGPException; -public class DetachInbandSignatureAndMessageTest { +public class InlineDetachTest { private PrintStream originalSout; private static File tempDir; From 7074ff5f2f18edc3a35053ec17433b3c58b7a3d3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 11 Jun 2022 11:45:54 +0200 Subject: [PATCH 0239/1204] Rename command tests and add generate-key test for encrypted keys --- .../{ArmorTest.java => ArmorCmdTest.java} | 2 +- .../{DearmorTest.java => DearmorCmdTest.java} | 2 +- ...tCertTest.java => ExtractCertCmdTest.java} | 2 +- .../cli/commands/GenerateCertCmdTest.java | 114 ++++++++++++++++++ .../cli/commands/GenerateCertTest.java | 53 -------- ...tachTest.java => InlineDetachCmdTest.java} | 2 +- ...va => RoundTripEncryptDecryptCmdTest.java} | 2 +- ...t.java => RoundTripSignVerifyCmdTest.java} | 2 +- 8 files changed, 120 insertions(+), 59 deletions(-) rename pgpainless-cli/src/test/java/org/pgpainless/cli/commands/{ArmorTest.java => ArmorCmdTest.java} (99%) rename pgpainless-cli/src/test/java/org/pgpainless/cli/commands/{DearmorTest.java => DearmorCmdTest.java} (99%) rename pgpainless-cli/src/test/java/org/pgpainless/cli/commands/{ExtractCertTest.java => ExtractCertCmdTest.java} (98%) create mode 100644 pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertCmdTest.java delete mode 100644 pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertTest.java rename pgpainless-cli/src/test/java/org/pgpainless/cli/commands/{InlineDetachTest.java => InlineDetachCmdTest.java} (99%) rename pgpainless-cli/src/test/java/org/pgpainless/cli/commands/{EncryptDecryptTest.java => RoundTripEncryptDecryptCmdTest.java} (98%) rename pgpainless-cli/src/test/java/org/pgpainless/cli/commands/{SignVerifyTest.java => RoundTripSignVerifyCmdTest.java} (99%) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java similarity index 99% rename from pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java rename to pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java index c4644319..711796e6 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java @@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.cli.PGPainlessCLI; -public class ArmorTest { +public class ArmorCmdTest { private static PrintStream originalSout; diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java similarity index 99% rename from pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorTest.java rename to pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java index 4dc43822..a49f4db1 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java @@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.cli.PGPainlessCLI; -public class DearmorTest { +public class DearmorCmdTest { private PrintStream originalSout; diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java similarity index 98% rename from pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertTest.java rename to pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java index 6f746445..1d20fb0e 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java @@ -23,7 +23,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.cli.PGPainlessCLI; import org.pgpainless.key.info.KeyRingInfo; -public class ExtractCertTest { +public class ExtractCertCmdTest { @Test @FailOnSystemExit diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertCmdTest.java new file mode 100644 index 00000000..8534decb --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertCmdTest.java @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.pgpainless.cli.TestUtils.ARMOR_PRIVATE_KEY_HEADER_BYTES; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import com.ginsberg.junit.exit.FailOnSystemExit; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.cli.PGPainlessCLI; +import org.pgpainless.key.info.KeyInfo; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; + +public class GenerateCertCmdTest { + + @Test + @FailOnSystemExit + public void testKeyGeneration() throws IOException, PGPException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + System.setOut(new PrintStream(out)); + PGPainlessCLI.execute("generate-key", "--armor", "Juliet Capulet "); + + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(out.toByteArray()); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertTrue(info.isFullyDecrypted()); + assertTrue(info.isUserIdValid("Juliet Capulet ")); + + for (PGPSecretKey key : secretKeys) { + assertTrue(testPassphrase(key, null)); + } + + byte[] outBegin = new byte[37]; + System.arraycopy(out.toByteArray(), 0, outBegin, 0, 37); + assertArrayEquals(outBegin, ARMOR_PRIVATE_KEY_HEADER_BYTES); + } + + @Test + @FailOnSystemExit + public void testGenerateKeyWithPassword() throws IOException, PGPException { + PrintStream orig = System.out; + try { + // Write password to file + Path tempDir = Files.createTempDirectory("genkey"); + File passwordFile = new File(tempDir.toFile(), "password"); + passwordFile.createNewFile(); + passwordFile.deleteOnExit(); + FileOutputStream fileOut = new FileOutputStream(passwordFile); + fileOut.write("sw0rdf1sh".getBytes(StandardCharsets.UTF_8)); + fileOut.flush(); + fileOut.close(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + System.setOut(new PrintStream(out)); + PGPainlessCLI.execute("generate-key", "Juliet Capulet ", + "--with-key-password", passwordFile.getAbsolutePath()); + + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(out.toByteArray()); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertFalse(info.isFullyDecrypted()); + assertTrue(info.isFullyEncrypted()); + + for (PGPSecretKey key : secretKeys) { + assertTrue(testPassphrase(key, "sw0rdf1sh")); + } + } finally { + System.setOut(orig); + } + } + + private boolean testPassphrase(PGPSecretKey key, String passphrase) throws PGPException { + if (KeyInfo.isEncrypted(key)) { + UnlockSecretKey.unlockSecretKey(key, Passphrase.fromPassword(passphrase)); + } else { + if (passphrase != null) { + return false; + } + UnlockSecretKey.unlockSecretKey(key, (PBESecretKeyDecryptor) null); + } + return true; + } + + @Test + @FailOnSystemExit + public void testNoArmor() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + System.setOut(new PrintStream(out)); + PGPainlessCLI.execute("generate-key", "--no-armor", "Test "); + + byte[] outBegin = new byte[37]; + System.arraycopy(out.toByteArray(), 0, outBegin, 0, 37); + assertFalse(Arrays.equals(outBegin, ARMOR_PRIVATE_KEY_HEADER_BYTES)); + } +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertTest.java deleted file mode 100644 index 4c0e5fa1..00000000 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertTest.java +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.cli.commands; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.pgpainless.cli.TestUtils.ARMOR_PRIVATE_KEY_HEADER_BYTES; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.util.Arrays; - -import com.ginsberg.junit.exit.FailOnSystemExit; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.cli.PGPainlessCLI; -import org.pgpainless.key.info.KeyRingInfo; - -public class GenerateCertTest { - - @Test - @FailOnSystemExit - public void testKeyGeneration() throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("generate-key", "--armor", "Juliet Capulet "); - - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(out.toByteArray()); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - assertTrue(info.isUserIdValid("Juliet Capulet ")); - - byte[] outBegin = new byte[37]; - System.arraycopy(out.toByteArray(), 0, outBegin, 0, 37); - assertArrayEquals(outBegin, ARMOR_PRIVATE_KEY_HEADER_BYTES); - } - - @Test - @FailOnSystemExit - public void testNoArmor() { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("generate-key", "--no-armor", "Test "); - - byte[] outBegin = new byte[37]; - System.arraycopy(out.toByteArray(), 0, outBegin, 0, 37); - assertFalse(Arrays.equals(outBegin, ARMOR_PRIVATE_KEY_HEADER_BYTES)); - } -} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java similarity index 99% rename from pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachTest.java rename to pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java index 07a212fc..aed4c581 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java @@ -28,7 +28,7 @@ import org.pgpainless.cli.PGPainlessCLI; import org.pgpainless.cli.TestUtils; import sop.exception.SOPGPException; -public class InlineDetachTest { +public class InlineDetachCmdTest { private PrintStream originalSout; private static File tempDir; diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/EncryptDecryptTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java similarity index 98% rename from pgpainless-cli/src/test/java/org/pgpainless/cli/commands/EncryptDecryptTest.java rename to pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java index 4e655864..dba9f47b 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/EncryptDecryptTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java @@ -24,7 +24,7 @@ import org.junit.jupiter.api.Test; import org.pgpainless.cli.PGPainlessCLI; import org.pgpainless.cli.TestUtils; -public class EncryptDecryptTest { +public class RoundTripEncryptDecryptCmdTest { private static File tempDir; private static PrintStream originalSout; diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java similarity index 99% rename from pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java rename to pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java index c207db4e..0fce1273 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/SignVerifyTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java @@ -39,7 +39,7 @@ import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.util.KeyRingUtils; -public class SignVerifyTest { +public class RoundTripSignVerifyCmdTest { private static File tempDir; private static PrintStream originalSout; From 3f16c5486793c8fadbf5f198afb216dd31a7d6a2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 11 Jun 2022 11:55:58 +0200 Subject: [PATCH 0240/1204] Create test util to write data to temp file --- .../test/java/org/pgpainless/cli/TestUtils.java | 12 ++++++++++++ .../cli/commands/GenerateCertCmdTest.java | 14 +++----------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/TestUtils.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/TestUtils.java index 16e1937c..8fca7381 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/TestUtils.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/TestUtils.java @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -40,6 +41,17 @@ public class TestUtils { return dir; } + public static File writeTempFile(File tempDir, byte[] value) throws IOException { + File tempFile = new File(tempDir, randomString(10)); + tempFile.createNewFile(); + tempFile.deleteOnExit(); + FileOutputStream fileOutputStream = new FileOutputStream(tempFile); + fileOutputStream.write(value); + fileOutputStream.flush(); + fileOutputStream.close(); + return tempFile; + } + private static String randomString(int length) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < length; i++) { diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertCmdTest.java index 8534decb..63afc39f 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertCmdTest.java @@ -11,12 +11,9 @@ import static org.pgpainless.cli.TestUtils.ARMOR_PRIVATE_KEY_HEADER_BYTES; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; import com.ginsberg.junit.exit.FailOnSystemExit; @@ -27,6 +24,7 @@ import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.cli.PGPainlessCLI; +import org.pgpainless.cli.TestUtils; import org.pgpainless.key.info.KeyInfo; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.UnlockSecretKey; @@ -61,14 +59,8 @@ public class GenerateCertCmdTest { PrintStream orig = System.out; try { // Write password to file - Path tempDir = Files.createTempDirectory("genkey"); - File passwordFile = new File(tempDir.toFile(), "password"); - passwordFile.createNewFile(); - passwordFile.deleteOnExit(); - FileOutputStream fileOut = new FileOutputStream(passwordFile); - fileOut.write("sw0rdf1sh".getBytes(StandardCharsets.UTF_8)); - fileOut.flush(); - fileOut.close(); + File tempDir = TestUtils.createTempDirectory(); + File passwordFile = TestUtils.writeTempFile(tempDir, "sw0rdf1sh".getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream out = new ByteArrayOutputStream(); System.setOut(new PrintStream(out)); From 2d60650cc6e8c62362bf2e49b56950c4ebcd8599 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 16 Jun 2022 13:09:42 +0200 Subject: [PATCH 0241/1204] Progress on SOP04 support --- .../ClearsignedMessageUtil.java | 8 +- .../org/pgpainless/sop/InlineDetachImpl.java | 2 +- .../{SignTest.java => DetachedSignTest.java} | 2 +- ...MessageTest.java => InlineDetachTest.java} | 57 +++++++++---- .../sop/InlineSignVerifyRoundtripTest.java | 81 +++++++++++++++++++ 5 files changed, 131 insertions(+), 19 deletions(-) rename pgpainless-sop/src/test/java/org/pgpainless/sop/{SignTest.java => DetachedSignTest.java} (99%) rename pgpainless-sop/src/test/java/org/pgpainless/sop/{DetachInbandSignatureAndMessageTest.java => InlineDetachTest.java} (59%) create mode 100644 pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java index ee166c99..cd0f6b35 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java @@ -42,7 +42,13 @@ public final class ClearsignedMessageUtil { public static PGPSignatureList detachSignaturesFromInbandClearsignedMessage(InputStream clearsignedInputStream, OutputStream messageOutputStream) throws IOException, WrongConsumingMethodException { - ArmoredInputStream in = ArmoredInputStreamFactory.get(clearsignedInputStream); + ArmoredInputStream in; + if (clearsignedInputStream instanceof ArmoredInputStream) { + in = (ArmoredInputStream) clearsignedInputStream; + } else { + in = ArmoredInputStreamFactory.get(clearsignedInputStream); + } + if (!in.isClearText()) { throw new WrongConsumingMethodException("Message is not using the Cleartext Signature Framework."); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java index f23434c2..517f9e9e 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java @@ -14,8 +14,8 @@ import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.util.io.Streams; -import org.pgpainless.exception.WrongConsumingMethodException; import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; +import org.pgpainless.exception.WrongConsumingMethodException; import org.pgpainless.util.ArmoredOutputStreamFactory; import sop.ReadyWithResult; import sop.Signatures; diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java similarity index 99% rename from pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java rename to pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java index 7e2d55dd..c6fcc267 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/SignTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java @@ -25,7 +25,7 @@ import sop.Verification; import sop.enums.SignAs; import sop.exception.SOPGPException; -public class SignTest { +public class DetachedSignTest { private static SOP sop; private static byte[] key; diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachInbandSignatureAndMessageTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java similarity index 59% rename from pgpainless-sop/src/test/java/org/pgpainless/sop/DetachInbandSignatureAndMessageTest.java rename to pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java index b76f069c..9df0345a 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachInbandSignatureAndMessageTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java @@ -29,12 +29,13 @@ import sop.ByteArrayAndResult; import sop.SOP; import sop.Signatures; import sop.Verification; +import sop.enums.InlineSignAs; -public class DetachInbandSignatureAndMessageTest { +public class InlineDetachTest { + private static final SOP sop = new SOPImpl(); @Test - public void testDetachingOfInbandSignaturesAndMessage() throws IOException, PGPException { - SOP sop = new SOPImpl(); + public void detachCleartextSignedMessage() throws IOException { byte[] key = sop.generateKey() .userId("Alice ") .generate() @@ -44,22 +45,15 @@ public class DetachInbandSignatureAndMessageTest { // Create a cleartext signed message byte[] data = "Hello, World\n".getBytes(StandardCharsets.UTF_8); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream signingStream = PGPainless.encryptAndOrSign() - .onOutputStream(out) - .withOptions( - ProducerOptions.sign( - SigningOptions.get() - .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), - secretKey, DocumentSignatureType.BINARY_DOCUMENT) - ).setCleartextSigned()); - - Streams.pipeAll(new ByteArrayInputStream(data), signingStream); - signingStream.close(); + byte[] cleartextSigned = sop.inlineSign() + .key(key) + .withKeyPassword("sw0rdf1sh") + .mode(InlineSignAs.CleartextSigned) + .data(data).getBytes(); // actually detach the message ByteArrayAndResult detachedMsg = sop.inlineDetach() - .message(out.toByteArray()) + .message(cleartextSigned) .toByteArrayAndResult(); byte[] message = detachedMsg.getBytes(); @@ -75,4 +69,35 @@ public class DetachInbandSignatureAndMessageTest { assertEquals(new OpenPgpV4Fingerprint(secretKey).toString(), verificationList.get(0).getSigningCertFingerprint()); assertArrayEquals(data, message); } + + @Test + public void detachInbandSignedMessage() throws IOException { + byte[] key = sop.generateKey() + .userId("Alice ") + .generate() + .getBytes(); + byte[] cert = sop.extractCert().key(key).getBytes(); + + byte[] data = "Hello, World\n".getBytes(StandardCharsets.UTF_8); + byte[] inlineSigned = sop.inlineSign() + .key(key) + .data(data).getBytes(); + + // actually detach the message + ByteArrayAndResult detachedMsg = sop.inlineDetach() + .message(inlineSigned) + .toByteArrayAndResult(); + + byte[] message = detachedMsg.getBytes(); + byte[] signature = detachedMsg.getResult().getBytes(); + + List verificationList = sop.verify() + .cert(cert) + .signatures(signature) + .data(message); + + assertFalse(verificationList.isEmpty()); + assertEquals(1, verificationList.size()); + assertArrayEquals(data, message); + } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java new file mode 100644 index 00000000..ee892501 --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import org.junit.jupiter.api.Test; +import sop.ByteArrayAndResult; +import sop.SOP; +import sop.Verification; +import sop.enums.InlineSignAs; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class InlineSignVerifyRoundtripTest { + + private static final SOP sop = new SOPImpl(); + + @Test + public void testInlineSignAndVerifyWithCleartextSignatures() throws IOException { + byte[] key = sop.generateKey() + .userId("Werner") + .withKeyPassword("sw0rdf1sh") + .generate().getBytes(); + + byte[] cert = sop.extractCert() + .key(key).getBytes(); + + byte[] message = "If you want something different, create a new protocol but don't try to\npush it onto a working system.\n".getBytes(StandardCharsets.UTF_8); + + byte[] inlineSigned = sop.inlineSign() + .key(key) + .withKeyPassword("sw0rdf1sh") + .mode(InlineSignAs.CleartextSigned) + .data(message).getBytes(); + + ByteArrayAndResult> result = sop.inlineVerify() + .cert(cert) + .data(inlineSigned) + .toByteArrayAndResult(); + + byte[] verified = result.getBytes(); + + assertFalse(result.getResult().isEmpty()); + assertArrayEquals(message, verified); + } + + @Test + public void testInlineSignAndVerifyWithBinarySignatures() throws IOException { + byte[] key = sop.generateKey() + .userId("Werner") + .withKeyPassword("sw0rdf1sh") + .generate().getBytes(); + + byte[] cert = sop.extractCert() + .key(key).getBytes(); + + byte[] message = "Yes, this is what has been deployed worldwide for years in millions of\ninstallations (decryption wise) and is meanwhile in active use.\n".getBytes(StandardCharsets.UTF_8); + + byte[] inlineSigned = sop.inlineSign() + .key(key) + .withKeyPassword("sw0rdf1sh") + .data(message).getBytes(); + + ByteArrayAndResult> result = sop.inlineVerify() + .cert(cert) + .data(inlineSigned) + .toByteArrayAndResult(); + + byte[] verified = result.getBytes(); + + assertFalse(result.getResult().isEmpty()); + assertArrayEquals(message, verified); + } + +} From 5375cd454f5f0c9943343508453a2568eab59d14 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 19 Jun 2022 16:56:32 +0200 Subject: [PATCH 0242/1204] Implement inline-detach for 3 different types of input --- .../org/pgpainless/sop/InlineDetachImpl.java | 85 +++++++++++++- .../org/pgpainless/sop/InlineDetachTest.java | 105 +++++++++++++++++- 2 files changed, 180 insertions(+), 10 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java index 517f9e9e..178018aa 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java @@ -10,12 +10,20 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.util.io.Streams; +import org.pgpainless.decryption_verification.OpenPgpInputStream; import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; import org.pgpainless.exception.WrongConsumingMethodException; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.util.ArmoredOutputStreamFactory; import sop.ReadyWithResult; import sop.Signatures; @@ -43,13 +51,80 @@ public class InlineDetachImpl implements InlineDetach { public Signatures writeTo(OutputStream messageOutputStream) throws SOPGPException.NoSignature, IOException { - PGPSignatureList signatures; - try { - signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(messageInputStream, messageOutputStream); - } catch (WrongConsumingMethodException e) { - throw new IOException(e); + PGPSignatureList signatures = null; + OpenPgpInputStream pgpIn = new OpenPgpInputStream(messageInputStream); + + if (pgpIn.isNonOpenPgp()) { + throw new SOPGPException.BadData("Data appears to be non-OpenPGP."); } + // handle ASCII armor + if (pgpIn.isAsciiArmored()) { + ArmoredInputStream armorIn = new ArmoredInputStream(pgpIn); + + // Handle cleartext signature framework + if (armorIn.isClearText()) { + try { + signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, messageOutputStream); + if (signatures == null) { + throw new SOPGPException.BadData("Data did not contain OpenPGP signatures."); + } + } catch (WrongConsumingMethodException e) { + throw new SOPGPException.BadData(e); + } + } + // else just dearmor + pgpIn = new OpenPgpInputStream(armorIn); + } + + // if data was not using cleartext signatures framework + if (signatures == null) { + + if (!pgpIn.isBinaryOpenPgp()) { + throw new SOPGPException.BadData("Data was containing ASCII armored non-OpenPGP data."); + } + + // handle binary OpenPGP data + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn); + Object next; + while ((next = objectFactory.nextObject()) != null) { + + if (next instanceof PGPOnePassSignatureList) { + // skip over ops + continue; + } + + if (next instanceof PGPLiteralData) { + // write out contents of literal data packet + PGPLiteralData literalData = (PGPLiteralData) next; + InputStream literalIn = literalData.getDataStream(); + Streams.pipeAll(literalIn, messageOutputStream); + literalIn.close(); + continue; + } + + if (next instanceof PGPCompressedData) { + // decompress compressed data + PGPCompressedData compressedData = (PGPCompressedData) next; + try { + objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(compressedData.getDataStream()); + } catch (PGPException e) { + throw new SOPGPException.BadData("Cannot decompress PGPCompressedData", e); + } + continue; + } + + if (next instanceof PGPSignatureList) { + signatures = (PGPSignatureList) next; + } + } + } + + if (signatures == null) { + throw new SOPGPException.BadData("Data did not contain OpenPGP signatures."); + } + + // write out signatures if (armor) { ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(sigOut); for (PGPSignature signature : signatures) { diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java index 9df0345a..b4bfe815 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java @@ -11,29 +11,44 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.List; +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.protection.SecretKeyRingProtector; import sop.ByteArrayAndResult; import sop.SOP; import sop.Signatures; import sop.Verification; import sop.enums.InlineSignAs; +import sop.exception.SOPGPException; public class InlineDetachTest { private static final SOP sop = new SOPImpl(); + + /** + * Construct a message which is signed using the cleartext signature framework. + * The message consists of an armor header followed by the dash-escaped message data, followed by an armored signature. + * + * Detaching must result in the unescaped message data plus the signature packet. + * Verifying the signature must work. + * + * @throws IOException in case of an IO error + */ @Test public void detachCleartextSignedMessage() throws IOException { byte[] key = sop.generateKey() @@ -70,6 +85,16 @@ public class InlineDetachTest { assertArrayEquals(data, message); } + /** + * Construct a message which is inline-signed. + * The message consists of a compressed data packet containing an OnePassSignature, a literal data packet and + * a signature packet. + * + * Detaching the message must result in the contents of the literal data packet, plus the signature packet. + * Verification must work. + * + * @throws IOException in case of an IO error + */ @Test public void detachInbandSignedMessage() throws IOException { byte[] key = sop.generateKey() @@ -100,4 +125,74 @@ public class InlineDetachTest { assertEquals(1, verificationList.size()); assertArrayEquals(data, message); } + + /** + * Construct a message which consists of a literal data packet followed by a signatures block. + * Detaching it must result in the contents of the literal data packet plus the signatures block. + * + * Verification must still work. + * + * @throws IOException in case of an IO error + */ + @Test + public void detachOpenPgpMessage() throws IOException { + byte[] key = sop.generateKey() + .userId("Alice ") + .generate() + .getBytes(); + byte[] cert = sop.extractCert().key(key).getBytes(); + + byte[] data = "Hello, World\n".getBytes(StandardCharsets.UTF_8); + byte[] inlineSigned = sop.inlineSign() + .key(key) + .data(data).getBytes(); + + ByteArrayOutputStream literalDataAndSignatures = new ByteArrayOutputStream(); + ArmoredInputStream armorIn = new ArmoredInputStream(new ByteArrayInputStream(inlineSigned)); + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(armorIn); + Object next; + while ((next = objectFactory.nextObject()) != null) { + if (next instanceof PGPCompressedData) { + PGPCompressedData compressedData = (PGPCompressedData) next; + try { + objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(compressedData.getDataStream()); + } catch (PGPException e) { + throw new SOPGPException.BadData("Cannot decompress compressed data", e); + } + continue; + } + if (next instanceof PGPLiteralData) { + PGPLiteralData litDat = (PGPLiteralData) next; + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(literalDataAndSignatures, (char) litDat.getFormat(), litDat.getFileName(), litDat.getModificationTime(), new byte[8192]); + Streams.pipeAll(litDat.getDataStream(), litOut); + litOut.close(); + continue; + } + + if (next instanceof PGPSignatureList) { + PGPSignatureList signatures = (PGPSignatureList) next; + for (PGPSignature signature : signatures) { + signature.encode(literalDataAndSignatures); + } + } + } + + // actually detach the message + ByteArrayAndResult detachedMsg = sop.inlineDetach() + .message(literalDataAndSignatures.toByteArray()) + .toByteArrayAndResult(); + + byte[] message = detachedMsg.getBytes(); + byte[] signature = detachedMsg.getResult().getBytes(); + + List verificationList = sop.verify() + .cert(cert) + .signatures(signature) + .data(message); + + assertFalse(verificationList.isEmpty()); + assertEquals(1, verificationList.size()); + assertArrayEquals(data, message); + } } From 75455f1a3c94b5d4232fdf3c8259adc0072177ce Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 19 Jun 2022 17:31:48 +0200 Subject: [PATCH 0243/1204] Add OpenPgpMetadata.isCleartextSigned and use it in sop to determine if message was cleartext signed --- .../DecryptionStreamFactory.java | 1 + .../OpenPgpMetadata.java | 58 +++++++++++++------ .../CleartextSignatureVerificationTest.java | 1 + .../DecryptAndVerifyMessageTest.java | 2 + ...eVerificationWithoutCertIsStillSigned.java | 1 + .../java/org/pgpainless/sop/DecryptImpl.java | 17 +++--- .../pgpainless/sop/DetachedVerifyImpl.java | 17 +++--- .../org/pgpainless/sop/InlineVerifyImpl.java | 45 +++++++------- 8 files changed, 88 insertions(+), 54 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 1913444e..77ad92bb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -155,6 +155,7 @@ public final class DecryptionStreamFactory { if (openPgpIn.isAsciiArmored()) { ArmoredInputStream armoredInputStream = ArmoredInputStreamFactory.get(openPgpIn); if (armoredInputStream.isClearText()) { + resultBuilder.setCleartextSigned(); return parseCleartextSignedMessage(armoredInputStream); } else { outerDecodingStream = armoredInputStream; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java index 7c589c2c..2d9feb33 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java @@ -40,6 +40,7 @@ public class OpenPgpMetadata { private final String fileName; private final Date modificationDate; private final StreamEncoding fileEncoding; + private final boolean cleartextSigned; public OpenPgpMetadata(Set recipientKeyIds, SubkeyIdentifier decryptionKey, @@ -51,7 +52,8 @@ public class OpenPgpMetadata { List invalidDetachedSignatures, String fileName, Date modificationDate, - StreamEncoding fileEncoding) { + StreamEncoding fileEncoding, + boolean cleartextSigned) { this.recipientKeyIds = Collections.unmodifiableSet(recipientKeyIds); this.decryptionKey = decryptionKey; @@ -64,6 +66,7 @@ public class OpenPgpMetadata { this.fileName = fileName; this.modificationDate = modificationDate; this.fileEncoding = fileEncoding; + this.cleartextSigned = cleartextSigned; } /** @@ -269,6 +272,15 @@ public class OpenPgpMetadata { return fileEncoding; } + /** + * Return true if the message was signed using the cleartext signature framework. + * + * @return true if cleartext signed. + */ + public boolean isCleartextSigned() { + return cleartextSigned; + } + public static Builder getBuilder() { return new Builder(); } @@ -282,6 +294,7 @@ public class OpenPgpMetadata { private String fileName; private StreamEncoding fileEncoding; private Date modificationDate; + private boolean cleartextSigned = false; private final List verifiedInbandSignatures = new ArrayList<>(); private final List verifiedDetachedSignatures = new ArrayList<>(); @@ -324,29 +337,38 @@ public class OpenPgpMetadata { return this; } + public Builder addVerifiedInbandSignature(SignatureVerification signatureVerification) { + this.verifiedInbandSignatures.add(signatureVerification); + return this; + } + + public Builder addVerifiedDetachedSignature(SignatureVerification signatureVerification) { + this.verifiedDetachedSignatures.add(signatureVerification); + return this; + } + + public Builder addInvalidInbandSignature(SignatureVerification signatureVerification, SignatureValidationException e) { + this.invalidInbandSignatures.add(new SignatureVerification.Failure(signatureVerification, e)); + return this; + } + + public Builder addInvalidDetachedSignature(SignatureVerification signatureVerification, SignatureValidationException e) { + this.invalidDetachedSignatures.add(new SignatureVerification.Failure(signatureVerification, e)); + return this; + } + + public Builder setCleartextSigned() { + this.cleartextSigned = true; + return this; + } + public OpenPgpMetadata build() { return new OpenPgpMetadata( recipientFingerprints, decryptionKey, sessionKey, compressionAlgorithm, verifiedInbandSignatures, invalidInbandSignatures, verifiedDetachedSignatures, invalidDetachedSignatures, - fileName, modificationDate, fileEncoding); - } - - public void addVerifiedInbandSignature(SignatureVerification signatureVerification) { - this.verifiedInbandSignatures.add(signatureVerification); - } - - public void addVerifiedDetachedSignature(SignatureVerification signatureVerification) { - this.verifiedDetachedSignatures.add(signatureVerification); - } - - public void addInvalidInbandSignature(SignatureVerification signatureVerification, SignatureValidationException e) { - this.invalidInbandSignatures.add(new SignatureVerification.Failure(signatureVerification, e)); - } - - public void addInvalidDetachedSignature(SignatureVerification signatureVerification, SignatureValidationException e) { - this.invalidDetachedSignatures.add(new SignatureVerification.Failure(signatureVerification, e)); + fileName, modificationDate, fileEncoding, cleartextSigned); } } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java index 12596988..521dd558 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java @@ -94,6 +94,7 @@ public class CleartextSignatureVerificationTest { OpenPgpMetadata result = decryptionStream.getResult(); assertTrue(result.isVerified()); + assertTrue(result.isCleartextSigned()); PGPSignature signature = result.getVerifiedSignatures().values().iterator().next(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java index 7c44f307..152f4c33 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java @@ -6,6 +6,7 @@ package org.pgpainless.decryption_verification; 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.assertTrue; import java.io.ByteArrayInputStream; @@ -68,6 +69,7 @@ public class DecryptAndVerifyMessageTest { assertTrue(metadata.isEncrypted()); assertTrue(metadata.isSigned()); + assertFalse(metadata.isCleartextSigned()); assertTrue(metadata.isVerified()); assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getSymmetricKeyAlgorithm()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java index 0e979ead..0b2f5d7d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java @@ -41,6 +41,7 @@ public class SignedMessageVerificationWithoutCertIsStillSigned { OpenPgpMetadata metadata = verificationStream.getResult(); + assertFalse(metadata.isCleartextSigned()); assertTrue(metadata.isSigned(), "Message is signed, even though we miss the verification cert."); assertFalse(metadata.isVerified(), "Message is not verified because we lack the verification cert."); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 4c813efe..b9539ed5 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -16,16 +16,15 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.util.Passphrase; import sop.DecryptionResult; import sop.ReadyWithResult; @@ -151,12 +150,8 @@ public class DecryptImpl implements Decrypt { OpenPgpMetadata metadata = decryptionStream.getResult(); List verificationList = new ArrayList<>(); - for (SubkeyIdentifier verifiedSigningKey : metadata.getVerifiedSignatures().keySet()) { - PGPSignature signature = metadata.getVerifiedSignatures().get(verifiedSigningKey); - verificationList.add(new Verification( - signature.getCreationTime(), - verifiedSigningKey.getSubkeyFingerprint().toString(), - verifiedSigningKey.getPrimaryKeyFingerprint().toString())); + for (SignatureVerification signatureVerification : metadata.getVerifiedInbandSignatures()) { + verificationList.add(map(signatureVerification)); } if (!consumerOptions.getCertificates().isEmpty()) { @@ -178,4 +173,10 @@ public class DecryptImpl implements Decrypt { } }; } + + private Verification map(SignatureVerification sigVerification) { + return new Verification(sigVerification.getSignature().getCreationTime(), + sigVerification.getSigningKey().getSubkeyFingerprint().toString(), + sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString()); + } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index d4db494f..81cb08ff 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -12,13 +12,12 @@ import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.decryption_verification.SignatureVerification; import sop.Verification; import sop.exception.SOPGPException; import sop.operation.DetachedVerify; @@ -75,12 +74,8 @@ public class DetachedVerifyImpl implements DetachedVerify { OpenPgpMetadata metadata = decryptionStream.getResult(); List verificationList = new ArrayList<>(); - for (SubkeyIdentifier verifiedSigningKey : metadata.getVerifiedSignatures().keySet()) { - PGPSignature signature = metadata.getVerifiedSignatures().get(verifiedSigningKey); - verificationList.add(new Verification( - signature.getCreationTime(), - verifiedSigningKey.getSubkeyFingerprint().toString(), - verifiedSigningKey.getPrimaryKeyFingerprint().toString())); + for (SignatureVerification signatureVerification : metadata.getVerifiedDetachedSignatures()) { + verificationList.add(map(signatureVerification)); } if (!options.getCertificates().isEmpty()) { @@ -94,4 +89,10 @@ public class DetachedVerifyImpl implements DetachedVerify { throw new SOPGPException.BadData(e); } } + + private Verification map(SignatureVerification sigVerification) { + return new Verification(sigVerification.getSignature().getCreationTime(), + sigVerification.getSigningKey().getSubkeyFingerprint().toString(), + sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString()); + } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 7c1f7ce2..7c31f44b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -4,20 +4,6 @@ package org.pgpainless.sop; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.key.SubkeyIdentifier; -import sop.ReadyWithResult; -import sop.Verification; -import sop.exception.SOPGPException; -import sop.operation.InlineVerify; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -25,6 +11,19 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.util.io.Streams; +import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.SignatureVerification; +import sop.ReadyWithResult; +import sop.Verification; +import sop.exception.SOPGPException; +import sop.operation.InlineVerify; + public class InlineVerifyImpl implements InlineVerify { private final ConsumerOptions options = new ConsumerOptions(); @@ -70,12 +69,12 @@ public class InlineVerifyImpl implements InlineVerify { OpenPgpMetadata metadata = decryptionStream.getResult(); List verificationList = new ArrayList<>(); - for (SubkeyIdentifier verifiedSigningKey : metadata.getVerifiedSignatures().keySet()) { - PGPSignature signature = metadata.getVerifiedSignatures().get(verifiedSigningKey); - verificationList.add(new Verification( - signature.getCreationTime(), - verifiedSigningKey.getSubkeyFingerprint().toString(), - verifiedSigningKey.getPrimaryKeyFingerprint().toString())); + List verifications = metadata.isCleartextSigned() ? + metadata.getVerifiedDetachedSignatures() : + metadata.getVerifiedInbandSignatures(); + + for (SignatureVerification signatureVerification : verifications) { + verificationList.add(map(signatureVerification)); } if (!options.getCertificates().isEmpty()) { @@ -91,4 +90,10 @@ public class InlineVerifyImpl implements InlineVerify { } }; } + + private Verification map(SignatureVerification sigVerification) { + return new Verification(sigVerification.getSignature().getCreationTime(), + sigVerification.getSigningKey().getSubkeyFingerprint().toString(), + sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString()); + } } From d64e749f2276edd73240c764cda4beba6515fd11 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 19 Jun 2022 17:50:31 +0200 Subject: [PATCH 0244/1204] Fix sop encrypt --sign-with allowing for protected keys --- .../java/org/pgpainless/sop/EncryptImpl.java | 41 ++++++++++++------- .../sop/EncryptDecryptRoundTripTest.java | 11 +++++ .../org/pgpainless/sop/IncapableKeysTest.java | 5 +++ 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 3c5f8e8a..0843ae08 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -8,6 +8,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; +import java.util.HashSet; +import java.util.Set; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; @@ -23,6 +25,8 @@ import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.KeyException; import org.pgpainless.exception.WrongPassphraseException; +import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.Passphrase; import sop.Ready; import sop.enums.EncryptAs; @@ -35,6 +39,7 @@ public class EncryptImpl implements Encrypt { EncryptionOptions encryptionOptions = new EncryptionOptions(); SigningOptions signingOptions = null; MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + private final Set signingKeys = new HashSet<>(); private EncryptAs encryptAs = EncryptAs.Binary; boolean armor = true; @@ -63,23 +68,15 @@ public class EncryptImpl implements Encrypt { if (keys.size() != 1) { throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size())); } - PGPSecretKeyRing signingKey = keys.iterator().next(); - protector.addSecretKey(signingKey); - try { - signingOptions.addInlineSignature( - protector, - signingKey, - (encryptAs == EncryptAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) - ); - } catch (IllegalArgumentException e) { - throw new SOPGPException.KeyCannotSign(); - } catch (WrongPassphraseException e) { - throw new SOPGPException.KeyIsProtected(); - } catch (PGPException e) { - throw new SOPGPException.BadData(e); + KeyRingInfo info = PGPainless.inspectKeyRing(signingKey); + if (info.getSigningSubkeys().isEmpty()) { + throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(signingKey) + " cannot sign."); } + + protector.addSecretKey(signingKey); + signingKeys.add(signingKey); } catch (IOException | PGPException e) { throw new SOPGPException.BadData(e); } @@ -122,6 +119,22 @@ public class EncryptImpl implements Encrypt { producerOptions.setAsciiArmor(armor); producerOptions.setEncoding(encryptAsToStreamEncoding(encryptAs)); + for (PGPSecretKeyRing signingKey : signingKeys) { + try { + signingOptions.addInlineSignature( + protector, + signingKey, + (encryptAs == EncryptAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) + ); + } catch (KeyException.UnacceptableSigningKeyException e) { + throw new SOPGPException.KeyCannotSign(); + } catch (WrongPassphraseException e) { + throw new SOPGPException.KeyIsProtected(); + } catch (PGPException e) { + throw new SOPGPException.BadData(e); + } + } + try { ProxyOutputStream proxy = new ProxyOutputStream(); EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java index 45a26586..7bc5bfdb 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -38,6 +38,7 @@ public class EncryptDecryptRoundTripTest { sop = new SOPImpl(); aliceKey = sop.generateKey() .userId("Alice ") + .withKeyPassword("wonderland.is.c00l") .generate() .getBytes(); aliceCert = sop.extractCert() @@ -56,6 +57,7 @@ public class EncryptDecryptRoundTripTest { public void basicRoundTripWithKey() throws IOException, SOPGPException.KeyCannotSign { byte[] encrypted = sop.encrypt() .signWith(aliceKey) + .withKeyPassword("wonderland.is.c00l") .withCert(aliceCert) .withCert(bobCert) .plaintext(message) @@ -426,6 +428,15 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, bytesAndResult.getBytes()); } + @Test + public void testEncryptWithWrongPassphraseThrowsKeyIsProtected() { + assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.encrypt() + .withKeyPassword("falsePassphrase") + .signWith(aliceKey) + .withCert(bobCert) + .plaintext(message)); + } + @Test public void testDecryptionWithSessionKey_VerificationWithCert() throws IOException { byte[] plaintext = "This is a test message.\nSit back and relax.\n".getBytes(StandardCharsets.UTF_8); diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java index a50acee1..5139bbf6 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java @@ -64,4 +64,9 @@ public class IncapableKeysTest { assertThrows(SOPGPException.KeyCannotSign.class, () -> sop.detachedSign().key(nonSigningKey)); assertThrows(SOPGPException.KeyCannotSign.class, () -> sop.inlineSign().key(nonSigningKey)); } + + @Test + public void encryptAndSignWithNonSigningKeyFails() { + assertThrows(SOPGPException.KeyCannotSign.class, () -> sop.encrypt().signWith(nonSigningKey)); + } } From 749a623d8834825df32e6b6be30a0b7744ccf632 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 19 Jun 2022 17:56:26 +0200 Subject: [PATCH 0245/1204] Bump SOP version --- .../src/main/java/org/pgpainless/sop/VersionImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index 7675fcb9..dbd1cf35 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -15,7 +15,7 @@ import sop.operation.Version; public class VersionImpl implements Version { // draft version - private static final String SOP_VERSION = "03"; + private static final String SOP_VERSION = "04"; @Override public String getName() { From 40f662edbca9455c25fa8e5895fc6aa0095ba461 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 19 Jun 2022 18:44:38 +0200 Subject: [PATCH 0246/1204] Bump sop-java to 4.0.0 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index ddb6de7d..a6230a41 100644 --- a/version.gradle +++ b/version.gradle @@ -12,6 +12,6 @@ allprojects { slf4jVersion = '1.7.36' logbackVersion = '1.2.11' junitVersion = '5.8.2' - sopJavaVersion = '1.2.4-SNAPSHOT' + sopJavaVersion = '4.0.0' } } From fd3e574d0d8a5589f74cacbd010373e0871f1809 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 19 Jun 2022 18:45:13 +0200 Subject: [PATCH 0247/1204] Update CHANGELOG --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ version.gradle | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f125f2f9..3d31c8ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,33 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.0 +- Add `RevokedKeyException` +- `KeyRingUtils.stripSecretKey()`: Disallow stripping of primary secret key +- Remove support for reading compressed detached signatures +- Add `PGPainless.generateKeyRing().modernKeyRing(userId)` shortcut method without passphrase +- Add `CollectionUtils.addAll(Iterator, Collection)` +- Add `SignatureUtils.getSignaturesForUserIdBy(key, userId, keyId)` +- Add `OpenPgpFingerprint.parseFromBinary(bytes)` +- `SignatureUtils.wasIssuedBy()`: Add support for V5 fingerprints +- Prevent integer overflows when setting expiration dates +- SOP: Properly throw `KeyCannotDecrypt` exception +- Fix performance issues of encrypt and sign operations by using buffering +- Fix performance issues of armor and dearmor operations +- Bump dependency `sop-java` to `4.0.0` +- Add support for SOP specification version 04 + - Implement `inline-sign` + - Implement `inline-verify` + - Rename `DetachInbandSignatureAndMessageImpl` to `InlineDetachImpl` + - Rename `SignImpl` to `DetachedSignImpl` + - Rename `VerifyImpl` to `DetachedVerifyImpl` + - Add support for `--with-key-password` option in `GenerateKeyImpl`, `DetachedSignImpl`, `DecryptImpl`, `EncryptImpl`. + - `InlineDetachImpl` now supports 3 different message types: + - Messages using Cleartext Signature Framework + - OpenPGP messages using OnePassSignatures + - OpenPGP messages without OnePassSignatures +- Introduce `OpenPgpMetadata.isCleartextSigned()` + ## 1.2.2 - `EncryptionOptions.addRecipients(collection)`: Disallow empty collections to prevent misuse from resulting in unencrypted messages - Deprecate default policy factory methods in favor of policy factory methods with expressive names diff --git a/version.gradle b/version.gradle index a6230a41..0c7faf27 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '1.2.3' + shortVersion = '1.3.0' isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 From c078c320c399bc495f0693c027d9548ea3425207 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 19 Jun 2022 18:49:30 +0200 Subject: [PATCH 0248/1204] PGPainless 1.3.0 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 0c7faf27..5c8840ea 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.0' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From 57743383e55d8b388d3649addde8fed088f712df Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 19 Jun 2022 18:51:44 +0200 Subject: [PATCH 0249/1204] PGPainless 1.3.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 5c8840ea..18354f3b 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.0' - isSnapshot = false + shortVersion = '1.3.1' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From 73da2cc889a0112e9c6248ae8e7a779f896d5fa6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 19 Jun 2022 22:14:11 +0200 Subject: [PATCH 0250/1204] Fix reproducible builds by specifying file and directory modes for archive tasks --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 3c4db676..7802bdb1 100644 --- a/build.gradle +++ b/build.gradle @@ -69,6 +69,9 @@ allprojects { tasks.withType(AbstractArchiveTask) { preserveFileTimestamps = false reproducibleFileOrder = true + + dirMode = 0755 + fileMode = 0644 } project.ext { From c1170773bc46b98d7dd9bb6c4dfad60b19fc8f85 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 7 May 2022 14:17:44 +0200 Subject: [PATCH 0251/1204] Implement certification of third party keys --- .../main/java/org/pgpainless/PGPainless.java | 5 + .../algorithm/CertificationType.java | 46 +++++++ .../key/certification/CertifyCertificate.java | 123 ++++++++++++++++++ .../key/certification/package-info.java | 8 ++ .../certification/CertifyCertificateTest.java | 67 ++++++++++ 5 files changed, 249 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 827085ce..0f47e26e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -16,6 +16,7 @@ import org.pgpainless.decryption_verification.DecryptionBuilder; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.encryption_signing.EncryptionBuilder; import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.key.certification.CertifyCertificate; import org.pgpainless.key.generation.KeyRingBuilder; import org.pgpainless.key.generation.KeyRingTemplates; import org.pgpainless.key.info.KeyRingInfo; @@ -163,4 +164,8 @@ public final class PGPainless { public static Policy getPolicy() { return Policy.getInstance(); } + + public static CertifyCertificate certifyCertificate() { + return new CertifyCertificate(); + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.java new file mode 100644 index 00000000..f5c8ec7e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.java @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import javax.annotation.Nonnull; + +/** + * Subset of {@link SignatureType}, reduced to certification types. + */ +public enum CertificationType { + + /** + * The issuer of this certification does not make any particular assertion as to how well the certifier has + * checked that the owner of the key is in fact the person described by the User ID. + */ + GENERIC(SignatureType.GENERIC_CERTIFICATION), + + /** + * The issuer of this certification has not done any verification of the claim that the owner of this key is + * the User ID specified. + */ + NONE(SignatureType.NO_CERTIFICATION), + + /** + * The issuer of this certification has done some casual verification of the claim of identity. + */ + CASUAL(SignatureType.CASUAL_CERTIFICATION), + + /** + * The issuer of this certification has done some casual verification of the claim of identity. + */ + POSITIVE(SignatureType.POSITIVE_CERTIFICATION), + ; + + private final SignatureType signatureType; + + CertificationType(@Nonnull SignatureType signatureType) { + this.signatureType = signatureType; + } + + public @Nonnull SignatureType asSignatureType() { + return signatureType; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java new file mode 100644 index 00000000..69029a84 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.certification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CertificationType; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.exception.KeyException; +import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.signature.builder.ThirdPartyCertificationSignatureBuilder; +import org.pgpainless.signature.subpackets.CertificationSubpackets; +import org.pgpainless.util.DateUtil; + +import java.util.Date; + +public class CertifyCertificate { + + CertifyUserId certifyUserId(PGPPublicKeyRing certificate, String userId) { + return new CertifyUserId(certificate, userId); + } + + public static class CertifyUserId { + + private final PGPPublicKeyRing certificate; + private final String userId; + private final CertificationType certificationType; + + CertifyUserId(PGPPublicKeyRing certificate, String userId) { + this(certificate, userId, CertificationType.GENERIC); + } + + CertifyUserId(PGPPublicKeyRing certificate, String userId, CertificationType certificationType) { + this.certificate = certificate; + this.userId = userId; + this.certificationType = certificationType; + } + + CertifyUserIdWithSubpackets withKey(PGPSecretKeyRing certificationKey, SecretKeyRingProtector protector) throws PGPException { + Date now = DateUtil.now(); + KeyRingInfo info = PGPainless.inspectKeyRing(certificationKey, now); + + // We only support certification-capable primary keys + OpenPgpFingerprint fingerprint = info.getFingerprint(); + PGPPublicKey certificationPubKey = info.getPublicKey(fingerprint); + if (!info.isKeyValidlyBound(certificationPubKey.getKeyID())) { + throw new KeyException.RevokedKeyException(fingerprint); + } + + Date expirationDate = info.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER); + if (expirationDate != null && expirationDate.before(now)) { + throw new KeyException.ExpiredKeyException(fingerprint, expirationDate); + } + + PGPSecretKey secretKey = certificationKey.getSecretKey(certificationPubKey.getKeyID()); + if (secretKey == null) { + throw new KeyException.MissingSecretKeyException(fingerprint, certificationPubKey.getKeyID()); + } + + ThirdPartyCertificationSignatureBuilder sigBuilder = new ThirdPartyCertificationSignatureBuilder( + certificationType.asSignatureType(), secretKey, protector); + + return new CertifyUserIdWithSubpackets(certificate, userId, sigBuilder); + } + } + + public static class CertifyUserIdWithSubpackets { + + private final PGPPublicKeyRing certificate; + private final String userId; + private final ThirdPartyCertificationSignatureBuilder sigBuilder; + + CertifyUserIdWithSubpackets(PGPPublicKeyRing certificate, String userId, ThirdPartyCertificationSignatureBuilder sigBuilder) { + this.certificate = certificate; + this.userId = userId; + this.sigBuilder = sigBuilder; + } + + public CertifyUserIdResult withSubpackets(CertificationSubpackets.Callback subpacketCallback) throws PGPException { + sigBuilder.applyCallback(subpacketCallback); + return build(); + } + + public CertifyUserIdResult build() throws PGPException { + PGPSignature signature = sigBuilder.build(certificate, userId); + + return new CertifyUserIdResult(certificate, userId, signature); + } + } + + public static class CertifyUserIdResult { + + private final PGPPublicKeyRing certificate; + private final String userId; + private final PGPSignature certification; + + CertifyUserIdResult(PGPPublicKeyRing certificate, String userId, PGPSignature certification) { + this.certificate = certificate; + this.userId = userId; + this.certification = certification; + } + + public PGPSignature getCertification() { + return certification; + } + + public PGPPublicKeyRing getCertifiedCertificate() { + // inject the signature + PGPPublicKeyRing certified = KeyRingUtils.injectCertification(certificate, userId, certification); + return certified; + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java new file mode 100644 index 00000000..db2f4857 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * API for key certifications. + */ +package org.pgpainless.key.certification; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java new file mode 100644 index 00000000..7093865d --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.certification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.Arrays; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.consumer.SignatureVerifier; +import org.pgpainless.util.CollectionUtils; +import org.pgpainless.util.DateUtil; + +public class CertifyCertificateTest { + + @Test + public void testSuccessfulCertificationOfUserId() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice ", null); + String bobUserId = "Bob "; + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing(bobUserId, null); + + PGPPublicKeyRing bobCertificate = PGPainless.extractCertificate(bob); + + CertifyCertificate.CertifyUserIdResult result = PGPainless.certifyCertificate() + .certifyUserId(bobCertificate, bobUserId) + .withKey(alice, protector) + .build(); + + assertNotNull(result); + PGPSignature signature = result.getCertification(); + assertNotNull(signature); + assertEquals(SignatureType.GENERIC_CERTIFICATION, SignatureType.valueOf(signature.getSignatureType())); + assertEquals(alice.getPublicKey().getKeyID(), signature.getKeyID()); + + assertTrue(SignatureVerifier.verifyUserIdCertification( + bobUserId, signature, alice.getPublicKey(), bob.getPublicKey(), PGPainless.getPolicy(), DateUtil.now())); + + PGPPublicKeyRing bobCertified = result.getCertifiedCertificate(); + PGPPublicKey bobCertifiedKey = bobCertified.getPublicKey(); + // There are 2 sigs now, bobs own and alice' + assertEquals(2, CollectionUtils.iteratorToList(bobCertifiedKey.getSignaturesForID(bobUserId)).size()); + List sigsByAlice = CollectionUtils.iteratorToList( + bobCertifiedKey.getSignaturesForKeyID(alice.getPublicKey().getKeyID())); + assertEquals(1, sigsByAlice.size()); + assertEquals(signature, sigsByAlice.get(0)); + + assertFalse(Arrays.areEqual(bobCertificate.getEncoded(), bobCertified.getEncoded())); + } +} From fa5ddfd11242c9b47bfb14d4f0e8d18f5a4901ef Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 7 May 2022 21:48:36 +0200 Subject: [PATCH 0252/1204] WIP: Implement delegations THERE ARE THINGS BROKEN NOW. DO NOT MERGE! --- .../main/java/org/pgpainless/PGPainless.java | 2 +- .../pgpainless/algorithm/Trustworthiness.java | 111 +++++++++++++ .../key/certification/CertifyCertificate.java | 146 ++++++++++++------ .../secretkeyring/SecretKeyRingEditor.java | 8 +- .../builder/DirectKeySignatureBuilder.java | 17 +- .../certification/CertifyCertificateTest.java | 40 ++++- .../DirectKeySignatureBuilderTest.java | 3 + 7 files changed, 272 insertions(+), 55 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 0f47e26e..a2f16ab9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -165,7 +165,7 @@ public final class PGPainless { return Policy.getInstance(); } - public static CertifyCertificate certifyCertificate() { + public static CertifyCertificate certify() { return new CertifyCertificate(); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java new file mode 100644 index 00000000..573bbf9d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +public class Trustworthiness { + + private final int amount; + private final int depth; + + public static final int THRESHOLD_FULLY_CONVINCED = 120; + public static final int THRESHOLD_MARGINALLY_CONVINCED = 60; + public static final int THRESHOLD_NOT_TRUSTED = 0; + + public Trustworthiness(int amount, int depth) { + this.amount = capAmount(amount); + this.depth = capDepth(depth); + } + + public int getAmount() { + return amount; + } + + public int getDepth() { + return depth; + } + + /** + * This means that we are fully convinced of the trustworthiness of the key. + * + * @return builder + */ + public static Builder fullyTrusted() { + return new Builder(THRESHOLD_FULLY_CONVINCED); + } + + /** + * This means that we are marginally (partially) convinced of the trustworthiness of the key. + * + * @return builder + */ + public static Builder marginallyTrusted() { + return new Builder(THRESHOLD_MARGINALLY_CONVINCED); + } + + /** + * This means that we do not trust the key. + * Can be used to overwrite previous trust. + * + * @return builder + */ + public static Builder untrusted() { + return new Builder(THRESHOLD_NOT_TRUSTED); + } + + public static final class Builder { + + private final int amount; + + private Builder(int amount) { + this.amount = amount; + } + + /** + * The key is a trusted introducer (depth 1). + * Certifications made by this key are considered trustworthy. + * + * @return trust + */ + public Trustworthiness introducer() { + return new Trustworthiness(amount, 1); + } + + /** + * The key is a meta introducer (depth 2). + * This key can introduce trusted introducers of depth 1. + * + * @return trust + */ + public Trustworthiness metaIntroducer() { + return new Trustworthiness(amount, 2); + } + + /** + * The key is a level
n
meta introducer. + * This key can introduce meta introducers of depth
n - 1
. + * + * @param n depth + * @return trust + */ + public Trustworthiness levelNIntroducer(int n) { + return new Trustworthiness(amount, n); + } + } + + private static int capAmount(int amount) { + if (amount < 0 || amount > 255) { + throw new IllegalArgumentException("Trust amount MUST be a value between 0 and 255"); + } + return amount; + } + + private static int capDepth(int depth) { + if (depth < 0 || depth > 255) { + throw new IllegalArgumentException("Trust depth MUST be a value between 0 and 255"); + } + return depth; + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java index 69029a84..77dab4e8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java @@ -13,111 +13,171 @@ import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CertificationType; import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.Trustworthiness; import org.pgpainless.exception.KeyException; import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.signature.builder.DirectKeySignatureBuilder; import org.pgpainless.signature.builder.ThirdPartyCertificationSignatureBuilder; import org.pgpainless.signature.subpackets.CertificationSubpackets; import org.pgpainless.util.DateUtil; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Date; public class CertifyCertificate { - CertifyUserId certifyUserId(PGPPublicKeyRing certificate, String userId) { - return new CertifyUserId(certificate, userId); + CertificationOnUserId userIdOnCertificate(@Nonnull String userId, @Nonnull PGPPublicKeyRing certificate) { + return new CertificationOnUserId(userId, certificate, CertificationType.GENERIC); } - public static class CertifyUserId { + CertificationOnUserId userIdOnCertificate(@Nonnull String userid, @Nonnull PGPPublicKeyRing certificate, @Nonnull CertificationType certificationType) { + return new CertificationOnUserId(userid, certificate, certificationType); + } + + DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate) { + return certificate(certificate, null); + } + + DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate, @Nullable Trustworthiness trustworthiness) { + return new DelegationOnCertificate(certificate, trustworthiness); + } + + public static class CertificationOnUserId { private final PGPPublicKeyRing certificate; private final String userId; private final CertificationType certificationType; - CertifyUserId(PGPPublicKeyRing certificate, String userId) { - this(certificate, userId, CertificationType.GENERIC); - } - - CertifyUserId(PGPPublicKeyRing certificate, String userId, CertificationType certificationType) { - this.certificate = certificate; + CertificationOnUserId(@Nonnull String userId, @Nonnull PGPPublicKeyRing certificate, @Nonnull CertificationType certificationType) { this.userId = userId; + this.certificate = certificate; this.certificationType = certificationType; } - CertifyUserIdWithSubpackets withKey(PGPSecretKeyRing certificationKey, SecretKeyRingProtector protector) throws PGPException { - Date now = DateUtil.now(); - KeyRingInfo info = PGPainless.inspectKeyRing(certificationKey, now); - - // We only support certification-capable primary keys - OpenPgpFingerprint fingerprint = info.getFingerprint(); - PGPPublicKey certificationPubKey = info.getPublicKey(fingerprint); - if (!info.isKeyValidlyBound(certificationPubKey.getKeyID())) { - throw new KeyException.RevokedKeyException(fingerprint); - } - - Date expirationDate = info.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER); - if (expirationDate != null && expirationDate.before(now)) { - throw new KeyException.ExpiredKeyException(fingerprint, expirationDate); - } - - PGPSecretKey secretKey = certificationKey.getSecretKey(certificationPubKey.getKeyID()); - if (secretKey == null) { - throw new KeyException.MissingSecretKeyException(fingerprint, certificationPubKey.getKeyID()); - } + CertificationOnUserIdWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, @Nonnull SecretKeyRingProtector protector) throws PGPException { + PGPSecretKey secretKey = getCertificationSecretKey(certificationKey); ThirdPartyCertificationSignatureBuilder sigBuilder = new ThirdPartyCertificationSignatureBuilder( certificationType.asSignatureType(), secretKey, protector); - return new CertifyUserIdWithSubpackets(certificate, userId, sigBuilder); + return new CertificationOnUserIdWithSubpackets(certificate, userId, sigBuilder); } } - public static class CertifyUserIdWithSubpackets { + public static class CertificationOnUserIdWithSubpackets { private final PGPPublicKeyRing certificate; private final String userId; private final ThirdPartyCertificationSignatureBuilder sigBuilder; - CertifyUserIdWithSubpackets(PGPPublicKeyRing certificate, String userId, ThirdPartyCertificationSignatureBuilder sigBuilder) { + CertificationOnUserIdWithSubpackets(@Nonnull PGPPublicKeyRing certificate, @Nonnull String userId, @Nonnull ThirdPartyCertificationSignatureBuilder sigBuilder) { this.certificate = certificate; this.userId = userId; this.sigBuilder = sigBuilder; } - public CertifyUserIdResult withSubpackets(CertificationSubpackets.Callback subpacketCallback) throws PGPException { + public CertificationResult withSubpackets(@Nonnull CertificationSubpackets.Callback subpacketCallback) throws PGPException { sigBuilder.applyCallback(subpacketCallback); return build(); } - public CertifyUserIdResult build() throws PGPException { + public CertificationResult build() throws PGPException { PGPSignature signature = sigBuilder.build(certificate, userId); - - return new CertifyUserIdResult(certificate, userId, signature); + PGPPublicKeyRing certifiedCertificate = KeyRingUtils.injectCertification(certificate, userId, signature); + return new CertificationResult(certifiedCertificate, signature); } } - public static class CertifyUserIdResult { + public static class DelegationOnCertificate { + + private final PGPPublicKeyRing certificate; + private final Trustworthiness trustworthiness; + + DelegationOnCertificate(@Nonnull PGPPublicKeyRing certificate, @Nullable Trustworthiness trustworthiness) { + this.certificate = certificate; + this.trustworthiness = trustworthiness; + } + + public DelegationOnCertificateWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, @Nonnull SecretKeyRingProtector protector) throws PGPException { + PGPSecretKey secretKey = getCertificationSecretKey(certificationKey); + + DirectKeySignatureBuilder sigBuilder = new DirectKeySignatureBuilder(secretKey, protector); + if (trustworthiness != null) { + sigBuilder.getHashedSubpackets().setTrust(true, trustworthiness.getDepth(), trustworthiness.getAmount()); + } + return new DelegationOnCertificateWithSubpackets(certificate, sigBuilder); + } + } + + public static class DelegationOnCertificateWithSubpackets { + + private final PGPPublicKeyRing certificate; + private final DirectKeySignatureBuilder sigBuilder; + + public DelegationOnCertificateWithSubpackets(@Nonnull PGPPublicKeyRing certificate, @Nonnull DirectKeySignatureBuilder sigBuilder) { + this.certificate = certificate; + this.sigBuilder = sigBuilder; + } + + public CertificationResult withSubpackets(@Nonnull CertificationSubpackets.Callback subpacketsCallback) throws PGPException { + sigBuilder.applyCallback(subpacketsCallback); + return build(); + } + + public CertificationResult build() throws PGPException { + PGPPublicKey delegatedKey = certificate.getPublicKey(); + PGPSignature delegation = sigBuilder.build(delegatedKey); + PGPPublicKeyRing delegatedCertificate = KeyRingUtils.injectCertification(certificate, delegatedKey, delegation); + return new CertificationResult(delegatedCertificate, delegation); + } + } + + public static class CertificationResult { private final PGPPublicKeyRing certificate; - private final String userId; private final PGPSignature certification; - CertifyUserIdResult(PGPPublicKeyRing certificate, String userId, PGPSignature certification) { + CertificationResult(@Nonnull PGPPublicKeyRing certificate, @Nonnull PGPSignature certification) { this.certificate = certificate; - this.userId = userId; this.certification = certification; } + @Nonnull public PGPSignature getCertification() { return certification; } + @Nonnull public PGPPublicKeyRing getCertifiedCertificate() { - // inject the signature - PGPPublicKeyRing certified = KeyRingUtils.injectCertification(certificate, userId, certification); - return certified; + return certificate; } } + + private static PGPSecretKey getCertificationSecretKey(PGPSecretKeyRing certificationKey) { + Date now = DateUtil.now(); + KeyRingInfo info = PGPainless.inspectKeyRing(certificationKey, now); + + // We only support certification-capable primary keys + OpenPgpFingerprint fingerprint = info.getFingerprint(); + PGPPublicKey certificationPubKey = info.getPublicKey(fingerprint); + if (!info.isKeyValidlyBound(certificationPubKey.getKeyID())) { + throw new KeyException.RevokedKeyException(fingerprint); + } + + Date expirationDate = info.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER); + if (expirationDate != null && expirationDate.before(now)) { + throw new KeyException.ExpiredKeyException(fingerprint, expirationDate); + } + + PGPSecretKey secretKey = certificationKey.getSecretKey(certificationPubKey.getKeyID()); + if (secretKey == null) { + throw new KeyException.MissingSecretKeyException(fingerprint, certificationPubKey.getKeyID()); + } + return secretKey; + } + } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index f5a5292c..19d7ffcc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -59,6 +59,7 @@ import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.signature.builder.DirectKeySignatureBuilder; import org.pgpainless.signature.builder.RevocationSignatureBuilder; import org.pgpainless.signature.builder.SelfSignatureBuilder; +import org.pgpainless.signature.subpackets.CertificationSubpackets; import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets; import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpackets; @@ -613,9 +614,11 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { final Date keyCreationTime = publicKey.getCreationTime(); DirectKeySignatureBuilder builder = new DirectKeySignatureBuilder(primaryKey, secretKeyRingProtector, prevDirectKeySig); - builder.applyCallback(new SelfSignatureSubpackets.Callback() { + System.out.println("FIXME"); // will cause checkstyle warning so I remember + /* + builder.applyCallback(new CertificationSubpackets.Callback() { @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { + public void modifyHashedSubpackets(CertificationSubpackets hashedSubpackets) { if (expiration != null) { hashedSubpackets.setKeyExpirationTime(keyCreationTime, expiration); } else { @@ -623,6 +626,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { } } }); + */ return builder.build(publicKey); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySignatureBuilder.java index caf08710..0e5498bd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySignatureBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySignatureBuilder.java @@ -10,9 +10,10 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; +import org.pgpainless.signature.subpackets.CertificationSubpackets; public class DirectKeySignatureBuilder extends AbstractSignatureBuilder { @@ -25,15 +26,15 @@ public class DirectKeySignatureBuilder extends AbstractSignatureBuilder", null); String bobUserId = "Bob "; @@ -39,8 +40,8 @@ public class CertifyCertificateTest { PGPPublicKeyRing bobCertificate = PGPainless.extractCertificate(bob); - CertifyCertificate.CertifyUserIdResult result = PGPainless.certifyCertificate() - .certifyUserId(bobCertificate, bobUserId) + CertifyCertificate.CertificationResult result = PGPainless.certify() + .userIdOnCertificate(bobUserId, bobCertificate) .withKey(alice, protector) .build(); @@ -64,4 +65,37 @@ public class CertifyCertificateTest { assertFalse(Arrays.areEqual(bobCertificate.getEncoded(), bobCertified.getEncoded())); } + + @Test + public void testKeyDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice ", null); + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("Bob ", null); + + PGPPublicKeyRing bobCertificate = PGPainless.extractCertificate(bob); + + CertifyCertificate.CertificationResult result = PGPainless.certify() + .certificate(bobCertificate, Trustworthiness.fullyTrusted().introducer()) + .withKey(alice, protector) + .build(); + + assertNotNull(result); + PGPSignature signature = result.getCertification(); + assertNotNull(signature); + assertEquals(SignatureType.DIRECT_KEY, SignatureType.valueOf(signature.getSignatureType())); + assertEquals(alice.getPublicKey().getKeyID(), signature.getKeyID()); + + assertTrue(SignatureVerifier.verifyDirectKeySignature( + signature, alice.getPublicKey(), bob.getPublicKey(), PGPainless.getPolicy(), DateUtil.now())); + + PGPPublicKeyRing bobCertified = result.getCertifiedCertificate(); + PGPPublicKey bobCertifiedKey = bobCertified.getPublicKey(); + + List sigsByAlice = CollectionUtils.iteratorToList( + bobCertifiedKey.getSignaturesForKeyID(alice.getPublicKey().getKeyID())); + assertEquals(1, sigsByAlice.size()); + assertEquals(signature, sigsByAlice.get(0)); + + assertFalse(Arrays.areEqual(bobCertificate.getEncoded(), bobCertified.getEncoded())); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java index 945a06a5..6c1c0cf4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java @@ -40,6 +40,8 @@ public class DirectKeySignatureBuilderTest { secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); + System.out.println("FIXME"); // will cause checkstyle warning, so I remember + /* dsb.applyCallback(new SelfSignatureSubpackets.Callback() { @Override public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { @@ -50,6 +52,7 @@ public class DirectKeySignatureBuilderTest { hashedSubpackets.setFeatures(Feature.MODIFICATION_DETECTION); } }); + */ Thread.sleep(1000); From d2b48e83d921c187eb9609ccce4474fd42bca6b8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 11 May 2022 12:27:11 +0200 Subject: [PATCH 0253/1204] Implement certifying of certifications --- .../pgpainless/algorithm/Trustworthiness.java | 32 ++++- .../key/certification/CertifyCertificate.java | 111 ++++++++++++++++-- .../secretkeyring/SecretKeyRingEditor.java | 12 +- .../DirectKeySelfSignatureBuilder.java | 57 +++++++++ ... ThirdPartyDirectKeySignatureBuilder.java} | 6 +- .../algorithm/TrustworthinessTest.java | 74 ++++++++++++ .../certification/CertifyCertificateTest.java | 9 +- ...rdPartyDirectKeySignatureBuilderTest.java} | 7 +- 8 files changed, 277 insertions(+), 31 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.java rename pgpainless-core/src/main/java/org/pgpainless/signature/builder/{DirectKeySignatureBuilder.java => ThirdPartyDirectKeySignatureBuilder.java} (81%) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/algorithm/TrustworthinessTest.java rename pgpainless-core/src/test/java/org/pgpainless/signature/builder/{DirectKeySignatureBuilderTest.java => ThirdPartyDirectKeySignatureBuilderTest.java} (94%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java index 573bbf9d..0df31764 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java @@ -10,8 +10,8 @@ public class Trustworthiness { private final int depth; public static final int THRESHOLD_FULLY_CONVINCED = 120; - public static final int THRESHOLD_MARGINALLY_CONVINCED = 60; - public static final int THRESHOLD_NOT_TRUSTED = 0; + public static final int MARGINALLY_CONVINCED = 60; + public static final int NOT_TRUSTED = 0; public Trustworthiness(int amount, int depth) { this.amount = capAmount(amount); @@ -26,6 +26,30 @@ public class Trustworthiness { return depth; } + public boolean isNotTrusted() { + return getAmount() == NOT_TRUSTED; + } + + public boolean isMarginallyTrusted() { + return getAmount() > NOT_TRUSTED; + } + + public boolean isFullyTrusted() { + return getAmount() >= THRESHOLD_FULLY_CONVINCED; + } + + public boolean isIntroducer() { + return getDepth() >= 1; + } + + public boolean canIntroduce(int otherDepth) { + return getDepth() > otherDepth; + } + + public boolean canIntroduce(Trustworthiness other) { + return canIntroduce(other.getDepth()); + } + /** * This means that we are fully convinced of the trustworthiness of the key. * @@ -41,7 +65,7 @@ public class Trustworthiness { * @return builder */ public static Builder marginallyTrusted() { - return new Builder(THRESHOLD_MARGINALLY_CONVINCED); + return new Builder(MARGINALLY_CONVINCED); } /** @@ -51,7 +75,7 @@ public class Trustworthiness { * @return builder */ public static Builder untrusted() { - return new Builder(THRESHOLD_NOT_TRUSTED); + return new Builder(NOT_TRUSTED); } public static final class Builder { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java index 77dab4e8..8503f8bd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java @@ -19,7 +19,7 @@ import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.signature.builder.DirectKeySignatureBuilder; +import org.pgpainless.signature.builder.ThirdPartyDirectKeySignatureBuilder; import org.pgpainless.signature.builder.ThirdPartyCertificationSignatureBuilder; import org.pgpainless.signature.subpackets.CertificationSubpackets; import org.pgpainless.util.DateUtil; @@ -28,20 +28,60 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Date; +/** + * API for creating certifications and delegations (Signatures) on keys. + * This API can be used to sign another persons OpenPGP key. + * + * A certification over a user-id is thereby used to attest, that the user believes that the user-id really belongs + * to the owner of the certificate. + * A delegation over a key can be used to delegate trust by marking the certificate as a trusted introducer. + */ public class CertifyCertificate { + /** + * Create a certification over a User-Id. + * By default, this method will use {@link CertificationType#GENERIC} to create the signature. + * If you need to create another type of certification, use + * {@link #userIdOnCertificate(String, PGPPublicKeyRing, CertificationType)} instead. + * + * @param userId user-id to certify + * @param certificate certificate + * @return API + */ CertificationOnUserId userIdOnCertificate(@Nonnull String userId, @Nonnull PGPPublicKeyRing certificate) { return new CertificationOnUserId(userId, certificate, CertificationType.GENERIC); } + /** + * Create a certification of the given {@link CertificationType} over a User-Id. + * + * @param userid user-id to certify + * @param certificate certificate + * @param certificationType type of signature + * @return API + */ CertificationOnUserId userIdOnCertificate(@Nonnull String userid, @Nonnull PGPPublicKeyRing certificate, @Nonnull CertificationType certificationType) { return new CertificationOnUserId(userid, certificate, certificationType); } + /** + * Create a delegation (direct key signature) over a certificate. + * + * @param certificate certificate + * @return API + */ DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate) { return certificate(certificate, null); } + /** + * Create a delegation (direct key signature) containing a {@link org.bouncycastle.bcpg.sig.TrustSignature} packet + * over a certificate. + * + * @param certificate certificate + * @param trustworthiness trustworthiness of the certificate + * @return API + */ DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate, @Nullable Trustworthiness trustworthiness) { return new DelegationOnCertificate(certificate, trustworthiness); } @@ -58,8 +98,16 @@ public class CertifyCertificate { this.certificationType = certificationType; } + /** + * Create the certification using the given key. + * + * @param certificationKey key used to create the certification + * @param protector protector to unlock the certification key + * @return API + * @throws PGPException in case of an OpenPGP related error + */ CertificationOnUserIdWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, @Nonnull SecretKeyRingProtector protector) throws PGPException { - PGPSecretKey secretKey = getCertificationSecretKey(certificationKey); + PGPSecretKey secretKey = getCertifyingSecretKey(certificationKey); ThirdPartyCertificationSignatureBuilder sigBuilder = new ThirdPartyCertificationSignatureBuilder( certificationType.asSignatureType(), secretKey, protector); @@ -80,11 +128,24 @@ public class CertifyCertificate { this.sigBuilder = sigBuilder; } - public CertificationResult withSubpackets(@Nonnull CertificationSubpackets.Callback subpacketCallback) throws PGPException { + /** + * Apply the given signature subpackets and build the certification. + * + * @param subpacketCallback callback to modify the signatures subpackets + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + public CertificationResult buildWithSubpackets(@Nonnull CertificationSubpackets.Callback subpacketCallback) throws PGPException { sigBuilder.applyCallback(subpacketCallback); return build(); } + /** + * Build the certification signature. + * + * @return result + * @throws PGPException in case of an OpenPGP related error + */ public CertificationResult build() throws PGPException { PGPSignature signature = sigBuilder.build(certificate, userId); PGPPublicKeyRing certifiedCertificate = KeyRingUtils.injectCertification(certificate, userId, signature); @@ -102,10 +163,18 @@ public class CertifyCertificate { this.trustworthiness = trustworthiness; } + /** + * Build the delegation using the given certification key. + * + * @param certificationKey key to create the certification with + * @param protector protector to unlock the certification key + * @return API + * @throws PGPException in case of an OpenPGP related error + */ public DelegationOnCertificateWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, @Nonnull SecretKeyRingProtector protector) throws PGPException { - PGPSecretKey secretKey = getCertificationSecretKey(certificationKey); + PGPSecretKey secretKey = getCertifyingSecretKey(certificationKey); - DirectKeySignatureBuilder sigBuilder = new DirectKeySignatureBuilder(secretKey, protector); + ThirdPartyDirectKeySignatureBuilder sigBuilder = new ThirdPartyDirectKeySignatureBuilder(secretKey, protector); if (trustworthiness != null) { sigBuilder.getHashedSubpackets().setTrust(true, trustworthiness.getDepth(), trustworthiness.getAmount()); } @@ -116,18 +185,31 @@ public class CertifyCertificate { public static class DelegationOnCertificateWithSubpackets { private final PGPPublicKeyRing certificate; - private final DirectKeySignatureBuilder sigBuilder; + private final ThirdPartyDirectKeySignatureBuilder sigBuilder; - public DelegationOnCertificateWithSubpackets(@Nonnull PGPPublicKeyRing certificate, @Nonnull DirectKeySignatureBuilder sigBuilder) { + DelegationOnCertificateWithSubpackets(@Nonnull PGPPublicKeyRing certificate, @Nonnull ThirdPartyDirectKeySignatureBuilder sigBuilder) { this.certificate = certificate; this.sigBuilder = sigBuilder; } - public CertificationResult withSubpackets(@Nonnull CertificationSubpackets.Callback subpacketsCallback) throws PGPException { + /** + * Apply the given signature subpackets and build the delegation signature. + * + * @param subpacketsCallback callback to modify the signatures subpackets + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + public CertificationResult buildWithSubpackets(@Nonnull CertificationSubpackets.Callback subpacketsCallback) throws PGPException { sigBuilder.applyCallback(subpacketsCallback); return build(); } + /** + * Build the delegation signature. + * + * @return result + * @throws PGPException in case of an OpenPGP related error + */ public CertificationResult build() throws PGPException { PGPPublicKey delegatedKey = certificate.getPublicKey(); PGPSignature delegation = sigBuilder.build(delegatedKey); @@ -146,18 +228,28 @@ public class CertifyCertificate { this.certification = certification; } + /** + * Return the signature. + * + * @return signature + */ @Nonnull public PGPSignature getCertification() { return certification; } + /** + * Return the certificate, which now contains the signature. + * + * @return certificate + signature + */ @Nonnull public PGPPublicKeyRing getCertifiedCertificate() { return certificate; } } - private static PGPSecretKey getCertificationSecretKey(PGPSecretKeyRing certificationKey) { + private static PGPSecretKey getCertifyingSecretKey(PGPSecretKeyRing certificationKey) { Date now = DateUtil.now(); KeyRingInfo info = PGPainless.inspectKeyRing(certificationKey, now); @@ -179,5 +271,4 @@ public class CertifyCertificate { } return secretKey; } - } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index 19d7ffcc..7418401f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -56,10 +56,9 @@ import org.pgpainless.key.protection.fixes.S2KUsageFix; import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.signature.builder.DirectKeySignatureBuilder; +import org.pgpainless.signature.builder.DirectKeySelfSignatureBuilder; import org.pgpainless.signature.builder.RevocationSignatureBuilder; import org.pgpainless.signature.builder.SelfSignatureBuilder; -import org.pgpainless.signature.subpackets.CertificationSubpackets; import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets; import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpackets; @@ -613,12 +612,10 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { PGPPublicKey publicKey = primaryKey.getPublicKey(); final Date keyCreationTime = publicKey.getCreationTime(); - DirectKeySignatureBuilder builder = new DirectKeySignatureBuilder(primaryKey, secretKeyRingProtector, prevDirectKeySig); - System.out.println("FIXME"); // will cause checkstyle warning so I remember - /* - builder.applyCallback(new CertificationSubpackets.Callback() { + DirectKeySelfSignatureBuilder builder = new DirectKeySelfSignatureBuilder(primaryKey, secretKeyRingProtector, prevDirectKeySig); + builder.applyCallback(new SelfSignatureSubpackets.Callback() { @Override - public void modifyHashedSubpackets(CertificationSubpackets hashedSubpackets) { + public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { if (expiration != null) { hashedSubpackets.setKeyExpirationTime(keyCreationTime, expiration); } else { @@ -626,7 +623,6 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { } } }); - */ return builder.build(publicKey); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.java new file mode 100644 index 00000000..cf99f8d6 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +import javax.annotation.Nullable; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; + +public class DirectKeySelfSignatureBuilder extends AbstractSignatureBuilder { + + public DirectKeySelfSignatureBuilder(PGPSecretKey certificationKey, SecretKeyRingProtector protector, PGPSignature archetypeSignature) + throws PGPException { + super(certificationKey, protector, archetypeSignature); + } + + public DirectKeySelfSignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector) throws PGPException { + super(SignatureType.DIRECT_KEY, signingKey, protector); + } + + public SelfSignatureSubpackets getHashedSubpackets() { + return hashedSubpackets; + } + + public SelfSignatureSubpackets getUnhashedSubpackets() { + return unhashedSubpackets; + } + + public void applyCallback(@Nullable SelfSignatureSubpackets.Callback callback) { + if (callback != null) { + callback.modifyHashedSubpackets(getHashedSubpackets()); + callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); + } + } + + public PGPSignature build(PGPPublicKey key) throws PGPException { + PGPSignatureGenerator signatureGenerator = buildAndInitSignatureGenerator(); + if (key.getKeyID() != publicSigningKey.getKeyID()) { + return signatureGenerator.generateCertification(publicSigningKey, key); + } else { + return signatureGenerator.generateCertification(key); + } + } + + @Override + protected boolean isValidSignatureType(SignatureType type) { + return type == SignatureType.DIRECT_KEY; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.java similarity index 81% rename from pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySignatureBuilder.java rename to pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.java index 0e5498bd..dd720bce 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySignatureBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.java @@ -15,14 +15,14 @@ import org.pgpainless.algorithm.SignatureType; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.subpackets.CertificationSubpackets; -public class DirectKeySignatureBuilder extends AbstractSignatureBuilder { +public class ThirdPartyDirectKeySignatureBuilder extends AbstractSignatureBuilder { - public DirectKeySignatureBuilder(PGPSecretKey certificationKey, SecretKeyRingProtector protector, PGPSignature archetypeSignature) + public ThirdPartyDirectKeySignatureBuilder(PGPSecretKey certificationKey, SecretKeyRingProtector protector, PGPSignature archetypeSignature) throws PGPException { super(certificationKey, protector, archetypeSignature); } - public DirectKeySignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector) throws PGPException { + public ThirdPartyDirectKeySignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector) throws PGPException { super(SignatureType.DIRECT_KEY, signingKey, protector); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/TrustworthinessTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/TrustworthinessTest.java new file mode 100644 index 00000000..017126aa --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/TrustworthinessTest.java @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class TrustworthinessTest { + + @Test + public void fullyTrustedIntroducer() { + Trustworthiness it = Trustworthiness.fullyTrusted().introducer(); + assertTrue(it.isFullyTrusted()); + assertFalse(it.isNotTrusted()); + + assertTrue(it.isIntroducer()); + assertFalse(it.canIntroduce(it)); + } + + @Test + public void marginallyTrustedIntroducer() { + Trustworthiness it = Trustworthiness.marginallyTrusted().introducer(); + assertFalse(it.isFullyTrusted()); + assertTrue(it.isMarginallyTrusted()); + assertFalse(it.isNotTrusted()); + + assertTrue(it.isIntroducer()); + assertFalse(it.canIntroduce(2)); + } + + @Test + public void nonTrustedIntroducer() { + Trustworthiness it = Trustworthiness.untrusted().introducer(); + assertTrue(it.isNotTrusted()); + assertFalse(it.isMarginallyTrusted()); + assertFalse(it.isFullyTrusted()); + + assertTrue(it.isIntroducer()); + } + + @Test + public void trustedMetaIntroducer() { + Trustworthiness it = Trustworthiness.fullyTrusted().metaIntroducer(); + assertTrue(it.isFullyTrusted()); + assertTrue(it.isIntroducer()); + + Trustworthiness that = Trustworthiness.fullyTrusted().introducer(); + assertTrue(it.canIntroduce(that)); + assertFalse(that.canIntroduce(it)); + } + + @Test + public void invalidArguments() { + assertThrows(IllegalArgumentException.class, () -> new Trustworthiness(300, 1)); + assertThrows(IllegalArgumentException.class, () -> new Trustworthiness(60, 300)); + assertThrows(IllegalArgumentException.class, () -> new Trustworthiness(-4, 1)); + assertThrows(IllegalArgumentException.class, () -> new Trustworthiness(120, -1)); + } + + @Test + public void inBetweenValues() { + Trustworthiness it = new Trustworthiness(30, 1); + assertTrue(it.isMarginallyTrusted()); + assertFalse(it.isFullyTrusted()); + + it = new Trustworthiness(140, 1); + assertTrue(it.isFullyTrusted()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java index c6028925..4a72276c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java @@ -14,6 +14,7 @@ import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.List; +import org.bouncycastle.bcpg.sig.TrustSignature; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; @@ -84,13 +85,19 @@ public class CertifyCertificateTest { assertNotNull(signature); assertEquals(SignatureType.DIRECT_KEY, SignatureType.valueOf(signature.getSignatureType())); assertEquals(alice.getPublicKey().getKeyID(), signature.getKeyID()); + TrustSignature trustSignaturePacket = signature.getHashedSubPackets().getTrust(); + assertNotNull(trustSignaturePacket); + Trustworthiness trustworthiness = new Trustworthiness(trustSignaturePacket.getTrustAmount(), trustSignaturePacket.getDepth()); + assertTrue(trustworthiness.isFullyTrusted()); + assertTrue(trustworthiness.isIntroducer()); + assertFalse(trustworthiness.canIntroduce(1)); assertTrue(SignatureVerifier.verifyDirectKeySignature( signature, alice.getPublicKey(), bob.getPublicKey(), PGPainless.getPolicy(), DateUtil.now())); PGPPublicKeyRing bobCertified = result.getCertifiedCertificate(); PGPPublicKey bobCertifiedKey = bobCertified.getPublicKey(); - + List sigsByAlice = CollectionUtils.iteratorToList( bobCertifiedKey.getSignaturesForKeyID(alice.getPublicKey().getKeyID())); assertEquals(1, sigsByAlice.size()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java similarity index 94% rename from pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java rename to pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java index 6c1c0cf4..46dc1ac5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/DirectKeySignatureBuilderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java @@ -29,19 +29,17 @@ import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -public class DirectKeySignatureBuilderTest { +public class ThirdPartyDirectKeySignatureBuilderTest { @Test public void testDirectKeySignatureBuilding() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InterruptedException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .modernKeyRing("Alice"); - DirectKeySignatureBuilder dsb = new DirectKeySignatureBuilder( + DirectKeySelfSignatureBuilder dsb = new DirectKeySelfSignatureBuilder( secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); - System.out.println("FIXME"); // will cause checkstyle warning, so I remember - /* dsb.applyCallback(new SelfSignatureSubpackets.Callback() { @Override public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { @@ -52,7 +50,6 @@ public class DirectKeySignatureBuilderTest { hashedSubpackets.setFeatures(Feature.MODIFICATION_DETECTION); } }); - */ Thread.sleep(1000); From 870af0e005be7b65a23ed6d4672fdaefec50dd95 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 12 May 2022 16:24:55 +0200 Subject: [PATCH 0254/1204] Add javadoc documentation to Trustworthiness class --- .../pgpainless/algorithm/Trustworthiness.java | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java index 0df31764..8d62f967 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java @@ -4,6 +4,11 @@ package org.pgpainless.algorithm; +/** + * Facade class for {@link org.bouncycastle.bcpg.sig.TrustSignature}. + * A trust signature subpacket marks the trustworthiness of a certificate and defines its capabilities to act + * as a trusted introducer. + */ public class Trustworthiness { private final int amount; @@ -18,34 +23,82 @@ public class Trustworthiness { this.depth = capDepth(depth); } + /** + * Get the trust amount. + * This value means how confident the issuer of the signature is in validity of the binding. + * + * @return trust amount + */ public int getAmount() { return amount; } + /** + * Get the depth of the trust signature. + * This value controls, whether the certificate can act as a trusted introducer. + * + * @return depth + */ public int getDepth() { return depth; } + /** + * Returns true, if the trust amount is equal to 0. + * This means the key is not trusted. + * + * Otherwise return false + * @return true if untrusted + */ public boolean isNotTrusted() { return getAmount() == NOT_TRUSTED; } + /** + * Return true if the certificate is at least marginally trusted. + * That is the case, if the trust amount is greater than 0. + * + * @return true if the cert is at least marginally trusted + */ public boolean isMarginallyTrusted() { return getAmount() > NOT_TRUSTED; } + /** + * Return true if the certificate is fully trusted. That is the case if the trust amount is + * greater than or equal to 120. + * + * @return true if the cert is fully trusted + */ public boolean isFullyTrusted() { return getAmount() >= THRESHOLD_FULLY_CONVINCED; } + /** + * Return true, if the cert is an introducer. That is the case if the depth is greater 0. + * + * @return true if introducer + */ public boolean isIntroducer() { return getDepth() >= 1; } + /** + * Return true, if the certified cert can introduce certificates with trust depth of
otherDepth
. + * + * @param otherDepth other certifications trust depth + * @return true if the cert can introduce the other + */ public boolean canIntroduce(int otherDepth) { return getDepth() > otherDepth; } + /** + * Return true, if the certified cert can introduce certificates with the given
other
trust depth. + * + * @param other other certificates trust depth + * @return true if the cert can introduce the other + */ public boolean canIntroduce(Trustworthiness other) { return canIntroduce(other.getDepth()); } @@ -107,13 +160,13 @@ public class Trustworthiness { } /** - * The key is a level
n
meta introducer. + * The key is a meta introducer of depth
n
. * This key can introduce meta introducers of depth
n - 1
. * * @param n depth * @return trust */ - public Trustworthiness levelNIntroducer(int n) { + public Trustworthiness metaIntroducerOfDepth(int n) { return new Trustworthiness(amount, n); } } From 1483ff9e24a411381185ec42ee499fd81ae7a459 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 12 May 2022 16:25:04 +0200 Subject: [PATCH 0255/1204] Add another test for Trustworthiness --- .../pgpainless/algorithm/TrustworthinessTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/TrustworthinessTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/TrustworthinessTest.java index 017126aa..bf87ed65 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/algorithm/TrustworthinessTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/TrustworthinessTest.java @@ -71,4 +71,17 @@ public class TrustworthinessTest { it = new Trustworthiness(140, 1); assertTrue(it.isFullyTrusted()); } + + @Test + public void depthHierarchyTest() { + Trustworthiness l1 = Trustworthiness.fullyTrusted().metaIntroducerOfDepth(1); + Trustworthiness l2 = Trustworthiness.fullyTrusted().metaIntroducerOfDepth(2); + Trustworthiness l3 = Trustworthiness.fullyTrusted().metaIntroducerOfDepth(3); + + assertTrue(l3.canIntroduce(l2)); + assertTrue(l3.canIntroduce(l1)); + assertTrue(l2.canIntroduce(l1)); + assertFalse(l1.canIntroduce(l2)); + assertFalse(l1.canIntroduce(l3)); + } } From bbd94c6c9abb4adb842967ccb5ae4e7bc8943b30 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 12 May 2022 16:26:57 +0200 Subject: [PATCH 0256/1204] More documentation --- .../main/java/org/pgpainless/algorithm/Trustworthiness.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java index 8d62f967..26e66e9c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java @@ -14,9 +14,9 @@ public class Trustworthiness { private final int amount; private final int depth; - public static final int THRESHOLD_FULLY_CONVINCED = 120; - public static final int MARGINALLY_CONVINCED = 60; - public static final int NOT_TRUSTED = 0; + public static final int THRESHOLD_FULLY_CONVINCED = 120; // greater or equal is fully trusted + public static final int MARGINALLY_CONVINCED = 60; // default value for marginally convinced + public static final int NOT_TRUSTED = 0; // 0 is not trusted public Trustworthiness(int amount, int depth) { this.amount = capAmount(amount); From 8d2afdf3b6f1a94e1246309394115355005ba2df Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 May 2022 18:32:59 +0200 Subject: [PATCH 0257/1204] Make certify() methods public --- .../key/certification/CertifyCertificate.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java index 8503f8bd..4a8d3985 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java @@ -48,7 +48,7 @@ public class CertifyCertificate { * @param certificate certificate * @return API */ - CertificationOnUserId userIdOnCertificate(@Nonnull String userId, @Nonnull PGPPublicKeyRing certificate) { + public CertificationOnUserId userIdOnCertificate(@Nonnull String userId, @Nonnull PGPPublicKeyRing certificate) { return new CertificationOnUserId(userId, certificate, CertificationType.GENERIC); } @@ -60,7 +60,7 @@ public class CertifyCertificate { * @param certificationType type of signature * @return API */ - CertificationOnUserId userIdOnCertificate(@Nonnull String userid, @Nonnull PGPPublicKeyRing certificate, @Nonnull CertificationType certificationType) { + public CertificationOnUserId userIdOnCertificate(@Nonnull String userid, @Nonnull PGPPublicKeyRing certificate, @Nonnull CertificationType certificationType) { return new CertificationOnUserId(userid, certificate, certificationType); } @@ -70,7 +70,7 @@ public class CertifyCertificate { * @param certificate certificate * @return API */ - DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate) { + public DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate) { return certificate(certificate, null); } @@ -82,7 +82,7 @@ public class CertifyCertificate { * @param trustworthiness trustworthiness of the certificate * @return API */ - DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate, @Nullable Trustworthiness trustworthiness) { + public DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate, @Nullable Trustworthiness trustworthiness) { return new DelegationOnCertificate(certificate, trustworthiness); } @@ -106,7 +106,7 @@ public class CertifyCertificate { * @return API * @throws PGPException in case of an OpenPGP related error */ - CertificationOnUserIdWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, @Nonnull SecretKeyRingProtector protector) throws PGPException { + public CertificationOnUserIdWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, @Nonnull SecretKeyRingProtector protector) throws PGPException { PGPSecretKey secretKey = getCertifyingSecretKey(certificationKey); ThirdPartyCertificationSignatureBuilder sigBuilder = new ThirdPartyCertificationSignatureBuilder( From 7223b40b23ac73702b14655eb793e17ff071ad38 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 24 May 2022 20:22:33 +0200 Subject: [PATCH 0258/1204] Add javadoc and indentation --- .../key/certification/CertifyCertificate.java | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java index 4a8d3985..a4f37b66 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java @@ -48,7 +48,8 @@ public class CertifyCertificate { * @param certificate certificate * @return API */ - public CertificationOnUserId userIdOnCertificate(@Nonnull String userId, @Nonnull PGPPublicKeyRing certificate) { + public CertificationOnUserId userIdOnCertificate(@Nonnull String userId, + @Nonnull PGPPublicKeyRing certificate) { return new CertificationOnUserId(userId, certificate, CertificationType.GENERIC); } @@ -60,12 +61,16 @@ public class CertifyCertificate { * @param certificationType type of signature * @return API */ - public CertificationOnUserId userIdOnCertificate(@Nonnull String userid, @Nonnull PGPPublicKeyRing certificate, @Nonnull CertificationType certificationType) { + public CertificationOnUserId userIdOnCertificate(@Nonnull String userid, + @Nonnull PGPPublicKeyRing certificate, + @Nonnull CertificationType certificationType) { return new CertificationOnUserId(userid, certificate, certificationType); } /** * Create a delegation (direct key signature) over a certificate. + * This can be used to mark a certificate as a trusted introducer + * (see {@link #certificate(PGPPublicKeyRing, Trustworthiness)}). * * @param certificate certificate * @return API @@ -77,12 +82,14 @@ public class CertifyCertificate { /** * Create a delegation (direct key signature) containing a {@link org.bouncycastle.bcpg.sig.TrustSignature} packet * over a certificate. + * This can be used to mark a certificate as a trusted introducer. * * @param certificate certificate * @param trustworthiness trustworthiness of the certificate * @return API */ - public DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate, @Nullable Trustworthiness trustworthiness) { + public DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate, + @Nullable Trustworthiness trustworthiness) { return new DelegationOnCertificate(certificate, trustworthiness); } @@ -92,7 +99,9 @@ public class CertifyCertificate { private final String userId; private final CertificationType certificationType; - CertificationOnUserId(@Nonnull String userId, @Nonnull PGPPublicKeyRing certificate, @Nonnull CertificationType certificationType) { + CertificationOnUserId(@Nonnull String userId, + @Nonnull PGPPublicKeyRing certificate, + @Nonnull CertificationType certificationType) { this.userId = userId; this.certificate = certificate; this.certificationType = certificationType; @@ -106,7 +115,9 @@ public class CertifyCertificate { * @return API * @throws PGPException in case of an OpenPGP related error */ - public CertificationOnUserIdWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, @Nonnull SecretKeyRingProtector protector) throws PGPException { + public CertificationOnUserIdWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, + @Nonnull SecretKeyRingProtector protector) + throws PGPException { PGPSecretKey secretKey = getCertifyingSecretKey(certificationKey); ThirdPartyCertificationSignatureBuilder sigBuilder = new ThirdPartyCertificationSignatureBuilder( @@ -122,7 +133,9 @@ public class CertifyCertificate { private final String userId; private final ThirdPartyCertificationSignatureBuilder sigBuilder; - CertificationOnUserIdWithSubpackets(@Nonnull PGPPublicKeyRing certificate, @Nonnull String userId, @Nonnull ThirdPartyCertificationSignatureBuilder sigBuilder) { + CertificationOnUserIdWithSubpackets(@Nonnull PGPPublicKeyRing certificate, + @Nonnull String userId, + @Nonnull ThirdPartyCertificationSignatureBuilder sigBuilder) { this.certificate = certificate; this.userId = userId; this.sigBuilder = sigBuilder; @@ -135,7 +148,8 @@ public class CertifyCertificate { * @return result * @throws PGPException in case of an OpenPGP related error */ - public CertificationResult buildWithSubpackets(@Nonnull CertificationSubpackets.Callback subpacketCallback) throws PGPException { + public CertificationResult buildWithSubpackets(@Nonnull CertificationSubpackets.Callback subpacketCallback) + throws PGPException { sigBuilder.applyCallback(subpacketCallback); return build(); } @@ -158,7 +172,8 @@ public class CertifyCertificate { private final PGPPublicKeyRing certificate; private final Trustworthiness trustworthiness; - DelegationOnCertificate(@Nonnull PGPPublicKeyRing certificate, @Nullable Trustworthiness trustworthiness) { + DelegationOnCertificate(@Nonnull PGPPublicKeyRing certificate, + @Nullable Trustworthiness trustworthiness) { this.certificate = certificate; this.trustworthiness = trustworthiness; } @@ -171,7 +186,9 @@ public class CertifyCertificate { * @return API * @throws PGPException in case of an OpenPGP related error */ - public DelegationOnCertificateWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, @Nonnull SecretKeyRingProtector protector) throws PGPException { + public DelegationOnCertificateWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, + @Nonnull SecretKeyRingProtector protector) + throws PGPException { PGPSecretKey secretKey = getCertifyingSecretKey(certificationKey); ThirdPartyDirectKeySignatureBuilder sigBuilder = new ThirdPartyDirectKeySignatureBuilder(secretKey, protector); @@ -187,7 +204,8 @@ public class CertifyCertificate { private final PGPPublicKeyRing certificate; private final ThirdPartyDirectKeySignatureBuilder sigBuilder; - DelegationOnCertificateWithSubpackets(@Nonnull PGPPublicKeyRing certificate, @Nonnull ThirdPartyDirectKeySignatureBuilder sigBuilder) { + DelegationOnCertificateWithSubpackets(@Nonnull PGPPublicKeyRing certificate, + @Nonnull ThirdPartyDirectKeySignatureBuilder sigBuilder) { this.certificate = certificate; this.sigBuilder = sigBuilder; } @@ -199,7 +217,8 @@ public class CertifyCertificate { * @return result * @throws PGPException in case of an OpenPGP related error */ - public CertificationResult buildWithSubpackets(@Nonnull CertificationSubpackets.Callback subpacketsCallback) throws PGPException { + public CertificationResult buildWithSubpackets(@Nonnull CertificationSubpackets.Callback subpacketsCallback) + throws PGPException { sigBuilder.applyCallback(subpacketsCallback); return build(); } From a944d2a6b9b660935ae19bde8bf4ad7bab636175 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 20 Jun 2022 15:09:02 +0200 Subject: [PATCH 0259/1204] Fix build errors --- .../src/main/java/org/pgpainless/PGPainless.java | 5 +++++ .../key/certification/CertifyCertificateTest.java | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index a2f16ab9..74a1f239 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -165,6 +165,11 @@ public final class PGPainless { return Policy.getInstance(); } + /** + * Create different kinds of signatures on other keys. + * + * @return builder + */ public static CertifyCertificate certify() { return new CertifyCertificate(); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java index 4a72276c..3dbc4988 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java @@ -35,9 +35,9 @@ public class CertifyCertificateTest { @Test public void testUserIdCertification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice ", null); + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice "); String bobUserId = "Bob "; - PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing(bobUserId, null); + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing(bobUserId); PGPPublicKeyRing bobCertificate = PGPainless.extractCertificate(bob); @@ -70,8 +70,8 @@ public class CertifyCertificateTest { @Test public void testKeyDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice ", null); - PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("Bob ", null); + PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice "); + PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("Bob "); PGPPublicKeyRing bobCertificate = PGPainless.extractCertificate(bob); From 82ff62b4e65f00e3dd63784ace3f1e38b86fe49d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 20 Jun 2022 17:58:27 +0200 Subject: [PATCH 0260/1204] Remove unused NotYetImplementedException --- .../exception/NotYetImplementedException.java | 12 ------------ .../java/org/pgpainless/key/util/KeyRingUtils.java | 3 --- 2 files changed, 15 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/NotYetImplementedException.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/NotYetImplementedException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/NotYetImplementedException.java deleted file mode 100644 index 72f9b569..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/NotYetImplementedException.java +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -/** - * Method that gets thrown if the user requests some functionality which is not yet implemented. - */ -public class NotYetImplementedException extends RuntimeException { - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 8306db34..21c5b575 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -26,7 +26,6 @@ import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; import org.pgpainless.PGPainless; -import org.pgpainless.exception.NotYetImplementedException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; @@ -354,8 +353,6 @@ public final class KeyRingUtils { /** * Inject a {@link PGPPublicKey} into the given key ring. * - * Note: Right now this method is broken and will throw a {@link NotYetImplementedException}. - * * @param keyRing key ring * @param publicKey public key * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} From ca39efda99bef968e6f247d02e60a42c763f2d10 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 20 Jun 2022 18:10:44 +0200 Subject: [PATCH 0261/1204] Add test for CleartextSignedMessageUtil --- .../CleartextSignatureVerificationTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java index 521dd558..cabfdbb1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java @@ -6,6 +6,7 @@ package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; @@ -13,6 +14,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; @@ -27,11 +29,13 @@ import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.exception.WrongConsumingMethodException; import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.consumer.CertificateValidator; @@ -241,6 +245,29 @@ public class CleartextSignatureVerificationTest { decryptionStream.close(); } + @Test + public void clearsignedMessageUtil_detachSignaturesFromInbandNonClearsignedMessageThrows() { + // Message is inband signed, but does not use cleartext signature framework + String message = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "owGbwMvMyCX29UzQdZ1/lUqMpw8YJDGAgJGjd3JgcqJTVUpylpOCmUK+l39asYGl\n" + + "k1NkcYSxgkuaR26EQplppGVuREGqn3NBRJRXoVm4T1BuhoJjcllOYV5xhmVKloVz\n" + + "UJaZQmhBSbqCr6uhQlVIkL9rqUJgaaWjpalCuVdiXkVhiFNuQHpmeLpChGNqVkG5\n" + + "U1iBgqmvo79LXlFVWK5rpEGkh0dBfrB/ngKXj5FhVlZuUpllTk6xb3m5QlWUT3Gh\n" + + "o7dCQXGIgnlwZkBYlI9FhEFAprdnkLGFe6KjZ2meQblCXkiWaWhUknl5YmmYb7JC\n" + + "noJJeWZYXmJarpFvXkpKpbGXQkcpC6MYF4M6K1PShlmCnAKwsBBTZJktcnnrHYXL\n" + + "h1oWr+qECTMw+O9i+KfUs3LXgzOuS102VbY+fLCqwFynLmyqVDE3b4Yu/5x68UCG\n" + + "/35qnVwnbYX8YrK6j+UdabAo/HnvZL7jk7pjRg1n3TIy+QE=\n" + + "=yFcL\n" + + "-----END PGP MESSAGE-----"; + + InputStream inputStream = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + assertThrows(WrongConsumingMethodException.class, + () -> ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(inputStream, outputStream)); + } + private String randomString(int maxWordLen, int wordCount) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < wordCount; i++) { From fed3080ae8275305b396bbaedcfe37ed669c3ce9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 20 Jun 2022 18:19:24 +0200 Subject: [PATCH 0262/1204] Add tests to increase coverage of v5 fingerprint class --- .../key/OpenPgpV5FingerprintTest.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java index 2e657d72..b8c74b8b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java @@ -8,6 +8,7 @@ import org.bouncycastle.util.encoders.Hex; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -34,6 +35,7 @@ public class OpenPgpV5FingerprintTest { assertTrue(parsed instanceof OpenPgpV5Fingerprint); OpenPgpV5Fingerprint v5fp = (OpenPgpV5Fingerprint) parsed; assertEquals(prettyPrint, v5fp.prettyPrint()); + assertEquals(5, v5fp.getVersion()); } @Test @@ -44,6 +46,9 @@ public class OpenPgpV5FingerprintTest { OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); assertEquals(hex, fingerprint.toString()); + + OpenPgpV5Fingerprint constructed = new OpenPgpV5Fingerprint(binary); + assertEquals(fingerprint, constructed); } @Test @@ -73,4 +78,18 @@ public class OpenPgpV5FingerprintTest { assertThrows(IllegalArgumentException.class, () -> OpenPgpFingerprint.parseFromBinary(binary)); } + + @Test + public void equalsTest() { + String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); + + assertNotEquals(parsed, null); + assertNotEquals(parsed, new Object()); + assertEquals(parsed, parsed.toString()); + + OpenPgpFingerprint parsed2 = new OpenPgpV5Fingerprint(prettyPrint); + assertEquals(parsed.hashCode(), parsed2.hashCode()); + assertEquals(0, parsed.compareTo(parsed2)); + } } From 2873de0d052683744888a82e098afbdf6a6dbd3f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 20 Jun 2022 18:35:31 +0200 Subject: [PATCH 0263/1204] Include mockito as test dependency --- pgpainless-core/build.gradle | 3 +++ version.gradle | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 50cbb700..f1b852ca 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -12,6 +12,9 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + // Mocking Components + testImplementation "org.mockito:mockito-core:$mockitoVersion" + // Logging api "org.slf4j:slf4j-api:$slf4jVersion" testImplementation "ch.qos.logback:logback-classic:$logbackVersion" diff --git a/version.gradle b/version.gradle index 18354f3b..b97d329f 100644 --- a/version.gradle +++ b/version.gradle @@ -9,9 +9,10 @@ allprojects { pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' - slf4jVersion = '1.7.36' - logbackVersion = '1.2.11' junitVersion = '5.8.2' + logbackVersion = '1.2.11' + mockitoVersion = '4.5.1' + slf4jVersion = '1.7.36' sopJavaVersion = '4.0.0' } } From 37441a81e83a4581c040e3c7546025e6d4cd87e7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 20 Jun 2022 18:35:48 +0200 Subject: [PATCH 0264/1204] Add OpenPgpV5Fingerprint constructor tests using mocked v5 keys --- .../key/OpenPgpV5FingerprintTest.java | 90 ++++++++++++++++++- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java index b8c74b8b..a250bef4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java @@ -4,13 +4,20 @@ package org.pgpainless.key; -import org.bouncycastle.util.encoders.Hex; -import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; public class OpenPgpV5FingerprintTest { @@ -92,4 +99,81 @@ public class OpenPgpV5FingerprintTest { assertEquals(parsed.hashCode(), parsed2.hashCode()); assertEquals(0, parsed.compareTo(parsed2)); } + + @Test + public void constructFromMockedPublicKey() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); + assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); + assertEquals(5, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedSecretKey() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPSecretKey secretKey = mock(PGPSecretKey.class); + when(secretKey.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = new OpenPgpV5Fingerprint(secretKey); + assertEquals(5, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedPublicKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPPublicKeyRing publicKeys = mock(PGPPublicKeyRing.class); + when(publicKeys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKeys); + assertEquals(5, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV5Fingerprint(publicKeys); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedSecretKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPSecretKeyRing secretKeys = mock(PGPSecretKeyRing.class); + when(secretKeys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(secretKeys); + assertEquals(5, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV5Fingerprint(secretKeys); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPKeyRing keys = mock(PGPKeyRing.class); + when(keys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(keys); + assertEquals(5, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV5Fingerprint(keys); + assertEquals(hex, fingerprint.toString()); + } + + private PGPPublicKey getMockedPublicKey(String hex) { + byte[] binary = Hex.decode(hex); + + PGPPublicKey mocked = mock(PGPPublicKey.class); + when(mocked.getVersion()).thenReturn(5); + when(mocked.getFingerprint()).thenReturn(binary); + return mocked; + } } From 0690a213609226d915595520f23986c42b36e5f3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 20 Jun 2022 18:48:27 +0200 Subject: [PATCH 0265/1204] Increase coverage of Policy class --- .../org/pgpainless/policy/PolicyTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java index 9959be68..4e3cb51e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java @@ -6,6 +6,7 @@ package org.pgpainless.policy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Arrays; @@ -167,4 +168,41 @@ public class PolicyTest { policy.getNotationRegistry().addKnownNotation("notation@pgpainless.org"); assertTrue(policy.getNotationRegistry().isKnownNotation("notation@pgpainless.org")); } + + @Test + public void testUnknownSymmetricKeyEncryptionAlgorithmIsNotAcceptable() { + assertFalse(policy.getSymmetricKeyEncryptionAlgorithmPolicy().isAcceptable(-1)); + } + + @Test + public void testUnknownSymmetricKeyDecryptionAlgorithmIsNotAcceptable() { + assertFalse(policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(-1)); + } + + @Test + public void testUnknownSignatureHashAlgorithmIsNotAcceptable() { + assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(-1)); + assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(-1, new Date())); + } + + @Test + public void testUnknownRevocationHashAlgorithmIsNotAcceptable() { + assertFalse(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(-1)); + assertFalse(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(-1, new Date())); + } + + @Test + public void testUnknownCompressionAlgorithmIsNotAcceptable() { + assertFalse(policy.getCompressionAlgorithmPolicy().isAcceptable(-1)); + } + + @Test + public void testUnknownPublicKeyAlgorithmIsNotAcceptable() { + assertFalse(policy.getPublicKeyAlgorithmPolicy().isAcceptable(-1, 4096)); + } + + @Test + public void setNullSignerUserIdValidationLevelThrows() { + assertThrows(NullPointerException.class, () -> policy.setSignerUserIdValidationLevel(null)); + } } From b6975b38f13501f323b549acf88450909c419045 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 20 Jun 2022 19:03:52 +0200 Subject: [PATCH 0266/1204] Add tests for KeyFlag bitmask methods --- .../org/pgpainless/algorithm/KeyFlagTest.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/algorithm/KeyFlagTest.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/KeyFlagTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/KeyFlagTest.java new file mode 100644 index 00000000..3ec5dcb6 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/KeyFlagTest.java @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +public class KeyFlagTest { + + @Test + public void testEmptyBitmaskHasNoFlags() { + int bitmask = KeyFlag.toBitmask(); + assertEquals(0, bitmask); + for (KeyFlag flag : KeyFlag.values()) { + assertFalse(KeyFlag.hasKeyFlag(bitmask, flag)); + } + } + + @Test + public void testEmptyBitmaskToKeyFlags() { + int emptyMask = 0; + List flags = KeyFlag.fromBitmask(emptyMask); + assertTrue(flags.isEmpty()); + } + + @Test + public void testSingleBitmaskToKeyFlags() { + for (KeyFlag flag : KeyFlag.values()) { + int singleMask = KeyFlag.toBitmask(flag); + List singletonList = KeyFlag.fromBitmask(singleMask); + assertEquals(1, singletonList.size()); + assertEquals(flag, singletonList.get(0)); + } + } + + @Test + public void testKeyFlagsToBitmaskToList() { + int bitMask = KeyFlag.toBitmask(KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE); + List flags = KeyFlag.fromBitmask(bitMask); + + assertEquals(2, flags.size()); + assertTrue(flags.contains(KeyFlag.ENCRYPT_COMMS)); + assertTrue(flags.contains(KeyFlag.ENCRYPT_STORAGE)); + } + + @Test + public void testSingleKeyFlagToBitmask() { + for (KeyFlag flag : KeyFlag.values()) { + int bitmask = KeyFlag.toBitmask(flag); + assertEquals(flag.getFlag(), bitmask); + } + } + + @Test + public void testDuplicateFlagsDoNotChangeMask() { + int mask = KeyFlag.toBitmask(KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_COMMS); + assertEquals(KeyFlag.toBitmask(KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE), mask); + } + + @Test + public void testMaskHasNot() { + int mask = KeyFlag.toBitmask(KeyFlag.ENCRYPT_STORAGE); + assertFalse(KeyFlag.hasKeyFlag(mask, KeyFlag.ENCRYPT_COMMS)); + } + + @Test + public void testMaskContainsNone() { + int mask = KeyFlag.toBitmask(KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE); + + assertFalse(KeyFlag.containsAny(mask, KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)); + } + + @Test + public void testContainsAnyContainsAllExact() { + int mask = KeyFlag.toBitmask(KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS); + assertTrue(KeyFlag.containsAny(mask, KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)); + } + + @Test + public void testContainsAnyContainsAll() { + int mask = KeyFlag.toBitmask(KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.AUTHENTICATION); + assertTrue(KeyFlag.containsAny(mask, KeyFlag.SIGN_DATA, KeyFlag.AUTHENTICATION)); + } + + @Test + public void testContainsAnyContainsSome() { + int mask = KeyFlag.toBitmask(KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.AUTHENTICATION); + assertTrue(KeyFlag.containsAny(mask, KeyFlag.CERTIFY_OTHER)); + } +} From 8d1794544a889920914c9d149c1e8d144964af57 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 21 Jun 2022 19:48:38 +0200 Subject: [PATCH 0267/1204] Fix indentation --- .../org/pgpainless/encryption_signing/EncryptionStream.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 0cba8093..dbfc737e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -87,9 +87,9 @@ public final class EncryptionStream extends OutputStream { if (options.hasComment()) { String[] commentLines = options.getComment().split("\n"); for (String commentLine : commentLines) { - if (!commentLine.trim().isEmpty()) { - ArmorUtils.addCommentHeader(armorOutputStream, commentLine.trim()); - } + if (!commentLine.trim().isEmpty()) { + ArmorUtils.addCommentHeader(armorOutputStream, commentLine.trim()); + } } } outermostStream = armorOutputStream; From e5ba4f99334be1abc59ff655903a381ab2c596cc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 21 Jun 2022 19:48:49 +0200 Subject: [PATCH 0268/1204] Add buffer to improve encryption performance --- .../org/pgpainless/encryption_signing/EncryptionStream.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index dbfc737e..50f5cb5f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -82,6 +82,9 @@ public final class EncryptionStream extends OutputStream { return; } + // ArmoredOutputStream better be buffered + outermostStream = new BufferedOutputStream(outermostStream); + LOGGER.debug("Wrap encryption output in ASCII armor"); armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream); if (options.hasComment()) { From bca359805b8b8f8158f7b6115a392e2a9c2813b7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 22 Jun 2022 22:14:22 +0200 Subject: [PATCH 0269/1204] SOP decrypt: Do not throw if no signatures found --- .../src/main/java/org/pgpainless/sop/DecryptImpl.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index b9539ed5..efed9472 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -154,12 +154,6 @@ public class DecryptImpl implements Decrypt { verificationList.add(map(signatureVerification)); } - if (!consumerOptions.getCertificates().isEmpty()) { - if (verificationList.isEmpty()) { - throw new SOPGPException.NoSignature(); - } - } - SessionKey sessionKey = null; if (metadata.getSessionKey() != null) { org.pgpainless.util.SessionKey sk = metadata.getSessionKey(); From 07f9c3ceeffa7532d95a755cdd10f70bc9a95591 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 23 Jun 2022 10:30:16 +0200 Subject: [PATCH 0270/1204] Fix decrypt no signatures test --- .../pgpainless/sop/EncryptDecryptRoundTripTest.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java index 7bc5bfdb..52db6a62 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -166,18 +166,20 @@ public class EncryptDecryptRoundTripTest { } @Test - public void encrypt_decryptAndVerifyYieldsNoSignatureException() throws IOException { + public void encrypt_decryptAndVerifyYieldsNoVerifications() throws IOException { byte[] encrypted = sop.encrypt() .withCert(bobCert) .plaintext(message) .getBytes(); - assertThrows(SOPGPException.NoSignature.class, () -> sop - .decrypt() + DecryptionResult result = sop.decrypt() .withKey(bobKey) .verifyWithCert(aliceCert) .ciphertext(encrypted) - .toByteArrayAndResult()); + .toByteArrayAndResult() + .getResult(); + + assertTrue(result.getVerifications().isEmpty()); } @Test From 0c28c7a389024a013e53bf4813756f8220aedfc8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 23 Jun 2022 11:46:19 +0200 Subject: [PATCH 0271/1204] symmetrically encrypted messages are still encrypted --- .../org/pgpainless/decryption_verification/OpenPgpMetadata.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java index 2d9feb33..130ea554 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java @@ -84,7 +84,7 @@ public class OpenPgpMetadata { * @return true if encrypted, false otherwise */ public boolean isEncrypted() { - return sessionKey != null && sessionKey.getAlgorithm() != SymmetricKeyAlgorithm.NULL && !getRecipientKeyIds().isEmpty(); + return sessionKey != null && sessionKey.getAlgorithm() != SymmetricKeyAlgorithm.NULL; } /** From efc0cb357b44d393a487786cbed29cb0e4dfc5f7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 23 Jun 2022 11:47:20 +0200 Subject: [PATCH 0272/1204] EncryptDecryptRoundTripTest: make passphrase constant --- .../java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java index 52db6a62..6f7e3d47 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -28,6 +28,7 @@ public class EncryptDecryptRoundTripTest { private static final Charset utf8 = Charset.forName("UTF8"); private static SOP sop; private static byte[] aliceKey; + private static final String alicePassword = "wonderland.is.c00l"; private static byte[] aliceCert; private static byte[] bobKey; private static byte[] bobCert; @@ -38,7 +39,7 @@ public class EncryptDecryptRoundTripTest { sop = new SOPImpl(); aliceKey = sop.generateKey() .userId("Alice ") - .withKeyPassword("wonderland.is.c00l") + .withKeyPassword(alicePassword) .generate() .getBytes(); aliceCert = sop.extractCert() @@ -57,7 +58,7 @@ public class EncryptDecryptRoundTripTest { public void basicRoundTripWithKey() throws IOException, SOPGPException.KeyCannotSign { byte[] encrypted = sop.encrypt() .signWith(aliceKey) - .withKeyPassword("wonderland.is.c00l") + .withKeyPassword(alicePassword) .withCert(aliceCert) .withCert(bobCert) .plaintext(message) From 14531f00500ec55bebd15fe3b0a717165ea53568 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 23 Jun 2022 11:47:48 +0200 Subject: [PATCH 0273/1204] Make sop decrypt throw for unencrypted data --- .../java/org/pgpainless/sop/DecryptImpl.java | 4 ++++ .../sop/EncryptDecryptRoundTripTest.java | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index efed9472..7d8481e3 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -149,6 +149,10 @@ public class DecryptImpl implements Decrypt { decryptionStream.close(); OpenPgpMetadata metadata = decryptionStream.getResult(); + if (!metadata.isEncrypted()) { + throw new SOPGPException.BadData("Data is not encrypted."); + } + List verificationList = new ArrayList<>(); for (SignatureVerification signatureVerification : metadata.getVerifiedInbandSignatures()) { verificationList.add(map(signatureVerification)); diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java index 6f7e3d47..515cf71d 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -515,4 +515,22 @@ public class EncryptDecryptRoundTripTest { assertThrows(SOPGPException.BadData.class, () -> sop.decrypt().withSessionKey(wrongSessionKey).ciphertext(ciphertext)); } + + @Test + public void decryptNonEncryptedDataFailsBadData() throws IOException { + byte[] signed = sop.inlineSign() + .key(aliceKey) + .withKeyPassword(alicePassword) + .data(message) + .getBytes(); + + assertThrows(SOPGPException.BadData.class, () -> + sop.decrypt() + .verifyWithCert(aliceCert) + .withKey(aliceKey) + .withKeyPassword(alicePassword) + .ciphertext(signed) + .toByteArrayAndResult() + ); + } } From e0be145541a55feeff335ee5405dd9833e0ecd87 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 23 Jun 2022 11:57:28 +0200 Subject: [PATCH 0274/1204] Update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d31c8ff..c5240e67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.1-SNAPSHOT +- Fix reproducibility of builds by setting fixed file permissions in archive task +- Improve encryption performance by buffering streams +- Fix `OpenPgpMetadata.isEncrypted()` to also return true for symmetrically encrypted messages +- SOP changes + - decrypt: Do not throw `NoSignatures` if no signatures found + - decrypt: Throw `BadData` when ciphertext is not encrypted + ## 1.3.0 - Add `RevokedKeyException` - `KeyRingUtils.stripSecretKey()`: Disallow stripping of primary secret key From 9654af15e6d394942473d72b555069d81a12fac1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 23 Jun 2022 12:09:01 +0200 Subject: [PATCH 0275/1204] PGPainless 1.3.1 --- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 390e8771..271024b0 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.2.2' + implementation 'org.pgpainless:pgpainless-core:1.3.1' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 0ef67686..58c1e9c9 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.2.2" + implementation "org.pgpainless:pgpainless-sop:1.3.1" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.2.2 + 1.3.1 ... diff --git a/version.gradle b/version.gradle index b97d329f..97e1dfb2 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.1' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From 62ef8b8d5fe608b7e0ed265069c0d0ad72bb1c0f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 23 Jun 2022 12:10:58 +0200 Subject: [PATCH 0276/1204] PGPainless 1.3.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 97e1dfb2..178eb300 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.1' - isSnapshot = false + shortVersion = '1.3.2' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From d0ad0ac3e4eed3ecfeb58d2a4eab880a20b57dfd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 23 Jun 2022 12:12:42 +0200 Subject: [PATCH 0277/1204] Fix heading of changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5240e67..39f795af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.3.1-SNAPSHOT +## 1.3.1 - Fix reproducibility of builds by setting fixed file permissions in archive task - Improve encryption performance by buffering streams - Fix `OpenPgpMetadata.isEncrypted()` to also return true for symmetrically encrypted messages From 3f40fb99ef54534cec32a68a6c073b40c0a39026 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 24 Jun 2022 12:04:38 +0200 Subject: [PATCH 0278/1204] Add RevocationState enum --- .../pgpainless/algorithm/RevocationState.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java new file mode 100644 index 00000000..5a0fa142 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +public enum RevocationState { + + /** + * Certificate is not revoked. + */ + notRevoked, + + /** + * Certificate is revoked with a soft revocation. + */ + softRevoked, + + /** + * Certificate is revoked with a hard revocation. + */ + hardRevoked +} From 0c0f82ce2e00aa329a47fdb12fdcf007869973c5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 24 Jun 2022 12:28:10 +0200 Subject: [PATCH 0279/1204] Add KeyRingInfo constructor that takes Policy instance --- .../java/org/pgpainless/key/info/KeyRingInfo.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index b73087b4..9d761e38 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -86,8 +86,19 @@ public class KeyRingInfo { * @param validationDate date of validation */ public KeyRingInfo(PGPKeyRing keys, Date validationDate) { + this(keys, PGPainless.getPolicy(), validationDate); + } + + /** + * Evaluate the key ring at the provided validation date. + * + * @param keys key ring + * @param policy policy + * @param validationDate validation date + */ + public KeyRingInfo(PGPKeyRing keys, Policy policy, Date validationDate) { this.keys = keys; - this.signatures = new Signatures(keys, validationDate, PGPainless.getPolicy()); + this.signatures = new Signatures(keys, validationDate, policy); this.evaluationDate = validationDate; this.primaryUserId = findPrimaryUserId(); } From 7e0b1b344c1aee0af35a6b997b369f1a92ef1aa1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 24 Jun 2022 12:47:35 +0200 Subject: [PATCH 0280/1204] s/{validation|evaluation}Date/referenceTime/g --- .../org/pgpainless/key/info/KeyRingInfo.java | 20 +-- .../consumer/SignatureValidator.java | 136 +++++++++++------ .../signature/consumer/SignatureVerifier.java | 141 ++++++++++-------- 3 files changed, 179 insertions(+), 118 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 9d761e38..7999b592 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -56,7 +56,7 @@ public class KeyRingInfo { private final PGPKeyRing keys; private final Signatures signatures; - private final Date evaluationDate; + private final Date referenceDate; private final String primaryUserId; /** @@ -99,7 +99,7 @@ public class KeyRingInfo { public KeyRingInfo(PGPKeyRing keys, Policy policy, Date validationDate) { this.keys = keys; this.signatures = new Signatures(keys, validationDate, policy); - this.evaluationDate = validationDate; + this.referenceDate = validationDate; this.primaryUserId = findPrimaryUserId(); } @@ -413,7 +413,7 @@ public class KeyRingInfo { } if (certification.getHashedSubPackets().isPrimaryUserID()) { Date keyExpiration = SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(certification, keys.getPublicKey()); - if (keyExpiration != null && evaluationDate.after(keyExpiration)) { + if (keyExpiration != null && referenceDate.after(keyExpiration)) { return false; } } @@ -1097,9 +1097,9 @@ public class KeyRingInfo { private final Map subkeyRevocations; private final Map subkeyBindings; - public Signatures(PGPKeyRing keyRing, Date evaluationDate, Policy policy) { - primaryKeyRevocation = SignaturePicker.pickCurrentRevocationSelfSignature(keyRing, policy, evaluationDate); - primaryKeySelfSignature = SignaturePicker.pickLatestDirectKeySignature(keyRing, policy, evaluationDate); + public Signatures(PGPKeyRing keyRing, Date referenceDate, Policy policy) { + primaryKeyRevocation = SignaturePicker.pickCurrentRevocationSelfSignature(keyRing, policy, referenceDate); + primaryKeySelfSignature = SignaturePicker.pickLatestDirectKeySignature(keyRing, policy, referenceDate); userIdRevocations = new HashMap<>(); userIdCertifications = new HashMap<>(); subkeyRevocations = new HashMap<>(); @@ -1107,11 +1107,11 @@ public class KeyRingInfo { for (Iterator it = keyRing.getPublicKey().getUserIDs(); it.hasNext(); ) { String userId = it.next(); - PGPSignature revocation = SignaturePicker.pickCurrentUserIdRevocationSignature(keyRing, userId, policy, evaluationDate); + PGPSignature revocation = SignaturePicker.pickCurrentUserIdRevocationSignature(keyRing, userId, policy, referenceDate); if (revocation != null) { userIdRevocations.put(userId, revocation); } - PGPSignature certification = SignaturePicker.pickLatestUserIdCertificationSignature(keyRing, userId, policy, evaluationDate); + PGPSignature certification = SignaturePicker.pickLatestUserIdCertificationSignature(keyRing, userId, policy, referenceDate); if (certification != null) { userIdCertifications.put(userId, certification); } @@ -1121,11 +1121,11 @@ public class KeyRingInfo { keys.next(); // Skip primary key while (keys.hasNext()) { PGPPublicKey subkey = keys.next(); - PGPSignature subkeyRevocation = SignaturePicker.pickCurrentSubkeyBindingRevocationSignature(keyRing, subkey, policy, evaluationDate); + PGPSignature subkeyRevocation = SignaturePicker.pickCurrentSubkeyBindingRevocationSignature(keyRing, subkey, policy, referenceDate); if (subkeyRevocation != null) { subkeyRevocations.put(subkey.getKeyID(), subkeyRevocation); } - PGPSignature subkeyBinding = SignaturePicker.pickLatestSubkeyBindingSignature(keyRing, subkey, policy, evaluationDate); + PGPSignature subkeyBinding = SignaturePicker.pickLatestSubkeyBindingSignature(keyRing, subkey, policy, referenceDate); if (subkeyBinding != null) { subkeyBindings.put(subkey.getKeyID(), subkeyBinding); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java index 6a10e1f3..56614f4f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java @@ -44,14 +44,15 @@ public abstract class SignatureValidator { /** * Check, whether there is the possibility that the given signature was created by the given key. - * {@link #verify(PGPSignature)} throws a {@link SignatureValidationException} if we can say with certainty that the signature - * was not created by the given key (e.g. if the sig carries another issuer, issuer fingerprint packet). + * {@link #verify(PGPSignature)} throws a {@link SignatureValidationException} if we can say with certainty that + * the signature was not created by the given key (e.g. if the sig carries another issuer, issuer fingerprint packet). * * If there is no information found in the signature about who created it (no issuer, no fingerprint), * {@link #verify(PGPSignature)} will simply return since it is plausible that the given key created the sig. * * @param signingKey signing key - * @return validator that throws a {@link SignatureValidationException} if the signature was not possibly made by the given key. + * @return validator that throws a {@link SignatureValidationException} if the signature was not possibly made by + * the given key. */ public static SignatureValidator wasPossiblyMadeByKey(PGPPublicKey signingKey) { return new SignatureValidator() { @@ -62,14 +63,17 @@ public abstract class SignatureValidator { Long issuer = SignatureSubpacketsUtil.getIssuerKeyIdAsLong(signature); if (issuer != null) { if (issuer != signingKey.getKeyID()) { - throw new SignatureValidationException("Signature was not created by " + signingKeyFingerprint + " (signature issuer: " + Long.toHexString(issuer) + ")"); + throw new SignatureValidationException("Signature was not created by " + + signingKeyFingerprint + " (signature issuer: " + Long.toHexString(issuer) + ")"); } } - OpenPgpFingerprint fingerprint = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature); + OpenPgpFingerprint fingerprint = + SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature); if (fingerprint != null) { if (!fingerprint.equals(signingKeyFingerprint)) { - throw new SignatureValidationException("Signature was not created by " + signingKeyFingerprint + " (signature fingerprint: " + fingerprint + ")"); + throw new SignatureValidationException("Signature was not created by " + + signingKeyFingerprint + " (signature fingerprint: " + fingerprint + ")"); } } @@ -86,10 +90,12 @@ public abstract class SignatureValidator { * @param primaryKey primary key * @param subkey subkey * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return validator */ - public static SignatureValidator hasValidPrimaryKeyBindingSignatureIfRequired(PGPPublicKey primaryKey, PGPPublicKey subkey, Policy policy, Date validationDate) { + public static SignatureValidator hasValidPrimaryKeyBindingSignatureIfRequired(PGPPublicKey primaryKey, + PGPPublicKey subkey, Policy policy, + Date referenceDate) { return new SignatureValidator() { @Override public void verify(PGPSignature signature) throws SignatureValidationException { @@ -117,7 +123,7 @@ public abstract class SignatureValidator { try { signatureStructureIsAcceptable(subkey, policy).verify(embedded); - signatureIsEffective(validationDate).verify(embedded); + signatureIsEffective(referenceDate).verify(embedded); correctPrimaryKeyBindingSignature(primaryKey, subkey).verify(embedded); hasValidPrimaryKeyBinding = true; @@ -129,7 +135,9 @@ public abstract class SignatureValidator { } if (!hasValidPrimaryKeyBinding) { - throw new SignatureValidationException("Missing primary key binding signature on signing capable subkey " + Long.toHexString(subkey.getKeyID()), rejectedEmbeddedSigs); + throw new SignatureValidationException( + "Missing primary key binding signature on signing capable subkey " + + Long.toHexString(subkey.getKeyID()), rejectedEmbeddedSigs); } } catch (PGPException e) { throw new SignatureValidationException("Cannot process list of embedded signatures.", e); @@ -165,7 +173,8 @@ public abstract class SignatureValidator { * @param signingKey signing key * @return validator */ - private static SignatureValidator signatureUsesAcceptablePublicKeyAlgorithm(Policy policy, PGPPublicKey signingKey) { + private static SignatureValidator signatureUsesAcceptablePublicKeyAlgorithm(Policy policy, + PGPPublicKey signingKey) { return new SignatureValidator() { @Override public void verify(PGPSignature signature) throws SignatureValidationException { @@ -176,7 +185,8 @@ public abstract class SignatureValidator { } if (!policy.getPublicKeyAlgorithmPolicy().isAcceptable(algorithm, bitStrength)) { throw new SignatureValidationException("Signature was made using unacceptable key. " + - algorithm + " (" + bitStrength + " bits) is not acceptable according to the public key algorithm policy."); + algorithm + " (" + bitStrength + + " bits) is not acceptable according to the public key algorithm policy."); } } }; @@ -194,13 +204,16 @@ public abstract class SignatureValidator { public void verify(PGPSignature signature) throws SignatureValidationException { try { HashAlgorithm hashAlgorithm = HashAlgorithm.requireFromId(signature.getHashAlgorithm()); - Policy.HashAlgorithmPolicy hashAlgorithmPolicy = getHashAlgorithmPolicyForSignature(signature, policy); + Policy.HashAlgorithmPolicy hashAlgorithmPolicy = + getHashAlgorithmPolicyForSignature(signature, policy); if (!hashAlgorithmPolicy.isAcceptable(signature.getHashAlgorithm(), signature.getCreationTime())) { - throw new SignatureValidationException("Signature uses unacceptable hash algorithm " + hashAlgorithm + - " (Signature creation time: " + DateUtil.formatUTCDate(signature.getCreationTime()) + ")"); + throw new SignatureValidationException("Signature uses unacceptable hash algorithm " + + hashAlgorithm + " (Signature creation time: " + + DateUtil.formatUTCDate(signature.getCreationTime()) + ")"); } } catch (NoSuchElementException e) { - throw new SignatureValidationException("Signature uses unknown hash algorithm " + signature.getHashAlgorithm()); + throw new SignatureValidationException("Signature uses unknown hash algorithm " + + signature.getHashAlgorithm()); } } }; @@ -214,10 +227,12 @@ public abstract class SignatureValidator { * @param policy revocation policy for revocation sigs, normal policy for non-rev sigs * @return policy */ - private static Policy.HashAlgorithmPolicy getHashAlgorithmPolicyForSignature(PGPSignature signature, Policy policy) { + private static Policy.HashAlgorithmPolicy getHashAlgorithmPolicyForSignature(PGPSignature signature, + Policy policy) { SignatureType type = SignatureType.valueOf(signature.getSignatureType()); Policy.HashAlgorithmPolicy hashAlgorithmPolicy; - if (type == SignatureType.CERTIFICATION_REVOCATION || type == SignatureType.KEY_REVOCATION || type == SignatureType.SUBKEY_REVOCATION) { + if (type == SignatureType.CERTIFICATION_REVOCATION || type == SignatureType.KEY_REVOCATION || + type == SignatureType.SUBKEY_REVOCATION) { hashAlgorithmPolicy = policy.getRevocationSignatureHashAlgorithmPolicy(); } else { hashAlgorithmPolicy = policy.getSignatureHashAlgorithmPolicy(); @@ -241,7 +256,8 @@ public abstract class SignatureValidator { continue; } if (!registry.isKnownNotation(notation.getNotationName())) { - throw new SignatureValidationException("Signature contains unknown critical notation '" + notation.getNotationName() + "' in its hashed area."); + throw new SignatureValidationException("Signature contains unknown critical notation '" + + notation.getNotationName() + "' in its hashed area."); } } } @@ -262,7 +278,9 @@ public abstract class SignatureValidator { try { SignatureSubpacket.requireFromCode(criticalTag); } catch (NoSuchElementException e) { - throw new SignatureValidationException("Signature contains unknown critical subpacket of type " + Long.toHexString(criticalTag)); + throw new SignatureValidationException( + "Signature contains unknown critical subpacket of type " + + Long.toHexString(criticalTag)); } } } @@ -281,15 +299,15 @@ public abstract class SignatureValidator { /** * Verify that a signature is effective at the given reference date. * - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return validator */ - public static SignatureValidator signatureIsEffective(Date validationDate) { + public static SignatureValidator signatureIsEffective(Date referenceDate) { return new SignatureValidator() { @Override public void verify(PGPSignature signature) throws SignatureValidationException { - signatureIsAlreadyEffective(validationDate).verify(signature); - signatureIsNotYetExpired(validationDate).verify(signature); + signatureIsAlreadyEffective(referenceDate).verify(signature); + signatureIsNotYetExpired(referenceDate).verify(signature); } }; } @@ -297,10 +315,10 @@ public abstract class SignatureValidator { /** * Verify that a signature was created prior to the given reference date. * - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return validator */ - public static SignatureValidator signatureIsAlreadyEffective(Date validationDate) { + public static SignatureValidator signatureIsAlreadyEffective(Date referenceDate) { return new SignatureValidator() { @Override public void verify(PGPSignature signature) throws SignatureValidationException { @@ -310,8 +328,9 @@ public abstract class SignatureValidator { return; } - if (signatureCreationTime.after(validationDate)) { - throw new SignatureValidationException("Signature was created at " + signatureCreationTime + " and is therefore not yet valid at " + validationDate); + if (signatureCreationTime.after(referenceDate)) { + throw new SignatureValidationException("Signature was created at " + signatureCreationTime + + " and is therefore not yet valid at " + referenceDate); } } }; @@ -320,10 +339,10 @@ public abstract class SignatureValidator { /** * Verify that a signature is not yet expired. * - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return validator */ - public static SignatureValidator signatureIsNotYetExpired(Date validationDate) { + public static SignatureValidator signatureIsNotYetExpired(Date referenceDate) { return new SignatureValidator() { @Override public void verify(PGPSignature signature) throws SignatureValidationException { @@ -333,8 +352,9 @@ public abstract class SignatureValidator { } Date signatureExpirationTime = SignatureSubpacketsUtil.getSignatureExpirationTimeAsDate(signature); - if (signatureExpirationTime != null && signatureExpirationTime.before(validationDate)) { - throw new SignatureValidationException("Signature is already expired (expiration: " + signatureExpirationTime + ", validation: " + validationDate + ")"); + if (signatureExpirationTime != null && signatureExpirationTime.before(referenceDate)) { + throw new SignatureValidationException("Signature is already expired (expiration: " + + signatureExpirationTime + ", validation: " + referenceDate + ")"); } } }; @@ -375,7 +395,8 @@ public abstract class SignatureValidator { public void verify(PGPSignature signature) throws SignatureValidationException { SignatureCreationTime creationTime = SignatureSubpacketsUtil.getSignatureCreationTime(signature); if (creationTime == null) { - throw new SignatureValidationException("Malformed signature. Signature has no signature creation time subpacket in its hashed area."); + throw new SignatureValidationException( + "Malformed signature. Signature has no signature creation time subpacket in its hashed area."); } } }; @@ -405,7 +426,8 @@ public abstract class SignatureValidator { Date signatureCreationTime = signature.getCreationTime(); if (keyCreationTime.after(signatureCreationTime)) { - throw new SignatureValidationException("Signature predates key (key creation: " + keyCreationTime + ", signature creation: " + signatureCreationTime + ")"); + throw new SignatureValidationException("Signature predates key (key creation: " + + keyCreationTime + ", signature creation: " + signatureCreationTime + ")"); } } }; @@ -425,7 +447,8 @@ public abstract class SignatureValidator { return; } boolean predatesBindingSig = true; - Iterator bindingSignatures = signingKey.getSignaturesOfType(SignatureType.SUBKEY_BINDING.getCode()); + Iterator bindingSignatures = + signingKey.getSignaturesOfType(SignatureType.SUBKEY_BINDING.getCode()); if (!bindingSignatures.hasNext()) { throw new SignatureValidationException("Signing subkey does not have a subkey binding signature."); } @@ -436,7 +459,8 @@ public abstract class SignatureValidator { } } if (predatesBindingSig) { - throw new SignatureValidationException("Signature was created before the signing key was bound to the key ring."); + throw new SignatureValidationException( + "Signature was created before the signing key was bound to the key ring."); } } }; @@ -457,7 +481,8 @@ public abstract class SignatureValidator { throw new SignatureValidationException("Primary key cannot be its own subkey."); } try { - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), primaryKey); + signature.init(ImplementationFactory.getInstance() + .getPGPContentVerifierBuilderProvider(), primaryKey); boolean valid = signature.verifyCertification(primaryKey, subkey); if (!valid) { throw new SignatureValidationException("Signature is not correct."); @@ -487,7 +512,8 @@ public abstract class SignatureValidator { throw new SignatureValidationException("Primary Key Binding Signature is not correct."); } } catch (PGPException | ClassCastException e) { - throw new SignatureValidationException("Cannot verify primary key binding signature correctness", e); + throw new SignatureValidationException( + "Cannot verify primary key binding signature correctness", e); } } }; @@ -554,7 +580,8 @@ public abstract class SignatureValidator { } } if (!valid) { - throw new SignatureValidationException("Signature is of type " + type + " while only " + Arrays.toString(signatureTypes) + " are allowed here."); + throw new SignatureValidationException("Signature is of type " + type + " while only " + + Arrays.toString(signatureTypes) + " are allowed here."); } } }; @@ -568,18 +595,22 @@ public abstract class SignatureValidator { * @param certifyingKey key that created the signature. * @return validator */ - public static SignatureValidator correctSignatureOverUserId(String userId, PGPPublicKey certifiedKey, PGPPublicKey certifyingKey) { + public static SignatureValidator correctSignatureOverUserId(String userId, PGPPublicKey certifiedKey, + PGPPublicKey certifyingKey) { return new SignatureValidator() { @Override public void verify(PGPSignature signature) throws SignatureValidationException { try { - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), certifyingKey); + signature.init(ImplementationFactory.getInstance() + .getPGPContentVerifierBuilderProvider(), certifyingKey); boolean valid = signature.verifyCertification(userId, certifiedKey); if (!valid) { - throw new SignatureValidationException("Signature over user-id '" + userId + "' is not correct."); + throw new SignatureValidationException("Signature over user-id '" + userId + + "' is not correct."); } } catch (PGPException | ClassCastException e) { - throw new SignatureValidationException("Cannot verify signature over user-id '" + userId + "'.", e); + throw new SignatureValidationException("Cannot verify signature over user-id '" + + userId + "'.", e); } } }; @@ -593,12 +624,15 @@ public abstract class SignatureValidator { * @param certifyingKey key that created the certification signature * @return validator */ - public static SignatureValidator correctSignatureOverUserAttributes(PGPUserAttributeSubpacketVector userAttributes, PGPPublicKey certifiedKey, PGPPublicKey certifyingKey) { + public static SignatureValidator correctSignatureOverUserAttributes(PGPUserAttributeSubpacketVector userAttributes, + PGPPublicKey certifiedKey, + PGPPublicKey certifyingKey) { return new SignatureValidator() { @Override public void verify(PGPSignature signature) throws SignatureValidationException { try { - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), certifyingKey); + signature.init(ImplementationFactory.getInstance() + .getPGPContentVerifierBuilderProvider(), certifyingKey); boolean valid = signature.verifyCertification(userAttributes, certifiedKey); if (!valid) { throw new SignatureValidationException("Signature over user-attribute vector is not correct."); @@ -616,12 +650,16 @@ public abstract class SignatureValidator { public void verify(PGPSignature signature) throws SignatureValidationException { Date timestamp = signature.getCreationTime(); if (notBefore != null && timestamp.before(notBefore)) { - throw new SignatureValidationException("Signature was made before the earliest allowed signature creation time. Created: " + - DateUtil.formatUTCDate(timestamp) + " Earliest allowed: " + DateUtil.formatUTCDate(notBefore)); + throw new SignatureValidationException( + "Signature was made before the earliest allowed signature creation time. Created: " + + DateUtil.formatUTCDate(timestamp) + " Earliest allowed: " + + DateUtil.formatUTCDate(notBefore)); } if (notAfter != null && timestamp.after(notAfter)) { - throw new SignatureValidationException("Signature was made after the latest allowed signature creation time. Created: " + - DateUtil.formatUTCDate(timestamp) + " Latest allowed: " + DateUtil.formatUTCDate(notAfter)); + throw new SignatureValidationException( + "Signature was made after the latest allowed signature creation time. Created: " + + DateUtil.formatUTCDate(timestamp) + " Latest allowed: " + + DateUtil.formatUTCDate(notAfter)); } } }; diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java index d4a61271..c4565197 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java @@ -36,12 +36,13 @@ public final class SignatureVerifier { * @param signingKey key that created the certification * @param keyWithUserId key carrying the user-id * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if signature verification is successful * * @throws SignatureValidationException if signature verification fails for some reason */ - public static boolean verifySignatureOverUserId(String userId, PGPSignature signature, PGPPublicKey signingKey, PGPPublicKey keyWithUserId, Policy policy, Date validationDate) + public static boolean verifySignatureOverUserId(String userId, PGPSignature signature, PGPPublicKey signingKey, + PGPPublicKey keyWithUserId, Policy policy, Date referenceDate) throws SignatureValidationException { SignatureType type = SignatureType.valueOf(signature.getSignatureType()); switch (type) { @@ -49,9 +50,9 @@ public final class SignatureVerifier { case NO_CERTIFICATION: case CASUAL_CERTIFICATION: case POSITIVE_CERTIFICATION: - return verifyUserIdCertification(userId, signature, signingKey, keyWithUserId, policy, validationDate); + return verifyUserIdCertification(userId, signature, signingKey, keyWithUserId, policy, referenceDate); case CERTIFICATION_REVOCATION: - return verifyUserIdRevocation(userId, signature, signingKey, keyWithUserId, policy, validationDate); + return verifyUserIdRevocation(userId, signature, signingKey, keyWithUserId, policy, referenceDate); default: throw new SignatureValidationException("Signature is not a valid user-id certification/revocation signature: " + type); } @@ -64,14 +65,15 @@ public final class SignatureVerifier { * @param signature certification signature * @param primaryKey primary key * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if the self-signature is verified successfully * * @throws SignatureValidationException if signature verification fails for some reason */ - public static boolean verifyUserIdCertification(String userId, PGPSignature signature, PGPPublicKey primaryKey, Policy policy, Date validationDate) + public static boolean verifyUserIdCertification(String userId, PGPSignature signature, PGPPublicKey primaryKey, + Policy policy, Date referenceDate) throws SignatureValidationException { - return verifyUserIdCertification(userId, signature, primaryKey, primaryKey, policy, validationDate); + return verifyUserIdCertification(userId, signature, primaryKey, primaryKey, policy, referenceDate); } /** @@ -82,17 +84,18 @@ public final class SignatureVerifier { * @param signingKey key that created the certification * @param keyWithUserId primary key that carries the user-id * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if signature verification is successful * * @throws SignatureValidationException if signature verification fails for some reason */ - public static boolean verifyUserIdCertification(String userId, PGPSignature signature, PGPPublicKey signingKey, PGPPublicKey keyWithUserId, Policy policy, Date validationDate) + public static boolean verifyUserIdCertification(String userId, PGPSignature signature, PGPPublicKey signingKey, + PGPPublicKey keyWithUserId, Policy policy, Date referenceDate) throws SignatureValidationException { SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); SignatureValidator.signatureIsCertification().verify(signature); SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); + SignatureValidator.signatureIsEffective(referenceDate).verify(signature); SignatureValidator.correctSignatureOverUserId(userId, keyWithUserId, signingKey).verify(signature); return true; @@ -105,14 +108,15 @@ public final class SignatureVerifier { * @param signature user-id revocation signature * @param primaryKey primary key * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if the user-id revocation signature is successfully verified * * @throws SignatureValidationException if signature verification fails for some reason */ - public static boolean verifyUserIdRevocation(String userId, PGPSignature signature, PGPPublicKey primaryKey, Policy policy, Date validationDate) + public static boolean verifyUserIdRevocation(String userId, PGPSignature signature, PGPPublicKey primaryKey, + Policy policy, Date referenceDate) throws SignatureValidationException { - return verifyUserIdRevocation(userId, signature, primaryKey, primaryKey, policy, validationDate); + return verifyUserIdRevocation(userId, signature, primaryKey, primaryKey, policy, referenceDate); } /** @@ -123,17 +127,18 @@ public final class SignatureVerifier { * @param signingKey key that created the revocation signature * @param keyWithUserId primary key carrying the user-id * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if the user-id revocation signature is successfully verified * * @throws SignatureValidationException if signature verification fails for some reason */ - public static boolean verifyUserIdRevocation(String userId, PGPSignature signature, PGPPublicKey signingKey, PGPPublicKey keyWithUserId, Policy policy, Date validationDate) + public static boolean verifyUserIdRevocation(String userId, PGPSignature signature, PGPPublicKey signingKey, + PGPPublicKey keyWithUserId, Policy policy, Date referenceDate) throws SignatureValidationException { SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); SignatureValidator.signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature); SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); + SignatureValidator.signatureIsEffective(referenceDate).verify(signature); SignatureValidator.correctSignatureOverUserId(userId, keyWithUserId, signingKey).verify(signature); return true; @@ -146,16 +151,17 @@ public final class SignatureVerifier { * @param signature certification self-signature * @param primaryKey primary key that carries the user-attributes * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if the signature can be verified successfully * * @throws SignatureValidationException if signature verification fails for some reason */ public static boolean verifyUserAttributesCertification(PGPUserAttributeSubpacketVector userAttributes, PGPSignature signature, PGPPublicKey primaryKey, - Policy policy, Date validationDate) + Policy policy, Date referenceDate) throws SignatureValidationException { - return verifyUserAttributesCertification(userAttributes, signature, primaryKey, primaryKey, policy, validationDate); + return verifyUserAttributesCertification(userAttributes, signature, primaryKey, primaryKey, policy, + referenceDate); } /** @@ -166,7 +172,7 @@ public final class SignatureVerifier { * @param signingKey key that created the user-attributes certification * @param keyWithUserAttributes key that carries the user-attributes certification * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if the signature can be verified successfully * * @throws SignatureValidationException if signature verification fails for some reason @@ -174,13 +180,14 @@ public final class SignatureVerifier { public static boolean verifyUserAttributesCertification(PGPUserAttributeSubpacketVector userAttributes, PGPSignature signature, PGPPublicKey signingKey, PGPPublicKey keyWithUserAttributes, Policy policy, - Date validationDate) + Date referenceDate) throws SignatureValidationException { SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); SignatureValidator.signatureIsCertification().verify(signature); SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); - SignatureValidator.correctSignatureOverUserAttributes(userAttributes, keyWithUserAttributes, signingKey).verify(signature); + SignatureValidator.signatureIsEffective(referenceDate).verify(signature); + SignatureValidator.correctSignatureOverUserAttributes(userAttributes, keyWithUserAttributes, signingKey) + .verify(signature); return true; } @@ -192,16 +199,16 @@ public final class SignatureVerifier { * @param signature user-attributes revocation signature * @param primaryKey primary key that carries the user-attributes * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if the revocation signature can be verified successfully * * @throws SignatureValidationException if signature verification fails for some reason */ public static boolean verifyUserAttributesRevocation(PGPUserAttributeSubpacketVector userAttributes, PGPSignature signature, PGPPublicKey primaryKey, - Policy policy, Date validationDate) + Policy policy, Date referenceDate) throws SignatureValidationException { - return verifyUserAttributesRevocation(userAttributes, signature, primaryKey, primaryKey, policy, validationDate); + return verifyUserAttributesRevocation(userAttributes, signature, primaryKey, primaryKey, policy, referenceDate); } /** @@ -212,7 +219,7 @@ public final class SignatureVerifier { * @param signingKey revocation key * @param keyWithUserAttributes key that carries the user-attributes * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if the revocation signature can be verified successfully * * @throws SignatureValidationException if signature verification fails for some reason @@ -220,13 +227,14 @@ public final class SignatureVerifier { public static boolean verifyUserAttributesRevocation(PGPUserAttributeSubpacketVector userAttributes, PGPSignature signature, PGPPublicKey signingKey, PGPPublicKey keyWithUserAttributes, Policy policy, - Date validationDate) + Date referenceDate) throws SignatureValidationException { SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); SignatureValidator.signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature); SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); - SignatureValidator.correctSignatureOverUserAttributes(userAttributes, keyWithUserAttributes, signingKey).verify(signature); + SignatureValidator.signatureIsEffective(referenceDate).verify(signature); + SignatureValidator.correctSignatureOverUserAttributes(userAttributes, keyWithUserAttributes, signingKey) + .verify(signature); return true; } @@ -238,18 +246,20 @@ public final class SignatureVerifier { * @param primaryKey primary key * @param subkey subkey * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if the binding signature can be verified successfully * * @throws SignatureValidationException if signature verification fails for some reason */ - public static boolean verifySubkeyBindingSignature(PGPSignature signature, PGPPublicKey primaryKey, PGPPublicKey subkey, Policy policy, Date validationDate) + public static boolean verifySubkeyBindingSignature(PGPSignature signature, PGPPublicKey primaryKey, + PGPPublicKey subkey, Policy policy, Date referenceDate) throws SignatureValidationException { SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_BINDING).verify(signature); SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); SignatureValidator.signatureDoesNotPredateSignee(subkey).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); - SignatureValidator.hasValidPrimaryKeyBindingSignatureIfRequired(primaryKey, subkey, policy, validationDate).verify(signature); + SignatureValidator.signatureIsEffective(referenceDate).verify(signature); + SignatureValidator.hasValidPrimaryKeyBindingSignatureIfRequired(primaryKey, subkey, policy, referenceDate) + .verify(signature); SignatureValidator.correctSubkeyBindingSignature(primaryKey, subkey).verify(signature); return true; @@ -262,16 +272,18 @@ public final class SignatureVerifier { * @param primaryKey primary key * @param subkey subkey * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if the subkey revocation signature can be verified successfully * * @throws SignatureValidationException if signature verification fails for some reason */ - public static boolean verifySubkeyBindingRevocation(PGPSignature signature, PGPPublicKey primaryKey, PGPPublicKey subkey, Policy policy, Date validationDate) throws SignatureValidationException { + public static boolean verifySubkeyBindingRevocation(PGPSignature signature, PGPPublicKey primaryKey, + PGPPublicKey subkey, Policy policy, Date referenceDate) + throws SignatureValidationException { SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_REVOCATION).verify(signature); SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); SignatureValidator.signatureDoesNotPredateSignee(subkey).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); + SignatureValidator.signatureIsEffective(referenceDate).verify(signature); SignatureValidator.correctSignatureOverKey(primaryKey, subkey).verify(signature); return true; @@ -283,14 +295,15 @@ public final class SignatureVerifier { * @param signature signature * @param primaryKey primary key * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if the signature can be verified successfully * * @throws SignatureValidationException if signature verification fails for some reason */ - public static boolean verifyDirectKeySignature(PGPSignature signature, PGPPublicKey primaryKey, Policy policy, Date validationDate) + public static boolean verifyDirectKeySignature(PGPSignature signature, PGPPublicKey primaryKey, + Policy policy, Date referenceDate) throws SignatureValidationException { - return verifyDirectKeySignature(signature, primaryKey, primaryKey, policy, validationDate); + return verifyDirectKeySignature(signature, primaryKey, primaryKey, policy, referenceDate); } /** @@ -300,17 +313,18 @@ public final class SignatureVerifier { * @param signingKey signing key * @param signedKey signed key * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if signature verification is successful * * @throws SignatureValidationException if signature verification fails for some reason */ - public static boolean verifyDirectKeySignature(PGPSignature signature, PGPPublicKey signingKey, PGPPublicKey signedKey, Policy policy, Date validationDate) + public static boolean verifyDirectKeySignature(PGPSignature signature, PGPPublicKey signingKey, + PGPPublicKey signedKey, Policy policy, Date referenceDate) throws SignatureValidationException { SignatureValidator.signatureIsOfType(SignatureType.DIRECT_KEY).verify(signature); SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); SignatureValidator.signatureDoesNotPredateSignee(signedKey).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); + SignatureValidator.signatureIsEffective(referenceDate).verify(signature); SignatureValidator.correctSignatureOverKey(signingKey, signedKey).verify(signature); return true; @@ -322,16 +336,17 @@ public final class SignatureVerifier { * @param signature signature * @param primaryKey primary key * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if signature verification is successful * * @throws SignatureValidationException if signature verification fails for some reason */ - public static boolean verifyKeyRevocationSignature(PGPSignature signature, PGPPublicKey primaryKey, Policy policy, Date validationDate) + public static boolean verifyKeyRevocationSignature(PGPSignature signature, PGPPublicKey primaryKey, + Policy policy, Date referenceDate) throws SignatureValidationException { SignatureValidator.signatureIsOfType(SignatureType.KEY_REVOCATION).verify(signature); SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); + SignatureValidator.signatureIsEffective(referenceDate).verify(signature); SignatureValidator.correctSignatureOverKey(primaryKey, primaryKey).verify(signature); return true; @@ -344,14 +359,16 @@ public final class SignatureVerifier { * @param signedData input stream containing the signed data * @param signingKey the key that created the signature * @param policy policy - * @param validationDate reference date of signature verification + * @param referenceDate reference date of signature verification * @return true if the signature is successfully verified * * @throws SignatureValidationException if the signature verification fails for some reason */ - public static boolean verifyUninitializedSignature(PGPSignature signature, InputStream signedData, PGPPublicKey signingKey, Policy policy, Date validationDate) throws SignatureValidationException { + public static boolean verifyUninitializedSignature(PGPSignature signature, InputStream signedData, + PGPPublicKey signingKey, Policy policy, Date referenceDate) + throws SignatureValidationException { initializeSignatureAndUpdateWithSignedData(signature, signedData, signingKey); - return verifyInitializedSignature(signature, signingKey, policy, validationDate); + return verifyInitializedSignature(signature, signingKey, policy, referenceDate); } /** @@ -363,7 +380,8 @@ public final class SignatureVerifier { * * @throws SignatureValidationException in case the signature cannot be verified for some reason */ - public static void initializeSignatureAndUpdateWithSignedData(PGPSignature signature, InputStream signedData, PGPPublicKey signingKey) + public static void initializeSignatureAndUpdateWithSignedData(PGPSignature signature, InputStream signedData, + PGPPublicKey signingKey) throws SignatureValidationException { try { signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signingKey); @@ -399,16 +417,17 @@ public final class SignatureVerifier { * @param signature OpenPGP signature * @param signingKey key that created the signature * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if signature is verified successfully * * @throws SignatureValidationException if signature verification fails for some reason */ - public static boolean verifyInitializedSignature(PGPSignature signature, PGPPublicKey signingKey, Policy policy, Date validationDate) + public static boolean verifyInitializedSignature(PGPSignature signature, PGPPublicKey signingKey, Policy policy, + Date referenceDate) throws SignatureValidationException { SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(validationDate).verify(signature); + SignatureValidator.signatureIsEffective(referenceDate).verify(signature); try { if (!signature.verify()) { @@ -420,7 +439,8 @@ public final class SignatureVerifier { } } - public static boolean verifyOnePassSignature(PGPSignature signature, PGPPublicKey signingKey, OnePassSignatureCheck onePassSignature, Policy policy) + public static boolean verifyOnePassSignature(PGPSignature signature, PGPPublicKey signingKey, + OnePassSignatureCheck onePassSignature, Policy policy) throws SignatureValidationException { try { SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); @@ -435,10 +455,12 @@ public final class SignatureVerifier { throw new IllegalStateException("No comparison signature provided."); } if (!onePassSignature.getOnePassSignature().verify(signature)) { - throw new SignatureValidationException("Bad signature of key " + Long.toHexString(signingKey.getKeyID())); + throw new SignatureValidationException("Bad signature of key " + + Long.toHexString(signingKey.getKeyID())); } } catch (PGPException e) { - throw new SignatureValidationException("Could not verify correctness of One-Pass-Signature: " + e.getMessage(), e); + throw new SignatureValidationException("Could not verify correctness of One-Pass-Signature: " + + e.getMessage(), e); } return true; @@ -451,13 +473,14 @@ public final class SignatureVerifier { * @param signature self-signature * @param primaryKey primary key that created the signature * @param policy policy - * @param validationDate reference date for signature verification + * @param referenceDate reference date for signature verification * @return true if the signature is successfully verified * * @throws SignatureValidationException if signature verification fails for some reason */ - public static boolean verifySignatureOverUserId(String userId, PGPSignature signature, PGPPublicKey primaryKey, Policy policy, Date validationDate) + public static boolean verifySignatureOverUserId(String userId, PGPSignature signature, PGPPublicKey primaryKey, + Policy policy, Date referenceDate) throws SignatureValidationException { - return verifySignatureOverUserId(userId, signature, primaryKey, primaryKey, policy, validationDate); + return verifySignatureOverUserId(userId, signature, primaryKey, primaryKey, policy, referenceDate); } } From b2a5351cc3c757b6e6366525b74a2f875c87cfa4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 29 Jun 2022 16:00:21 +0200 Subject: [PATCH 0281/1204] Delete unused KeyRingValidator class --- .../org/pgpainless/key/KeyRingValidator.java | 146 -------- .../pgpainless/key/KeyRingValidatorTest.java | 324 ------------------ .../signature/KeyRingValidationTest.java | 125 ------- 3 files changed, 595 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/KeyRingValidator.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/KeyRingValidatorTest.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/signature/KeyRingValidationTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/KeyRingValidator.java b/pgpainless-core/src/main/java/org/pgpainless/key/KeyRingValidator.java deleted file mode 100644 index c3355ff8..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/KeyRingValidator.java +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.consumer.SignatureCreationDateComparator; -import org.pgpainless.signature.consumer.SignatureVerifier; -import org.pgpainless.util.CollectionUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class KeyRingValidator { - - private KeyRingValidator() { - - } - - private static final Logger LOGGER = LoggerFactory.getLogger(KeyRingValidator.class); - - public static R validate(R keyRing, Policy policy) { - try { - return validate(keyRing, policy, new Date()); - } catch (PGPException e) { - return null; - } - } - - public static R validate(R keyRing, Policy policy, Date validationDate) throws PGPException { - return getKeyRingAtDate(keyRing, policy, validationDate); - } - - private static R getKeyRingAtDate(R keyRing, Policy policy, Date validationDate) throws PGPException { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - primaryKey = evaluatePrimaryKey(primaryKey, policy, validationDate); - if (keyRing instanceof PGPPublicKeyRing) { - PGPPublicKeyRing publicKeys = (PGPPublicKeyRing) keyRing; - publicKeys = PGPPublicKeyRing.insertPublicKey(publicKeys, primaryKey); - keyRing = (R) publicKeys; - } - - return keyRing; - } - - private static PGPPublicKey evaluatePrimaryKey(PGPPublicKey primaryKey, Policy policy, Date validationDate) throws PGPException { - - PGPPublicKey blank = new PGPPublicKey(primaryKey.getPublicKeyPacket(), ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - - Iterator directKeyIterator = primaryKey.getSignaturesOfType(SignatureType.DIRECT_KEY.getCode()); - List directKeyCertifications = CollectionUtils.iteratorToList(directKeyIterator); - Collections.sort(directKeyCertifications, new SignatureCreationDateComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - for (PGPSignature signature : directKeyCertifications) { - try { - if (SignatureVerifier.verifyDirectKeySignature(signature, blank, policy, validationDate)) { - blank = PGPPublicKey.addCertification(blank, signature); - } - } catch (SignatureValidationException e) { - LOGGER.debug("Rejecting direct key signature: {}", e.getMessage(), e); - } - } - - Iterator revocationIterator = primaryKey.getSignaturesOfType(SignatureType.KEY_REVOCATION.getCode()); - List directKeyRevocations = CollectionUtils.iteratorToList(revocationIterator); - Collections.sort(directKeyRevocations, new SignatureCreationDateComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - for (PGPSignature signature : directKeyRevocations) { - try { - if (SignatureVerifier.verifyKeyRevocationSignature(signature, primaryKey, policy, validationDate)) { - blank = PGPPublicKey.addCertification(blank, signature); - } - } catch (SignatureValidationException e) { - LOGGER.debug("Rejecting key revocation signature: {}", e.getMessage(), e); - } - } - - Iterator userIdIterator = primaryKey.getUserIDs(); - while (userIdIterator.hasNext()) { - String userId = userIdIterator.next(); - Iterator userIdSigs = primaryKey.getSignaturesForID(userId); - List signatures = CollectionUtils.iteratorToList(userIdSigs); - Collections.sort(signatures, new SignatureCreationDateComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - for (PGPSignature signature : signatures) { - if (signature.getKeyID() != primaryKey.getKeyID()) { - // Signature was not made by primary key - continue; - } - try { - if (SignatureType.valueOf(signature.getSignatureType()) == SignatureType.CERTIFICATION_REVOCATION) { - if (SignatureVerifier.verifyUserIdRevocation(userId, signature, primaryKey, policy, validationDate)) { - blank = PGPPublicKey.addCertification(blank, userId, signature); - } - } else { - if (SignatureVerifier.verifyUserIdCertification(userId, signature, primaryKey, policy, validationDate)) { - blank = PGPPublicKey.addCertification(blank, userId, signature); - } - } - } catch (SignatureValidationException e) { - LOGGER.debug("Rejecting user-id certification for user-id {}: {}", userId, e.getMessage(), e); - } - } - } - - Iterator userAttributes = primaryKey.getUserAttributes(); - while (userAttributes.hasNext()) { - PGPUserAttributeSubpacketVector userAttribute = userAttributes.next(); - Iterator userAttributeSignatureIterator = primaryKey.getSignaturesForUserAttribute(userAttribute); - while (userAttributeSignatureIterator.hasNext()) { - PGPSignature signature = userAttributeSignatureIterator.next(); - if (signature.getKeyID() != primaryKey.getKeyID()) { - // Signature was not made by primary key - continue; - } - try { - if (SignatureType.valueOf(signature.getSignatureType()) == SignatureType.CERTIFICATION_REVOCATION) { - if (SignatureVerifier.verifyUserAttributesRevocation(userAttribute, signature, primaryKey, policy, validationDate)) { - blank = PGPPublicKey.addCertification(blank, userAttribute, signature); - } - } else { - if (SignatureVerifier.verifyUserAttributesCertification(userAttribute, signature, primaryKey, policy, validationDate)) { - blank = PGPPublicKey.addCertification(blank, userAttribute, signature); - } - } - } catch (SignatureValidationException e) { - LOGGER.debug("Rejecting user-attribute signature: {}", e.getMessage(), e); - } - } - } - - return blank; - } - -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/KeyRingValidatorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/KeyRingValidatorTest.java deleted file mode 100644 index 7d5c0520..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/key/KeyRingValidatorTest.java +++ /dev/null @@ -1,324 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Date; -import java.util.Iterator; -import java.util.Random; - -import org.bouncycastle.bcpg.attr.ImageAttribute; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVectorGenerator; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.util.ArmorUtils; -import org.pgpainless.util.CollectionUtils; -import org.pgpainless.util.DateUtil; - -public class KeyRingValidatorTest { - - @Test - public void testRevokedSubkey() throws IOException { - String key = "-----BEGIN PGP ARMORED FILE-----\n" + - "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + - "\n" + - "xsBNBFpJegABCAC1ePFquP0135m8DYhcybhv7l+ecojitFOd/jRM7hCczIqKgalD\n" + - "1Ro1gNr3VmH6FjRIKIvGT+sOzCKne1v3KyAAPoxtwxjkATTKdOGo15I6v5ZjmO1d\n" + - "rLQOLSt1TF7XbQSt+ns6PUZWJL907DvECUU5b9FkNUqfQ14QqY+gi7MOyAQez3b7\n" + - "Pg5Cyz/kVWQ6TSMW/myDEDEertQ4rDBsptEDFHCC2+iF4hO2LqfiCriu5qyLcKCQ\n" + - "pd6dEuwJQ/jjT0D9A9Fwf+i04x6ZPKSU9oNAWqn8OSAq3/0B/hu9V+0U0iHPnJxe\n" + - "quykvJk7maxhiGhxBWYXTvDJmoon0NOles7LABEBAAHCwHwEHwEKAA8Fgl4L4QAC\n" + - "FQoCmwMCHgEAIQkQaE+tYtwDj7sWIQTy0VCk/piSXVHpFTloT61i3AOPu8ffB/9Q\n" + - "60dg60qhA2rPnd/1dCL2B+c8RWnq44PpijE3gA1RQvcRQE5jNzMSo/MnG0mSL5wH\n" + - "eTsjSd/DRI3nHP06rs6Qub11NoKhNuya3maz9gyzeZMc/jNib83/BzFCrxsSQm+9\n" + - "WHurxXeWXOPMLZs3xS/jG0EDtCJ2Fm4UF19fcIydwN/ssF4NGpfCY82+wTSx4joI\n" + - "3cRKObCFJaaBgG5nl+eFr7cfjEIuqCJCaQsXiqBe7d6V3KqN18t+CgSaybMZXcys\n" + - "Q/USxEkLhIB2pOZwcz4E3TTFgxRAxcr4cs4Bd2PRz3Z5FKTzo0ma/Ft0UfFJR+fC\n" + - "cs55+n6kC9K0y/E7BY2hwsB8BB8BCgAPBYJaSXoAAhUKApsDAh4BACEJEGhPrWLc\n" + - "A4+7FiEE8tFQpP6Ykl1R6RU5aE+tYtwDj7uqDQf7BqTD6GNTwXPOt/0kHQPYmbdI\n" + - "tX+pWP+o3jaB6VTHDXcn27bttA5M82EXZfae4+bC1dMB+1uLal4ciVgO9ImJC9Nw\n" + - "s5fc3JH4R5uuSvpjzjudkJsGu3cAKE3hwiT93Mi6t6ENpLCDSxqxzAmfoOQbVJYW\n" + - "Y7gP7Z4Cj0IAP29aprEc0JWoMjHKpKgYF6u0sWgHWBuEXk/6o6GYb2HZYK4ycpY2\n" + - "WXKgVhy7/iQDYO1FOfcWQXHVGLn8OzILjobKohNenTT20ZhAASi3LUDSDMTQfxSS\n" + - "Vt0nhzWuXJJ4R8PzUVeRJ0A0oMyjZVHivHC6GwMsiQuSUTx8e/GnOByOqfGne80S\n" + - "anVsaWV0QGV4YW1wbGUub3JnwsBzBBMBCgAGBYJaSXoAACEJEGhPrWLcA4+7FiEE\n" + - "8tFQpP6Ykl1R6RU5aE+tYtwDj7tDfQf+PnxsIFu/0juKBUjjtAYfRzkrrYtMepPj\n" + - "taTvGfo1SzUkX/6F/GjdSeVg5Iq6YcBrj8c+cB3EoZpHnScTgWQHwceWQLd9Hhbg\n" + - "TrUNvW1eg2CVzN0RBuYMtWu9JM4pH7ssJW1NmN+/N9B67qb2y+JfBwH/la508NzC\n" + - "rl3xWTxjT5wNy+FGkNZg23s/0qlO2uxCjc+mRAuAlp5EmTOVWOIBbM0xttjBOx39\n" + - "ZmWWQKJZ0nrFjK1jppHqazwWWNX7RHkK81tlbSUtOPoTIJDz38NaiyMcZH3p9okN\n" + - "3DU4XtF+oE18M+Z/E0xUQmumbkajFzcUjmd7enozP5BnGESzdNS5Xc7ATQRaSsuA\n" + - "AQgAykb8tqlWXtqHGGkBqAq3EnpmvBqrKvqejjtZKAXqEszJ9NlibCGUuLwnNOVO\n" + - "R/hcOUlOGH+cyMcApBWJB+7d/83K1eCCdv88nDFVav7hKLKlEBbZJNHgHpJ313pl\n" + - "etzCR4x3STEISrEtO71l2HBdrKSYXaxGgILxYwcSi3i2EjzxRDy+0zyy8s7d+OD5\n" + - "ShFYexgSrKH3Xx1cxQAJzGGJVx75HHU9GVh3xHwJ7nDm26KzHegG2XPIBXJ2z8vm\n" + - "sSVTWyj0AjT4kVVapN0f84AKKjyQ7fguCzXGHFV9jmxDx+YH+9HhjIrHSzbDx6+4\n" + - "wyRsxj7Su+hu/bogJ28nnbTzQwARAQABwsCTBCgBCgAmBYJcKq2AHx3IVW5rbm93\n" + - "biByZXZvY2F0aW9uIHJlYXNvbiAyMDAAIQkQaE+tYtwDj7sWIQTy0VCk/piSXVHp\n" + - "FTloT61i3AOPu6RDCACgqNPoLWPsjWDyZxvF8MyYTB3JivI7RVf8W6mNJTxMDD69\n" + - "iWwiC0F6R8M3ljk8vc85C6tQ8iWPVT6cGHhFgQn14a1MYpgyVTTdwjbqvjxmPeyS\n" + - "We31yZGz54dAsONnrWScO4ZdKVTtKhu115KELiPmguoN/JwG+OIbgvKvzQX+8D4M\n" + - "Gl823A6Ua8/zJm/TAOQolo6X9Sqr9bO1v/z3ecuYkuNeGhQOC3/VQ0TH2xRbmykD\n" + - "5XbgffPi0sjg2ZRrDikg/W+40gxW+oHxQ6ZIaIn/OFooj7xooH+jn++f8W8faEk5\n" + - "pLOoCwsX0SucDbGvt85D1DhOUD9H0CEkaZbO+113wsGsBBgBCgAJBYJeC+EAApsC\n" + - "AVcJEGhPrWLcA4+7wHSgBBkBCgAGBYJeC+EAACEJEEpyNKOhITplFiEEUXksDkji\n" + - "/alOk7kRSnI0o6EhOmWnSQgAiu/zdEmHf6Wbwfbs/c6FObfPxGuzLkQr4fZKcqK8\n" + - "1MtR1mh1WVLJRgXW4u8cHtZyH5pThngMcUiyzWsa0g6Jaz8w6sr/Wv3e1qdTCITs\n" + - "kMrWCDaoDhD2teAjmWuk9u8ZBPJ7xhme+Q/UQ90xomQ/NdCJafirk2Ds92p7N7RK\n" + - "SES1KywBhfONJbPw1TdZ9Mts+DGjkucYbe+ZzPxrLpWXur1BSGEqBtTAGW3dS/xp\n" + - "wBYNlhasXHjYMr4HeIYYYOx+oR5JgDYoVfp2k0DwK/QXogbja+/Vjv+LrXdNY0t1\n" + - "bA35FNnl637M8iCNrXvIoRFARbNyge8c/jSWGPLB/tIyNhYhBPLRUKT+mJJdUekV\n" + - "OWhPrWLcA4+7FLwIAK1GngNMnruxWM4EoghKTSmKNrd6p/d3Wsd+y2019A7Nz+4O\n" + - "ydkEDvmNVVhlUcfgOf2L6Bf63wdN0ho+ODhCuNSqHe6NL1NhdITbMGnDdKb57IIB\n" + - "9CuJFpILn9LZ1Ei6JPEpmpiSEaL+VJt1fMnfc8jtF8N3WcRVfJsq1aslXe8Npg70\n" + - "9YVgm2OXsNWgktl9fciu4ENTybQGjpN9WTa1aU1nkko6NUoIfjtM+PO4VU7x00M+\n" + - "dTJsYGhnc96EtT8EfSAIFBKZRAkMBFhEcdkxa8hCKI3+nyI3gTq0TcFST3wy05Am\n" + - "oV7wlgzUAMsW7MV2NpG7fJul2Q7puKw+udBUc0TCwawEGAEKAAkFglro/4ACmwIB\n" + - "VwkQaE+tYtwDj7vAdKAEGQEKAAYFglro/4AAIQkQSnI0o6EhOmUWIQRReSwOSOL9\n" + - "qU6TuRFKcjSjoSE6ZeFHB/92jhUTXrEgho6DYhmVFuXa3NGhAjIyZo3yYHMoL9aZ\n" + - "3DUyjxhAyRDpI2CrahQ4JsPhej2m+3fHWa34/tb5mpHYFWEahQvdWSFCcU7p2NUK\n" + - "cq2zNA6ixO2+fQQhmbrYR+TFxYmhLjCGUNt14E/XaIL1VxPQOA5KbiRPpa8BsUNl\n" + - "Nik9ASPWyn0ZA0rjJ1ZV7nJarXVbuZDEcUDuDm3cA5tup7juB8fTz2BDcg3Ka+Oc\n" + - "PEz0GgZfq9K40di3r9IHLBhNPHieFVIj9j/JyMnTvVOceM3J/Rb0MCWJVbXNBKpR\n" + - "MDibCQh+7fbqyQEM/zIpmk0TgBpTZZqMP0gxYdWImT1IFiEE8tFQpP6Ykl1R6RU5\n" + - "aE+tYtwDj7tOtggAhgAqvOB142L2SkS3ZIdwuhAtWLPHCtEwBOqGtP8Z204rqAmb\n" + - "nJymzo77+OT+SScnDTrwzOUJnCi0qPUxfuxhvHxnBxBIjaoMcF++iKsqF1vf6WuX\n" + - "OjbJ1N8I08pB2niht5MxIZ9rMGDeASj79X7I9Jjzsd30OVGfTZyy3VyYPxcJ6n/s\n" + - "ZocNmaTv0/F8K3TirSH6JDXdY5zirRi99GJ3R+AL6OzxrChuvLFSEtIRJrW5XVfg\n" + - "3whc0XD+5J9RsHoL33ub9ZhQHFKsjrf0nGYbEFwMhSdysfTYYMbwKi0CcQeQtPP0\n" + - "Y87zSryajDMFXQS0exdvhN4AXDlPlB3Rrkj7CQ==\n" + - "=yTKS\n" + - "-----END PGP ARMORED FILE-----\n"; - - PGPPublicKeyRing keyRing = PGPainless.readKeyRing().publicKeyRing(key); - PGPPublicKeyRing validated = KeyRingValidator.validate(keyRing, PGPainless.getPolicy()); - - Iterator keys = validated.getPublicKeys(); - assertFalse(keys.next().hasRevocation()); - assertTrue(keys.next().hasRevocation()); - } - - @Test - public void badSignatureTest() throws IOException { - String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "\n" + - "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\n" + - "bVrj5KjhX2PVNEJd3XZRzaXZE2Z/MQ==\n" + - "=6+l9\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"; - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); - PGPPublicKeyRing validated = KeyRingValidator.validate(publicKeys, PGPainless.getPolicy()); - // CHECKSTYLE:OFF - System.out.println(ArmorUtils.toAsciiArmoredString(validated)); - // CHECKSTYLE:ON - } - - @Test - public void unboundSubkey() throws IOException { - String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "\n" + - "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\n" + - "=T4QR\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"; - - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); - PGPPublicKey unbound = CollectionUtils.iteratorToList(publicKeys.getPublicKeys()).get(2); - assertNotNull(unbound); - - Date validationDate = DateUtil.parseUTCDate("2019-10-15 10:18:26 UTC"); - KeyRingInfo info = new KeyRingInfo(publicKeys, validationDate); - for (PGPPublicKey publicKey : publicKeys) { - if (publicKey != unbound) { - assertTrue(info.isKeyValidlyBound(publicKey.getKeyID())); - } else { - assertFalse(info.isKeyValidlyBound(publicKey.getKeyID())); - } - } - } - - @Test - public void expired() throws IOException { - String key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "\n" + - "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\n" + - "=LxAY\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"; - - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); - PGPPublicKeyRing validated = KeyRingValidator.validate(publicKeys, PGPainless.getPolicy()); - } - - @Test - public void testKeyWithUserAttributes() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice "); - PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); - PGPPublicKey publicKey = secretKeys.getPublicKey(); - PGPSecretKey secretKey = secretKeys.getSecretKey(); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, SecretKeyRingProtector.unprotectedKeys()); - - PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( - ImplementationFactory.getInstance().getPGPContentSignerBuilder(publicKey.getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId()) - ); - - signatureGenerator.init(SignatureType.CASUAL_CERTIFICATION.getCode(), privateKey); - PGPUserAttributeSubpacketVectorGenerator userAttrGen = new PGPUserAttributeSubpacketVectorGenerator(); - byte[] image = new byte[100]; - new Random().nextBytes(image); - userAttrGen.setImageAttribute(ImageAttribute.JPEG, image); - PGPUserAttributeSubpacketVector userAttr = userAttrGen.generate(); - - PGPSignature certification = signatureGenerator.generateCertification(userAttr, publicKey); - publicKey = PGPPublicKey.addCertification(publicKey, userAttr, certification); - publicKeys = PGPPublicKeyRing.insertPublicKey(publicKeys, publicKey); - secretKeys = PGPSecretKeyRing.replacePublicKeys(secretKeys, publicKeys); - - secretKeys = KeyRingValidator.validate(secretKeys, PGPainless.getPolicy()); - assertTrue(secretKeys.getPublicKey().getUserAttributes().hasNext()); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/KeyRingValidationTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/KeyRingValidationTest.java deleted file mode 100644 index 0352abba..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/KeyRingValidationTest.java +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature; - -import java.io.IOException; -import java.util.Collections; -import java.util.Date; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.key.KeyRingValidator; -import org.pgpainless.policy.Policy; -import org.pgpainless.util.ArmorUtils; -import org.pgpainless.util.DateUtil; - -public class KeyRingValidationTest { - - private static Policy.HashAlgorithmPolicy defaultSignatureHashAlgorithmPolicy; - - @BeforeAll - public static void setCustomPolicy() { - Policy policy = PGPainless.getPolicy(); - defaultSignatureHashAlgorithmPolicy = policy.getSignatureHashAlgorithmPolicy(); - - policy.setSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA256, Collections.singletonList(HashAlgorithm.SHA256))); - } - - @AfterAll - public static void resetCustomPolicy() { - PGPainless.getPolicy().setSignatureHashAlgorithmPolicy(defaultSignatureHashAlgorithmPolicy); - } - - @Test - public void testSignatureValidationOnPrimaryKey() throws IOException, PGPException { - String key = "-----BEGIN PGP ARMORED FILE-----\n" + - "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + - "\n" + - "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\n" + - "exdvhN4AXDlPlB3Rrkj7CQ==\n" + - "=qQpG\n" + - "-----END PGP ARMORED FILE-----\n"; - - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); - - Date validationDate = DateUtil.parseUTCDate("2019-05-01 00:00:00 UTC"); - Policy policy = PGPainless.getPolicy(); - PGPPublicKeyRing evaluated = KeyRingValidator.validate(publicKeys, policy, validationDate); - - // CHECKSTYLE:OFF - System.out.println(ArmorUtils.toAsciiArmoredString(evaluated)); - // CHECKSTYLE:ON - - - } -} From a99ce159692e7445eda6d76d0f63dff4c5f62643 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 30 Jun 2022 13:11:27 +0200 Subject: [PATCH 0282/1204] Forward userIdOnCertificate() method call --- .../org/pgpainless/key/certification/CertifyCertificate.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java index a4f37b66..05e64879 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java @@ -50,7 +50,7 @@ public class CertifyCertificate { */ public CertificationOnUserId userIdOnCertificate(@Nonnull String userId, @Nonnull PGPPublicKeyRing certificate) { - return new CertificationOnUserId(userId, certificate, CertificationType.GENERIC); + return userIdOnCertificate(userId, certificate, CertificationType.GENERIC); } /** From 8b66b3527ec9f4baa9f27401d1480b16f2f9bc05 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 30 Jun 2022 13:16:15 +0200 Subject: [PATCH 0283/1204] Add tests for pet name certification and scoped delegation --- .../certification/CertifyCertificateTest.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java index 3dbc4988..f837be1b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java @@ -23,10 +23,13 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.Arrays; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CertificationType; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.algorithm.Trustworthiness; +import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.consumer.SignatureVerifier; +import org.pgpainless.signature.subpackets.CertificationSubpackets; import org.pgpainless.util.CollectionUtils; import org.pgpainless.util.DateUtil; @@ -105,4 +108,58 @@ public class CertifyCertificateTest { assertFalse(Arrays.areEqual(bobCertificate.getEncoded(), bobCertified.getEncoded())); } + + @Test + public void testPetNameCertification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing aliceKey = PGPainless.generateKeyRing() + .modernKeyRing("Alice "); + PGPSecretKeyRing bobKey = PGPainless.generateKeyRing() + .modernKeyRing("Bob "); + + PGPPublicKeyRing bobCert = PGPainless.extractCertificate(bobKey); + String petName = "Bobby"; + + CertifyCertificate.CertificationResult result = PGPainless.certify() + .userIdOnCertificate(petName, bobCert) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .buildWithSubpackets(new CertificationSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(CertificationSubpackets hashedSubpackets) { + hashedSubpackets.setExportable(false); + } + }); + + PGPSignature certification = result.getCertification(); + assertEquals(aliceKey.getPublicKey().getKeyID(), certification.getKeyID()); + assertEquals(CertificationType.GENERIC.asSignatureType().getCode(), certification.getSignatureType()); + + PGPPublicKeyRing certWithPetName = result.getCertifiedCertificate(); + KeyRingInfo info = PGPainless.inspectKeyRing(certWithPetName); + assertTrue(info.getUserIds().contains(petName)); + assertFalse(info.getValidUserIds().contains(petName)); + } + + @Test + public void testScopedDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing aliceKey = PGPainless.generateKeyRing() + .modernKeyRing("Alice "); + PGPSecretKeyRing caKey = PGPainless.generateKeyRing() + .modernKeyRing("CA "); + PGPPublicKeyRing caCert = PGPainless.extractCertificate(caKey); + + CertifyCertificate.CertificationResult result = PGPainless.certify() + .certificate(caCert, Trustworthiness.fullyTrusted().introducer()) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .buildWithSubpackets(new CertificationSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(CertificationSubpackets hashedSubpackets) { + hashedSubpackets.setRegularExpression("^.*<.+@example.com>.*$"); + } + }); + + PGPSignature certification = result.getCertification(); + assertEquals(SignatureType.DIRECT_KEY.getCode(), certification.getSignatureType()); + assertEquals("^.*<.+@example.com>.*$", + certification.getHashedSubPackets().getRegularExpression().getRegex()); + } } From 170aaaa0c58a1694f45d01b7bcba936678bdc711 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Jul 2022 11:05:16 +0200 Subject: [PATCH 0284/1204] Document KO protection utility class --- .../key/util/PublicKeyParameterValidationUtil.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java index fbddb080..d3c7fe68 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java @@ -38,6 +38,15 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.exception.KeyIntegrityException; import org.pgpainless.implementation.ImplementationFactory; +/** + * Utility class to verify keys against Key Overwriting (KO) attacks. + * This class of attacks is only possible if the attacker has access to the (encrypted) secret key material. + * To execute the attack, they would modify the unauthenticated parameters of the users public key. + * Using the modified public key in combination with the unmodified secret key material can then lead to the + * extraction of secret key parameters via weakly crafted messages. + * + * @see Key Overwriting (KO) Attacks against OpenPGP + */ public class PublicKeyParameterValidationUtil { public static void verifyPublicKeyParameterIntegrity(PGPPrivateKey privateKey, PGPPublicKey publicKey) From 762391659e27061a744b65cf7e668318f3609606 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Jul 2022 23:56:08 +0200 Subject: [PATCH 0285/1204] Add node_modules to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index f12ea5f8..744e25e8 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ pgpainless-core/.project pgpainless-core/.settings/ push_html.sh + +docs/node_modules From 16c44e670e65c29f9b701d0166f05e970c00611a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Jul 2022 23:56:41 +0200 Subject: [PATCH 0286/1204] Initial sphinx-based documentation --- ECOSYSTEM.md | 92 - docs/Makefile | 20 + docs/README.md | 16 + docs/make.bat | 35 + docs/package-lock.json | 2626 +++++++++++++++++++++ docs/package.json | 5 + docs/source/conf.py | 53 + docs/source/ecosystem.md | 85 + docs/source/index.rst | 25 + docs/source/pgpainless-core/quickstart.md | 27 + docs/source/pgpainless-sop/quickstart.md | 369 +++ docs/source/quickstart.md | 20 + docs/source/sop.md | 3 + 13 files changed, 3284 insertions(+), 92 deletions(-) delete mode 100644 ECOSYSTEM.md create mode 100644 docs/Makefile create mode 100644 docs/README.md create mode 100644 docs/make.bat create mode 100644 docs/package-lock.json create mode 100644 docs/package.json create mode 100644 docs/source/conf.py create mode 100644 docs/source/ecosystem.md create mode 100644 docs/source/index.rst create mode 100644 docs/source/pgpainless-core/quickstart.md create mode 100644 docs/source/pgpainless-sop/quickstart.md create mode 100644 docs/source/quickstart.md create mode 100644 docs/source/sop.md diff --git a/ECOSYSTEM.md b/ECOSYSTEM.md deleted file mode 100644 index b0b4125b..00000000 --- a/ECOSYSTEM.md +++ /dev/null @@ -1,92 +0,0 @@ - - -# Ecosystem - -PGPainless consists of an ecosystem of different libraries and projects. - -```mermaid -flowchart TB - subgraph SOP-JAVA - sop-java-picocli-->sop-java - end - subgraph PGPAINLESS - pgpainless-sop-->pgpainless-core - pgpainless-sop-->sop-java - pgpainless-cli-->pgpainless-sop - pgpainless-cli-->sop-java-picocli - end - subgraph WKD-JAVA - wkd-java-cli-->wkd-java - wkd-test-suite-->wkd-java - wkd-test-suite-->pgpainless-core - end - subgraph CERT-D-JAVA - pgp-cert-d-java-->pgp-certificate-store - pgp-cert-d-java-jdbc-sqlite-lookup-->pgp-cert-d-java - end - subgraph CERT-D-PGPAINLESS - pgpainless-cert-d-->pgpainless-core - pgpainless-cert-d-->pgp-cert-d-java - pgpainless-cert-d-cli-->pgpainless-cert-d - pgpainless-cert-d-cli-->pgp-cert-d-java-jdbc-sqlite-lookup - end - subgraph VKS-JAVA - vks-java-cli-->vks-java - end - subgraph PGPEASY - pgpeasy-->pgpainless-cli - pgpeasy-->wkd-java-cli - pgpeasy-->vks-java-cli - pgpeasy-->pgpainless-cert-d-cli - end - wkd-java-cli-->pgpainless-cert-d - wkd-java-->pgp-certificate-store -``` - -## [PGPainless](https://github.com/pgpainless/pgpainless) - -The main repository contains the following components: - -* `pgpainless-core` - core implementation - powerful, yet easy to use OpenPGP API -* `pgpainless-sop` - super simple OpenPGP implementation. Drop-in for `sop-java` -* `pgpainless-cli` - SOP CLI implementation using PGPainless - -## [SOP-Java](https://github.com/pgpainless/sop-java) - -An API definition and CLI implementation of the [Stateless OpenPGP Protocol](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-03.html). - -* `sop-java` - generic OpenPGP API definition -* `sop-java-picocli` - Abstract CLI implementation for `sop-java` - -## [WKD-Java](https://github.com/pgpainless/wkd-java) - -Implementation of the [Web Key Directory](https://www.ietf.org/archive/id/draft-koch-openpgp-webkey-service-13.html). - -* `wkd-java` - abstract WKD discovery implementation -* `wkd-java-cli` - CLI application implementing WKD discovery using PGPainless -* `wkd-test-suite` - Generator for test vectors for testing WKD implementations - -## [VKS-Java](https://github.com/pgpainless/vks-java) - -Client-side API for communicating with Verifying Key Servers, such as https://keys.openpgp.org/. - -* `vks-java` - VKS client implementation - -## [Cert-D-Java](https://github.com/pgpainless/cert-d-java) - -Implementations of the [Shared OpenPGP Certificate Directory specification](https://sequoia-pgp.gitlab.io/pgp-cert-d/). - -* `pgp-certificate-store` - abstract definitions of OpenPGP certificate stores -* `pgp-cert-d-java` - implementation of `pgp-certificate-store` following the PGP-CERT-D spec. -* `pgp-cert-d-java-jdbc-sqlite-lookup` - subkey lookup using sqlite database - -## [Cert-D-PGPainless](https://github.com/pgpainless/cert-d-pgpainless) - -Implementation of the [Shared OpenPGP Certificate Directory specification](https://sequoia-pgp.gitlab.io/pgp-cert-d/) using PGPainless. - -* `pgpainless-cert-d` - PGPainless-based implementation of `pgp-cert-d-java`. -* `pgpainless-cert-d-cli` - CLI frontend for `pgpainless-cert-d`. \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d0c3cbf1 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..1ed74654 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,16 @@ +# User Guide for PGPainless + +## Build the Guide + +```shell +$ make {html|epub|latexpdf} +``` + +Note: Building requires `mermaid-cli` to be installed in this directory: +```shell +$ # Move here +$ cd pgpainless/docs +$ npm install @mermaid-js/mermaid-cli +``` + +TODO: This is ugly. Install mermaid-cli globally? Perhaps point to user-installed mermaid-cli in conf.py's mermaid_cmd \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..6247f7e2 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 00000000..c75cb36f --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,2626 @@ +{ + "name": "docs", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "@mermaid-js/mermaid-cli": "^9.1.3" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz", + "integrity": "sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w==" + }, + "node_modules/@mermaid-js/mermaid-cli": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@mermaid-js/mermaid-cli/-/mermaid-cli-9.1.3.tgz", + "integrity": "sha512-R7VFArRIhczOejWtKT2Ii8MVKayjpiY6hebGqtcmA8FGSUXDgB4QzK5z9zpOfh1k90XH0PzPpTlL4KXnFfDx1Q==", + "dependencies": { + "chalk": "^4.1.0", + "commander": "^9.0.0", + "mermaid": "^9.0.0", + "puppeteer": "^14.1.0" + }, + "bin": { + "mmdc": "index.bundle.js" + } + }, + "node_modules/@types/node": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", + "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/commander": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz", + "integrity": "sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/d3": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.6.1.tgz", + "integrity": "sha512-txMTdIHFbcpLx+8a0IFhZsbp+PfBBPt8yfbmukZTQFroKuFqIwqswF0qE5JXWefylaAVpSXFoKm3yP+jpNLFLw==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz", + "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.0.tgz", + "integrity": "sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.0.1.tgz", + "integrity": "sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz", + "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz", + "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "node_modules/dagre-d3": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/dagre-d3/-/dagre-d3-0.6.4.tgz", + "integrity": "sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==", + "dependencies": { + "d3": "^5.14", + "dagre": "^0.8.5", + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "node_modules/dagre-d3/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/dagre-d3/node_modules/d3": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz", + "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", + "dependencies": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "node_modules/dagre-d3/node_modules/d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "node_modules/dagre-d3/node_modules/d3-brush": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.6.tgz", + "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==", + "dependencies": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "dependencies": { + "d3-array": "1", + "d3-path": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + }, + "node_modules/dagre-d3/node_modules/d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "dependencies": { + "d3-array": "^1.1.1" + } + }, + "node_modules/dagre-d3/node_modules/d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" + }, + "node_modules/dagre-d3/node_modules/d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", + "dependencies": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-dsv": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz", + "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", + "dependencies": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json", + "csv2tsv": "bin/dsv2dsv", + "dsv2dsv": "bin/dsv2dsv", + "dsv2json": "bin/dsv2json", + "json2csv": "bin/json2dsv", + "json2dsv": "bin/json2dsv", + "json2tsv": "bin/json2dsv", + "tsv2csv": "bin/dsv2dsv", + "tsv2json": "bin/dsv2json" + } + }, + "node_modules/dagre-d3/node_modules/d3-ease": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz", + "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==" + }, + "node_modules/dagre-d3/node_modules/d3-fetch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz", + "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", + "dependencies": { + "d3-dsv": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "dependencies": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" + }, + "node_modules/dagre-d3/node_modules/d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "dependencies": { + "d3-array": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + }, + "node_modules/dagre-d3/node_modules/d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "dependencies": { + "d3-color": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "node_modules/dagre-d3/node_modules/d3-polygon": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz", + "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" + }, + "node_modules/dagre-d3/node_modules/d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" + }, + "node_modules/dagre-d3/node_modules/d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "node_modules/dagre-d3/node_modules/d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "dependencies": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "node_modules/dagre-d3/node_modules/d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "dependencies": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-selection": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" + }, + "node_modules/dagre-d3/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "node_modules/dagre-d3/node_modules/d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "dependencies": { + "d3-time": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" + }, + "node_modules/dagre-d3/node_modules/d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", + "dependencies": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "dependencies": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "node_modules/dagre-d3/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1001819", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1001819.tgz", + "integrity": "sha512-G6OsIFnv/rDyxSqBa2lDLR6thp9oJioLsb2Gl+LbQlyoA9/OBAkrTU9jiCcQ8Pnh7z4d6slDiLaogR5hzgJLmQ==" + }, + "node_modules/dompurify": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz", + "integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/khroma": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", + "integrity": "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/mermaid": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.1.3.tgz", + "integrity": "sha512-jTIYiqKwsUXVCoxHUVkK8t0QN3zSKIdJlb9thT0J5jCnzXyc+gqTbZE2QmjRfavFTPPn5eRy5zaFp7V+6RhxYg==", + "dependencies": { + "@braintree/sanitize-url": "^6.0.0", + "d3": "^7.0.0", + "dagre": "^0.8.5", + "dagre-d3": "^0.6.4", + "dompurify": "2.3.8", + "graphlib": "^2.1.8", + "khroma": "^2.0.0", + "moment-mini": "^2.24.0", + "stylis": "^4.0.10" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/moment-mini": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment-mini/-/moment-mini-2.24.0.tgz", + "integrity": "sha512-9ARkWHBs+6YJIvrIp0Ik5tyTTtP9PoV0Ssu2Ocq5y9v8+NOOpWiRshAp8c4rZVWTOe+157on/5G+zj5pwIQFEQ==" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "14.4.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-14.4.1.tgz", + "integrity": "sha512-+H0Gm84aXUvSLdSiDROtLlOofftClgw2TdceMvvCU9UvMryappoeS3+eOLfKvoy4sm8B8MWnYmPhWxVFudAOFQ==", + "hasInstallScript": true, + "dependencies": { + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1001819", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "pkg-dir": "4.2.0", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.7.0" + }, + "engines": { + "node": ">=14.1.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz", + "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==" + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/stylis": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.1.tgz", + "integrity": "sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ==" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz", + "integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + }, + "dependencies": { + "@braintree/sanitize-url": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz", + "integrity": "sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w==" + }, + "@mermaid-js/mermaid-cli": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@mermaid-js/mermaid-cli/-/mermaid-cli-9.1.3.tgz", + "integrity": "sha512-R7VFArRIhczOejWtKT2Ii8MVKayjpiY6hebGqtcmA8FGSUXDgB4QzK5z9zpOfh1k90XH0PzPpTlL4KXnFfDx1Q==", + "requires": { + "chalk": "^4.1.0", + "commander": "^9.0.0", + "mermaid": "^9.0.0", + "puppeteer": "^14.1.0" + } + }, + "@types/node": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", + "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==", + "optional": true + }, + "@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "commander": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz", + "integrity": "sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "requires": { + "node-fetch": "2.6.7" + } + }, + "d3": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.6.1.tgz", + "integrity": "sha512-txMTdIHFbcpLx+8a0IFhZsbp+PfBBPt8yfbmukZTQFroKuFqIwqswF0qE5JXWefylaAVpSXFoKm3yP+jpNLFLw==", + "requires": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + } + }, + "d3-array": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz", + "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==", + "requires": { + "internmap": "1 - 2" + } + }, + "d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" + }, + "d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + } + }, + "d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "requires": { + "d3-path": "1 - 3" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" + }, + "d3-contour": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.0.tgz", + "integrity": "sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==", + "requires": { + "d3-array": "^3.2.0" + } + }, + "d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "requires": { + "delaunator": "5" + } + }, + "d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" + }, + "d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + } + }, + "d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "requires": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + } + } + }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, + "d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "requires": { + "d3-dsv": "1 - 3" + } + }, + "d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" + }, + "d3-geo": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.0.1.tgz", + "integrity": "sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==", + "requires": { + "d3-array": "2.5.0 - 3" + } + }, + "d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==" + }, + "d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "requires": { + "d3-color": "1 - 3" + } + }, + "d3-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz", + "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==" + }, + "d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==" + }, + "d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" + }, + "d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==" + }, + "d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "requires": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + } + }, + "d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "requires": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + } + }, + "d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + }, + "d3-shape": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz", + "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==", + "requires": { + "d3-path": "1 - 3" + } + }, + "d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", + "requires": { + "d3-array": "2 - 3" + } + }, + "d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "requires": { + "d3-time": "1 - 3" + } + }, + "d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" + }, + "d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "requires": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + } + }, + "dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "requires": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "dagre-d3": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/dagre-d3/-/dagre-d3-0.6.4.tgz", + "integrity": "sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==", + "requires": { + "d3": "^5.14", + "dagre": "^0.8.5", + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "d3": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz", + "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", + "requires": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "d3-brush": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.6.tgz", + "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "requires": { + "d3-array": "1", + "d3-path": "1" + } + }, + "d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" + }, + "d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", + "requires": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "d3-dsv": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz", + "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } + }, + "d3-ease": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz", + "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==" + }, + "d3-fetch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz", + "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", + "requires": { + "d3-dsv": "1" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" + }, + "d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "requires": { + "d3-array": "1" + } + }, + "d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "d3-polygon": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz", + "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" + }, + "d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" + }, + "d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" + }, + "d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" + }, + "d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "requires": { + "robust-predicates": "^3.0.0" + } + }, + "devtools-protocol": { + "version": "0.0.1001819", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1001819.tgz", + "integrity": "sha512-G6OsIFnv/rDyxSqBa2lDLR6thp9oJioLsb2Gl+LbQlyoA9/OBAkrTU9jiCcQ8Pnh7z4d6slDiLaogR5hzgJLmQ==" + }, + "dompurify": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz", + "integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "requires": { + "pend": "~1.2.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "requires": { + "lodash": "^4.17.15" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" + }, + "khroma": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", + "integrity": "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "mermaid": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.1.3.tgz", + "integrity": "sha512-jTIYiqKwsUXVCoxHUVkK8t0QN3zSKIdJlb9thT0J5jCnzXyc+gqTbZE2QmjRfavFTPPn5eRy5zaFp7V+6RhxYg==", + "requires": { + "@braintree/sanitize-url": "^6.0.0", + "d3": "^7.0.0", + "dagre": "^0.8.5", + "dagre-d3": "^0.6.4", + "dompurify": "2.3.8", + "graphlib": "^2.1.8", + "khroma": "^2.0.0", + "moment-mini": "^2.24.0", + "stylis": "^4.0.10" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "moment-mini": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment-mini/-/moment-mini-2.24.0.tgz", + "integrity": "sha512-9ARkWHBs+6YJIvrIp0Ik5tyTTtP9PoV0Ssu2Ocq5y9v8+NOOpWiRshAp8c4rZVWTOe+157on/5G+zj5pwIQFEQ==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + } + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "puppeteer": { + "version": "14.4.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-14.4.1.tgz", + "integrity": "sha512-+H0Gm84aXUvSLdSiDROtLlOofftClgw2TdceMvvCU9UvMryappoeS3+eOLfKvoy4sm8B8MWnYmPhWxVFudAOFQ==", + "requires": { + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1001819", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "pkg-dir": "4.2.0", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.7.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "robust-predicates": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz", + "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==" + }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "stylis": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.1.tgz", + "integrity": "sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "ws": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz", + "integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==", + "requires": {} + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..96ba9fc2 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@mermaid-js/mermaid-cli": "^9.1.3" + } +} diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..7de6dcbf --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,53 @@ +import os +# Configuration file for the Sphinx documentation builder. + +# -- Project information + +project = 'PGPainless' +copyright = '2022, Paul Schaub' +author = 'Paul Schaub' + +# https://protips.readthedocs.io/git-tag-version.html +latest_tag = os.popen('git describe --abbrev=0').read().strip() +release = latest_tag +version = release + +myst_substitutions = { + "repo_host" : "codeberg.org" +} + + + + +# -- General configuration + +extensions = [ + 'myst_parser', + 'sphinxcontrib.mermaid', + 'sphinx.ext.duration', + 'sphinx.ext.doctest', + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', +] + +source_suffix = ['.rst', '.md'] + +myst_enable_extensions = [ + 'colon_fence', + 'substitution', +] + +myst_heading_anchors = 3 + +templates_path = ['_templates'] + +# -- Options for HTML output + +html_theme = 'sphinx_rtd_theme' + +# -- Options for EPUB output +#epub_show_urls = 'footnote' + +mermaid_cmd = "./node_modules/.bin/mmdc" +mermaid_output_format = 'png' +mermaid_params = ['--theme', 'default', '--width', '800', '--backgroundColor', 'transparent'] diff --git a/docs/source/ecosystem.md b/docs/source/ecosystem.md new file mode 100644 index 00000000..30d8d6a3 --- /dev/null +++ b/docs/source/ecosystem.md @@ -0,0 +1,85 @@ +# The PGPainless Ecosystem + +PGPainless consists of an ecosystem of different libraries and projects. + +The diagram below shows, how the different projects relate to one another. + +```{mermaid} +flowchart LR + subgraph SOP-JAVA + sop-java-picocli-->sop-java + end + subgraph PGPAINLESS + pgpainless-sop-->pgpainless-core + pgpainless-sop-->sop-java + pgpainless-cli-->pgpainless-sop + pgpainless-cli-->sop-java-picocli + end + subgraph WKD-JAVA + wkd-java-cli-->wkd-java + wkd-test-suite-->wkd-java + wkd-test-suite-->pgpainless-core + end + subgraph CERT-D-JAVA + pgp-cert-d-java-->pgp-certificate-store + pgp-cert-d-java-jdbc-sqlite-lookup-->pgp-cert-d-java + end + subgraph CERT-D-PGPAINLESS + pgpainless-cert-d-->pgpainless-core + pgpainless-cert-d-->pgp-cert-d-java + pgpainless-cert-d-cli-->pgpainless-cert-d + pgpainless-cert-d-cli-->pgp-cert-d-java-jdbc-sqlite-lookup + end + subgraph VKS-JAVA + vks-java-cli-->vks-java + end + subgraph PGPEASY + pgpeasy-->pgpainless-cli + pgpeasy-->wkd-java-cli + pgpeasy-->vks-java-cli + pgpeasy-->pgpainless-cert-d-cli + end + wkd-java-cli-->pgpainless-cert-d + wkd-java-->pgp-certificate-store +``` + +## Libraries and Tools + +* {{ '[PGPainless](https://{}/pgpainless/pgpainless)'.format(repo_host) }} + The main repository contains the following components: + * `pgpainless-core` - core implementation - powerful, yet easy to use OpenPGP API + * `pgpainless-sop` - super simple OpenPGP implementation. Drop-in for `sop-java` + * `pgpainless-cli` - SOP CLI implementation using PGPainless + +* {{ '[SOP-Java](https://{}/pgpainless/sop-java)'.format(repo_host) }} + An API definition and CLI implementation of the [Stateless OpenPGP Protocol](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) (SOP). + Consumers of the SOP API can simply depend on `sop-java` and then switch out the backend as they wish. + Read more about the [SOP](sop) protocol here. + * `sop-java` - generic OpenPGP API definition + * `sop-java-picocli` - CLI frontend for `sop-java` + +* {{ '[WKD-Java](https://{}/pgpainless/wkd-java)'.format(repo_host) }} + Implementation of the [Web Key Directory](https://www.ietf.org/archive/id/draft-koch-openpgp-webkey-service-13.html). + * `wkd-java` - generic WKD discovery implementation + * `wkd-java-cli` - CLI frontend for WKD discovery using PGPainless + * `wkd-test-suite` - Generator for test vectors for testing WKD implementations + +* {{ '[VKS-Java](https://{}/pgpainless/vks-java)'.format(repo_host) }} + Client-side API for communicating with Verifying Key Servers, such as https://keys.openpgp.org/. + * `vks-java` - VKS client implementation + * `vks-java-cli` - CLI frontend for `vks-java` + +* {{ '[Cert-D-Java](https://{}/pgpainless/cert-d-java)'.format(repo_host) }} + Implementations of the [Shared OpenPGP Certificate Directory specification](https://sequoia-pgp.gitlab.io/pgp-cert-d/). + * `pgp-certificate-store` - abstract definitions of OpenPGP certificate stores + * `pgp-cert-d-java` - implementation of `pgp-certificate-store` following the PGP-CERT-D spec + * `pgp-cert-d-java-jdbc-sqlite-lookup` - subkey lookup using sqlite database + +* {{ '[Cert-D-PGPainless](https://{}/pgpainless/cert-d-pgpainless)'.format(repo_host) }} + Implementation of the [Shared OpenPGP Certificate Directory specification](https://sequoia-pgp.gitlab.io/pgp-cert-d/) using PGPainless. + * `pgpainless-cert-d` - PGPainless-based implementation of `pgp-cert-d-java` + * `pgpainless-cert-d-cli` - CLI frontend for `pgpainless-cert-d` + +* {{ '[PGPeasy](https://{}/pgpainless/pgpeasy)'.format(repo_host) }} + Prototypical, comprehensive OpenPGP CLI application + * `pgpeasy` - CLI application \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..19390f15 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,25 @@ +PGPainless - Painless OpenPGP +============================= + +**OpenPGP** (`RFC 4480 `_) is an Internet Standard mostly used for email encryption. +It provides mechanisms to ensure *confidentiality*, *integrity* and *authenticity* of messages. +However, OpenPGP can also be used for other purposes, such as secure messaging or as a signature mechanism for software distribution. + +**PGPainless** strives to improve the (currently pretty dire) state of the ecosystem of Java libraries and tooling for OpenPGP. + +The library focuses on being easy and intuitive to use without getting into your way. +Common functions such as creating keys, encrypting data, and so on are implemented using a builder structure that guides you through the necessary steps. + +Internally, it is based on `Bouncy Castles `_ mighty, but low-level OpenPGP API. +PGPainless' goal is to empower you to use OpenPGP without needing to write all the boilerplate code required by Bouncy Castle. +It aims to be secure by default while allowing customization if required. + + +Contents +-------- + +.. toctree:: + + quickstart.md + ecosystem.md + sop.md diff --git a/docs/source/pgpainless-core/quickstart.md b/docs/source/pgpainless-core/quickstart.md new file mode 100644 index 00000000..c1d39d47 --- /dev/null +++ b/docs/source/pgpainless-core/quickstart.md @@ -0,0 +1,27 @@ +## PGPainless API with pgpainless-core + +Coming soon. + +### Setup +bla + +### Generate a Key +bla + +### Extract a Certificate +bla + +### Apply / Remove ASCII Armor +bla + +### Encrypt a Message +bla + +### Decrypt a Message +bla + +### Sign a Message +bla + +### Verify a Signature +bla diff --git a/docs/source/pgpainless-sop/quickstart.md b/docs/source/pgpainless-sop/quickstart.md new file mode 100644 index 00000000..10a06541 --- /dev/null +++ b/docs/source/pgpainless-sop/quickstart.md @@ -0,0 +1,369 @@ +## SOP API with pgpainless-sop + +The Stateless OpenPGP Protocol (SOP) defines a simplistic interface for the most important OpenPGP operations. +It allows you to encrypt, decrypt, sign and verify messages, generate keys and add/remove ASCII armor from data. +However, it does not yet provide tools for key management. +Furthermore, the implementation is deciding for you, which (secure) algorithms to use, and it doesn't let you +change those. + +If you want to read more about the background of the SOP protocol, there is a [whole chapter](../sop) dedicated to it. + +### Setup + +PGPainless' releases are published to and can be fetched from Maven Central. +To get started, you first need to include `pgpainless-sop` in your projects build script. +``` +// If you use Gradle +... +dependencies { + ... + implementation "org.pgpainless:pgpainless-sop:XYZ" + ... +} + +// If you use Maven +... + + ... + + org.pgpainless + pgpainless-sop + XYZ + + ... + +``` + +:::{important} +Replace `XYZ` with the current version, e.g. {{ env.config.version }}! +::: + +The entry point to the API is the `SOP` interface, for which `pgpainless-sop` provides a concrete implementation +`SOPImpl`. + +```java +// Instantiate the API +SOP sop = new SOPImpl(); +``` + +Now you are ready to go! + +### Generate a Key + +To generate a new OpenPGP key, the method `SOP.generateKey()` is your friend: + +```java +// generate key +byte[] keyBytes = sop.generateKey() + .userId("John Doe ") + .withKeyPassword("f00b4r") + .generate() + .getBytes(); +``` + +The call `userId(String userId)` can be called multiple times to add multiple user-ids to the key, but it MUST +be called at least once. +The argument given in the first invocation will become the keys primary user-id. + +Optionally, the key can be protected with a password by calling `withKeyPassword(String password)`. +If this method is not called, the key will be unprotected. + +The `generate()` method call generates the key and returns a `Ready` object. +This in turn can be used to write the result to a stream via `writeTo(OutputStream out)`, or to get the result +as bytes via `getBytes()`. +In both cases, the resulting output will be the UTF8 encoded, ASCII armored OpenPGP secret key. + +To disable ASCII armoring, call `noArmor()` before calling `generate()`. + +At the time of writing, the resulting OpenPGP secret key will consist of a certification-capable 256-bits +ed25519 EdDSA primary key, a 256-bits ed25519 EdDSA subkey used for signing, as well as a 256-bits X25519 +ECDH subkey for encryption. + +The whole key does not have an expiration date set. + +### Extract a Certificate + +Now that you generated your secret key, you probably want to share the public key with your contacts. +To extract the OpenPGP public key (which we will call *certificate* from now on) from the secret key, +use the `SOP.extractCert()` method call: + +```java +// extract certificate +byte[] certificateBytes = sop.extractCert() + .key(keyBytes) + .getBytes(); +``` + +The `key(_)` method either takes a byte array (like in the example), or an `InputStream`. +In both cases it returns another `Ready` object from which the certificate can be accessed, either via +`writeTo(OutputStream out)` or `getBytes()`. + +By default, the resulting certificate will be ASCII armored, regardless of whether the input key was armored or not. +To disable ASCII armoring, call `noArmor()` before calling `key(_)`. + +In our example, `certificateBytes` can now safely be shared with anyone. + +### Apply / Remove ASCII Armor + +Perhaps you want to print your secret key onto a piece of paper for backup purposes, +but you accidentally called `noArmor()` when generating the key. + +To add ASCII armor to some binary OpenPGP data, the `armor()` API can be used: + +```java +// wrap data in ASCII armor +byte[] armoredData = sop.armor() + .data(binaryData) + .getBytes(); +``` + +The `data(_)` method can either be called by providing a byte array, or an `InputStream`. + +:::{note} +There is a `label(ArmorLabel label)` method, which could theoretically be used to define the label used in the +ASCII armor header. +However, this method is not (yet?) supported by `pgpainless-sop` and will currently throw an `UnsupportedOption` +exception. +Instead, the implementation will figure out the data type and set the respective label on its own. +::: + +To remove ASCII armor from armored data, simply use the `dearmor()` API: + +```java +// remove ASCII armor +byte[] binaryData = sop.unarmor() + .data(armoredData) + .getBytes(); +``` + +Once again, the `data(_)` method can be called either with a byte array or an `InputStream` as argument. + +If the input data is not validly armored OpenPGP data, the `data(_)` method call will throw a `BadData` exception. + +### Encrypt a Message + +Now lets get to the juicy part and finally encrypt a message! +In this example, we will assume that Alice is the sender that wants to send a message to Bob. +Beforehand, Alice acquired Bobs certificate, e.g. by fetching it from a key server. + +To encrypt a message, you can make use of the `encrypt()` API: + +```java +// encrypt and sign a message +byte[] aliceKey = ...; // Alice' secret key +byte[] aliceCert = ...; // Alice' certificate (e.g. via extractCert()) +byte[] bobCert = ...; // Bobs certificate + +byte[] plaintext = "Hello, World!\n".getBytes(); // plaintext + +byte[] ciphertext = sop.encrypt() + // encrypt for each recipient + .withCert(bobCert) + .withCert(aliceCert) + // Optionally: Sign the message + .signWith(aliceKey) + .withKeyPassword("sw0rdf1sh") // if signing key is protected + // provide the plaintext + .plaintext(plaintext) + .getBytes(); +``` + +Here you encrypt the message for each recipient (Alice probably wants to be able to decrypt the message too!) +by calling `withCert(_)` with the recipients certificate as argument. It does not matter, if the certificate +is ASCII armored or not, and the method can either be called with a byte array or an `InputStream` as argument. + +The API not only supports asymmetric encryption via OpenPGP certificates, but it can also encrypt messages +symmetrically using one or more passwords. Both mechanisms can even be used together in the same message! +To (additionally or exclusively) encrypt the message for a password, simply call `withPassword(String password)` +before the `plaintext(_)` method call. + +It is recommended (but not required) to sign encrypted messages. +In order to sign the message before encryption is applied, call `signWith(_)` with the signing key as argument. +This method call can be repeated multiple times to sign the message with multiple signing keys. + +If any keys used for signing are password protected, you need to provide the signing key password via +`withKeyPassword(_)`. +It does not matter in which order signing keys and key passwords are provided, the implementation will figure out +matches on its own. If different key passwords are used, the `withKeyPassword(_)` method can be called multiple times. + +By default, the encrypted message will be ASCII armored. To disable ASCII armor, call `noArmor()` before the +`plaintext(_)` method call. + +Lastly, you need to provide the plaintext by calling `plaintext(_)` with either a byte array or an `InputStream` +as argument. +The ciphertext can then be accessed from the resulting `Ready` object as usual. + +### Decrypt a Message + +Now let's switch perspective and help Bob decrypt the message from Alice. + +Decrypting encrypted messages is done in a similar fashion using the `decrypt()` API: + +```java +// decrypt a message and verify its signature(s) +byte[] aliceCert = ...; // Alice' certificate +byte[] bobKey = ...; // Bobs secret key +byte[] bobCert = ...; // Bobs certificate + +byte[] ciphertext = ...; // the encrypted message + +ReadyWithResult readyWithResult = sop.decrypt() + .withKey(bobKey) + .verifyWith(aliceCert) + .withKeyPassword("password123") // if decryption key is protected + .ciphertext(ciphertext); +``` + +The `ReadyWithResult` can now be processed in two different ways, depending on whether you want the +plaintext as bytes or simply write it out to an `OutputStream`. + +To get the plaintext bytes directly, you shall proceed as follows: + +```java +ByteArrayAndResult bytesAndResult = readyWithResult.toByteArrayAndResult(); +DecryptionResult result = bytesAndResult.getResult(); +byte[] plaintext = bytesAndResult.getBytes(); +``` + +If you instead want to write the plaintext out to an `OutputStream`, the following code can be used: + +```java +OutputStream out = ...; +DecryptionResult result = readyWithResult.writeTo(out); +``` + +Note, that in both cases you acquire a `DecryptionResult` object. This contains information about the message, +such as which signatures could successfully be verified. + +If you provided the senders certificate for the purpose of signature verification via `verifyWith(_)`, you now +probably want to check, if the message was actually signed by the sender by checking `result.getVerifications()`. + +:::{note} +Signature verification will be discussed in more detail in section [](#verify-a-signature) +::: + +If the message was encrypted symmetrically using a password, you can also decrypt is symmetrically by calling +`withPassword(String password)` before the `ciphertext(_)` method call. This method call can be repeated multiple +times. The implementation will try different passwords until it finds a matching one. + +### Sign a Message + +There are three different main ways of signing a message: +* Inline Signatures +* Cleartext Signatures +* Detached Signatures + +An inline-signature will be part of the message itself (e.g. like with messages that are encrypted *and* signed). +Inline-signed messages are not human-readable without prior processing. + +A cleartext signature makes use of the [cleartext signature framework](https://datatracker.ietf.org/doc/html/rfc4880#section-7). +Messages signed in this way do have an ASCII armor header and footer, yet the content of the message is still +human-readable without special software. + +Lastly, a detached signature can be distributed as an extra file alongside the message without altering it. +This is useful if the plaintext itself cannot be modified (e.g. if a binary file is signed). + +The SOP API can generate all of those signature types. + +#### Inline-Signatures + +Let's start with an inline signature: + +```java +byte[] signingKey = ...; +byte[] message = ...; + +byte[] inlineSignedMessage = sop.inlineSign() + .mode(InlineSignAs.Text) // or 'Binary' + .key(signingKey) + .withKeyPassword("fnord") + .data(message) + .getBytes(); +``` + +You can choose between two different signature formats which can be set using `mode(InlineSignAs mode)`. +The default value is `Binary`. You can also set it to `Text` which signals to the receiver that the data is +UTF8 text. + +:::{note} +For inline signatures, do NOT set the `mode()` to `CleartextSigned`, as that will create message which uses the +cleartext signature framework (see further below). +::: + +You must provide at least one signing key using `key(_)` in order to be able to sign the message. + +If any key is password protected, you need to provide its password using `withKeyPassword(_)` which +can be called multiple times to provide multiple passwords. + +Once you provide the plaintext using `data(_)` with either a byte array or an `InputStream` as argument, +you will get a `Ready` object back, from which the signed message can be retrieved as usual. + +By default, the signed message will be ASCII armored. This can be disabled by calling `noArmor()` +before the `data(_)` method call. + +#### Cleartext Signatures + +A cleartext-signed message can be generated in a similar way to an inline-signed message, however, +there are is one subtle difference: + +```java +byte[] signingKey = ...; +byte[] message = ...; + +byte[] cleartextSignedMessage = sop.inlineSign() + .mode(InlineSignAs.CleartextSigned) // This MUST be set + .key(signingKey) + .withKeyPassword("fnord") + .data(message) + .getBytes(); +``` + +:::{important} +In order to produce a cleartext-signed message, the signature mode MUST be set to `CleartextSigned` +by calling `mode(InlineSignAs.CleartextSigned)`. +::: + +:::{note} +Calling `noArmor()` will have no effect for cleartext-signed messages, so such method call will be ignored. +::: + +#### Detached Signatures + +As the name suggests, detached signatures are detached from the message itself and can be distributed separately. + +To produce a detached signature, the `detachedSign()` API is used: + +```java +byte[] signingKey = ...; +byte[] message = ...; + +ReadyWithResult readyWithResult = sop.detachedSign() + .key(signingKey) + .withKeyPassword("fnord") + .data(message); +``` + +Here you have the choice, how you want to write out the signature. +If you want to write the signature to an `OutputStream`, you can do the following: + +```java +OutputStream out = ...; +SigningResult result = readyWithResult.writeTo(out); +``` + +If instead you want to get the signature as a byte array, do this instead: + +```java +ByteArrayAndResult bytesAndResult = readyWithResult.toByteArrayAndResult(); +SigningResult result = bytesAndResult.getResult(); +byte[] detachedSignature = bytesAndResult.getBytes(); +``` + +In any case, the detached signature can now be distributed alongside the original message. + +By default, the resulting detached signature will be ASCII armored. This can be disabled by calling `noArmor()` +prior to calling `data(_)`. + +The `SigningResult` object you got back in both cases contains information about the signature. + +### Verify a Signature \ No newline at end of file diff --git a/docs/source/quickstart.md b/docs/source/quickstart.md new file mode 100644 index 00000000..7cfee56c --- /dev/null +++ b/docs/source/quickstart.md @@ -0,0 +1,20 @@ +# Quickstart Guide + +In this guide, we will get you started with OpenPGP using PGPainless as quickly as possible. + +At first though, you need to decide which API you want to use; + +* PGPainless' core API is powerful and heavily customizable +* The SOP API is a bit less powerful, but *dead* simple to use + +The SOP API is the recommended way to go if you just want to get started already. + +In case you need more technical documentation, Javadoc can be found in the following places: +* For the core API: {{ '[pgpainless-core](https://javadoc.io/doc/org.pgpainless/pgpainless-core/{}/index.html)'.format(env.config.version) }} +* For the SOP API: {{ '[pgpainless-sop](https://javadoc.io/doc/org.pgpainless/pgpainless-sop/{}/index.html)'.format(env.config.version) }} + +```{include} pgpainless-sop/quickstart.md +``` + +```{include} pgpainless-core/quickstart.md +``` \ No newline at end of file diff --git a/docs/source/sop.md b/docs/source/sop.md new file mode 100644 index 00000000..5dbf6dc7 --- /dev/null +++ b/docs/source/sop.md @@ -0,0 +1,3 @@ +# Stateless OpenPGP Protocol (SOP) + +Lorem ipsum dolor sit amet. \ No newline at end of file From c916cec0425a8f43abcc26266b2661c4595f1f3f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Jul 2022 00:13:50 +0200 Subject: [PATCH 0287/1204] URL footnotes in pdf and conf.py documentation --- docs/source/conf.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7de6dcbf..9c42bc9b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,12 +13,9 @@ release = latest_tag version = release myst_substitutions = { - "repo_host" : "codeberg.org" + "repo_host" : "codeberg.org" # or 'github.com' } - - - # -- General configuration extensions = [ @@ -45,9 +42,11 @@ templates_path = ['_templates'] html_theme = 'sphinx_rtd_theme' -# -- Options for EPUB output +# Show URLs as footnotes #epub_show_urls = 'footnote' +latex_show_urls = 'footnote' mermaid_cmd = "./node_modules/.bin/mmdc" +# 'raw' does not work for epub and pdf, neither does 'svg' mermaid_output_format = 'png' mermaid_params = ['--theme', 'default', '--width', '800', '--backgroundColor', 'transparent'] From 556496dc87d62b7f44c0c2385f88915a137502df Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Jul 2022 00:44:28 +0200 Subject: [PATCH 0288/1204] Add .readthedocs.yaml --- .readthedocs.yaml | 30 ++++++++++++++++++++++++++++++ docs/requirements.txt | 2 ++ docs/source/conf.py | 2 ++ 3 files changed, 34 insertions(+) create mode 100644 .readthedocs.yaml create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..12a92ce6 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,30 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-20.04 + tools: + python: "3.9" + # You can also specify other tool versions: + nodejs: "16" + # rust: "1.55" + # golang: "1.17" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# If using Sphinx, optionally build your docs in additional formats such as PDF +formats: + - pdf + - epub + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..1d5e0d5b --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +myst-parser>=0.17 +sphinxcontrib-mermaid>=0.7.1 diff --git a/docs/source/conf.py b/docs/source/conf.py index 9c42bc9b..a715c183 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,6 +7,8 @@ project = 'PGPainless' copyright = '2022, Paul Schaub' author = 'Paul Schaub' +master_doc = 'index' + # https://protips.readthedocs.io/git-tag-version.html latest_tag = os.popen('git describe --abbrev=0').read().strip() release = latest_tag From 6f2b5ed1ca65a40ec6b2faa3b496d9e50f8e5a40 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 8 Jul 2022 00:21:40 +0200 Subject: [PATCH 0289/1204] Fix mermaid-cli cmd --- .gitignore | 2 +- .readthedocs.yaml | 12 +- .reuse/dep5 | 9 + docs/package-lock.json | 2626 --------------------- docs/package.json | 5 - docs/source/conf.py | 3 +- docs/source/ecosystem.md | 41 +- docs/source/ecosystem_dia.md | 38 + docs/source/ecosystem_dia.png | Bin 0 -> 110743 bytes docs/source/ecosystem_dia.svg | 1 + docs/source/index.rst | 2 +- docs/source/pgpainless-core/quickstart.md | 4 + docs/source/pgpainless-sop/quickstart.md | 101 +- docs/source/sop.md | 9 +- 14 files changed, 177 insertions(+), 2676 deletions(-) delete mode 100644 docs/package-lock.json delete mode 100644 docs/package.json create mode 100644 docs/source/ecosystem_dia.md create mode 100644 docs/source/ecosystem_dia.png create mode 100644 docs/source/ecosystem_dia.svg diff --git a/.gitignore b/.gitignore index 744e25e8..0a0ff0f1 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,4 @@ pgpainless-core/.settings/ push_html.sh -docs/node_modules +node_modules diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 12a92ce6..1d6088af 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,12 +8,22 @@ version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-20.04 + # apt_packages: + # - libgtk-3-0 + # - libasound2 + # - libnss3 + # - libxss1 + # - libgbm1 + # - libxshmfence1 tools: python: "3.9" # You can also specify other tool versions: - nodejs: "16" + # nodejs: "16" # rust: "1.55" # golang: "1.17" + # jobs: + # post_install: + # - npm install -g @mermaid-js/mermaid-cli # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/.reuse/dep5 b/.reuse/dep5 index f820f003..f7656261 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -9,6 +9,15 @@ Source: https://pgpainless.org # Copyright: $YEAR $NAME <$CONTACT> # License: ... +# Documentation +Files: docs/* +Copyright: 2022 Paul Schaub +License: CC-BY-3.0 + +Files: .readthedocs.yaml +Copyright: 2022 Paul Schaub +License: CC0-1.0 + # Gradle build tool Files: gradle* Copyright: 2015 the original author or authors. diff --git a/docs/package-lock.json b/docs/package-lock.json deleted file mode 100644 index c75cb36f..00000000 --- a/docs/package-lock.json +++ /dev/null @@ -1,2626 +0,0 @@ -{ - "name": "docs", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "dependencies": { - "@mermaid-js/mermaid-cli": "^9.1.3" - } - }, - "node_modules/@braintree/sanitize-url": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz", - "integrity": "sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w==" - }, - "node_modules/@mermaid-js/mermaid-cli": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@mermaid-js/mermaid-cli/-/mermaid-cli-9.1.3.tgz", - "integrity": "sha512-R7VFArRIhczOejWtKT2Ii8MVKayjpiY6hebGqtcmA8FGSUXDgB4QzK5z9zpOfh1k90XH0PzPpTlL4KXnFfDx1Q==", - "dependencies": { - "chalk": "^4.1.0", - "commander": "^9.0.0", - "mermaid": "^9.0.0", - "puppeteer": "^14.1.0" - }, - "bin": { - "mmdc": "index.bundle.js" - } - }, - "node_modules/@types/node": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", - "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==", - "optional": true - }, - "node_modules/@types/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "engines": { - "node": "*" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/commander": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz", - "integrity": "sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==", - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "dependencies": { - "node-fetch": "2.6.7" - } - }, - "node_modules/d3": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.6.1.tgz", - "integrity": "sha512-txMTdIHFbcpLx+8a0IFhZsbp+PfBBPt8yfbmukZTQFroKuFqIwqswF0qE5JXWefylaAVpSXFoKm3yP+jpNLFLw==", - "dependencies": { - "d3-array": "3", - "d3-axis": "3", - "d3-brush": "3", - "d3-chord": "3", - "d3-color": "3", - "d3-contour": "4", - "d3-delaunay": "6", - "d3-dispatch": "3", - "d3-drag": "3", - "d3-dsv": "3", - "d3-ease": "3", - "d3-fetch": "3", - "d3-force": "3", - "d3-format": "3", - "d3-geo": "3", - "d3-hierarchy": "3", - "d3-interpolate": "3", - "d3-path": "3", - "d3-polygon": "3", - "d3-quadtree": "3", - "d3-random": "3", - "d3-scale": "4", - "d3-scale-chromatic": "3", - "d3-selection": "3", - "d3-shape": "3", - "d3-time": "3", - "d3-time-format": "4", - "d3-timer": "3", - "d3-transition": "3", - "d3-zoom": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-array": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz", - "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-axis": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-brush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "3", - "d3-transition": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-chord": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "dependencies": { - "d3-path": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-collection": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", - "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-contour": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.0.tgz", - "integrity": "sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==", - "dependencies": { - "d3-array": "^3.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-delaunay": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", - "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", - "dependencies": { - "delaunator": "5" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "dependencies": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json.js", - "csv2tsv": "bin/dsv2dsv.js", - "dsv2dsv": "bin/dsv2dsv.js", - "dsv2json": "bin/dsv2json.js", - "json2csv": "bin/json2dsv.js", - "json2dsv": "bin/json2dsv.js", - "json2tsv": "bin/json2dsv.js", - "tsv2csv": "bin/dsv2dsv.js", - "tsv2json": "bin/dsv2json.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-fetch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "dependencies": { - "d3-dsv": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.0.1.tgz", - "integrity": "sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz", - "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-polygon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-random": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale-chromatic": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", - "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz", - "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==", - "dependencies": { - "d3-path": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz", - "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-voronoi": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", - "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dagre": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", - "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", - "dependencies": { - "graphlib": "^2.1.8", - "lodash": "^4.17.15" - } - }, - "node_modules/dagre-d3": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/dagre-d3/-/dagre-d3-0.6.4.tgz", - "integrity": "sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==", - "dependencies": { - "d3": "^5.14", - "dagre": "^0.8.5", - "graphlib": "^2.1.8", - "lodash": "^4.17.15" - } - }, - "node_modules/dagre-d3/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/dagre-d3/node_modules/d3": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz", - "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", - "dependencies": { - "d3-array": "1", - "d3-axis": "1", - "d3-brush": "1", - "d3-chord": "1", - "d3-collection": "1", - "d3-color": "1", - "d3-contour": "1", - "d3-dispatch": "1", - "d3-drag": "1", - "d3-dsv": "1", - "d3-ease": "1", - "d3-fetch": "1", - "d3-force": "1", - "d3-format": "1", - "d3-geo": "1", - "d3-hierarchy": "1", - "d3-interpolate": "1", - "d3-path": "1", - "d3-polygon": "1", - "d3-quadtree": "1", - "d3-random": "1", - "d3-scale": "2", - "d3-scale-chromatic": "1", - "d3-selection": "1", - "d3-shape": "1", - "d3-time": "1", - "d3-time-format": "2", - "d3-timer": "1", - "d3-transition": "1", - "d3-voronoi": "1", - "d3-zoom": "1" - } - }, - "node_modules/dagre-d3/node_modules/d3-array": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", - "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" - }, - "node_modules/dagre-d3/node_modules/d3-axis": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", - "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" - }, - "node_modules/dagre-d3/node_modules/d3-brush": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.6.tgz", - "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==", - "dependencies": { - "d3-dispatch": "1", - "d3-drag": "1", - "d3-interpolate": "1", - "d3-selection": "1", - "d3-transition": "1" - } - }, - "node_modules/dagre-d3/node_modules/d3-chord": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", - "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", - "dependencies": { - "d3-array": "1", - "d3-path": "1" - } - }, - "node_modules/dagre-d3/node_modules/d3-color": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", - "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" - }, - "node_modules/dagre-d3/node_modules/d3-contour": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", - "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", - "dependencies": { - "d3-array": "^1.1.1" - } - }, - "node_modules/dagre-d3/node_modules/d3-dispatch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", - "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" - }, - "node_modules/dagre-d3/node_modules/d3-drag": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", - "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", - "dependencies": { - "d3-dispatch": "1", - "d3-selection": "1" - } - }, - "node_modules/dagre-d3/node_modules/d3-dsv": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz", - "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", - "dependencies": { - "commander": "2", - "iconv-lite": "0.4", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json", - "csv2tsv": "bin/dsv2dsv", - "dsv2dsv": "bin/dsv2dsv", - "dsv2json": "bin/dsv2json", - "json2csv": "bin/json2dsv", - "json2dsv": "bin/json2dsv", - "json2tsv": "bin/json2dsv", - "tsv2csv": "bin/dsv2dsv", - "tsv2json": "bin/dsv2json" - } - }, - "node_modules/dagre-d3/node_modules/d3-ease": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz", - "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==" - }, - "node_modules/dagre-d3/node_modules/d3-fetch": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz", - "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", - "dependencies": { - "d3-dsv": "1" - } - }, - "node_modules/dagre-d3/node_modules/d3-force": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", - "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", - "dependencies": { - "d3-collection": "1", - "d3-dispatch": "1", - "d3-quadtree": "1", - "d3-timer": "1" - } - }, - "node_modules/dagre-d3/node_modules/d3-format": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", - "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" - }, - "node_modules/dagre-d3/node_modules/d3-geo": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", - "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", - "dependencies": { - "d3-array": "1" - } - }, - "node_modules/dagre-d3/node_modules/d3-hierarchy": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", - "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" - }, - "node_modules/dagre-d3/node_modules/d3-interpolate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", - "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", - "dependencies": { - "d3-color": "1" - } - }, - "node_modules/dagre-d3/node_modules/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" - }, - "node_modules/dagre-d3/node_modules/d3-polygon": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz", - "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" - }, - "node_modules/dagre-d3/node_modules/d3-quadtree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", - "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" - }, - "node_modules/dagre-d3/node_modules/d3-random": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", - "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" - }, - "node_modules/dagre-d3/node_modules/d3-scale": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", - "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", - "dependencies": { - "d3-array": "^1.2.0", - "d3-collection": "1", - "d3-format": "1", - "d3-interpolate": "1", - "d3-time": "1", - "d3-time-format": "2" - } - }, - "node_modules/dagre-d3/node_modules/d3-scale-chromatic": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", - "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", - "dependencies": { - "d3-color": "1", - "d3-interpolate": "1" - } - }, - "node_modules/dagre-d3/node_modules/d3-selection": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", - "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" - }, - "node_modules/dagre-d3/node_modules/d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "dependencies": { - "d3-path": "1" - } - }, - "node_modules/dagre-d3/node_modules/d3-time": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", - "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" - }, - "node_modules/dagre-d3/node_modules/d3-time-format": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", - "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", - "dependencies": { - "d3-time": "1" - } - }, - "node_modules/dagre-d3/node_modules/d3-timer": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", - "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" - }, - "node_modules/dagre-d3/node_modules/d3-transition": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz", - "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", - "dependencies": { - "d3-color": "1", - "d3-dispatch": "1", - "d3-ease": "1", - "d3-interpolate": "1", - "d3-selection": "^1.1.0", - "d3-timer": "1" - } - }, - "node_modules/dagre-d3/node_modules/d3-zoom": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", - "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", - "dependencies": { - "d3-dispatch": "1", - "d3-drag": "1", - "d3-interpolate": "1", - "d3-selection": "1", - "d3-transition": "1" - } - }, - "node_modules/dagre-d3/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/delaunator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", - "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", - "dependencies": { - "robust-predicates": "^3.0.0" - } - }, - "node_modules/devtools-protocol": { - "version": "0.0.1001819", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1001819.tgz", - "integrity": "sha512-G6OsIFnv/rDyxSqBa2lDLR6thp9oJioLsb2Gl+LbQlyoA9/OBAkrTU9jiCcQ8Pnh7z4d6slDiLaogR5hzgJLmQ==" - }, - "node_modules/dompurify": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz", - "integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==" - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/graphlib": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", - "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", - "dependencies": { - "lodash": "^4.17.15" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "engines": { - "node": ">=12" - } - }, - "node_modules/khroma": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", - "integrity": "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==" - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/mermaid": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.1.3.tgz", - "integrity": "sha512-jTIYiqKwsUXVCoxHUVkK8t0QN3zSKIdJlb9thT0J5jCnzXyc+gqTbZE2QmjRfavFTPPn5eRy5zaFp7V+6RhxYg==", - "dependencies": { - "@braintree/sanitize-url": "^6.0.0", - "d3": "^7.0.0", - "dagre": "^0.8.5", - "dagre-d3": "^0.6.4", - "dompurify": "2.3.8", - "graphlib": "^2.1.8", - "khroma": "^2.0.0", - "moment-mini": "^2.24.0", - "stylis": "^4.0.10" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" - }, - "node_modules/moment-mini": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment-mini/-/moment-mini-2.24.0.tgz", - "integrity": "sha512-9ARkWHBs+6YJIvrIp0Ik5tyTTtP9PoV0Ssu2Ocq5y9v8+NOOpWiRshAp8c4rZVWTOe+157on/5G+zj5pwIQFEQ==" - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/puppeteer": { - "version": "14.4.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-14.4.1.tgz", - "integrity": "sha512-+H0Gm84aXUvSLdSiDROtLlOofftClgw2TdceMvvCU9UvMryappoeS3+eOLfKvoy4sm8B8MWnYmPhWxVFudAOFQ==", - "hasInstallScript": true, - "dependencies": { - "cross-fetch": "3.1.5", - "debug": "4.3.4", - "devtools-protocol": "0.0.1001819", - "extract-zip": "2.0.1", - "https-proxy-agent": "5.0.1", - "pkg-dir": "4.2.0", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "rimraf": "3.0.2", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "ws": "8.7.0" - }, - "engines": { - "node": ">=14.1.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/robust-predicates": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz", - "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==" - }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/stylis": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.1.tgz", - "integrity": "sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ==" - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "dependencies": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/ws": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz", - "integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - } - }, - "dependencies": { - "@braintree/sanitize-url": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz", - "integrity": "sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w==" - }, - "@mermaid-js/mermaid-cli": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@mermaid-js/mermaid-cli/-/mermaid-cli-9.1.3.tgz", - "integrity": "sha512-R7VFArRIhczOejWtKT2Ii8MVKayjpiY6hebGqtcmA8FGSUXDgB4QzK5z9zpOfh1k90XH0PzPpTlL4KXnFfDx1Q==", - "requires": { - "chalk": "^4.1.0", - "commander": "^9.0.0", - "mermaid": "^9.0.0", - "puppeteer": "^14.1.0" - } - }, - "@types/node": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", - "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==", - "optional": true - }, - "@types/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", - "optional": true, - "requires": { - "@types/node": "*" - } - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - } - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "commander": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz", - "integrity": "sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "requires": { - "node-fetch": "2.6.7" - } - }, - "d3": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.6.1.tgz", - "integrity": "sha512-txMTdIHFbcpLx+8a0IFhZsbp+PfBBPt8yfbmukZTQFroKuFqIwqswF0qE5JXWefylaAVpSXFoKm3yP+jpNLFLw==", - "requires": { - "d3-array": "3", - "d3-axis": "3", - "d3-brush": "3", - "d3-chord": "3", - "d3-color": "3", - "d3-contour": "4", - "d3-delaunay": "6", - "d3-dispatch": "3", - "d3-drag": "3", - "d3-dsv": "3", - "d3-ease": "3", - "d3-fetch": "3", - "d3-force": "3", - "d3-format": "3", - "d3-geo": "3", - "d3-hierarchy": "3", - "d3-interpolate": "3", - "d3-path": "3", - "d3-polygon": "3", - "d3-quadtree": "3", - "d3-random": "3", - "d3-scale": "4", - "d3-scale-chromatic": "3", - "d3-selection": "3", - "d3-shape": "3", - "d3-time": "3", - "d3-time-format": "4", - "d3-timer": "3", - "d3-transition": "3", - "d3-zoom": "3" - } - }, - "d3-array": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.0.tgz", - "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==", - "requires": { - "internmap": "1 - 2" - } - }, - "d3-axis": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" - }, - "d3-brush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "requires": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "3", - "d3-transition": "3" - } - }, - "d3-chord": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "requires": { - "d3-path": "1 - 3" - } - }, - "d3-collection": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", - "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" - }, - "d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" - }, - "d3-contour": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.0.tgz", - "integrity": "sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==", - "requires": { - "d3-array": "^3.2.0" - } - }, - "d3-delaunay": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", - "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", - "requires": { - "delaunator": "5" - } - }, - "d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" - }, - "d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "requires": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - } - }, - "d3-dsv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "requires": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" - }, - "dependencies": { - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" - } - } - }, - "d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" - }, - "d3-fetch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "requires": { - "d3-dsv": "1 - 3" - } - }, - "d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "requires": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - } - }, - "d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" - }, - "d3-geo": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.0.1.tgz", - "integrity": "sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==", - "requires": { - "d3-array": "2.5.0 - 3" - } - }, - "d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==" - }, - "d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "requires": { - "d3-color": "1 - 3" - } - }, - "d3-path": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz", - "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==" - }, - "d3-polygon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==" - }, - "d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" - }, - "d3-random": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==" - }, - "d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "requires": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - } - }, - "d3-scale-chromatic": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", - "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", - "requires": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - } - }, - "d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" - }, - "d3-shape": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz", - "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==", - "requires": { - "d3-path": "1 - 3" - } - }, - "d3-time": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.0.0.tgz", - "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", - "requires": { - "d3-array": "2 - 3" - } - }, - "d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "requires": { - "d3-time": "1 - 3" - } - }, - "d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" - }, - "d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "requires": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - } - }, - "d3-voronoi": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", - "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" - }, - "d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "requires": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - } - }, - "dagre": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", - "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", - "requires": { - "graphlib": "^2.1.8", - "lodash": "^4.17.15" - } - }, - "dagre-d3": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/dagre-d3/-/dagre-d3-0.6.4.tgz", - "integrity": "sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==", - "requires": { - "d3": "^5.14", - "dagre": "^0.8.5", - "graphlib": "^2.1.8", - "lodash": "^4.17.15" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "d3": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz", - "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", - "requires": { - "d3-array": "1", - "d3-axis": "1", - "d3-brush": "1", - "d3-chord": "1", - "d3-collection": "1", - "d3-color": "1", - "d3-contour": "1", - "d3-dispatch": "1", - "d3-drag": "1", - "d3-dsv": "1", - "d3-ease": "1", - "d3-fetch": "1", - "d3-force": "1", - "d3-format": "1", - "d3-geo": "1", - "d3-hierarchy": "1", - "d3-interpolate": "1", - "d3-path": "1", - "d3-polygon": "1", - "d3-quadtree": "1", - "d3-random": "1", - "d3-scale": "2", - "d3-scale-chromatic": "1", - "d3-selection": "1", - "d3-shape": "1", - "d3-time": "1", - "d3-time-format": "2", - "d3-timer": "1", - "d3-transition": "1", - "d3-voronoi": "1", - "d3-zoom": "1" - } - }, - "d3-array": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", - "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" - }, - "d3-axis": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", - "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" - }, - "d3-brush": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.6.tgz", - "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==", - "requires": { - "d3-dispatch": "1", - "d3-drag": "1", - "d3-interpolate": "1", - "d3-selection": "1", - "d3-transition": "1" - } - }, - "d3-chord": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", - "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", - "requires": { - "d3-array": "1", - "d3-path": "1" - } - }, - "d3-color": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", - "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" - }, - "d3-contour": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", - "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", - "requires": { - "d3-array": "^1.1.1" - } - }, - "d3-dispatch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", - "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" - }, - "d3-drag": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", - "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", - "requires": { - "d3-dispatch": "1", - "d3-selection": "1" - } - }, - "d3-dsv": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz", - "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", - "requires": { - "commander": "2", - "iconv-lite": "0.4", - "rw": "1" - } - }, - "d3-ease": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz", - "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==" - }, - "d3-fetch": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz", - "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", - "requires": { - "d3-dsv": "1" - } - }, - "d3-force": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", - "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", - "requires": { - "d3-collection": "1", - "d3-dispatch": "1", - "d3-quadtree": "1", - "d3-timer": "1" - } - }, - "d3-format": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", - "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" - }, - "d3-geo": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", - "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", - "requires": { - "d3-array": "1" - } - }, - "d3-hierarchy": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", - "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" - }, - "d3-interpolate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", - "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", - "requires": { - "d3-color": "1" - } - }, - "d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" - }, - "d3-polygon": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz", - "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" - }, - "d3-quadtree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", - "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" - }, - "d3-random": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", - "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" - }, - "d3-scale": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", - "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", - "requires": { - "d3-array": "^1.2.0", - "d3-collection": "1", - "d3-format": "1", - "d3-interpolate": "1", - "d3-time": "1", - "d3-time-format": "2" - } - }, - "d3-scale-chromatic": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", - "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", - "requires": { - "d3-color": "1", - "d3-interpolate": "1" - } - }, - "d3-selection": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", - "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" - }, - "d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "requires": { - "d3-path": "1" - } - }, - "d3-time": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", - "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" - }, - "d3-time-format": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", - "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", - "requires": { - "d3-time": "1" - } - }, - "d3-timer": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", - "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" - }, - "d3-transition": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz", - "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", - "requires": { - "d3-color": "1", - "d3-dispatch": "1", - "d3-ease": "1", - "d3-interpolate": "1", - "d3-selection": "^1.1.0", - "d3-timer": "1" - } - }, - "d3-zoom": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", - "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", - "requires": { - "d3-dispatch": "1", - "d3-drag": "1", - "d3-interpolate": "1", - "d3-selection": "1", - "d3-transition": "1" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "delaunator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", - "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", - "requires": { - "robust-predicates": "^3.0.0" - } - }, - "devtools-protocol": { - "version": "0.0.1001819", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1001819.tgz", - "integrity": "sha512-G6OsIFnv/rDyxSqBa2lDLR6thp9oJioLsb2Gl+LbQlyoA9/OBAkrTU9jiCcQ8Pnh7z4d6slDiLaogR5hzgJLmQ==" - }, - "dompurify": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz", - "integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "requires": { - "@types/yauzl": "^2.9.1", - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - } - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "requires": { - "pend": "~1.2.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "requires": { - "pump": "^3.0.0" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graphlib": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", - "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", - "requires": { - "lodash": "^4.17.15" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" - }, - "khroma": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz", - "integrity": "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==" - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "mermaid": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-9.1.3.tgz", - "integrity": "sha512-jTIYiqKwsUXVCoxHUVkK8t0QN3zSKIdJlb9thT0J5jCnzXyc+gqTbZE2QmjRfavFTPPn5eRy5zaFp7V+6RhxYg==", - "requires": { - "@braintree/sanitize-url": "^6.0.0", - "d3": "^7.0.0", - "dagre": "^0.8.5", - "dagre-d3": "^0.6.4", - "dompurify": "2.3.8", - "graphlib": "^2.1.8", - "khroma": "^2.0.0", - "moment-mini": "^2.24.0", - "stylis": "^4.0.10" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" - }, - "moment-mini": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment-mini/-/moment-mini-2.24.0.tgz", - "integrity": "sha512-9ARkWHBs+6YJIvrIp0Ik5tyTTtP9PoV0Ssu2Ocq5y9v8+NOOpWiRshAp8c4rZVWTOe+157on/5G+zj5pwIQFEQ==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "requires": { - "wrappy": "1" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "requires": { - "find-up": "^4.0.0" - } - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" - }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "puppeteer": { - "version": "14.4.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-14.4.1.tgz", - "integrity": "sha512-+H0Gm84aXUvSLdSiDROtLlOofftClgw2TdceMvvCU9UvMryappoeS3+eOLfKvoy4sm8B8MWnYmPhWxVFudAOFQ==", - "requires": { - "cross-fetch": "3.1.5", - "debug": "4.3.4", - "devtools-protocol": "0.0.1001819", - "extract-zip": "2.0.1", - "https-proxy-agent": "5.0.1", - "pkg-dir": "4.2.0", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "rimraf": "3.0.2", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "ws": "8.7.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "robust-predicates": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz", - "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==" - }, - "rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "stylis": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.1.tgz", - "integrity": "sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ==" - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "requires": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "requires": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "ws": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz", - "integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==", - "requires": {} - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - } - } -} diff --git a/docs/package.json b/docs/package.json deleted file mode 100644 index 96ba9fc2..00000000 --- a/docs/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "@mermaid-js/mermaid-cli": "^9.1.3" - } -} diff --git a/docs/source/conf.py b/docs/source/conf.py index a715c183..7c6a130e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -48,7 +48,6 @@ html_theme = 'sphinx_rtd_theme' #epub_show_urls = 'footnote' latex_show_urls = 'footnote' -mermaid_cmd = "./node_modules/.bin/mmdc" # 'raw' does not work for epub and pdf, neither does 'svg' mermaid_output_format = 'png' -mermaid_params = ['--theme', 'default', '--width', '800', '--backgroundColor', 'transparent'] +mermaid_params = ['--theme', 'default', '--width', '1600', '--backgroundColor', 'transparent'] diff --git a/docs/source/ecosystem.md b/docs/source/ecosystem.md index 30d8d6a3..cd88cfd8 100644 --- a/docs/source/ecosystem.md +++ b/docs/source/ecosystem.md @@ -4,44 +4,11 @@ PGPainless consists of an ecosystem of different libraries and projects. The diagram below shows, how the different projects relate to one another. -```{mermaid} -flowchart LR - subgraph SOP-JAVA - sop-java-picocli-->sop-java - end - subgraph PGPAINLESS - pgpainless-sop-->pgpainless-core - pgpainless-sop-->sop-java - pgpainless-cli-->pgpainless-sop - pgpainless-cli-->sop-java-picocli - end - subgraph WKD-JAVA - wkd-java-cli-->wkd-java - wkd-test-suite-->wkd-java - wkd-test-suite-->pgpainless-core - end - subgraph CERT-D-JAVA - pgp-cert-d-java-->pgp-certificate-store - pgp-cert-d-java-jdbc-sqlite-lookup-->pgp-cert-d-java - end - subgraph CERT-D-PGPAINLESS - pgpainless-cert-d-->pgpainless-core - pgpainless-cert-d-->pgp-cert-d-java - pgpainless-cert-d-cli-->pgpainless-cert-d - pgpainless-cert-d-cli-->pgp-cert-d-java-jdbc-sqlite-lookup - end - subgraph VKS-JAVA - vks-java-cli-->vks-java - end - subgraph PGPEASY - pgpeasy-->pgpainless-cli - pgpeasy-->wkd-java-cli - pgpeasy-->vks-java-cli - pgpeasy-->pgpainless-cert-d-cli - end - wkd-java-cli-->pgpainless-cert-d - wkd-java-->pgp-certificate-store +![Ecosystem](ecosystem_dia.*) + ## Libraries and Tools diff --git a/docs/source/ecosystem_dia.md b/docs/source/ecosystem_dia.md new file mode 100644 index 00000000..6469faaa --- /dev/null +++ b/docs/source/ecosystem_dia.md @@ -0,0 +1,38 @@ +```mermaid +flowchart LR + subgraph SOP-JAVA + sop-java-picocli-->sop-java + end + subgraph PGPAINLESS + pgpainless-sop-->pgpainless-core + pgpainless-sop-->sop-java + pgpainless-cli-->pgpainless-sop + pgpainless-cli-->sop-java-picocli + end + subgraph WKD-JAVA + wkd-java-cli-->wkd-java + wkd-test-suite-->wkd-java + wkd-test-suite-->pgpainless-core + end + subgraph CERT-D-JAVA + pgp-cert-d-java-->pgp-certificate-store + pgp-cert-d-java-jdbc-sqlite-lookup-->pgp-cert-d-java + end + subgraph CERT-D-PGPAINLESS + pgpainless-cert-d-->pgpainless-core + pgpainless-cert-d-->pgp-cert-d-java + pgpainless-cert-d-cli-->pgpainless-cert-d + pgpainless-cert-d-cli-->pgp-cert-d-java-jdbc-sqlite-lookup + end + subgraph VKS-JAVA + vks-java-cli-->vks-java + end + subgraph PGPEASY + pgpeasy-->pgpainless-cli + pgpeasy-->wkd-java-cli + pgpeasy-->vks-java-cli + pgpeasy-->pgpainless-cert-d-cli + end + wkd-java-cli-->pgpainless-cert-d + wkd-java-->pgp-certificate-store +``` \ No newline at end of file diff --git a/docs/source/ecosystem_dia.png b/docs/source/ecosystem_dia.png new file mode 100644 index 0000000000000000000000000000000000000000..70872a9f97b0918aef1108c8cb78a4bae603d44a GIT binary patch literal 110743 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

Aq#)kEz)VC2N< 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%0A&-k7dr4E!zH$VqYQ(9QmRLwNn% z=YW;gC5Rcfc^qqAX)En+6504L*^7&bL8O^!%+zRW0;qVP*DeM=J$3Rjrn6?GcBp*i z`e@C{bX#L@>Gzt%K`n&#r?K6pHaQ)T>@1l0j+dXZzYY@uI;~LlTF>Z8tb^T~zwR5J zEq9m@k#@GM*}s23D@<=^0^)pF|8+jBmkl1&2KnWv;Ctp4N{zdIEiHmgxw5<_oE3b! zEAh7kIlNxc5Jh`D6PtuX(>CSLXOoGL`y<6Is6`wSCPCSjZU6%-8TPmtDSamf#~b7o z_tcu~c`N+F+nx^|Vnt8u&7r{wnbw!0xZ*taSqjLVIBk1`7!DW z3d#CYQa+mz%hMwEwd&uR1WVJp$7k-sx92h7!(V)WV!{{R)FIvJt?9`~{jK$dYO_GE zvB^l~ui+J>eySw{LL49Ipyp#qAS^Saw z8uBgf&lk5_!=?F)S1}ljgRepI=eML?zvE}D(~&C2i*c)iU#%r zdM3%SOXSp0dY`92A6Uz#s8P*=Cs}BME@>TF-KQUBp3nR1uRv*!p82wWglZ;@l)^0P|7u%g&=fQ>9< z!*~`0+p0$NWgphF{XavDM(wb}cQzKAdIw4ta>OjgA;ZNUVkxR^wdGh?DU9EV(w)8_DJcn}vawcn#xOTU+o`L>_MBF+b)R7s z78TYp+)sCf43Br^PT2;}`|OE$a^rKCuG$}eGAp_Y@oX6MY%OoM$)AIaG<>%0V5{4; zo!nBRu0%OM$aRaVxZdygAE>%mweikrVzin1Yotl~gpbm@1)2lP*|vzA{KSnXYLlx^ z>?$H7Os%NjUQ%Q!J44UT%PsVaVP)KW>2_Ti_``7thZHtV5K;q;G2jfsjz!RH&32)l zn%V-A$I`5VvgbzVs=u=fZJAFcY3KDe{Q$_ay@N)}hf%YCg3U2NBC4pNVhA;CB-Q`g zWb6JMP%{!F?v}4faNJy~S23?gedp0NL%jOs6=M#@Js#xGh$Q#yUH{)4YEMJ;-YB{q&i;U|-j)#5P-lO5vr1_=ZeCOdA`qwkrIr5UojWQH=Jo!V{mO9(sIn_H zPnuQSD4$N6Uqty=BE(IBjjewKKJuV$^v2bUZ?5yp4%6=WC{v^6$NxUs{|fm-Zv8*V zCI7G8Zf}(elV)ZD3LpJ4{te<2ir(8?Bwm#0z8gyiP@TGs{|r~wEQ!h-f_-Pqd~cbZ jB=+xY|9AgA+qS#U<#NV=jF`_Y

PGPEASY
VKS-JAVA
CERT-D-PGPAINLESS
CERT-D-JAVA
WKD-JAVA
PGPAINLESS
SOP-JAVA
pgpeasy
vks-java
vks-java-cli
pgpainless-cert-d
pgpainless-cert-d-cli
pgp-certificate-store
pgp-cert-d-java
pgp-cert-d-java-jdbc-sqlite-lookup
wkd-java
wkd-java-cli
wkd-test-suite
pgpainless-core
pgpainless-sop
pgpainless-cli
sop-java
sop-java-picocli
\ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 19390f15..a6618021 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -20,6 +20,6 @@ Contents .. toctree:: - quickstart.md ecosystem.md + quickstart.md sop.md diff --git a/docs/source/pgpainless-core/quickstart.md b/docs/source/pgpainless-core/quickstart.md index c1d39d47..4be4f59e 100644 --- a/docs/source/pgpainless-core/quickstart.md +++ b/docs/source/pgpainless-core/quickstart.md @@ -2,6 +2,10 @@ Coming soon. +:::{note} +This chapter is work in progress. +::: + ### Setup bla diff --git a/docs/source/pgpainless-sop/quickstart.md b/docs/source/pgpainless-sop/quickstart.md index 10a06541..765cc42d 100644 --- a/docs/source/pgpainless-sop/quickstart.md +++ b/docs/source/pgpainless-sop/quickstart.md @@ -35,7 +35,7 @@ dependencies { ``` :::{important} -Replace `XYZ` with the current version, e.g. {{ env.config.version }}! +Replace `XYZ` with the current version, in this case {{ env.config.version }}! ::: The entry point to the API is the `SOP` interface, for which `pgpainless-sop` provides a concrete implementation @@ -366,4 +366,101 @@ prior to calling `data(_)`. The `SigningResult` object you got back in both cases contains information about the signature. -### Verify a Signature \ No newline at end of file +### Verify a Signature + +In order to verify signed messages, there are two API endpoints available. + +#### Inline and Cleartext Signatures + +To verify inline-signed messages, or messages that make use of the cleartext signature framework, +use the `inlineVerify()` API: + +```java +byte[] signingCert = ...; +byte[] signedMessage = ...; + +ReadyWithResult> readyWithResult = sop.inlineVerify() + .cert(signingCert) + .data(signedMessage); +``` + +The `cert(_)` method MUST be called at least once. It takes either a byte array or an `InputStream` containing +an OpenPGP certificate. +If you are not sure, which certificate was used to sign the message, you can provide multiple certificates. + +It is also possible to reject signatures that were not made within a certain time window by calling +`notBefore(Date timestamp)` and/or `notAfter(Date timestamp)`. +Signatures made before the `notBefore(_)` or after the `notAfter(_)` constraints will be rejected. + +You can now either write out the plaintext message to an `OutputStream`... + +```java +OutputStream out = ...; +List verifications = readyWithResult.writeTo(out); +``` + +... or you can acquire the plaintext message as a byte array directly: + +```java +ByteArrayAndResult> bytesAndResult = readyWithResult.toByteArrayAndResult(); +byte[] plaintextMessage = bytesAndResult.getBytes(); +List verifications = bytesAndResult.getResult(); +``` + +In both cases, the plaintext message will have the signatures stripped. + +#### Detached Signatures + +To verify detached signatures (signatures that come separate from the message itself), you can use the +`detachedVerify()` API: + +```java +byte[] signingCert = ...; +byte[] message = ...; +byte[] detachedSignature = ...; + +List verifications = sop.detachedVerify() + .cert(signingCert) + .signatures(detachedSignature) + .data(signedMessage); +``` + +You can provide one or more OpenPGP certificates using `cert(_)`, providing either a byte array or an `InputStream`. + +The detached signatures need to be provided separately using the `signatures(_)` method call. +You can provide as many detached signatures as you like, and those can be binary or ASCII armored. + +Like with Inline Signatures, you can constrain the time window for signature validity using +`notAfter(_)` and `notBefore(_)`. + +#### Verifications + +In all above cases, the `verifications` list will contain `Verification` objects for each verifiable, valid signature. +Those objects contain information about the signatures: +`verification.getSigningCertFingerprint()` will return the fingerprint of the certificate that created the signature. +`verification.getSigningKeyFingerprint()` will return the fingerprint of the used signing subkey within that certificate. + +### Detach Signatures from Messages + +It is also possible, to detach inline or cleartext signatures from signed messages to transform them into +detached signatures. +The same way you can turn inline or cleartext signed messages into plaintext messages. + +To detach signatures from messages, use the `inlineDetach()` API: + +```java +byte[] signedMessage = ...; + +ReadyWithResult readyWithResult = sop.inlineDetach() + .message(signedMessage); +ByteArrayAndResult bytesAndResult = readyWithResult.toByteArrayAndResult(); + +byte[] plaintext = bytesAndResult.getBytes(); +Signatures signatures = bytesAndResult.getResult(); +byte[] encodedSignatures = signatures.getBytes(); +``` + +By default, the signatures output will be ASCII armored. This can be disabled by calling `noArmor()` +prior to `message(_)`. + +The detached signatures can now be verified like in the section above. diff --git a/docs/source/sop.md b/docs/source/sop.md index 5dbf6dc7..d092f50b 100644 --- a/docs/source/sop.md +++ b/docs/source/sop.md @@ -1,3 +1,10 @@ # Stateless OpenPGP Protocol (SOP) -Lorem ipsum dolor sit amet. \ No newline at end of file +The [Stateless OpenPGP Protocol](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) +(short *SOP*) is a specification of a standardized command line interface for a limited set of OpenPGP operations. + +By standardizing the interface, users are able to choose between different, compatible implementations. + +:::{note} +This chapter is work in progress. +::: \ No newline at end of file From 6169a37086e8c8804e5eac90e8022aaaa53803de Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 8 Jul 2022 00:29:43 +0200 Subject: [PATCH 0290/1204] Add readthedocs badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 271024b0..92a3e3ee 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ SPDX-License-Identifier: Apache-2.0 [![Interoperability Test-Suite](https://badgen.net/badge/Sequoia%20Test%20Suite/%232/green)](https://tests.sequoia-pgp.org/) [![PGP](https://img.shields.io/badge/pgp-A027%20DB2F%203E1E%20118A-blue)](https://keyoxide.org/7F9116FEA90A5983936C7CFAA027DB2F3E1E118A) [![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/pgpainless)](https://api.reuse.software/info/github.com/pgpainless/pgpainless) +[![Documentation Status](https://readthedocs.org/projects/pgpainless/badge/?version=latest)](https://pgpainless.readthedocs.io/en/latest/?badge=latest) **PGPainless is an easy-to-use OpenPGP library for Java and Android applications** From ac52c4bbc55a249236dc60651bb466af49a153db Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 8 Jul 2022 00:32:18 +0200 Subject: [PATCH 0291/1204] Fix documentation link to verifications section --- docs/source/pgpainless-sop/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/pgpainless-sop/quickstart.md b/docs/source/pgpainless-sop/quickstart.md index 765cc42d..8c8aabb4 100644 --- a/docs/source/pgpainless-sop/quickstart.md +++ b/docs/source/pgpainless-sop/quickstart.md @@ -239,7 +239,7 @@ If you provided the senders certificate for the purpose of signature verificatio probably want to check, if the message was actually signed by the sender by checking `result.getVerifications()`. :::{note} -Signature verification will be discussed in more detail in section [](#verify-a-signature) +Signature verification will be discussed in more detail in section [](#verifications) ::: If the message was encrypted symmetrically using a password, you can also decrypt is symmetrically by calling From 3842aa9cedef5376e1ae1826b8d8e2339f9882cc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 8 Jul 2022 15:08:45 +0200 Subject: [PATCH 0292/1204] Add test to explore behavior when dealing with V3 keys --- .../org/pgpainless/key/V3KeyBehaviorTest.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/V3KeyBehaviorTest.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/V3KeyBehaviorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/V3KeyBehaviorTest.java new file mode 100644 index 00000000..a1d768ae --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/V3KeyBehaviorTest.java @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * V3 keys are not supported by PGPainless. + * However, some basic functions like parsing the keys or converting a secret key to a certificate still work. + */ +public class V3KeyBehaviorTest { + + private static final String V3Cert = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "mQCNA2JqgDIAAAEEAOYdcIKFQ5ZWBx0D5DKwMMNFcIhFyqmfDJ0v23ehMxOkXN/o\n" + + "HO/43+dq6ZqQn0gNw53Tp9no+EmcCYNrZuN0C4Zu8XHSyY6UB+CqzNkz/CwmV10E\n" + + "dRDipcG1O6scJyy2MWpuOG67til+o+wOLgEkkVkSW8Bl2oqtzVVP4swtKLRZAAUR\n" + + "tClKb2huIFEuIFNtaXRoIDwxMjM0NS42Nzg5QGNvbXB1c2VydmUuY29tPokAlQMF\n" + + "EGJqgDJVT+LMLSi0WQEBgiwEALKQnuzza+oIgp7CAukW6qhUaOV/Cf3P4bWhru+v\n" + + "8bED+YUOvgTytnXK1QUxQJ/PLnYV860NBRVR46kCtpZDgl+NeQe4O5lxbZVGHZy1\n" + + "P+FUcbvUaA5ZQEfcR5cBJKcWO9RUTf28SMSyJ1ozFm0yPmOa2J5MwHylIbVAlc9c\n" + + "ag3J\n" + + "=GebS\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + private static final String V3Key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "lQHYA2JqgDIAAAEEAOYdcIKFQ5ZWBx0D5DKwMMNFcIhFyqmfDJ0v23ehMxOkXN/o\n" + + "HO/43+dq6ZqQn0gNw53Tp9no+EmcCYNrZuN0C4Zu8XHSyY6UB+CqzNkz/CwmV10E\n" + + "dRDipcG1O6scJyy2MWpuOG67til+o+wOLgEkkVkSW8Bl2oqtzVVP4swtKLRZAAUR\n" + + "AAP+JBiyRqt+DYr8GKE85NBX9nlS6DMaxUYgGKgibR5OSVsJjIjNUtG0sNmODjTN\n" + + "sPMZqlNln6wS3l7APMWNoStNGc9JG9Puz3eR2W69lPDzhuxuxrHIUBO+3UlEQB/p\n" + + "N3NPhnwCjh3OWHSMM6rzsX5ExUv0Z4FypnzvMG1x6GRJDVECAO6PyY8NDHsktMVN\n" + + "HAdgC61iIOz+GbLhNGeikuB+DQpSoyckAF0N5reBxRbyjzNZQ7aVvWpxigUp5OdK\n" + + "HMK7YcwTAgD275bcqhd+oWHDhyesi6RVswlqGfix48qahf9wOmDkc0nzp8evy/4V\n" + + "4Qu5zUJGVzi4aEIbFaAnc5lMD9/ydTNjAf485vh4MDFRd3tPvx9mPrHQgaArCBX8\n" + + "9oImPDk0oaKixwSIFzXeg1qZQeLiwv26Fs8gawWsLVZpR4+zZc1nhZlGnrQpSm9o\n" + + "biBRLiBTbWl0aCA8MTIzNDUuNjc4OUBjb21wdXNlcnZlLmNvbT6JAJUDBRBiaoAy\n" + + "VU/izC0otFkBAYIsBACykJ7s82vqCIKewgLpFuqoVGjlfwn9z+G1oa7vr/GxA/mF\n" + + "Dr4E8rZ1ytUFMUCfzy52FfOtDQUVUeOpAraWQ4JfjXkHuDuZcW2VRh2ctT/hVHG7\n" + + "1GgOWUBH3EeXASSnFjvUVE39vEjEsidaMxZtMj5jmtieTMB8pSG1QJXPXGoNyQ==\n" + + "=p7Lr\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + @Test + public void readV3PublicKey() throws IOException { + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(V3Cert); + assertEquals(3, cert.getPublicKey().getVersion()); + assertEquals("John Q. Smith <12345.6789@compuserve.com>", cert.getPublicKey().getUserIDs().next()); + } + + @Test + public void readV3SecretKey() throws IOException { + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(V3Key); + assertEquals(3, key.getPublicKey().getVersion()); + assertEquals("John Q. Smith <12345.6789@compuserve.com>", key.getPublicKey().getUserIDs().next()); + } + + @Test + public void extractV3Cert() throws IOException { + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(V3Key); + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(V3Cert); + + PGPPublicKeyRing extractedCert = PGPainless.extractCertificate(key); + assertArrayEquals(cert.getEncoded(), extractedCert.getEncoded()); + } + + @Test + public void v3FingerprintNotSupported() throws IOException { + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(V3Key); + assertThrows(IllegalArgumentException.class, () -> OpenPgpFingerprint.of(key)); + + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(V3Cert); + assertThrows(IllegalArgumentException.class, () -> OpenPgpFingerprint.of(cert)); + } +} From a131fe32aa241d535938b45bd8eee7cd41671efc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 8 Jul 2022 15:57:43 +0200 Subject: [PATCH 0293/1204] Extend sphinx documentation --- docs/source/conf.py | 5 +- docs/source/pgpainless-core/quickstart.md | 112 ++++++++++++++++++++-- docs/source/pgpainless-sop/quickstart.md | 2 +- 3 files changed, 108 insertions(+), 11 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7c6a130e..adea465d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,7 +15,10 @@ release = latest_tag version = release myst_substitutions = { - "repo_host" : "codeberg.org" # or 'github.com' + "repo_host" : "codeberg.org", # or 'github.com' +# "repo_host" : "github.com", + "repo_pgpainless_src" : "codeberg.org/pgpainless/pgpainless/src/branch", +# "repo_pgpainless_src" : "github.com/pgpainless/pgpainless/tree", } # -- General configuration diff --git a/docs/source/pgpainless-core/quickstart.md b/docs/source/pgpainless-core/quickstart.md index 4be4f59e..e20a53ec 100644 --- a/docs/source/pgpainless-core/quickstart.md +++ b/docs/source/pgpainless-core/quickstart.md @@ -1,31 +1,125 @@ ## PGPainless API with pgpainless-core -Coming soon. +The `pgpainless-core` module contains the bulk of the actual OpenPGP implementation. :::{note} This chapter is work in progress. ::: ### Setup -bla + +PGPainless' releases are published to and can be fetched from Maven Central. +To get started, you first need to include `pgpainless-core` in your projects build script: + +``` +// If you use Gradle +... +dependencies { + ... + implementation "org.pgpainless:pgpainless-core:XYZ" + ... +} + +// If you use Maven +... + + ... + + org.pgpainless + pgpainless-core + XYZ + + ... + +``` + +This will automatically pull in PGPainless' dependencies, such as Bouncy Castle. + +:::{important} +Replace `XYZ` with the current version, in this case {{ env.config.version }}! +::: + +The entry point to the API is the `PGPainless` class. +For many common use-cases, examples can be found in the +{{ '[examples package](https://{}/main/pgpainless-core/src/test/java/org/pgpainless/example)'.format(repo_pgpainless_src) }}. +There is a very good chance that you can find code examples there that fit your needs. + +### Read and Write Keys +Reading keys from ASCII armored strings or from binary files is easy: + +```java +String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"...; +PGPSecretKeyRing secretKey = PGPainless.readKeyRing() + .secretKeyRing(key); +``` + +Similarly, keys or certificates can quickly be exported: + +```java +// ASCII armored key +PGPSecretKeyRing secretKey = ...; +String armored = PGPainless.asciiArmor(secretKey); + +// binary (unarmored) key +byte[] binary = secretKey.getEncoded(); +``` ### Generate a Key -bla +PGPainless comes with a simple to use `KeyRingBuilder` class that helps you to quickly generate modern OpenPGP keys. +There are some predefined key archetypes, but it is possible to fully customize the key generation to fit your needs. + +```java +// EdDSA primary key with EdDSA signing- and XDH encryption subkeys +PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Romeo ", "thisIsAPassword"); + +// RSA key without additional subkeys +PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .simpleRsaKeyRing("Juliet ", RsaLength._4096); +``` + +To generate a customized key, use `PGPainless.buildKeyRing()` instead: + +```java +// Customized key +PGPSecretKeyRing keyRing = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder( + RSA.withLength(RsaLength._8192), + KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER) + .overrideCompressionAlgorithms(CompressionAlgorithm.ZLIB) + ).addSubkey( + KeySpec.getBuilder(ECDSA.fromCurve(EllipticCurve._P256), KeyFlag.SIGN_DATA) + ).addSubkey( + KeySpec.getBuilder( + ECDH.fromCurve(EllipticCurve._P256), + KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE) + ).addUserId("Juliet ") + .addUserId("xmpp:juliet@capulet.lit") + .setPassphrase(Passphrase.fromPassword("romeo_oh_Romeo<3")) + .build(); +``` + +As you can see, it is possible to generate all kinds of different keys. ### Extract a Certificate -bla +If you have a secret key, you might want to extract a public key certificate from it: + +```java +PGPSecretKeyRing secretKey = ...; +PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); +``` ### Apply / Remove ASCII Armor -bla +TODO ### Encrypt a Message -bla +TODO ### Decrypt a Message -bla +TODO ### Sign a Message -bla +TODO ### Verify a Signature -bla +TODO diff --git a/docs/source/pgpainless-sop/quickstart.md b/docs/source/pgpainless-sop/quickstart.md index 8c8aabb4..ecd7d81d 100644 --- a/docs/source/pgpainless-sop/quickstart.md +++ b/docs/source/pgpainless-sop/quickstart.md @@ -239,7 +239,7 @@ If you provided the senders certificate for the purpose of signature verificatio probably want to check, if the message was actually signed by the sender by checking `result.getVerifications()`. :::{note} -Signature verification will be discussed in more detail in section [](#verifications) +Signature verification will be discussed in more detail in section "Verifications". ::: If the message was encrypted symmetrically using a password, you can also decrypt is symmetrically by calling From 7169b369b320b751a2a191f465af58cbae515ac5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 8 Jul 2022 17:45:58 +0200 Subject: [PATCH 0294/1204] Add documentation about documentation to documentation readme heh --- docs/README.md | 19 +++++++++++++------ docs/source/conf.py | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/README.md b/docs/README.md index 1ed74654..73f97fab 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,16 +1,23 @@ # User Guide for PGPainless +Documentation for PGPainless is built from Markdown using Sphinx and MyST. + +A built version of the documentation is available on http://pgpainless.rtfd.io/ + +## Useful resources + +* [Sphix Documentation Generator](https://www.sphinx-doc.org/en/master/) +* [MyST Markdown Syntax](https://myst-parser.readthedocs.io/en/latest/index.html) + ## Build the Guide +To build: + ```shell $ make {html|epub|latexpdf} ``` -Note: Building requires `mermaid-cli` to be installed in this directory: +Note: Building diagrams from source requires `mermaid-cli` to be installed. ```shell -$ # Move here -$ cd pgpainless/docs -$ npm install @mermaid-js/mermaid-cli +$ npm install -g @mermaid-js/mermaid-cli ``` - -TODO: This is ugly. Install mermaid-cli globally? Perhaps point to user-installed mermaid-cli in conf.py's mermaid_cmd \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index adea465d..aeb057b3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,7 +15,7 @@ release = latest_tag version = release myst_substitutions = { - "repo_host" : "codeberg.org", # or 'github.com' + "repo_host" : "codeberg.org", # "repo_host" : "github.com", "repo_pgpainless_src" : "codeberg.org/pgpainless/pgpainless/src/branch", # "repo_pgpainless_src" : "github.com/pgpainless/pgpainless/tree", From b217b8b218ad7810ec784256fced46d76b397eb5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 8 Jul 2022 18:26:45 +0200 Subject: [PATCH 0295/1204] cli: Use dedicated shadow plugin for building fat jar 'gradle shadowJar' can be used to build a fat jar 'gradle jar' now only builds slim jar --- pgpainless-cli/README.md | 4 ++-- pgpainless-cli/build.gradle | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pgpainless-cli/README.md b/pgpainless-cli/README.md index f3cf2861..3cf0c97f 100644 --- a/pgpainless-cli/README.md +++ b/pgpainless-cli/README.md @@ -13,14 +13,14 @@ PGPainless-CLI is an implementation of the [Stateless OpenPGP Command Line Inter It plugs `pgpainless-sop` into `sop-java-picocli`. ## Build -To build an executable, `gradle jar` should be sufficient. The resulting jar file can be found in `pgpainless-sop/build/libs/`. +To build an executable, `gradle shadowJar` should be sufficient. The resulting jar file can be found in `pgpainless-cli/build/libs/`. ## Execute The jar file produced in the step above is executable as is. ``` -java -jar pgpainless-cli-XXX.jar help +java -jar pgpainless-cli-XXX-all.jar help ``` Alternatively you can use the provided `./pgpainless-cli` script to directly build and execute PGPainless' Stateless Command Line Interface from within Gradle. diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle index 7317524b..3d9a6a09 100644 --- a/pgpainless-cli/build.gradle +++ b/pgpainless-cli/build.gradle @@ -4,6 +4,7 @@ plugins { id 'application' + id "com.github.johnrengelman.shadow" version "6.1.0" } def generatedVersionDir = "${buildDir}/generated-version" @@ -51,7 +52,7 @@ mainClassName = 'org.pgpainless.cli.PGPainlessCLI' application { mainClass = mainClassName } - +/** jar { duplicatesStrategy(DuplicatesStrategy.EXCLUDE) manifest { @@ -66,6 +67,7 @@ jar { exclude "META-INF/*.RSA" } } + */ run { // https://stackoverflow.com/questions/59445306/pipe-into-gradle-run @@ -76,4 +78,4 @@ run { } } -tasks."jar".dependsOn(":pgpainless-core:assemble", ":pgpainless-sop:assemble") +// tasks."jar".dependsOn(":pgpainless-core:assemble", ":pgpainless-sop:assemble") From 1bd2d68a1178758e271f6daa6a12f8f892b73fff Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 8 Jul 2022 18:57:03 +0200 Subject: [PATCH 0296/1204] Start pgpainless-cli usage guide --- docs/source/index.rst | 1 + docs/source/pgpainless-cli/usage.md | 78 +++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 docs/source/pgpainless-cli/usage.md diff --git a/docs/source/index.rst b/docs/source/index.rst index a6618021..cbe82355 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,4 +22,5 @@ Contents ecosystem.md quickstart.md + pgpainless-cli/usage.md sop.md diff --git a/docs/source/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md new file mode 100644 index 00000000..3e7d5055 --- /dev/null +++ b/docs/source/pgpainless-cli/usage.md @@ -0,0 +1,78 @@ +# User Guide PGPainless-CLI + +The module `pgpainless-cli` contains a command line application which conforms to the +[Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/). + +You can use it to generate keys, encrypt, sign and decrypt messages, as well as verify signatures. + +## Implementation + +Essentially, `pgpainless-cli` is just a very small composing module, which injects `pgpainless-sop` as a +concrete implementation of `sop-java` into `sop-java-picocli`. + +## Build + +To build a standalone *fat*-jar: +```shell +$ cd pgpainless-cli/ +$ gradle shadowJar +``` + +The fat-jar can afterwards be found in `build/libs/`. + +To build a [distributable](https://docs.gradle.org/current/userguide/distribution_plugin.html): + +```shell +$ cd pgpainless-cli/ +$ gradle installDist +``` + +Afterwards, an uncompressed distributable is installed in `build/install/`. +To execute the application, you can call `build/install/bin/pgpainless-cli{.bat}` + +## Usage + +Hereafter, the program will be referred to as `pgpainless-cli`. + +``` +$ pgpainless-cli help +Stateless OpenPGP Protocol +Usage: pgpainless-cli [COMMAND] + +Commands: + help Stateless OpenPGP Protocol + armor Stateless OpenPGP Protocol + dearmor Stateless OpenPGP Protocol + decrypt Stateless OpenPGP Protocol + inline-detach Stateless OpenPGP Protocol + encrypt Stateless OpenPGP Protocol + extract-cert Stateless OpenPGP Protocol + generate-key Stateless OpenPGP Protocol + sign Stateless OpenPGP Protocol + verify Stateless OpenPGP Protocol + inline-sign Stateless OpenPGP Protocol + inline-verify Stateless OpenPGP Protocol + version Stateless OpenPGP Protocol + +Exit Codes: + 0 Successful program execution. + 1 Generic program error + 3 Verification requested but no verifiable signature found + 13 Unsupported asymmetric algorithm + 17 Certificate is not encryption capable + 19 Usage error: Missing argument + 23 Incomplete verification instructions + 29 Unable to decrypt + 31 Password is not human-readable + 37 Unsupported Option + 41 Invalid data or data of wrong type encountered + 53 Non-text input received where text was expected + 59 Output file already exists + 61 Input file does not exist + 67 Cannot unlock password protected secret key + 69 Unsupported subcommand + 71 Unsupported special prefix (e.g. "@env/@fd") of indirect parameter + 73 Ambiguous input (a filename matching the designator already exists) + 79 Key is not signing capable +Powered by picocli +``` \ No newline at end of file From 520fcd7cbf044485786db1bb82416ce8b13e95ac Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 10 Jul 2022 22:44:48 +0200 Subject: [PATCH 0297/1204] Fix help text in pgpainless-cli documentation --- docs/source/pgpainless-cli/usage.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/source/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md index 3e7d5055..d1694ea7 100644 --- a/docs/source/pgpainless-cli/usage.md +++ b/docs/source/pgpainless-cli/usage.md @@ -40,19 +40,20 @@ Stateless OpenPGP Protocol Usage: pgpainless-cli [COMMAND] Commands: - help Stateless OpenPGP Protocol - armor Stateless OpenPGP Protocol - dearmor Stateless OpenPGP Protocol - decrypt Stateless OpenPGP Protocol - inline-detach Stateless OpenPGP Protocol - encrypt Stateless OpenPGP Protocol - extract-cert Stateless OpenPGP Protocol - generate-key Stateless OpenPGP Protocol - sign Stateless OpenPGP Protocol - verify Stateless OpenPGP Protocol - inline-sign Stateless OpenPGP Protocol - inline-verify Stateless OpenPGP Protocol - version Stateless OpenPGP Protocol + help Display usage information for the specified subcommand + armor Add ASCII Armor to standard input + dearmor Remove ASCII Armor from standard input + decrypt Decrypt a message from standard input + inline-detach Split signatures from a clearsigned message + encrypt Encrypt a message from standard input + extract-cert Extract a public key certificate from a secret key from + standard input + generate-key Generate a secret key + sign Create a detached signature on the data from standard input + verify Verify a detached signature over the data from standard input + inline-sign Create an inline-signed message from data on standard input + inline-verify Verify inline-signed data from standard input + version Display version information about the tool Exit Codes: 0 Successful program execution. From 52c8439da5abc36f6995653ba4531e76f8c185c7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 10 Jul 2022 23:02:00 +0200 Subject: [PATCH 0298/1204] Prevent third-party assigned user-ids from being accidentally returned as primary user-id Fixes #293 --- .../main/java/org/pgpainless/key/info/KeyRingInfo.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 7999b592..b69e301b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -289,16 +289,16 @@ public class KeyRingInfo { return null; } - String firstUserId = userIds.get(0); - if (userIds.size() == 1) { - return firstUserId; - } - + String firstUserId = null; for (String userId : userIds) { PGPSignature certification = signatures.userIdCertifications.get(userId); if (certification == null) { continue; } + + if (firstUserId == null) { + firstUserId = userId; + } Date creationTime = certification.getCreationTime(); if (certification.getHashedSubPackets().isPrimaryUserID()) { From 50d31eb46364e9a0eecdf2174cbbf5afea484682 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 11 Jul 2022 14:15:54 +0200 Subject: [PATCH 0299/1204] KeyRingTemplates: Add methods taking Passphrase as argument --- .../key/generation/KeyRingTemplates.java | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java index e2cf7190..444e7d74 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java @@ -58,7 +58,7 @@ public final class KeyRingTemplates { */ public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull String userId, @Nonnull RsaLength length) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId, length, null); + return simpleRsaKeyRing(userId, length, Passphrase.emptyPassphrase()); } /** @@ -96,13 +96,19 @@ public final class KeyRingTemplates { */ public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull String userId, @Nonnull RsaLength length, String password) throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { + Passphrase passphrase = Passphrase.emptyPassphrase(); + if (!isNullOrEmpty(password)) { + passphrase = Passphrase.fromPassword(password); + } + return simpleRsaKeyRing(userId, length, passphrase); + } + + public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull String userId, @Nonnull RsaLength length, @Nonnull Passphrase passphrase) + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { KeyRingBuilder builder = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) - .addUserId(userId); - - if (!isNullOrEmpty(password)) { - builder.setPassphrase(Passphrase.fromPassword(password)); - } + .addUserId(userId) + .setPassphrase(passphrase); return builder.build(); } @@ -139,7 +145,7 @@ public final class KeyRingTemplates { */ public PGPSecretKeyRing simpleEcKeyRing(@Nonnull String userId) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId, null); + return simpleEcKeyRing(userId, Passphrase.emptyPassphrase()); } /** @@ -177,14 +183,20 @@ public final class KeyRingTemplates { */ public PGPSecretKeyRing simpleEcKeyRing(@Nonnull String userId, String password) throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { + Passphrase passphrase = Passphrase.emptyPassphrase(); + if (!isNullOrEmpty(password)) { + passphrase = Passphrase.fromPassword(password); + } + return simpleEcKeyRing(userId, passphrase); + } + + public PGPSecretKeyRing simpleEcKeyRing(@Nonnull String userId, @Nonnull Passphrase passphrase) + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { KeyRingBuilder builder = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) - .addUserId(userId); - - if (!isNullOrEmpty(password)) { - builder.setPassphrase(Passphrase.fromPassword(password)); - } + .addUserId(userId) + .setPassphrase(passphrase); return builder.build(); } From df7505eadba46aa6dffd0ccdfa04abcfcbe31028 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 11 Jul 2022 16:11:40 +0200 Subject: [PATCH 0300/1204] Add more documentation --- docs/source/index.rst | 1 + docs/source/pgpainless-core/edit_keys.md | 1 + docs/source/pgpainless-core/generate_keys.md | 101 +++++++++++++++++++ docs/source/pgpainless-core/indepth.rst | 14 +++ docs/source/pgpainless-core/passphrase.md | 60 +++++++++++ docs/source/pgpainless-core/quickstart.md | 25 +---- docs/source/pgpainless-core/userids.md | 33 ++++++ 7 files changed, 213 insertions(+), 22 deletions(-) create mode 100644 docs/source/pgpainless-core/edit_keys.md create mode 100644 docs/source/pgpainless-core/generate_keys.md create mode 100644 docs/source/pgpainless-core/indepth.rst create mode 100644 docs/source/pgpainless-core/passphrase.md create mode 100644 docs/source/pgpainless-core/userids.md diff --git a/docs/source/index.rst b/docs/source/index.rst index cbe82355..362d340d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -24,3 +24,4 @@ Contents quickstart.md pgpainless-cli/usage.md sop.md + pgpainless-core/indepth.rst \ No newline at end of file diff --git a/docs/source/pgpainless-core/edit_keys.md b/docs/source/pgpainless-core/edit_keys.md new file mode 100644 index 00000000..d7ed93e7 --- /dev/null +++ b/docs/source/pgpainless-core/edit_keys.md @@ -0,0 +1 @@ +# Edit Keys diff --git a/docs/source/pgpainless-core/generate_keys.md b/docs/source/pgpainless-core/generate_keys.md new file mode 100644 index 00000000..2b84ce1e --- /dev/null +++ b/docs/source/pgpainless-core/generate_keys.md @@ -0,0 +1,101 @@ +# PGPainless In-Depth: Generate Keys + +There are two API endpoints for generating OpenPGP keys using `pgpainless-core`: + +`PGPainless.generateKeyRing()` presents a selection of pre-configured OpenPGP key archetypes: + +```java +// Modern, EC-based OpenPGP key with dedicated primary certification key +// This method is recommended by the authors +PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() + .modernKeyRing( + "Alice ", + Passphrase.fromPassword("sw0rdf1sh")); + +// Simple, EC-based OpenPGP key with combined certification and signing key +// plus encryption subkey +PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() + .simpleEcKeyRing( + "Alice ", + Passphrase.fromPassword("0r4ng3")); + +// Simple, RSA OpenPGP key made of a single RSA key used for all operations +PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() + .simpleRsaKeyRing( + "Alice ", + RsaLength._4096, Passphrase.fromPassword("m0nk3y")): +``` + +If you have special requirements on algorithms you can use `PGPainless.buildKeyRing()` instead, which offers more +control over parameters: + +```java +// Customized key + +// Specification for primary key +KeySpecBuilder primaryKeySpec = KeySpec.getBuilder( + KeyType.RSA(RsaLength._8192), // 8192 bits RSA key + KeyFlag.CERTIFY_OTHER) // used for certification + // optionally override algorithm preferences + .overridePreferredCompressionAlgorithms(CompressionAlgorithm.ZLIB) + .overridePreferredHashAlgorithms(HashAlgorithm.SHA512, HashAlgorithm.SHA384) + .overridePreferredSymmetricKeyAlgorithms(SymmetricKeyAlgorithm.AES256); + +// Specification for a signing subkey +KeySpecBuilder signingSubKeySpec = KeySpec.getBuilder( + KeyType.ECDSA(EllipticCurve._P256), // P-256 ECDSA key + KeyFlag.SIGN_DATA); // Used for signing + +// Specification for an encryption subkey +KeySpecBuilder encryptionSubKeySpec = KeySpec.getBuilder( + KeyType.ECDH(EllipticCurve._P256), + KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE); + +// Build the key itself +PGPSecretKeyRing secretKey = PGPainless.buildKeyRing() + .setPrimaryKey(primaryKeySpec) + .addSubkey(signingSubKeySpec) + .addSubkey(encryptionSubKeySpec) + .addUserId("Juliet ") // Primary User-ID + .addUserId("xmpp:juliet@capulet.lit") // Additional User-ID + .setPassphrase(Passphrase.fromPassword("romeo_oh_Romeo<3")) // passphrase protection + .build(); +``` + +To specify, which algorithm to use for a single (sub) key, `KeySpec.getBuilder(_)` can be used, passing a `KeyType`, +as well as some `KeyFlag`s as argument. + +`KeyType` defines an algorithm and its parameters, e.g. RSA with a certain key size, or ECDH over a certain +elliptic curve. +Currently, PGPainless supports the following `KeyType`s: +* `KeyType.RSA(_)`: Signing, Certification, Encryption +* `KeyType.ECDH(_)`: Encryption +* `KeyType.ECDSA(_)`: Signing, Certification +* `KeyType.EDDSA(_)`: Signing, Certification +* `KeyType.XDH(_)`: Encryption + +The `KeyFlag`s are used to specify, how the key will be used later on. A signing key can only be used for signing, +if it carries the `KeyFlag.SIGN_DATA`. +A key can carry multiple key flags. + +It is possible to override the default algorithm preferences used by PGPainless with custom preferences. +An algorithm preference list contains algorithms from most to least preferred. + +Every OpenPGP key MUST have a primary key. The primary key MUST be capable of certification, so you MUST use an +algorithm that can be used to generate signatures. +The primary key can be set by calling `setPrimaryKey(primaryKeySpec)`. + +Furthermore, an OpenPGP key can contain zero or more subkeys. +Those can be set by repeatedly calling `addSubkey(subkeySpec)`. + +OpenPGP keys are usually bound to User-IDs like names and/or email addresses. +There can be multiple user-ids bound to a key, in which case the very first User-ID will be marked as primary. +To add a User-ID to the key, call `addUserId(userId)`. + +By default, keys do not have an expiration date. This can be changed by setting an expiration date using +`setExpirationDate(date)`. + +To enable password protection for the OpenPGP key, you can call `setPassphrase(passphrase)`. +If this method is not called, or if the passed in `Passphrase` is empty, the key will be unprotected. + +Finally, calling `build()` will generate a fresh OpenPGP key according to the specifications given. \ No newline at end of file diff --git a/docs/source/pgpainless-core/indepth.rst b/docs/source/pgpainless-core/indepth.rst new file mode 100644 index 00000000..b1c4beba --- /dev/null +++ b/docs/source/pgpainless-core/indepth.rst @@ -0,0 +1,14 @@ +In-Depth Guide to pgpainless-core +================================= + +This is an in-depth introduction to OpenPGP using PGPainless. +If you are looking for a quickstart introduction instead, check out [](quickstart.md). + +Contents +-------- + +.. toctree:: + generate_keys.md + edit_keys.md + userids.md + passphrase.md diff --git a/docs/source/pgpainless-core/passphrase.md b/docs/source/pgpainless-core/passphrase.md new file mode 100644 index 00000000..27769aad --- /dev/null +++ b/docs/source/pgpainless-core/passphrase.md @@ -0,0 +1,60 @@ +# Passwords + +In Java based applications, passing passwords as `String` objects has the +[disadvantage](https://stackoverflow.com/a/8881376/11150851) that you have to rely on garbage collection to clean up +once they are no longer used. +For that reason, `char[]` is the preferred method for dealing with passwords. +Once a password is no longer used, the character array can simply be overwritten to remove the sensitive data from +memory. + +## Passphrase +PGPainless uses a wrapper class `Passphrase`, which takes care for the wiping of unused passwords: + +```java +Passphrase passphrase = new Passphrase(new char[] {'h', 'e', 'l', 'l', 'o'}); +assertTrue(passphrase.isValid()); + +assertArrayEquals(new char[] {'h', 'e', 'l', 'l', 'o'}, passphrase.getChars()): + +// Once we are done, we can clean the data +passphrase.clear(); + +assertFalse(passphrase.isValid()); +assertNull(passphrase.getChars()); +``` + +Furthermore, `Passphrase` can also wrap empty passphrases, which increases null-safety of the API: + +```java +Passphrase empty = Passphrase.emptyPassphrase(); +assertTrue(empty.isValid()); +assertTrue(empty.isEmpty()); +assertNull(empty.getChars()); + +empty.clear(); + +assertFalse(empty.isValid()); +``` + +## SecretKeyRingProtector + +There are certain operations that require you to provide the passphrase for a key. +Examples are decryption of messages, or creating signatures / certifications. + +The primary way of telling PGPainless, which password to use for a certain key is the `SecretKeyRingProtector` +interface. +There are multiple implementations of this interface, which may or may not suite your needs: + +```java +// If your key is not password protected, this implementation is for you: +SecretKeyRingProtector unprotected = SecretKeyRingProtector + .unprotectedKeys(); + +// If you use a single passphrase for all (sub-) keys, take this: +SecretKeyRingProtector singlePassphrase = SecretKeyRingProtector + .unlockAnyKeyWith(passphrase); + +// If you want to be flexible, use this: +CachingSecretKeyRingProtector flexible = SecretKeyRingProtector + .defaultSecretKeyRingProtector(passphraseCallback); +``` \ No newline at end of file diff --git a/docs/source/pgpainless-core/quickstart.md b/docs/source/pgpainless-core/quickstart.md index e20a53ec..f2a018ef 100644 --- a/docs/source/pgpainless-core/quickstart.md +++ b/docs/source/pgpainless-core/quickstart.md @@ -2,6 +2,8 @@ The `pgpainless-core` module contains the bulk of the actual OpenPGP implementation. +This is a quickstart guide. For more in-depth exploration of the API, checkout [](indepth.md). + :::{note} This chapter is work in progress. ::: @@ -65,7 +67,7 @@ byte[] binary = secretKey.getEncoded(); ``` ### Generate a Key -PGPainless comes with a simple to use `KeyRingBuilder` class that helps you to quickly generate modern OpenPGP keys. +PGPainless comes with a method to quickly generate modern OpenPGP keys. There are some predefined key archetypes, but it is possible to fully customize the key generation to fit your needs. ```java @@ -78,27 +80,6 @@ PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .simpleRsaKeyRing("Juliet ", RsaLength._4096); ``` -To generate a customized key, use `PGPainless.buildKeyRing()` instead: - -```java -// Customized key -PGPSecretKeyRing keyRing = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder( - RSA.withLength(RsaLength._8192), - KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER) - .overrideCompressionAlgorithms(CompressionAlgorithm.ZLIB) - ).addSubkey( - KeySpec.getBuilder(ECDSA.fromCurve(EllipticCurve._P256), KeyFlag.SIGN_DATA) - ).addSubkey( - KeySpec.getBuilder( - ECDH.fromCurve(EllipticCurve._P256), - KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE) - ).addUserId("Juliet ") - .addUserId("xmpp:juliet@capulet.lit") - .setPassphrase(Passphrase.fromPassword("romeo_oh_Romeo<3")) - .build(); -``` - As you can see, it is possible to generate all kinds of different keys. ### Extract a Certificate diff --git a/docs/source/pgpainless-core/userids.md b/docs/source/pgpainless-core/userids.md new file mode 100644 index 00000000..ff2db82c --- /dev/null +++ b/docs/source/pgpainless-core/userids.md @@ -0,0 +1,33 @@ +# User-IDs + +User-IDs are identities that users go by. A User-ID might be a name, an email address or both. +User-IDs can also contain both and even have a comment. + +In general, the format of a User-ID is not fixed, so it can contain arbitrary strings. +However, it is agreed upon to use the +Below is a selection of possible User-IDs: + +``` +Firstname Lastname [Comment] +Firstname Lastname +Firstname Lastname [Comment] + + [Comment] +``` + +PGPainless comes with a builder class `UserId`, which can be used to safely construct User-IDs: + +```java +UserId nameAndEMail = UserId.nameAndEmail("Jane Doe", "jane@pgpainless.org"); +assertEquals("Jane Doe ", nameAndEmail.toString()): + +UserId onlyEmail = UserId.onlyEmail("john@pgpainless.org"); +assertEquals("", onlyEmail.toString()); + +UserId full = UserId.newBuilder() + .withName("Peter Pattern") + .withEmail("peter@pgpainless.org") + .withComment("Work Address") + .build(); +assertEquals("Peter Pattern [Work Address]", full.toString()); +``` \ No newline at end of file From 56abb5175778ba44f4ef609515680ab8a46e9997 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 12 Jul 2022 08:49:30 +0200 Subject: [PATCH 0301/1204] Small formatting changes of doc index --- docs/source/index.rst | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 362d340d..a29e6c03 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,17 +1,23 @@ PGPainless - Painless OpenPGP ============================= -**OpenPGP** (`RFC 4480 `_) is an Internet Standard mostly used for email encryption. +**OpenPGP** (`RFC 4480 `_) is an Internet Standard mostly used for email +encryption. It provides mechanisms to ensure *confidentiality*, *integrity* and *authenticity* of messages. -However, OpenPGP can also be used for other purposes, such as secure messaging or as a signature mechanism for software distribution. +However, OpenPGP can also be used for other purposes, such as secure messaging or as a signature mechanism for +software distribution. -**PGPainless** strives to improve the (currently pretty dire) state of the ecosystem of Java libraries and tooling for OpenPGP. +**PGPainless** strives to improve the (currently pretty dire) state of the ecosystem of Java libraries and tooling +for OpenPGP. The library focuses on being easy and intuitive to use without getting into your way. -Common functions such as creating keys, encrypting data, and so on are implemented using a builder structure that guides you through the necessary steps. +Common functions such as creating keys, encrypting data, and so on are implemented using a builder structure that +guides you through the necessary steps. -Internally, it is based on `Bouncy Castles `_ mighty, but low-level OpenPGP API. -PGPainless' goal is to empower you to use OpenPGP without needing to write all the boilerplate code required by Bouncy Castle. +Internally, it is based on `Bouncy Castles `_ mighty, but low-level ``bcpg`` +OpenPGP API. +PGPainless' goal is to empower you to use OpenPGP without needing to write all the boilerplate code required by +Bouncy Castle. It aims to be secure by default while allowing customization if required. From 223cf009fc1e58d826c0a2e990229059613eb3d4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 12 Jul 2022 10:33:43 +0200 Subject: [PATCH 0302/1204] Fix User-ID format in documentation and note invalid user-id formats in tests --- docs/source/pgpainless-core/userids.md | 7 +++---- .../test/java/org/pgpainless/key/info/KeyRingInfoTest.java | 2 ++ .../java/org/pgpainless/signature/SignatureUtilsTest.java | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/source/pgpainless-core/userids.md b/docs/source/pgpainless-core/userids.md index ff2db82c..78126828 100644 --- a/docs/source/pgpainless-core/userids.md +++ b/docs/source/pgpainless-core/userids.md @@ -8,11 +8,10 @@ However, it is agreed upon to use the Below is a selection of possible User-IDs: ``` -Firstname Lastname [Comment] +Firstname Lastname (Comment) Firstname Lastname -Firstname Lastname [Comment] +Firstname Lastname (Comment) - [Comment] ``` PGPainless comes with a builder class `UserId`, which can be used to safely construct User-IDs: @@ -29,5 +28,5 @@ UserId full = UserId.newBuilder() .withEmail("peter@pgpainless.org") .withComment("Work Address") .build(); -assertEquals("Peter Pattern [Work Address]", full.toString()); +assertEquals("Peter Pattern (Work Address) ", full.toString()); ``` \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java index 43273315..f49d5a46 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java @@ -704,6 +704,8 @@ public class KeyRingInfoTest { @Test public void getEmailsTest() throws IOException { + // NOTE: The User-ID Format for the ID "Alice Anderson [Primary Mail Address]" is incorrect. + // TODO: Fix? String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: B4A8 9FE8 9D59 31E6 BCF7 DC2F 6BA1 2CC7 9A08 8D73\n" + diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java index ac67d2de..f57ed2c4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java @@ -119,6 +119,7 @@ public class SignatureUtilsTest { "-----END PGP PUBLIC KEY BLOCK-----\n"; String aliceId = "Alice "; + // TODO: Fix wrong user-id format of pet name String charliesPetNameForAlice = "Alice Example [from work]"; long aliceKeyId = 1059762964264170602L; From 4730ac427b461c0de1354b4c7ce78c855aa4b9fd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 13 Jul 2022 14:54:16 +0200 Subject: [PATCH 0303/1204] Add test for #298 --- ...dDoesNotBreakEncryptionCapabilityTest.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/modification/FixUserIdDoesNotBreakEncryptionCapabilityTest.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/FixUserIdDoesNotBreakEncryptionCapabilityTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/FixUserIdDoesNotBreakEncryptionCapabilityTest.java new file mode 100644 index 00000000..1995b8bf --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/FixUserIdDoesNotBreakEncryptionCapabilityTest.java @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test for #298. + */ +public class FixUserIdDoesNotBreakEncryptionCapabilityTest { + + private static final String SECRET_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "lFgEYsyc4hYJKwYBBAHaRw8BAQdAjm3bQ61H2E6/xzjjHjl6G+mNl72r7fwdux9f\n" + + "CXQrCpoAAQDwY5Vblm+7Dq8NfP5gqThyv+23aMBYLr3UgJAZyAgu/RDBtCQoQilv\n" + + "YiAoSilvaG5zb24gPGJqQGV2YWx1YXRpb24udGVzdD6IkAQTFggAOBYhBI70BlHo\n" + + "XvYV3ufIc8MDl+w8xmx4BQJizJziAhsjBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA\n" + + "AAoJEMMDl+w8xmx4ZAMBAIZsBqoClMlwymvNWIENCAZMQSy9NpBABk3jDyEjbhgs\n" + + "AP9sGI7URQNUDXiV+sIzvastNX/nOZ7fkwp6Xrx+74WxC5xdBGLMnOISCisGAQQB\n" + + "l1UBBQEBB0CGU2EGdS4mvy0apuPukStWSqEDH16AFSGEeTt2GyN1IQMBCAcAAP9J\n" + + "nrIGndqzxxIUHVsoImYIu9SFl9Z1tCSia6mADTtbsA88iHgEGBYIACAWIQSO9AZR\n" + + "6F72Fd7nyHPDA5fsPMZseAUCYsyc4gIbDAAKCRDDA5fsPMZseACnAQDIR7QwBTIs\n" + + "Hfu4XIpZTyipOy6ZOEKlY3akyb9TtOq1wAD8Da+0Insssuf0J5WPqShJ/wMX3+xk\n" + + "gqeRV2HyogQ7aAE=\n" + + "=6zZo\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + private static final String CERTIFICATE = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "mDMEYsyc4hYJKwYBBAHaRw8BAQdAjm3bQ61H2E6/xzjjHjl6G+mNl72r7fwdux9f\n" + + "CXQrCpq0JChCKW9iIChKKW9obnNvbiA8YmpAZXZhbHVhdGlvbi50ZXN0PoiQBBMW\n" + + "CAA4FiEEjvQGUehe9hXe58hzwwOX7DzGbHgFAmLMnOICGyMFCwkIBwIGFQoJCAsC\n" + + "BBYCAwECHgECF4AACgkQwwOX7DzGbHhkAwEAhmwGqgKUyXDKa81YgQ0IBkxBLL02\n" + + "kEAGTeMPISNuGCwA/2wYjtRFA1QNeJX6wjO9qy01f+c5nt+TCnpevH7vhbELuDgE\n" + + "Ysyc4hIKKwYBBAGXVQEFAQEHQIZTYQZ1Lia/LRqm4+6RK1ZKoQMfXoAVIYR5O3Yb\n" + + "I3UhAwEIB4h4BBgWCAAgFiEEjvQGUehe9hXe58hzwwOX7DzGbHgFAmLMnOICGwwA\n" + + "CgkQwwOX7DzGbHgApwEAyEe0MAUyLB37uFyKWU8oqTsumThCpWN2pMm/U7TqtcAA\n" + + "/A2vtCJ7LLLn9CeVj6koSf8DF9/sZIKnkVdh8qIEO2gB\n" + + "=3sNT\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + private static final String userIdBefore = "(B)ob (J)ohnson "; + private static final String userIdAfter = "\"(B)ob (J)ohnson\" "; + + @Test + public void replaceUserIdWithFixedVersionDoesNotHinderEncryptionCapability() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPSecretKeyRing modified = PGPainless.modifyKeyRing(secretKeys) + .addUserId(userIdAfter, new SelfSignatureSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { + hashedSubpackets.setPrimaryUserId(); + } + }, protector) + .removeUserId(userIdBefore, protector) + .done(); + + KeyRingInfo before = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo after = PGPainless.inspectKeyRing(modified); + + assertTrue(before.isUsableForEncryption()); + assertTrue(before.isUsableForSigning()); + assertTrue(before.isUserIdValid(userIdBefore)); + assertFalse(before.isUserIdValid(userIdAfter)); + + assertTrue(after.isUsableForEncryption()); + assertTrue(after.isUsableForSigning()); + assertFalse(after.isUserIdValid(userIdBefore)); + assertTrue(after.isUserIdValid(userIdAfter)); + } +} From 2ad67a85fbead82e593a975427a17726de82c174 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 15 Jul 2022 13:20:23 +0200 Subject: [PATCH 0304/1204] Add test to make sure we do not allow unencrypted as sym alg preference --- ...upidAlgorithmPreferenceEncryptionTest.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/generation/StupidAlgorithmPreferenceEncryptionTest.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/StupidAlgorithmPreferenceEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/StupidAlgorithmPreferenceEncryptionTest.java new file mode 100644 index 00000000..42aa0856 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/StupidAlgorithmPreferenceEncryptionTest.java @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class StupidAlgorithmPreferenceEncryptionTest { + + // RSA key with symmetric algorithm preference "NULL" (unencrypted). + private static final String STUPID_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 6CA6 C4F3 CF01 CAC9 C954 0BD6 5BDA 78ED C479 A8E9\n" + + "Comment: This is Stupid\n" + + "\n" + + "lQcYBGLRTLUBEADg2W4x7HgoBDJS7qYCqheyY/J/SNKJ7AvPTTd8V7S9rxZ6kZlo\n" + + "556q1+KJA+/jWJ3LFvuNCyTW/utQxxLYylDFTaI4KJ5MWWLtvlph+VjolI/+u8B6\n" + + "qSeZl72uXiEFW8vnQfBliXOyWCudLHh4cRj/DgxEbL3Cm9FXH0/XYRKweSPRhOkX\n" + + "Mh/veiTe7a6RcwGOi0DNcRth5g4MggspgHQmNcHq2XGeOOpQUeJXPjNsKZIShI3M\n" + + "nYVz78xqVNdn2xJjUq6H/bREV5fDn73xWcNCxyNSl7rvk7kcYNKf977MgoVPSUlN\n" + + "sOZ19/YFMU+Bexqsdvzf/txn9BzgUhwufpeUrSFLIwiy+X7fY+pURR4G4hpeg1eD\n" + + "xAl5FFRAtDqk7C6XlaP9j0qxZmbYrTfqbIS5+u0iJ93nO3CIWE87pnzhZH9E00jM\n" + + "H6e6IV4op8pcG+6mvg1NPeJg6uwpUwzmbmnvF85WtQ4QVmOSPjFigZPSkNf/4qNm\n" + + "05IP8mCGNd7Xl+J4I6JGVNjhB9Vk9/3wbVrtjD00EAkTgiSmjT2lt5TiE6rVjQJ4\n" + + "krvte70UNH4yuoGOap1w993HmcYSObe4Q3JBhLA7F9lnprAi+kL8gc0SFEAk5tOX\n" + + "H4Cqnk4ml01IamGOzXa01p//1NrxyMVYefD1+JT6X4wogUzvwLJqTehICQARAQAB\n" + + "AA/+MRiI7TG7EtHHw0AE07QcNIGKY6yc/Cykb4FmyinEd16Rw/Wiz7szdA5rkotf\n" + + "h/7DhaLhDm0OgDttWlf9j4StmkdXUnfcCMPDzDGyPo5ZkX9O6cpJPv9MfEcbzcUT\n" + + "5L2kijxlp2YZ8yk5bLpXG8VmNdr1ZsNvs9yeGy3lGxxBHnN1FLy2wK/bNUkwX9T6\n" + + "NxwrjNpvLeyyk+/NxYFnuoon0mgOjZ8pJek7kIowp+gXBlkVYiG7bKBAkY4czmL0\n" + + "HeNB4podLeiBwiJ2KuroaJi3AA/HcLNcyA8zbjTeCLvp13Hwdd2EuggUalHYUE3y\n" + + "FE2zB1F76dUWf0RYQcrsCGLv6cf3uu2yxNttUlzKrni1lcwvA6ZyauUuLg8E9chi\n" + + "oT5zkpxnEdR1eVRBZQDWGXQuXpDiDxAu/bMyW3uhLRLKhP2D0+m0TeCBsIwU5Fdz\n" + + "nn13IjY/zsUUQT2rxCSnU4ooFXkzT2FTVAKbX+0/raFS6JJsQI4wJPoFMm5QycwR\n" + + "kWRiKUzcDTLKXtyISwZvPaQnomuExdKJFaW/+FEtYgqyfFYHR7Nmo1e7HxkEjmkm\n" + + "IxYMOApNXAkwBR8nMdUzchN0C6fogCUk+gj41eU4s6yzHjiRvmt85/N8T+34+MUk\n" + + "xRCtytNEseTvfs4vFPKksz9QK8vUpjIhXdxVrr/Asl4At5kIAOTZ9Psq/jQydg9u\n" + + "4A4X5JHTPszw9GKo43u020PPxgOe6nuYlF0sWBzjPoKvkkN4mEOYi5SRmmnChrZV\n" + + "r82QEsjhQToxE0I4PYnhXJgNlNn4aNCK4a3oD1rqQQC4DE/hDtogFQIovqiLT3cS\n" + + "kdcLcnUs2G7QYEdXWPXowdclxQQwlpCjd9VO2UNdNITmkloT5nZ0WLik323kEF7z\n" + + "3cZXj/uRnPa/Y5whFa1AA7cPIDht5B+BXw+TEzeTkmjxO/GEVMdJl8unUIyt+G5N\n" + + "FKsFG86nezLT9m77Yu3rrA/z+uRa0vVot63is8Spiuc6hkRBFlC6xsBVnC/OZnrf\n" + + "dG+Fk2MIAPuF7x82gAdX+M1u1kvrAR1Ze0fy+CYkvUokA+q0C7hsc1Gdh2bemBWf\n" + + "Dt+pr3Td2xn2YfCb8lSF6+o8p8qe4kkq2eNLd1/k5SSA1SSrP2J97ByhWdX6tNnf\n" + + "mT37izMdS2mRVjBcVR1zdhwaXN3m5+bqUpggrmuz9lwemnUPfSn5shO0YomQp9z1\n" + + "I1dkuIAF+InCZH7NyNWAoVLJ0Bk7MPQTZNVn/iEsfDWcPQEpumF1aY8+cpvUqeEZ\n" + + "r8nPsZ1r2P2WAjC7o9uYEL+47AAFMxD3Ps8GHR9cG9GSVn88NetNQ22Vm2GafRYi\n" + + "Y+Fny5cfxWTLUSldQuehwmLsJZXp0KMIAKL7XxabHx5B0h5leCdB6y82fz3TTQV2\n" + + "khwodAQ06itzG0fhMAF0O87KFDOq9p+Bx4n6MzHunEL2rJuF+KLOAMnyHRVQ40a1\n" + + "V/8JnsjCViceicLKWVQgxlBIFFAlHRoHrAc2g1Sup96zt5kmojNhEtFeJNQkQcpa\n" + + "x7atpyrlD2j8oMdr0dJluQcSGhS3/Y/LLCWtNbijykGDfBMQn0tHEgESwVx5muIj\n" + + "Hn5yfRIA0/MBg9Py9H4r52Ab6XBs7mDZSrmOoXcCzQY3RgykP01Q8/ipIo7C/Yi6\n" + + "aun8QzBLvykeRFfEu1SM523jLMn66CLqKDQR+8IxDgORFwXhwSfGVVl2iLQOVGhp\n" + + "cyBpcyBTdHVwaWSJAkgEEwEKADwFAmLRTLUJEFvaeO3EeajpFiEEbKbE888BysnJ\n" + + "VAvWW9p47cR5qOkCngECmw8FFgIDAQACCwACFQoCmQEAABRQEACNK0cNjEUcOEJh\n" + + "cfG2Hjs6oWClklV9eSNcVQU7S3wT+xr+WwV5S8TJaZtLlvsnp9ZaS4u5/1zY1+ll\n" + + "Ahzclg5BU4jihs4N+aBWRf2XovnH8cEHEneu9pXs0Sunj/DVCfHRuRe5Z8Vng9SN\n" + + "rofxSfvL9PD5zTwWLHJUZD+yTxI9hmj3G37OTVQbmhtjXGZ0IRaa5fjO9FTwUske\n" + + "fhB/7TIwrSDByhj857uF1j4d1i3WjSvps3FuVBuUYc+RLEm8QgkGWGu/QHjvkmUe\n" + + "fM1onDXL8JHYX0ULZ1s/sH5oTUXVH6ZQOdFIQeENeYSDZN5P5bDzshO/Dwn5t2f8\n" + + "yuV9nxdlK6TGUByyOINkoMf6U7IJfPmdThcqmUoPjUav2ha4uNOhJEL3a1R9RrDk\n" + + "q+kiKrT65QTuVl5pE5JjjEuVuGuMjlWnh2aieG9z8sIXqela/1LlOe0MboVTTVv9\n" + + "FvR4kz3GGfqSYHTu229R7QEthd9y3NMrPeW8i2ZCZMch3FMhym5sDiSw1okKC4rE\n" + + "hhNECnwdCw9DGdqEPgh8RWpjVKQNdCk1gjH+/EnCjlMl7pRcdzkBbKE0TuZTP3ww\n" + + "V0q86MQmyWkFO9fQQ9LakERO1OzP0fMob7mce8ZdP7qENAqAYRQLJR4iHPSjcRFz\n" + + "WFv0UlYdnSNZfY0vroIMxnM1w4XJVg==\n" + + "=4plS\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + @Test + public void testEncryptionIsNotUnencrypted() throws PGPException, IOException { + PGPSecretKeyRing stupidKey = PGPainless.readKeyRing().secretKeyRing(STUPID_KEY); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(stupidKey); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt( + new EncryptionOptions().addRecipient(certificate) + )); + + encryptionStream.write("Hello".getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + + EncryptionResult metadata = encryptionStream.getResult(); + assertTrue(metadata.isEncryptedFor(certificate)); + assertEquals(PGPainless.getPolicy().getSymmetricKeyEncryptionAlgorithmPolicy().getDefaultSymmetricKeyAlgorithm(), + metadata.getEncryptionAlgorithm()); + } +} From 32e1f1234bb9be20d1be3bc68c311418ef9f87fd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 15 Jul 2022 13:21:59 +0200 Subject: [PATCH 0305/1204] Add KeyRingUtils.publicKeyRingCollectionFrom(PGPSecretKeyRingCollection) --- .../org/pgpainless/key/util/KeyRingUtils.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 21c5b575..66536b88 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -161,6 +161,25 @@ public final class KeyRingUtils { return publicKeyRing; } + /** + * Extract {@link PGPPublicKeyRing PGPPublicKeyRings} from all {@link PGPSecretKeyRing PGPSecretKeyRings} in + * the given {@link PGPSecretKeyRingCollection} and return them as a {@link PGPPublicKeyRingCollection}. + * + * @param secretKeyRings secret key ring collection + * @return public key ring collection + * @throws PGPException TODO: remove + * @throws IOException TODO: remove + */ + @Nonnull + public static PGPPublicKeyRingCollection publicKeyRingCollectionFrom(@Nonnull PGPSecretKeyRingCollection secretKeyRings) + throws PGPException, IOException { + List certificates = new ArrayList<>(); + for (PGPSecretKeyRing secretKey : secretKeyRings) { + certificates.add(PGPainless.extractCertificate(secretKey)); + } + return new PGPPublicKeyRingCollection(certificates); + } + /** * Unlock a {@link PGPSecretKey} and return the resulting {@link PGPPrivateKey}. * From dec3c8be60e44125918c0935ae8bc14cc4a10b0a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 15 Jul 2022 14:00:41 +0200 Subject: [PATCH 0306/1204] Add SecretKeyRingEditor.replaceUserId(old,new,protector) --- .../secretkeyring/SecretKeyRingEditor.java | 51 +++++++++++- .../SecretKeyRingEditorInterface.java | 20 +++++ ...dDoesNotBreakEncryptionCapabilityTest.java | 80 ++++++++++++++++++- 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index 7418401f..e1b1b23e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -205,12 +205,61 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { @Override public SecretKeyRingEditorInterface removeUserId( CharSequence userId, - SecretKeyRingProtector protector) throws PGPException { + SecretKeyRingProtector protector) + throws PGPException { return removeUserId( SelectUserId.exactMatch(userId.toString()), protector); } + @Override + public SecretKeyRingEditorInterface replaceUserId(@Nonnull CharSequence oldUserId, + @Nonnull CharSequence newUserId, + @Nonnull SecretKeyRingProtector protector) + throws PGPException { + String oldUID = oldUserId.toString().trim(); + String newUID = newUserId.toString().trim(); + if (oldUID.isEmpty()) { + throw new IllegalArgumentException("Old user-id cannot be empty."); + } + + if (newUID.isEmpty()) { + throw new IllegalArgumentException("New user-id cannot be empty."); + } + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing); + if (!info.isUserIdValid(oldUID)) { + throw new NoSuchElementException("Key does not carry user-id '" + oldUID + "', or it is not valid."); + } + + PGPSignature oldCertification = info.getLatestUserIdCertification(oldUID); + if (oldCertification == null) { + throw new AssertionError("Certification for old user-id MUST NOT be null."); + } + + // Bind new user-id + addUserId(newUserId, new SelfSignatureSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { + SignatureSubpacketsHelper.applyFrom(oldCertification.getHashedSubPackets(), (SignatureSubpackets) hashedSubpackets); + // Primary user-id + if (oldUID.equals(info.getPrimaryUserId())) { + // Implicit primary user-id + if (!oldCertification.getHashedSubPackets().isPrimaryUserID()) { + hashedSubpackets.setPrimaryUserId(); + } + } + } + + @Override + public void modifyUnhashedSubpackets(SelfSignatureSubpackets unhashedSubpackets) { + SignatureSubpacketsHelper.applyFrom(oldCertification.getUnhashedSubPackets(), (SignatureSubpackets) unhashedSubpackets); + } + }, protector); + + return revokeUserId(oldUID, protector); + } + // TODO: Move to utility class? private String sanitizeUserId(@Nonnull CharSequence userId) { // TODO: Further research how to sanitize user IDs. diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java index 6f4d34b8..7014d518 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java @@ -104,6 +104,26 @@ public interface SecretKeyRingEditorInterface { SecretKeyRingProtector protector) throws PGPException; + /** + * Replace a user-id on the key with a new one. + * The old user-id gets soft revoked and the new user-id gets bound with the same signature subpackets as the + * old one, with one exception: + * If the old user-id was implicitly primary (did not carry a {@link org.bouncycastle.bcpg.sig.PrimaryUserID} packet, + * but effectively was primary, then the new user-id will be explicitly marked as primary. + * + * @param oldUserId old user-id + * @param newUserId new user-id + * @param protector protector to unlock the secret key + * @return the builder + * @throws PGPException in case we cannot generate a revocation and certification signature + * @throws java.util.NoSuchElementException if the old user-id was not found on the key; or if the oldUserId + * was already invalid + */ + SecretKeyRingEditorInterface replaceUserId(CharSequence oldUserId, + CharSequence newUserId, + SecretKeyRingProtector protector) + throws PGPException; + /** * Add a subkey to the key ring. * The subkey will be generated from the provided {@link KeySpec}. diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/FixUserIdDoesNotBreakEncryptionCapabilityTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/FixUserIdDoesNotBreakEncryptionCapabilityTest.java index 1995b8bf..e0429287 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/FixUserIdDoesNotBreakEncryptionCapabilityTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/FixUserIdDoesNotBreakEncryptionCapabilityTest.java @@ -5,16 +5,31 @@ package org.pgpainless.key.modification; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.NoSuchElementException; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -55,7 +70,7 @@ public class FixUserIdDoesNotBreakEncryptionCapabilityTest { private static final String userIdAfter = "\"(B)ob (J)ohnson\" "; @Test - public void replaceUserIdWithFixedVersionDoesNotHinderEncryptionCapability() throws IOException, PGPException { + public void manualReplaceUserIdWithFixedVersionDoesNotHinderEncryptionCapability() throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); PGPSecretKeyRing modified = PGPainless.modifyKeyRing(secretKeys) @@ -81,4 +96,67 @@ public class FixUserIdDoesNotBreakEncryptionCapabilityTest { assertFalse(after.isUserIdValid(userIdBefore)); assertTrue(after.isUserIdValid(userIdAfter)); } + + @Test + public void testReplaceUserId_missingOldUserIdThrows() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); + assertThrows(NoSuchElementException.class, () -> PGPainless.modifyKeyRing(secretKeys) + .replaceUserId("missing", userIdAfter, SecretKeyRingProtector.unprotectedKeys())); + } + + @Test + public void testReplaceUserId_emptyOldUserIdThrows() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); + assertThrows(IllegalArgumentException.class, () -> PGPainless.modifyKeyRing(secretKeys) + .replaceUserId(" ", userIdAfter, SecretKeyRingProtector.unprotectedKeys())); + } + + @Test + public void testReplaceUserId_emptyNewUserIdThrows() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); + assertThrows(IllegalArgumentException.class, () -> PGPainless.modifyKeyRing(secretKeys) + .replaceUserId(userIdBefore, " ", SecretKeyRingProtector.unprotectedKeys())); + } + + @Test + public void testReplaceImplicitUserIdDoesNotBreakStuff() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); + + PGPSecretKeyRing edited = PGPainless.modifyKeyRing(secretKeys) + .replaceUserId(userIdBefore, userIdAfter, SecretKeyRingProtector.unprotectedKeys()) + .done(); + + KeyRingInfo info = PGPainless.inspectKeyRing(edited); + assertTrue(info.isUserIdValid(userIdAfter)); + assertEquals(userIdAfter, info.getPrimaryUserId()); + + assertTrue(info.getLatestUserIdCertification(userIdAfter).getHashedSubPackets().isPrimaryUserID()); + + PGPPublicKeyRing cert = PGPainless.extractCertificate(edited); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(new EncryptionOptions() + .addRecipient(cert))); + + encryptionStream.write("Hello".getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + + EncryptionResult result = encryptionStream.getResult(); + assertTrue(result.isEncryptedFor(cert)); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + ByteArrayOutputStream plain = new ByteArrayOutputStream(); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(new ConsumerOptions() + .addDecryptionKey(edited)); + + Streams.pipeAll(decryptionStream, plain); + decryptionStream.close(); + + OpenPgpMetadata metadata = decryptionStream.getResult(); + assertTrue(metadata.isEncrypted()); + } } From ba191a1d0ff1064da279227da429c93ab1d4ece5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 15 Jul 2022 14:19:45 +0200 Subject: [PATCH 0307/1204] Prevent adding NULL to symmetric algorithm preference when generating key Fixes #301 --- .../pgpainless/key/generation/KeySpecBuilder.java | 5 +++++ .../StupidAlgorithmPreferenceEncryptionTest.java | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java index 2d7010d8..559dd3ce 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java @@ -64,6 +64,11 @@ public class KeySpecBuilder implements KeySpecBuilderInterface { @Override public KeySpecBuilder overridePreferredSymmetricKeyAlgorithms( @Nonnull SymmetricKeyAlgorithm... preferredSymmetricKeyAlgorithms) { + for (SymmetricKeyAlgorithm algo : preferredSymmetricKeyAlgorithms) { + if (algo == SymmetricKeyAlgorithm.NULL) { + throw new IllegalArgumentException("NULL (unencrypted) is an invalid symmetric key algorithm preference."); + } + } this.preferredSymmetricAlgorithms = new LinkedHashSet<>(Arrays.asList(preferredSymmetricKeyAlgorithms)); return this; } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/StupidAlgorithmPreferenceEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/StupidAlgorithmPreferenceEncryptionTest.java index 42aa0856..19d5fdd1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/StupidAlgorithmPreferenceEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/StupidAlgorithmPreferenceEncryptionTest.java @@ -9,20 +9,34 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.rsa.RsaLength; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; public class StupidAlgorithmPreferenceEncryptionTest { + @Test + public void testPreventUnencryptedAlgorithmPreferenceDuringKeyGeneration() { + KeySpecBuilder specBuilder = KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.CERTIFY_OTHER); + assertThrows(IllegalArgumentException.class, () -> + specBuilder.overridePreferredSymmetricKeyAlgorithms( + SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.AES_192, + SymmetricKeyAlgorithm.AES_128, SymmetricKeyAlgorithm.NULL)); + } + // RSA key with symmetric algorithm preference "NULL" (unencrypted). private static final String STUPID_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + From fe913172d5bb940ade085c9c6df2643c849380a2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 16 Jul 2022 12:58:22 +0200 Subject: [PATCH 0308/1204] Add missing javadoc --- .../src/main/java/org/pgpainless/key/OpenPgpFingerprint.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java index 6abe207b..86ac8265 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java @@ -70,8 +70,8 @@ public abstract class OpenPgpFingerprint implements CharSequence, Comparable Date: Sat, 16 Jul 2022 13:00:15 +0200 Subject: [PATCH 0309/1204] Update changelog --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39f795af..c6075d1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.2 +- Add `KeyRingInfo(Policy)` constructor +- Delete unused `KeyRingValidator` class +- Add `PGPainless.certify()` API + - `certify().userIdOnCertificate()` can be used to certify other users User-IDs + - `certify().certificate()` can be used to create direct-key signatures on other users keys +- We now have a [User Guide!](https://pgpainless.rtfd.io/) +- Fixed build script + - `pgpainless-cli`s `gradle build` task no longer builds fat jar + - Fat jars are now built by dedicated shadow plugin +- Fix third-party assigned user-ids on keys to accidentally get picked up as primary user-id +- Add `KeyRingUtils.publicKeyRingCollectionFrom(PGPSecretKeyRingCollection)` +- Add `SecretKeyRingEditor.replaceUserId(oldUid, newUid, protector)` +- Prevent adding `SymmetricKeyAlgorithm.NULL` (unencrypted) as encryption algo preference when generating keys + ## 1.3.1 - Fix reproducibility of builds by setting fixed file permissions in archive task - Improve encryption performance by buffering streams From 93e50b75feb17acf39f1c7dddb279980d9675869 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 16 Jul 2022 13:02:55 +0200 Subject: [PATCH 0310/1204] PGPainless 1.3.2 --- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 92a3e3ee..aee0c2f2 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.1' + implementation 'org.pgpainless:pgpainless-core:1.3.2' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 58c1e9c9..8eb0ed50 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.1" + implementation "org.pgpainless:pgpainless-sop:1.3.2" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.1 + 1.3.2 ... diff --git a/version.gradle b/version.gradle index 178eb300..bb3114e5 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.2' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From d4953bd8f66c6d78a58890eab72b40421f1ce51f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 16 Jul 2022 13:05:08 +0200 Subject: [PATCH 0311/1204] PGPainless 1.3.3-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index bb3114e5..7f57ac2a 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.2' - isSnapshot = false + shortVersion = '1.3.3' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From 59adbe1d0ac4082c377c3f79e1a94a7c46397484 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 18 Jul 2022 11:30:25 +0200 Subject: [PATCH 0312/1204] Add SHA3 hash algorithms to HashAlgorithm class --- .../src/main/java/org/pgpainless/algorithm/HashAlgorithm.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java index 12feb678..0b9368bb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java @@ -27,6 +27,8 @@ public enum HashAlgorithm { SHA384 (HashAlgorithmTags.SHA384, "SHA384"), SHA512 (HashAlgorithmTags.SHA512, "SHA512"), SHA224 (HashAlgorithmTags.SHA224, "SHA224"), + SHA3_256 (12, "SHA3-256"), + SHA3_512 (14, "SHA3-512"), ; private static final Map ID_MAP = new HashMap<>(); From cd5982cd478818ca374eaa7eb3a600d21e8bdf32 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 18 Jul 2022 11:30:37 +0200 Subject: [PATCH 0313/1204] Add AEADAlgorithm class and test --- .../pgpainless/algorithm/AEADAlgorithm.java | 95 +++++++++++++++++++ .../algorithm/AEADAlgorithmTest.java | 50 ++++++++++ 2 files changed, 145 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/algorithm/AEADAlgorithmTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java new file mode 100644 index 00000000..106d6bff --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +/** + * List of AEAD algorithms defined in crypto-refresh-06. + * + * @see + * Crypto-Refresh-06 §9.6 - AEAD Algorithms + */ +public enum AEADAlgorithm { + + EAX(1, 16, 16), + OCB(2, 15, 16), + GCM(3, 12, 16), + ; + + private final int algorithmId; + private final int ivLength; + private final int tagLength; + + private static final Map MAP = new HashMap<>(); + + static { + for (AEADAlgorithm h : AEADAlgorithm.values()) { + MAP.put(h.algorithmId, h); + } + } + + AEADAlgorithm(int id, int ivLength, int tagLength) { + this.algorithmId = id; + this.ivLength = ivLength; + this.tagLength = tagLength; + } + + public int getAlgorithmId() { + return algorithmId; + } + + /** + * Return the length (in octets) of the IV. + * + * @return iv length + */ + public int getIvLength() { + return ivLength; + } + + /** + * Return the length (in octets) of the authentication tag. + * + * @return tag length + */ + public int getTagLength() { + return tagLength; + } + + /** + * Return the {@link AEADAlgorithm} value that corresponds to the provided algorithm id. + * If an invalid algorithm id was provided, null is returned. + * + * @param id numeric id + * @return enum value + */ + @Nullable + public static AEADAlgorithm fromId(int id) { + return MAP.get(id); + } + + /** + * Return the {@link AEADAlgorithm} value that corresponds to the provided algorithm id. + * If an invalid algorithm id was provided, throw a {@link NoSuchElementException}. + * + * @param id algorithm id + * @return enum value + * @throws NoSuchElementException in case of an unknown algorithm id + */ + @Nonnull + public static AEADAlgorithm requireFromId(int id) { + AEADAlgorithm algorithm = fromId(id); + if (algorithm == null) { + throw new NoSuchElementException("No AEADAlgorithm found for id " + id); + } + return algorithm; + } + +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/AEADAlgorithmTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/AEADAlgorithmTest.java new file mode 100644 index 00000000..58423190 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/AEADAlgorithmTest.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import org.junit.jupiter.api.Test; + +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class AEADAlgorithmTest { + + @Test + public void testEAXParameters() { + AEADAlgorithm eax = AEADAlgorithm.EAX; + assertEquals(1, eax.getAlgorithmId()); + assertEquals(16, eax.getIvLength()); + assertEquals(16, eax.getTagLength()); + } + + @Test + public void testOCBParameters() { + AEADAlgorithm ocb = AEADAlgorithm.OCB; + assertEquals(2, ocb.getAlgorithmId()); + assertEquals(15, ocb.getIvLength()); + assertEquals(16, ocb.getTagLength()); + } + + @Test + public void testGCMParameters() { + AEADAlgorithm gcm = AEADAlgorithm.GCM; + assertEquals(3, gcm.getAlgorithmId()); + assertEquals(12, gcm.getIvLength()); + assertEquals(16, gcm.getTagLength()); + } + + @Test + public void testFromId() { + assertEquals(AEADAlgorithm.EAX, AEADAlgorithm.requireFromId(1)); + assertEquals(AEADAlgorithm.OCB, AEADAlgorithm.requireFromId(2)); + assertEquals(AEADAlgorithm.GCM, AEADAlgorithm.requireFromId(3)); + + assertNull(AEADAlgorithm.fromId(99)); + assertThrows(NoSuchElementException.class, () -> AEADAlgorithm.requireFromId(99)); + } +} From 9b6d08f3c5d09497604328bb8be09b4528feacc3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 18 Jul 2022 12:03:16 +0200 Subject: [PATCH 0314/1204] Add MODIFICATION_DETECTION_2 feature constant --- .../java/org/pgpainless/algorithm/Feature.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java index 52de27bf..a0fc2974 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java @@ -23,7 +23,8 @@ import javax.annotation.Nullable; public enum Feature { /** - * Support for Symmetrically Encrypted Integrity Protected Data Packets using Modification Detection Code Packets. + * Support for Symmetrically Encrypted Integrity Protected Data Packets (version 1) using Modification + * Detection Code Packets. * * @see * RFC-4880 §5.14: Modification Detection Code Packet @@ -35,6 +36,7 @@ public enum Feature { * If a key announces this feature, it signals support for consuming AEAD Encrypted Data Packets. * * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! + * NOTE: This value is currently RESERVED. * * @see * AEAD Encrypted Data Packet @@ -49,11 +51,20 @@ public enum Feature { * In addition, fingerprints of version 5 keys are calculated differently from version 4 keys. * * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! + * NOTE: This value is currently RESERVED. * * @see * Public-Key Packet Formats */ - VERSION_5_PUBLIC_KEY(Features.FEATURE_VERSION_5_PUBLIC_KEY) + VERSION_5_PUBLIC_KEY(Features.FEATURE_VERSION_5_PUBLIC_KEY), + + /** + * Support for Symmetrically Encrypted Integrity Protected Data packet version 2. + * + * @see + * crypto-refresh-06 §5.13.2. Version 2 Sym. Encrypted Integrity Protected Data Packet Format + */ + MODIFICATION_DETECTION_2((byte) 0x08), ; private static final Map MAP = new ConcurrentHashMap<>(); From e67d5b405cb21b203be1e07053d94a11ff616a7e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 18 Jul 2022 14:50:53 +0200 Subject: [PATCH 0315/1204] Add javadoc to ProducerOptions.noEncryptionNoSigning() --- .../org/pgpainless/encryption_signing/ProducerOptions.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java index c0e60181..a015a581 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java @@ -69,6 +69,12 @@ public final class ProducerOptions { return new ProducerOptions(encryptionOptions, null); } + /** + * Only wrap the data in an OpenPGP packet. + * No encryption or signing will be applied. + * + * @return builder + */ public static ProducerOptions noEncryptionNoSigning() { return new ProducerOptions(null, null); } From f966c1ed073a0e378f84083546d5971f83f748b6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 20 Jul 2022 18:07:42 +0200 Subject: [PATCH 0316/1204] Explicitly cast Long to long to fix ambiguity in debian tests --- .../decryption_verification/MessageInspectorTest.java | 2 +- .../VerifyWithMissingPublicKeyCallback.java | 2 +- .../key/protection/CachingSecretKeyRingProtectorTest.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageInspectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageInspectorTest.java index dd47ec4b..b7382360 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageInspectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageInspectorTest.java @@ -33,7 +33,7 @@ public class MessageInspectorTest { assertFalse(info.isSignedOnly()); assertTrue(info.isEncrypted()); assertEquals(1, info.getKeyIds().size()); - assertEquals(KeyIdUtil.fromLongKeyId("4766F6B9D5F21EB6"), info.getKeyIds().get(0)); + assertEquals(KeyIdUtil.fromLongKeyId("4766F6B9D5F21EB6"), (long) info.getKeyIds().get(0)); } @Test diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java index e4e9427b..bcbad822 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java @@ -66,7 +66,7 @@ public class VerifyWithMissingPublicKeyCallback { @Nullable @Override public PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId) { - assertEquals(signingKey.getKeyID(), keyId, "Signing key-ID mismatch."); + assertEquals(signingKey.getKeyID(), (long) keyId, "Signing key-ID mismatch."); return signingPubKeys; } })); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java index 6c103ea3..d1f5f9e6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java @@ -129,7 +129,7 @@ public class CachingSecretKeyRingProtectorTest { Passphrase passphrase = withCallback.getPassphraseFor(x); assertNotNull(passphrase); - assertEquals(doubled, Long.valueOf(new String(passphrase.getChars()))); + assertEquals(doubled, (long) Long.valueOf(new String(passphrase.getChars()))); } } From 914f2bf5b6e11f6c4c440ec49354ccd6baf0cc77 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 20 Jul 2022 18:15:38 +0200 Subject: [PATCH 0317/1204] Add IRC channel to readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index aee0c2f2..4cc41e52 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,8 @@ Do you need a custom feature? Are you unsure of what's the best way to integrate We offer paid professional services. Don't hesitate to send an inquiry to [info@pgpainless.org](mailto:info@pgpainless.org). ## Development +Join the projects IRC channel [**#pgpainless**](ircs://irc.oftc.net:6697/#pgpainless) on OFTC if you have any questions! + PGPainless is developed in - and accepts contributions from - the following places: * [Github](https://github.com/pgpainless/pgpainless) From cb8e0d795103736bd7c4b39bb16011ae35b96e16 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Jul 2022 13:17:26 +0200 Subject: [PATCH 0318/1204] Create FUNDING.yml --- .github/FUNDING.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..ca136f30 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: vanitasvitae # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +#patreon: # Replace with a single Patreon username +#open_collective: # Replace with a single Open Collective username +#ko_fi: # Replace with a single Ko-fi username +#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +#liberapay: # Replace with a single Liberapay username +#issuehunt: # Replace with a single IssueHunt username +#otechie: # Replace with a single Otechie username +#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From c4bffad4785670d9a27c32c0a84a0fc7da6f3032 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Jul 2022 21:34:44 +0200 Subject: [PATCH 0319/1204] Abort (skip) tests reading from resources --- .../key/parsing/KeyRingReaderTest.java | 82 ++++++++++++------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java index 9349a864..b3fe66d4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java @@ -14,6 +14,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -50,11 +51,38 @@ class KeyRingReaderTest { private InputStream requireResource(String resourceName) { InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourceName); if (inputStream == null) { - throw new TestAbortedException("Cannot read resource " + resourceName); + throw new TestAbortedException("Cannot read resource " + resourceName + ": InputStream is null."); } return inputStream; } + private URI getResourceURI(String resourceName) { + try { + URL url = getClass().getClassLoader().getResource(resourceName); + if (url == null) { + throw new TestAbortedException("Cannot read resource " + resourceName + ": URL is null."); + } + return url.toURI(); + } catch (URISyntaxException | IllegalArgumentException e) { + throw new TestAbortedException("Cannot read resource " + resourceName, e); + } + } + + private File getFileFromResource(String resourceName) { + URI uri = getResourceURI(resourceName); + try { + return new File(uri); + } catch (IllegalArgumentException e) { + // When executing the tests from pgpainless-test.jar, we cannot read resources as + // URI is not hierarchical. + throw new TestAbortedException("Cannot read resource " + resourceName, e); + } + } + + private byte[] readFromResource(String resourceName) throws IOException { + return Files.readAllBytes(getFileFromResource(resourceName).toPath()); + } + @Test public void assertThatPGPUtilsDetectAsciiArmoredData() throws IOException, PGPException { InputStream inputStream = requireResource("pub_keys_10_pieces.asc"); @@ -93,18 +121,16 @@ class KeyRingReaderTest { } @Test - void publicKeyRingCollectionFromString() throws IOException, PGPException, URISyntaxException { - URL resource = getClass().getClassLoader().getResource("pub_keys_10_pieces.asc"); - String armoredString = new String(Files.readAllBytes(new File(resource.toURI()).toPath())); + void publicKeyRingCollectionFromString() throws IOException, PGPException { + String armoredString = new String(readFromResource("pub_keys_10_pieces.asc")); InputStream inputStream = new ByteArrayInputStream(armoredString.getBytes(StandardCharsets.UTF_8)); PGPPublicKeyRingCollection rings = PGPainless.readKeyRing().publicKeyRingCollection(inputStream); assertEquals(10, rings.size()); } @Test - void publicKeyRingCollectionFromBytes() throws IOException, PGPException, URISyntaxException { - URL resource = getClass().getClassLoader().getResource("pub_keys_10_pieces.asc"); - byte[] bytes = Files.readAllBytes(new File(resource.toURI()).toPath()); + void publicKeyRingCollectionFromBytes() throws IOException, PGPException { + byte[] bytes = readFromResource("pub_keys_10_pieces.asc"); InputStream byteArrayInputStream = new ByteArrayInputStream(bytes); PGPPublicKeyRingCollection rings = PGPainless.readKeyRing().publicKeyRingCollection(byteArrayInputStream); assertEquals(10, rings.size()); @@ -114,7 +140,7 @@ class KeyRingReaderTest { * One armored pub key */ @Test - void parsePublicKeysSingleArmored() throws IOException, PGPException, URISyntaxException { + void parsePublicKeysSingleArmored() throws IOException, PGPException { assertEquals(1, getPgpPublicKeyRingsFromResource("single_pub_key_armored.asc").size()); } @@ -122,7 +148,7 @@ class KeyRingReaderTest { * One binary pub key */ @Test - void parsePublicKeysSingleBinary() throws IOException, PGPException, URISyntaxException { + void parsePublicKeysSingleBinary() throws IOException, PGPException { assertEquals(1, getPgpPublicKeyRingsFromResource("single_pub_key_binary.key").size()); } @@ -130,7 +156,7 @@ class KeyRingReaderTest { * Many armored pub keys with a single -----BEGIN PGP PUBLIC KEY BLOCK-----...-----END PGP PUBLIC KEY BLOCK----- */ @Test - void parsePublicKeysMultiplyArmoredSingleHeader() throws IOException, PGPException, URISyntaxException { + void parsePublicKeysMultiplyArmoredSingleHeader() throws IOException, PGPException { assertEquals(10, getPgpPublicKeyRingsFromResource("10_pub_keys_armored_single_header.asc").size()); } @@ -138,7 +164,7 @@ class KeyRingReaderTest { * Many armored pub keys where each has own -----BEGIN PGP PUBLIC KEY BLOCK-----...-----END PGP PUBLIC KEY BLOCK----- */ @Test - void parsePublicKeysMultiplyArmoredOwnHeader() throws IOException, PGPException, URISyntaxException { + void parsePublicKeysMultiplyArmoredOwnHeader() throws IOException, PGPException { assertEquals(10, getPgpPublicKeyRingsFromResource("10_pub_keys_armored_own_header.asc").size()); } @@ -147,7 +173,7 @@ class KeyRingReaderTest { * Each of those blocks can have a different count of keys. */ @Test - void parsePublicKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException, URISyntaxException { + void parsePublicKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException { assertEquals(10, getPgpPublicKeyRingsFromResource("10_pub_keys_armored_own_with_single_header.asc").size()); } @@ -155,7 +181,7 @@ class KeyRingReaderTest { * Many binary pub keys */ @Test - void parsePublicKeysMultiplyBinary() throws IOException, PGPException, URISyntaxException { + void parsePublicKeysMultiplyBinary() throws IOException, PGPException { assertEquals(10, getPgpPublicKeyRingsFromResource("10_pub_keys_binary.key").size()); } @@ -163,7 +189,7 @@ class KeyRingReaderTest { * One armored private key */ @Test - void parseSecretKeysSingleArmored() throws IOException, PGPException, URISyntaxException { + void parseSecretKeysSingleArmored() throws IOException, PGPException { assertEquals(1, getPgpSecretKeyRingsFromResource("single_prv_key_armored.asc").size()); } @@ -171,7 +197,7 @@ class KeyRingReaderTest { * One binary private key */ @Test - void parseSecretKeysSingleBinary() throws IOException, PGPException, URISyntaxException { + void parseSecretKeysSingleBinary() throws IOException, PGPException { assertEquals(1, getPgpSecretKeyRingsFromResource("single_prv_key_binary.key").size()); } @@ -180,7 +206,7 @@ class KeyRingReaderTest { * -----BEGIN PGP PRIVATE KEY BLOCK-----...-----END PGP PRIVATE KEY BLOCK----- */ @Test - void parseSecretKeysMultiplyArmoredSingleHeader() throws IOException, PGPException, URISyntaxException { + void parseSecretKeysMultiplyArmoredSingleHeader() throws IOException, PGPException { assertEquals(10, getPgpSecretKeyRingsFromResource("10_prv_keys_armored_single_header.asc").size()); } @@ -188,7 +214,7 @@ class KeyRingReaderTest { * Many armored private keys where each has own -----BEGIN PGP PRIVATE KEY BLOCK-----...-----END PGP PRIVATE KEY BLOCK----- */ @Test - void parseSecretKeysMultiplyArmoredOwnHeader() throws IOException, PGPException, URISyntaxException { + void parseSecretKeysMultiplyArmoredOwnHeader() throws IOException, PGPException { assertEquals(10, getPgpSecretKeyRingsFromResource("10_prv_keys_armored_own_header.asc").size()); } @@ -197,7 +223,7 @@ class KeyRingReaderTest { * Each of those blocks can have a different count of keys. */ @Test - void parseSecretKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException, URISyntaxException { + void parseSecretKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException { assertEquals(10, getPgpSecretKeyRingsFromResource("10_prv_keys_armored_own_with_single_header.asc").size()); } @@ -205,7 +231,7 @@ class KeyRingReaderTest { * Many binary private keys */ @Test - void parseSecretKeysMultiplyBinary() throws IOException, PGPException, URISyntaxException { + void parseSecretKeysMultiplyBinary() throws IOException, PGPException { assertEquals(10, getPgpSecretKeyRingsFromResource("10_prv_keys_binary.key").size()); } @@ -213,7 +239,7 @@ class KeyRingReaderTest { * Many armored keys(private or pub) where each has own -----BEGIN PGP ... KEY BLOCK-----...-----END PGP ... KEY BLOCK----- */ @Test - void parseKeysMultiplyArmoredOwnHeader() throws IOException, PGPException, URISyntaxException { + void parseKeysMultiplyArmoredOwnHeader() throws IOException, PGPException { assertEquals(10, getPGPKeyRingsFromResource("10_prv_and_pub_keys_armored_own_header.asc").size()); } @@ -222,7 +248,7 @@ class KeyRingReaderTest { * Each of those blocks can have a different count of keys. */ @Test - void parseKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException, URISyntaxException { + void parseKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException { assertEquals(10, getPGPKeyRingsFromResource("10_prv_and_pub_keys_armored_own_with_single_header.asc").size()); } @@ -230,28 +256,26 @@ class KeyRingReaderTest { * Many binary keys(private or pub) */ @Test - void parseKeysMultiplyBinary() throws IOException, PGPException, URISyntaxException { + void parseKeysMultiplyBinary() throws IOException, PGPException { assertEquals(10, getPGPKeyRingsFromResource("10_prv_and_pub_keys_binary.key").size()); } - private InputStream getFileInputStreamFromResource(String fileName) throws IOException, URISyntaxException { - URL resource = getClass().getClassLoader().getResource(fileName); - assert resource != null; - return new FileInputStream(new File(resource.toURI())); + private InputStream getFileInputStreamFromResource(String fileName) throws IOException { + return new FileInputStream(getFileFromResource(fileName)); } private PGPKeyRingCollection getPGPKeyRingsFromResource(String fileName) - throws IOException, URISyntaxException, PGPException { + throws IOException, PGPException { return PGPainless.readKeyRing().keyRingCollection(getFileInputStreamFromResource(fileName), true); } private PGPPublicKeyRingCollection getPgpPublicKeyRingsFromResource(String fileName) - throws IOException, URISyntaxException, PGPException { + throws IOException, PGPException { return PGPainless.readKeyRing().publicKeyRingCollection(getFileInputStreamFromResource(fileName)); } private PGPSecretKeyRingCollection getPgpSecretKeyRingsFromResource(String fileName) - throws IOException, URISyntaxException, PGPException { + throws IOException, PGPException { return PGPainless.readKeyRing().secretKeyRingCollection(getFileInputStreamFromResource(fileName)); } From 5a86d9db629e1c411fcdc47111201a08fefb3499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Charaoui?= Date: Thu, 21 Jul 2022 19:38:22 -0400 Subject: [PATCH 0320/1204] Fix tests that read from jar-embedded resources It seems that none of the functions used here actually require a File object as arguments, and will happily work on InputStream objects. This also changes readFromResource() to use InputStream.readAllBytes() instead of File.readAllBytes(), which is available from Java 9. --- .../key/parsing/KeyRingReaderTest.java | 37 +++---------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java index b3fe66d4..23479a78 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java @@ -14,7 +14,6 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -56,31 +55,9 @@ class KeyRingReaderTest { return inputStream; } - private URI getResourceURI(String resourceName) { - try { - URL url = getClass().getClassLoader().getResource(resourceName); - if (url == null) { - throw new TestAbortedException("Cannot read resource " + resourceName + ": URL is null."); - } - return url.toURI(); - } catch (URISyntaxException | IllegalArgumentException e) { - throw new TestAbortedException("Cannot read resource " + resourceName, e); - } - } - - private File getFileFromResource(String resourceName) { - URI uri = getResourceURI(resourceName); - try { - return new File(uri); - } catch (IllegalArgumentException e) { - // When executing the tests from pgpainless-test.jar, we cannot read resources as - // URI is not hierarchical. - throw new TestAbortedException("Cannot read resource " + resourceName, e); - } - } - private byte[] readFromResource(String resourceName) throws IOException { - return Files.readAllBytes(getFileFromResource(resourceName).toPath()); + InputStream inputStream = requireResource(resourceName); + return inputStream.readAllBytes(); } @Test @@ -260,23 +237,19 @@ class KeyRingReaderTest { assertEquals(10, getPGPKeyRingsFromResource("10_prv_and_pub_keys_binary.key").size()); } - private InputStream getFileInputStreamFromResource(String fileName) throws IOException { - return new FileInputStream(getFileFromResource(fileName)); - } - private PGPKeyRingCollection getPGPKeyRingsFromResource(String fileName) throws IOException, PGPException { - return PGPainless.readKeyRing().keyRingCollection(getFileInputStreamFromResource(fileName), true); + return PGPainless.readKeyRing().keyRingCollection(requireResource(fileName), true); } private PGPPublicKeyRingCollection getPgpPublicKeyRingsFromResource(String fileName) throws IOException, PGPException { - return PGPainless.readKeyRing().publicKeyRingCollection(getFileInputStreamFromResource(fileName)); + return PGPainless.readKeyRing().publicKeyRingCollection(requireResource(fileName)); } private PGPSecretKeyRingCollection getPgpSecretKeyRingsFromResource(String fileName) throws IOException, PGPException { - return PGPainless.readKeyRing().secretKeyRingCollection(getFileInputStreamFromResource(fileName)); + return PGPainless.readKeyRing().secretKeyRingCollection(requireResource(fileName)); } @Test From cb23cad625cb525641a501c9a681e14a525f5322 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 22 Jul 2022 13:59:15 +0200 Subject: [PATCH 0321/1204] Fix checkstyle issues and java API compatibility --- .../org/pgpainless/key/parsing/KeyRingReaderTest.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java index 23479a78..404440ca 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java @@ -10,14 +10,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.net.URISyntaxException; -import java.net.URL; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -34,6 +29,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.opentest4j.TestAbortedException; import org.pgpainless.PGPainless; @@ -57,7 +53,7 @@ class KeyRingReaderTest { private byte[] readFromResource(String resourceName) throws IOException { InputStream inputStream = requireResource(resourceName); - return inputStream.readAllBytes(); + return Streams.readAll(inputStream); } @Test From 895fcced9aaebead36951212f01382f19e06e656 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 22 Jul 2022 20:21:02 +0200 Subject: [PATCH 0322/1204] Add gradle CI action --- .github/workflows/gradle_push.yml | 32 +++++++++++++++++++ .../OpenPgpInputStreamTest.java | 3 +- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/gradle_push.yml diff --git a/.github/workflows/gradle_push.yml b/.github/workflows/gradle_push.yml new file mode 100644 index 00000000..189dc6ce --- /dev/null +++ b/.github/workflows/gradle_push.yml @@ -0,0 +1,32 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: CI on main branch + +on: + push: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Build, Check and Coverage + uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + with: + arguments: check jacocoRootReport coveralls \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java index a39ee70f..d731fb87 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java @@ -23,7 +23,6 @@ import org.bouncycastle.openpgp.PGPCompressedDataGenerator; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; @@ -36,7 +35,7 @@ public class OpenPgpInputStreamTest { private static final Random RANDOM = new Random(); - @RepeatedTest(100) + @Test public void randomBytesDoNotContainOpenPgpData() throws IOException { byte[] randomBytes = new byte[1000000]; RANDOM.nextBytes(randomBytes); From 55c9b6ed9e9dd7bc3c5b3b024da9e6f20c8da082 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 22 Jul 2022 20:48:53 +0200 Subject: [PATCH 0323/1204] Add reuse headers to files in .github --- .github/FUNDING.yml | 4 ++++ .github/workflows/gradle_push.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ca136f30..36f6550a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 + # These are supported funding model platforms github: vanitasvitae # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] diff --git a/.github/workflows/gradle_push.yml b/.github/workflows/gradle_push.yml index 189dc6ce..395fed9b 100644 --- a/.github/workflows/gradle_push.yml +++ b/.github/workflows/gradle_push.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 + # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support From 324244ae134a79e01fb50d855a43337bbe332cd0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 22 Jul 2022 20:50:01 +0200 Subject: [PATCH 0324/1204] Update badges --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4cc41e52..6eae7902 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless - Use OpenPGP Painlessly! -[![Travis (.com)](https://travis-ci.com/pgpainless/pgpainless.svg?branch=master)](https://travis-ci.com/pgpainless/pgpainless) +[![Travis (.com)](https://travis-ci.com/pgpainless/pgpainless.svg?branch=main)](https://travis-ci.com/pgpainless/pgpainless) [![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-core)](https://search.maven.org/artifact/org.pgpainless/pgpainless-core) -[![Coverage Status](https://coveralls.io/repos/github/pgpainless/pgpainless/badge.svg?branch=master)](https://coveralls.io/github/pgpainless/pgpainless?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/pgpainless/pgpainless/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/pgpainless?branch=main) [![Interoperability Test-Suite](https://badgen.net/badge/Sequoia%20Test%20Suite/%232/green)](https://tests.sequoia-pgp.org/) [![PGP](https://img.shields.io/badge/pgp-A027%20DB2F%203E1E%20118A-blue)](https://keyoxide.org/7F9116FEA90A5983936C7CFAA027DB2F3E1E118A) [![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/pgpainless)](https://api.reuse.software/info/github.com/pgpainless/pgpainless) From e622a2256e3aa471ae86ff6f5f2319476e20c2ef Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 22 Jul 2022 20:53:42 +0200 Subject: [PATCH 0325/1204] Rename build job and point build badge to github action --- .github/workflows/gradle_push.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle_push.yml b/.github/workflows/gradle_push.yml index 395fed9b..0d32e038 100644 --- a/.github/workflows/gradle_push.yml +++ b/.github/workflows/gradle_push.yml @@ -9,7 +9,7 @@ # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle -name: CI on main branch +name: Build on: push: diff --git a/README.md b/README.md index 6eae7902..02253a83 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless - Use OpenPGP Painlessly! -[![Travis (.com)](https://travis-ci.com/pgpainless/pgpainless.svg?branch=main)](https://travis-ci.com/pgpainless/pgpainless) +[![Build Status](https://github.com/pgpainless/pgpainless/actions/workflows/gradle_push.yml/badge.svg)](https://github.com/pgpainless/pgpainless/actions/workflows/gradle_push.yml) [![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-core)](https://search.maven.org/artifact/org.pgpainless/pgpainless-core) [![Coverage Status](https://coveralls.io/repos/github/pgpainless/pgpainless/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/pgpainless?branch=main) [![Interoperability Test-Suite](https://badgen.net/badge/Sequoia%20Test%20Suite/%232/green)](https://tests.sequoia-pgp.org/) From cc9af93e750dbcee8e5f48363a6a0bef17459b30 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 23 Jul 2022 01:16:28 +0200 Subject: [PATCH 0326/1204] Fix command usage strings not being picked up due to renamed parent command --- .../src/main/java/org/pgpainless/cli/PGPainlessCLI.java | 1 - 1 file changed, 1 deletion(-) diff --git a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java index 35791a3e..c53ced17 100644 --- a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java +++ b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java @@ -14,7 +14,6 @@ import sop.cli.picocli.SopCLI; public class PGPainlessCLI { static { - SopCLI.EXECUTABLE_NAME = "pgpainless-cli"; SopCLI.setSopInstance(new SOPImpl()); } From bd10630fb4b93deb0d4bcf17a0b5e42a1e311e07 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 24 Jul 2022 12:42:16 +0200 Subject: [PATCH 0327/1204] PGPainless 1.3.3 --- CHANGELOG.md | 5 +++++ README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6075d1c..cd96baef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.3 +- Improve test compatibility against older JUnit versions +- Fix tests that read from jar-embedded resources (thanks @jcharaoui) +- `pgpainless-cli help`: Fix i18n strings + ## 1.3.2 - Add `KeyRingInfo(Policy)` constructor - Delete unused `KeyRingValidator` class diff --git a/README.md b/README.md index 02253a83..091b0cba 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.2' + implementation 'org.pgpainless:pgpainless-core:1.3.3' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 8eb0ed50..46aa8c21 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.2" + implementation "org.pgpainless:pgpainless-sop:1.3.3" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.2 + 1.3.3 ... diff --git a/version.gradle b/version.gradle index 7f57ac2a..c2cd69bf 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.3' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From 8bbb3aa8bac34efc47cd3bd39dc29cb475b1c39b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 24 Jul 2022 12:45:16 +0200 Subject: [PATCH 0328/1204] PGPainless 1.3.4-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index c2cd69bf..9c5cae1f 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.3' - isSnapshot = false + shortVersion = '1.3.4' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From c1de66e1d7b8458aca2979e20884c3c378b04a4d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 2 Aug 2022 16:53:01 +0200 Subject: [PATCH 0329/1204] Fix javadoc lying about only encrypting to single subkeys Fixes #305 --- .../pgpainless/encryption_signing/EncryptionOptions.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 99059c0a..2a75ee4e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -48,11 +48,11 @@ import org.pgpainless.util.Passphrase; * This will cause PGPainless to use the provided algorithm for message encryption, instead of negotiating an algorithm * by inspecting the provided recipient keys. * - * By default, PGPainless will only encrypt to a single encryption capable subkey per recipient key. - * This behavior can be changed, e.g. by calling + * By default, PGPainless will encrypt to all suitable, encryption capable subkeys on each recipient's certificate. + * This behavior can be changed per recipient, e.g. by calling *
  * {@code
- * opt.addRecipient(aliceKey, EncryptionOptions.encryptToAllCapableSubkeys());
+ * opt.addRecipient(aliceKey, EncryptionOptions.encryptToFirstSubkey());
  * }
  * 
* when adding the recipient key. From ca09ac62caa1134ac84774aeb034341f6255ef86 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 Aug 2022 13:37:18 +0200 Subject: [PATCH 0330/1204] KeyRingInfo.isUsableFor*(): Check if primary key is revoked --- .../org/pgpainless/key/info/KeyRingInfo.java | 6 ++- .../key/modification/RevokedKeyTest.java | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokedKeyTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index b69e301b..85cc8f28 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -1048,10 +1048,14 @@ public class KeyRingInfo { * @return true if usable for encryption */ public boolean isUsableForEncryption(@Nonnull EncryptionPurpose purpose) { - return !getEncryptionSubkeys(purpose).isEmpty(); + return isKeyValidlyBound(getKeyId()) && !getEncryptionSubkeys(purpose).isEmpty(); } public boolean isUsableForSigning() { + if (!isKeyValidlyBound(getKeyId())) { + return false; + } + List signingKeys = getSigningSubkeys(); for (PGPPublicKey pk : signingKeys) { PGPSecretKey sk = getSecretKey(pk.getKeyID()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokedKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokedKeyTest.java new file mode 100644 index 00000000..6082308d --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokedKeyTest.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification; + +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class RevokedKeyTest { + + private static final String REVOKED = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "xjMEYumXWhYJKwYBBAHaRw8BAQdAsesa7C2dtchG2LDYRPVgNiyXDDltTIW0\n" + + "7hbPKuklr+LCeAQgFgoACQUCYume7wIdAAAhCRARRbJWkH7x7hYhBCHmM1W/\n" + + "k8Vt/xDX4xFFslaQfvHusjoBAKeMumYgtr1uwbcNobWhojRjik+Uq7jER1Ph\n" + + "zrZPPwyaAP9NpV4//AB5BbwUgHMhCErD8L6GZEBOpCWYDgS00eKmCc0kVGVz\n" + + "dCBVc2VyIDx0ZXN0LnVzZXJAZmxvd2NyeXB0LnRlc3Q+wqcEExYIADgWIQQh\n" + + "5jNVv5PFbf8Q1+MRRbJWkH7x7gUCYumXWgIbAwULCQgHAgYVCgkICwIEFgID\n" + + "AQIeAQIXgAAhCRARRbJWkH7x7hYhBCHmM1W/k8Vt/xDX4xFFslaQfvHu0GUB\n" + + "AJ/FAi0K0YQ/gv9fO2EwSLH9imrXSxtfkzAyCQS32A/IAQDdqUfbABEoQvo2\n" + + "n1ktpVXroW3XPe3HlYFwSQzpVSHADc44BGLpl1oSCisGAQQBl1UBBQEBB0DJ\n" + + "8e0hG6v64O4P3qa9n8FxrkNoKS+J+fAW1Vzpf5tBUQMBCAfCjwQYFggAIBYh\n" + + "BCHmM1W/k8Vt/xDX4xFFslaQfvHuBQJi6ZdaAhsMACEJEBFFslaQfvHuFiEE\n" + + "IeYzVb+TxW3/ENfjEUWyVpB+8e51yAD/ewAe43L4bXYehVAKq+/CSfXEpYxU\n" + + "8kZv/mfA6nRfvOIA/iTx2uNw5NzC6TM5ZCBrXVxVGPmR9SwjnBHRmzVAmT8B\n" + + "=pY9e\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + + @Test + public void test() throws IOException { + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(REVOKED); + KeyRingInfo info = PGPainless.inspectKeyRing(cert); + + assertFalse(info.isUsableForSigning()); + assertFalse(info.isUsableForEncryption()); + } +} From a48ca4ede7a96570dc7c75239f6d13954274e072 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 Aug 2022 19:57:59 +0200 Subject: [PATCH 0331/1204] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd96baef..4a8be4a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.4-SNAPSHOT +- Fix `KeyRingInfo.isUsableForEncryption()`, `KeyRingInfo.isUsableForSigning()` not detecting revoked primary keys + ## 1.3.3 - Improve test compatibility against older JUnit versions - Fix tests that read from jar-embedded resources (thanks @jcharaoui) From 3ceb4c1a36c26be9422f64e88a0e65d7cef7f4d2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 Aug 2022 20:02:58 +0200 Subject: [PATCH 0332/1204] Acknowledge donors --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 091b0cba..1ea9faaf 100644 --- a/README.md +++ b/README.md @@ -232,4 +232,7 @@ Parts of PGPainless development ([project page](https://nlnet.nl/project/PGPainl NGI Assure is made possible with financial support from the [European Commission](https://ec.europa.eu/)'s [Next Generation Internet](https://ngi.eu/) programme, under the aegis of [DG Communications Networks, Content and Technology](https://ec.europa.eu/info/departments/communications-networks-content-and-technology_en). [![NGI Assure Logo](https://blog.jabberhead.tk/wp-content/uploads/2022/05/NGIAssure_tag.svg)](https://nlnet.nl/assure/) -Continuous Integration is kindly provided by [Travis-CI.com](https://travis-ci.com/). +Big thank you also to those who decided to support the work by donating! +Notably @msfjarvis + +You make my day! From b23a1d77a3367fab6f889d17808cc69c36760441 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 4 Aug 2022 14:05:12 +0200 Subject: [PATCH 0333/1204] Bump sop-java version to 4.0.1 and override executable name --- .../src/main/java/org/pgpainless/cli/PGPainlessCLI.java | 1 + version.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java index c53ced17..35791a3e 100644 --- a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java +++ b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java @@ -14,6 +14,7 @@ import sop.cli.picocli.SopCLI; public class PGPainlessCLI { static { + SopCLI.EXECUTABLE_NAME = "pgpainless-cli"; SopCLI.setSopInstance(new SOPImpl()); } diff --git a/version.gradle b/version.gradle index 9c5cae1f..fbed35bd 100644 --- a/version.gradle +++ b/version.gradle @@ -13,6 +13,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '4.0.0' + sopJavaVersion = '4.0.1' } } From e36c1a00b1e9f75195f54723c043e44c86e658bc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 4 Aug 2022 14:12:33 +0200 Subject: [PATCH 0334/1204] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a8be4a2..531bef0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ SPDX-License-Identifier: CC0-1.0 ## 1.3.4-SNAPSHOT - Fix `KeyRingInfo.isUsableForEncryption()`, `KeyRingInfo.isUsableForSigning()` not detecting revoked primary keys +- Bump `sop-java` and `sop-java-picocli` to `4.0.1` + - Fixes help text strings being resolved properly while allowing to override executable name ## 1.3.3 - Improve test compatibility against older JUnit versions From 6c936a25702ce83a7601f51fef5d334364635677 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 4 Aug 2022 14:55:08 +0200 Subject: [PATCH 0335/1204] PGPainless 1.3.4 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 531bef0f..98324bea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.3.4-SNAPSHOT +## 1.3.4 - Fix `KeyRingInfo.isUsableForEncryption()`, `KeyRingInfo.isUsableForSigning()` not detecting revoked primary keys - Bump `sop-java` and `sop-java-picocli` to `4.0.1` - Fixes help text strings being resolved properly while allowing to override executable name diff --git a/README.md b/README.md index 1ea9faaf..c62cfc78 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.3' + implementation 'org.pgpainless:pgpainless-core:1.3.4' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 46aa8c21..3dc4e6c7 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.3" + implementation "org.pgpainless:pgpainless-sop:1.3.4" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.3 + 1.3.4 ... diff --git a/version.gradle b/version.gradle index fbed35bd..637bf204 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.4' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From ff750d32fbdc7a7c1c17cc8a23df978c514cd6cf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 4 Aug 2022 14:58:04 +0200 Subject: [PATCH 0336/1204] PGPainless 1.3.5-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 637bf204..bd645100 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.4' - isSnapshot = false + shortVersion = '1.3.5' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From 38d04dca3526b0815644bca7d90c0ed67ff21bd6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 4 Aug 2022 15:15:58 +0200 Subject: [PATCH 0337/1204] Merge gh-pages stuff into main --- CNAME | 1 + _config.yml | 2 ++ _layouts/default.html | 78 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 CNAME create mode 100644 _config.yml create mode 100644 _layouts/default.html diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..b0cdad30 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +gh.pgpainless.org \ No newline at end of file diff --git a/_config.yml b/_config.yml new file mode 100644 index 00000000..40d208bd --- /dev/null +++ b/_config.yml @@ -0,0 +1,2 @@ +logo: /assets/logo.png +theme: jekyll-theme-minimal diff --git a/_layouts/default.html b/_layouts/default.html new file mode 100644 index 00000000..1df1e014 --- /dev/null +++ b/_layouts/default.html @@ -0,0 +1,78 @@ + + + + + + {{ site.title | default: site.github.repository_name }} by {{ +site.github.owner_name }} + + + + + + +
+
+

{{ site.title | default: site.github.repository_name }}

+ {% if site.logo %} + Logo + {% endif %} +

{{ site.description | default: site.github.project_tagline +}}

+ + Home +
+ Releases +
+ Javadoc +
+ Coverage +
+ + + {% if site.github.is_project_page %} +

View the Project on GitHub {{ github_name }}

+ {% endif %} + + {% if site.github.is_user_page %} +

View My +GitHub Profile

+ {% endif %} + + {% if site.show_downloads %} + + {% endif %} +
+
+ + {{ content }} + +
+ +
+ + + From 5b186fc387e6702907053c25b566227736fe56ee Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 4 Aug 2022 15:21:16 +0200 Subject: [PATCH 0338/1204] Fix licenses for gh pages layout stuff --- .reuse/dep5 | 13 +++++ LICENSES/CC-BY-SA-3.0.txt | 99 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 LICENSES/CC-BY-SA-3.0.txt diff --git a/.reuse/dep5 b/.reuse/dep5 index f7656261..329ef6a7 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -43,3 +43,16 @@ License: CC0-1.0 Files: audit/* Copyright: 2021 Paul Schaub License: CC0-1.0 + +# GH Pages +Files: CNAME +Copyright: 2022 Paul Schaub +License: CC0-1.0 + +Files: _config.yml +Copyright: 2022 Paul Schaub +License: CC0-1.0 + +Files: _layouts/* +Copyright: 2022 Paul Schaub , 2017 Steve Smith +License: CC-BY-SA-3.0 \ No newline at end of file diff --git a/LICENSES/CC-BY-SA-3.0.txt b/LICENSES/CC-BY-SA-3.0.txt new file mode 100644 index 00000000..39a8591c --- /dev/null +++ b/LICENSES/CC-BY-SA-3.0.txt @@ -0,0 +1,99 @@ +Creative Commons Attribution-ShareAlike 3.0 Unported + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. + +1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License. + + b. "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined below) for the purposes of this License. + + c. "Creative Commons Compatible License" means a license that is listed at http://creativecommons.org/compatiblelicenses that has been approved by Creative Commons as being essentially equivalent to this License, including, at a minimum, because that license: (i) contains terms that have the same purpose, meaning and effect as the License Elements of this License; and, (ii) explicitly permits the relicensing of adaptations of works made available under that license under this License or a Creative Commons jurisdiction license with the same License Elements as this License. + + d. "Distribute" means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale or other transfer of ownership. + + e. "License Elements" means the following high-level license attributes as selected by Licensor and indicated in the title of this License: Attribution, ShareAlike. + + f. "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License. + + g. "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast. + + h. "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work. + + i. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. + + j. "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images. + + k. "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections; + + b. to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified."; + + c. to Distribute and Publicly Perform the Work including as incorporated in Collections; and, + + d. to Distribute and Publicly Perform Adaptations. + + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; + + ii. Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor waives the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; and, + + iii. Voluntary License Schemes. The Licensor waives the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License. + +The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved. + +4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(c), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(c), as requested. + + b. You may Distribute or Publicly Perform an Adaptation only under the terms of: (i) this License; (ii) a later version of this License with the same License Elements as this License; (iii) a Creative Commons jurisdiction license (either this or a later license version) that contains the same License Elements as this License (e.g., Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible License. If you license the Adaptation under one of the licenses mentioned in (iv), you must comply with the terms of that license. If you license the Adaptation under the terms of any of the licenses mentioned in (i), (ii) or (iii) (the "Applicable License"), you must comply with the terms of the Applicable License generally and the following provisions: (I) You must include a copy of, or the URI for, the Applicable License with every copy of each Adaptation You Distribute or Publicly Perform; (II) You may not offer or impose any terms on the Adaptation that restrict the terms of the Applicable License or the ability of the recipient of the Adaptation to exercise the rights granted to that recipient under the terms of the Applicable License; (III) You must keep intact all notices that refer to the Applicable License and to the disclaimer of warranties with every copy of the Work as included in the Adaptation You Distribute or Publicly Perform; (IV) when You Distribute or Publicly Perform the Adaptation, You may not impose any effective technological measures on the Adaptation that restrict the ability of a recipient of the Adaptation from You to exercise the rights granted to that recipient under the terms of the Applicable License. This Section 4(b) applies to the Adaptation as incorporated in a Collection, but this does not require the Collection apart from the Adaptation itself to be made subject to the terms of the Applicable License. + + c. If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and (iv) , consistent with Ssection 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4(c) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties. + + d. Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author's honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. + + b. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. + +8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. + + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License. + + c. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + + d. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. + + e. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. + + f. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law. + +Creative Commons Notice + +Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor. + +Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of the License. + +Creative Commons may be contacted at http://creativecommons.org/. From 06b66dcc17dd13ebf5d31d7aedad55af52ce3983 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 4 Aug 2022 15:33:09 +0200 Subject: [PATCH 0339/1204] Exclude irrelevant md files from gh-pages --- _config.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/_config.yml b/_config.yml index 40d208bd..bdfcc3ab 100644 --- a/_config.yml +++ b/_config.yml @@ -1,2 +1,8 @@ logo: /assets/logo.png theme: jekyll-theme-minimal + +exclude: + - CHANGELOG.md + - CODE_OF_CONDUCT.md + - SECURITY.md + - docs From 0257c4d99ea69efc8857ce901c5c266355c7ddb0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 4 Aug 2022 15:38:44 +0200 Subject: [PATCH 0340/1204] Add pages logo --- .reuse/dep5 | 6 +++++- _layouts/default.html | 4 ++-- assets/logo.png | Bin 0 -> 14740 bytes 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 assets/logo.png diff --git a/.reuse/dep5 b/.reuse/dep5 index 329ef6a7..28dd7917 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -32,6 +32,10 @@ Files: assets/pgpainless.svg Copyright: 2021 Paul Schaub License: CC-BY-3.0 +Files: assets/logo.png +Copyright: 2022 Paul Schaub +License: CC-BY-3.0 + Files: assets/test_vectors/* Copyright: 2018 Paul Schaub License: CC0-1.0 @@ -55,4 +59,4 @@ License: CC0-1.0 Files: _layouts/* Copyright: 2022 Paul Schaub , 2017 Steve Smith -License: CC-BY-SA-3.0 \ No newline at end of file +License: CC-BY-SA-3.0 diff --git a/_layouts/default.html b/_layouts/default.html index 1df1e014..8fa354dc 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -30,10 +30,10 @@ src="//html5shiv.googlecode.com/svn/trunk/html5.js"> href="https://search.maven.org/search?q=g:org.pgpainless%20AND%20a:pgpainless-core&core=gav">Releases
Javadoc +href=" https://javadoc.io/doc/org.pgpainless ">Javadoc
Coverage +href="https://coveralls.io/github/pgpainless/pgpainless">Coverage
diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6cfae8208b74a71af609b752921d128d4b60f6d4 GIT binary patch literal 14740 zcmbVzcQn`k-}lGL9+^ot*_4@`O=M(dWR)#K_MRD;$tHY|$jFLhtL%KNLL&2%nVEe* z^}GJL&$-XJ?(6!U^F6=P+k3rU&*$^8-qG5cD#Qd=2@nVbv6`ylJp=+H@!~%`O!x`u zkNhkILfk}6QBKc0d+mphsotY=*)!UGB~hi@`*-zJpLF6V;_I+nzT8RF-5;8$;lkS~ zIEo@~txrtNG;z3xU!mRLXBAq_#gZT5^p5jbl()0r^{@0}v~57YtIgokoYich&E>qs z#)^Z+!TwnH!EAT4s{sK4aP3)45flyr5!}pFg+OFugy0}%iST$42;18h7>HX~n1qNn zS~4R9VlcQ36XALZn;H=;^xqx&mB|b-dm~zrg*vwAp8bIWTe})i;M!LnLM>xyo76>w<+4I+xmFM#bvVwwwZ{ED&=H`Y!OioSFuKxJ? z^=n^WUweD|9Y$a4WQn=#Z*5i8)v=oq zGT~#YKmGC1@1SN#X6mD#7+RikYr9XN((J?dz`%14=3rQbF?k(5Il1-_EZpASUj7Ho zoYb*uhR7RcW@fUovbeZWTU%Ry=0BH}w%3GnV`Hl8dw6--+SqJN*71moXL#^^;We&Z z*_$xW6MPgCA5Q}JM!{62Cl4>x!(+G6eOZ+zG@_y5AXnN?IsTej6In1lHg(?E=qR$N z$gJ7>COdmkV`GMg_!VLXDeu8-aSRNMkrJ(jckfutNZN2PV;hoFQfT;1RhtMf$tfs0 zJ33SiF{*;;UP_IkQ12Wk_{TJOLT*=i|M?~Dw_jUVm&A|#3fI=iU-$lfT59T-Nl9`| zWF1mcQpw55YwPQ3G??0|LOVB(Gijm!=pWqUgqz~&Jgq4OH~2K`_5DK zNfnq(2wS6T*OOEO&kmR2ho=Xv=dcL4*q1J~>`ywvqumH2_L8%B^6fKYP;=m^FG}CF zzxZyJ=;LX4zE^B`yqX1^zeh`T38{IR%fRqKXf(R??OQ$`9!a0=g*5ZP=zQgenmH08E;CXn{ovNy7S(;(;_gPb z=<;5^9B+EM+Whe&6d*LQ%k`$Zy1GV2-h02t_)O}VoTyWwsEU!u#0tz4xF%myQ&YXm z&ldZU>!^s6?!G=p*g&w;Ha0dCB8l234Xv%M17SbzEZ5Z3jG$0e`$dUxp7Q%O%7%t% z9?Y}(GHsY2KYrY7Ip#+Xb(j|v7Q%sketsE@1Q{AHGcstYsFZw(&d6zKRCFkJ6fn;4 z2dj+cVAJsP^CQdt=Gl)FumAYq)%EPMe14(@Ep*|alRqkoirDJ8>zrI%Aq}425V@UJ z5eo|sQS2vL8XEo46dV_=K=ds(W{5WDH4%qV_=&+=W2O4!z|ZmYtY(3tL;S%^7J_7{(D(6`tec$I$qg z{_4j?N0(MqT$d)&GQ5BPa4E}${2Hf#K;`(j0jl%WZ#Z%a>ZsJ^RZGj6mh358N{?zn zNHg-D{hd1m?YeKFTP8DL>Enhd-Me>>ymT`+FVEb-fEtaVt*ogzxc0q9?YW7Yn;`t% zev60s=GfSnsHkZFL0M7J)7_QfSFe~h`1>a&CcI!5&dyq}s6T~f_bR}b`b$C?<9&wr zZi@s-fv_lHVPTC~2B)pem$0$BK7ArbpH7$uv7-cn+gQ*Y5rq9-SINk#YzD3_9d}y} zWQv@fo%J91ot^kBN#dL`Rej;)=hwV@_t85QFRz2nC{jjA&;D=U(t85`+!Pg+2|W3m zuMjo;<44At7&KTUGQ5YOoz;~zHW;==TU(oX;9*2H!;c?7F6#N=!!Dx)4F_lEoSW94 z-kLOASsniQ^Bk(>1`pTHEKzLm&%op7J+;4szQt79j|IKi+n;g~`aTc4P*6xHW9{-T zl+#ruoey4{Ht#-*yDx=DMhbps-I14F?2(d?tF#7aJuhX?STM zDQGZV%^3&f@0V^76Qhc$=Zz(`omin^!D{-pz)+I)57Dp z`T0G)yl7&(-%Fwr*eYim1AAEE+Jtb>n_ zPlQap&2-Cm_q9VY{2^V~o?J-49!?Z=gf1cSz-EAEwFIMMXl5oiB;Lx(3Tm*TqGIs2 z`5Npe_Z_3Nrwl!Zngu>AeZEc0V#=@S<6mirOzj&s|$WM}+&7J*A_E-uYGW>^78U4E6*t4bQu}?hk$P2O0&h13NUZm z+FrQi=H`}Y=3JHV;M|D6g{-J}+#ZTUR8D1JVgmIP|A7WZnuf)amFxN0X`(^GO);^b zHMXQ!^f%p~VPI2JT=A}vm5}g->VNMySD;gB8lB!kJ!Jrx(x2v#pUy)((T)b@qHh}!EWIC z`uf4<&(D(=m+07Aov5y_EGiPQ>AzBLw$%ok(6zNR?D#eo9`E(p+!kq3(ecqyN=F(g z3kQc7zkr(BTH*3__{TM92v$}s1dGU1tGh2WDv`+@!yO*rT-Fm6fbRF22Zb4yE+b*C^xK7CRyH$&j@HoI27Z*&;rzVzT|4wr|gY-Po++^h|=?7f`D zT*NWOHFEOMx&WEW1Q9Up+a{Il?YYMH;gvo}O-(hLvb7=5oq<{1H7O&t8BYg4K0G}9 zdvyXpPgPahgv|LYLq$i2JltvegG;Q@iUn`qKCcXLaoIEvI;Ao{ZWeZ)6!xzfu(P)Z zfNWE2V{q3sGUD|%!2?wjH{@n&Uvqo$=He9fLy;GXEe3pXr|4AOM)YaF!H3d8f z4ePLn`>Cs|vvg)`p}W&2ZTFtEDJJaypIH14ILQC*&~O-%NJopbmX;P=QU*P!50Nc*%XMh-NIaHnkf>H>sgoR78yT6}&Mv{~t6Ox*j zx4F2ebQFdgybV}fZ-+%^@o|&+N8hJMyJ!Gph!FSX!GT=qEDx%l%rL{a5wqbn*Z($v(veED*BcQ?|}^4Xni zODbw=nVo*2ixU@~oSeW^1FR&OdaKj8HBcH&j%ai2j3h>?TVSI z7OoUaotl=GayB^Ey{z7`v}6OA%pW$MNm5i&di3ZK@DfDvBO4oKW##?dT|nlf zD6!!x@8ujX)dU^{+sA_$KV)l!(biV6pPXC>w^X8h%*&TAV`F1sF*7nU-a>gle_mQz z3fnp&{yD0tE;Q<`ql5;0YdBTr_=f3GOO~!(h|v4;b$Jqs!1~43cOrg(Yw#w z(u;~7d3bpE`Zg)DFbA+5!|ygYegCzzbZG>ehYzl_YpEFL60-M+&L#0I(RBzq&5ZBr z+n5*vl*(ogFcv^a&*S1$6DSVKO&gPUhZW#x9163Zq0!M@fcPyQ=zCgPr0Ds5k9(!U ze03_KRXqDBEFuC-8xWOd6Pbr9$MrjwmX^qBO#E5)vEV{xzLl zIg3i@U4Wi7({IQ9wbw-PO+!KMr6%l1kS1PNRo!4`@1C5@^x!jv zg?N#es14NgXcbUjSqgs?e)D1I7F!i(42v12r-6Y1Xbpeoq8Nees!ep@!terIJJ{QM zRosmYZCtgwzRqrjc~uIZcZN>f?O|YGOI{u$TK=z1M>xLN3M6-kJ|^4S|RYz5?4 z|B==Z{s$fU!GjC_VQM-JLJ$lJGqY0{FHc|;0Oy>YoFa*6f32+C@?0}Ij)M1TO2YU|I>3is5$wjYg?5}Ro{4S-SYm~5`Zit+x_`&pueBjdj(ab4rq(OSyM;v ze3ByJ)XUx7osp3dUMbX#R+i|%w{KVM#|VIbPBr_uKYhB_9){avfO$rR#%=$4>&Y){ zbckYqZ!h$Mn6=lUVq%Ok{m_G&XcvJpiHejfZ}@2Abs`VOmM9$ zn&P1f6;*EYi9cvxiHT$={a1hbg@%AKbMx|Qn49O#Rc!uj&4a0ynySC8{p3lt#&zrW zmqbKFph?-%m!%hK9zk`T1zxh0YWK@r1!DrYa*vUELi}_B7&6 z0s>A!&=CK_l$??A-NfbN)8#?>>z|ile4fU3xPgH6!Bx5n%C@ke;Le>pSFV%+kpTJ< zc(|Ar7#L{kKj;Tf#u)f_EVy{;kf`G2DAC{(1bP2>`K% zO9vo`zrCqEc>((wxw#DS`yr;#8?Ty5bsB&46&b&IxKfy)y2(NdOK_tqVQzqy8k?FH z6cpHB>B@y=Wjrvi9xV&d;9-L{@i(%eu5JvN(=I=Eq*w?}S~xbZQPrzgui6F&NqqXG zrKSJ;`2)2$Rqs6iS(2z1NM&4XY#%HHZ2a)%C#(bJcjQU2As#_C2kJSex)6c zqZPVAEN^2|TvH>2KDDMXGy5UX@)vU?7u-De za-;#!9{e6FTOP_wu#TVAFEe0vp&%l{jMb5mk-3#SrJ<^7)bKb*k6UR*P&OMl=?|}; zN;BnYX@+`wl-oA#w^j3g<+q=y%M%kn_nPr3DJvsfj|YJixQ>RdZp!Xib&fXY?!keU zrsm6@hj^b`8ydWp2Xne?w|%*1Ov&XfZZ6EWhkZFXg29CJiuIE@T47zlWf2leS#)_M zZ&6Lyht=2FiFL>i!~XRW%O_#<@888GC9Ja7E*koQ6H_xj@B66k@QDY0#Li(}8`BxW z_V@JlsU~go<>1Lcuh7bsimt!E5OqcBGB&j*2zC#eJh|dwmB??f2E;^NaumSt?Cc!gKqs@St8P*Rd3$?n zQ$}p9_;2@cAP>+z-tKC6ArVRW`IhePLLEP#!oo|+hGXL56mM~eDCw;Ed#1r+=LN<= zdwOVP6^bm4jEqc7Ok`Jo`MRUvL}L=hB{YZ4nUAqZk$mmbJFdqwut&> zW?D*0aK}hmPPY4yUeAV#H8MhftM2XX;o#sT&E$V>r=`B~Oq+AM!38ul>g4eYSeKsu z49T^n2nY&~2%2l^dlxdA#BvX&3?b8~XOE@xd?ZK3M&U6`1d+NWkefJi5M2Fven{D}RH=?~(! z>LfKW<{u~AM6MDEdCC(XkNW?uD z58tY-?M1-~3Gny7P)`(u>BMB1K>&FXWfn-Ks(lay9mSxZLe-489 zpsTGNT)^^5DoK@+ntFU{O1&lrHo)NL&$v1|2u#A9r>o<`K}W^4wYz1NE%JJoUoAq1 z1jx8M_5Ix=!Wg$;I4A>11DlY4rAdQks^JIAETG&6ySrbVx`59!^vOr=4!y8T3@C4J zYeV!*f0jMj!fOAB(4wTLfBuVNGRh^u#iz}hcvb+A8*IU%@2a}G6v-wQh_|3BmXwr? zxZrL)WX1@IXp0qSQLTx9wUw1^ZEBKGq{4W}8ACuq!q3N7^qu6C6hF94{n(Q=X7FQcd0!$rp1q*cXf3s zDk{F>REbDxiH^KfnzbRHe?wL_&*l$c@LiXX(-g3antgUi7-dZL_4kYtD3t)IIv#E> zevy21+rnZNx>%RhA8X06@$r|LnOCW(ih#fs{)DERiEGSDO-d(9q~fgAot3pgW>nj} zH*O-#(?u# zk{8GU0LM_K2;sU13PfoiA0J`TgKF!~WG~rd@pwI%$&CP2f+i@;LrO}jVloB`{zj43 zu=2s9oqMm24>k?0N)lzO(Q9kGsdrLeFKmK7KWxBG`k!CjT^{4(=9d3(<3(7Qg0*#_ zTQ?GkoP94?6Hr#WeGRO!K_6?*vK9 zhTiUxI3Y!BAS`6Mqbp1rNc3+4-c=S!7R+#htGp&CR4_E8nR^o-OU);*|Lx!IO9zE3 zpIce;qUW;dB?F^qX_3jH24hc~O2})&RPSzSS=mUCDtR;%PQLNj<#n=CkV}P4nB3@M zIW8t9QJy8xIJ1wwshaK3mR@gAqgPgd)xV1?J-W>+bUM zm^oHpNko#+$Go`$0GEgmQ=Jy{U}3^skR{((B~GvS5Gm<^*YKKKPyWf1o7*|^w`e2d z<29j@a;#+j&SL3xe+m0HXnS8Z)^38x`>v)&hpNVPPG0Y>Gz%p`k}MfhE2!|l(&N}U zIaL9zsOef&r!S%Ec}?jv!O(j#x4Efh>mJ@WXXohX=j&VGu)Yaq%oAbtDo1JQk6gON zuN<7Ab9;z-d3iO|)+*}9{SI!s_iblLc5`6?xw`~h1@@4Neq3dxKyisQv?*2Tls#Yr zHLZ)Gp`oi2l{_CCwK@IgpNnZz6-qEwQ7XH0FO(Tp#&lchb;E4Gq_3MWJTjttM5~v% zwN5$tGU#xTMj{I~Jm&gT{2Rn4^_%dLfGU-YbgWM`45zZblC2p)X#e=5GYahTMqQle zdpm|ZK(lywcsv$gW8?95zi(_DZpJP=v^~7Jo2GgN(J>K8$&#e{@7N=b?EDeTQ+-~2 zV(gbzb}~5psdsALz5ANV`ug?j+utUw!8zU%kqO@3cGIji)zL}H%2F@EdD*Pj#!O}u z19UUL#z$HC(xmLRl9Cdh$?*q(VPGI>JSQdksmbhe2+hbtwB+!~Jvlf${}5l&Z=bix z_-q+eZfH_=Q)_7%8PSFJeO^i9cTWlqU;&_xt)NatgcMy76B8?HSO?tiy~Tub!zPao zU&4|*kDR58P>H{0@8BRmvj#o*a!c!X0e}&M`ph{ADGs4+_lR;hs)n+~!>pMr!E|J9 zRS!U;*fVJ)J*2$1W?`jNX4d>RKTb?d#numiHU+i4pN-uS=+jxh$H<2$k^ ze5rR57-i#|3S>e5C)C%aRQk9xD4E_ewGBcu2|m7@_e2l?BVk6lzhcmw0Lke1?f`&h z;fq>^?Qna!CKZf!ef@~q?AB}IZVM-eJF1yYu5%p#)suvkM!I8ZNa-cQk)<#ju1%z$ z+EqjdIgaxp7q_lqzraAj~lHyfX`Q4*jvWH*>~Rpx`hIf(rrHG@rUO5Phg z$VJ{Muo!T>-T)j3Sc2AR8ay5WA>q!}79=Gsk;3dC3xNpcn*QJ@Zh-xZ=rPIo5MVhXl*cFcVi zsQ`Zh3koUVjO^1Lys&&=QNezyt*s3j!*Ta&bWoprx z8Ve2yF9o|9LPHWIol;VKY)s&wLPA2IZp9S9po6KF;6gw%eRiA(GYcd@C^wMHjCetm zsd)Rgdl#811@^|P-TMA%yLntZUY}u~^$b;uQ?q%6hmN<)8X6jYGlN|kJ;)p>PPc%Y z7QQVgBBJkWQcjA2A-b}*rsrz{*9zxB@6chtY+gB`b#io6FebF1&y4!^?A~ zhC2o&NX0f%6#fyph#1fJc{aDmCk_i=_U_$1wDTNM?13GFRr-(u+|PD_kl{42n51z` zqo_eB|E*9Z&{>Iw>Q8aBVYS!VSb&du1hOM{8nnltyqP#SRM5_+Cnt$; z*Ig46hMbh#4bD*GYHIkuW4vL=VgS&+<3dM97FeU$=4i~?N$YCUa zPCfTWrzKO=ITJO7I@B&@f;MUT!SgPLI66FYwcgF?fcvAQ07Xh7{AQS19XzD*U0S1g7Dan)cBC(W)lhRB3 z3iFb)mw^5M${8X^+UOyy0kPlu6sF|dO9wxLN@@;O)ZpxE?QDt zT;S~99Yf_|aD|JL^R_X`xts-8q>aTuhEUX?#k*NFZ%;JA3isMR@bw-1eJ_!dPW6 zm4UXJiF0FMs$1F)X6M&5g4zueRZmAJwF?GV)Rm?lk&*QTHyH5rLzRl7UC#r{ATjTNe(OX5fmJnH^lAWMZCJd z1syigke!kMZsziHPAyQ$XVdebsTp5S8Rl9WGc;FvFow{~z7_IVTMc~UN0d}l!~P2b zf`a5`;z2NoI-hX2+XHLyadzh0wn-B(*Id4KpF1^I%7@%u>EfBxAg_b0IfTfU0D;f@( z%bf1&UK?;5j;EJ&<<*ve{thxS*T+VU>&GDVBpM*)0Tdb<8L8U3O9%{*F!Av{AAEDv z%$(ArnJ4qS@R635mPA(C%aKHvm*%{{qv*jUAk?7LLAkDQAL%+c{06q0ntLr37c{48E`9%%A-niZppy1LY*DfIag&A#h-z6KJXlDd)-&VY*BQ7b+m zQWd>nb6-C{KoAG(Q;?$B{`8%Tk8faPqy`44Paoi{pVij51IiF}vwT3@q-vg0Ah*CU z&I%|&colS`Plhv&l^-MR((i+Fav!~zmiK!^OSb5Kt`V=OpkRmIjBdL$?^fDHPQ!)} zhyk?QXT3fbaR7k%H$?`?!43zsoOXP4bfZqzcJNvzADA{B^M;_=GjBn>4dn#nd=^qC z5O#XT?Q5#1Hwrrb4H3<3RW{=mfAQkzbwC%uVKsuRR-u)#yiu*YFnhw**z+?pAwWrc z`jL~H8}+rVZYvWK4vm;PDER&`anU z8{cWo0lZhpx!ePlb8>nL21Rfs;dPkEkd+}^jgbS_W|t09SM4|L!vXusD{!ZR+5|EP z%E@-PK&kH~pMu^f1t1X!=PfNS&##8kg2ai3kKcauVCDsv?n-MaSLz+8V1mj*R)wQX zO5%41RSftfB+Bn3c?`N~@IeWnt2?<>UiJTXY zcNZd95e}`8T}dzXS?o=H?eGcXC!18lpY84BJ4LDvGQc*3MEbBG=VnA@1k($`84B;r z!Fq%~(*J@UoI*pxD5O(38@Zalzf{Yz{rTYV@Z~W)VPRn+dWjbynE+GA9RYM9mh{P7v)y$rxfceb}D>zz$JJ?nw7*9inoz>khM zTZ2ybCP3{5Im6Oz6O_W2{ARvjBa_AMeOgq;iz;&6~gigwjXYJT+g z8W*|gG2!~i>>dTkh#^+(Uq@L@OR16FoU@>_LTq0%A2pTSIovL%B`7V7G;ex6D%q3a* zdXAJ&a&_K$swjHZO>F%7L3Q;eZExV43%FK5Ly-q-TotWJ7A*`UX)~1{e39No#mlc2pEEPXlJs7>+HM^ayCGw5AWZ< zauyd8o9OH;%+JSguCZW3Qcw_UW?Sw-zDV)-;W#S@Z6I!3{}{9;`1H3Pd+x_XTqPaw zPmu>&4_qU0UU)#>3KVLb&)a`s8iM?_Tf)5j{37~UQey2py9)BxLi_07X@&o|9|o-; z!7ycL9OgX~{C+{2Sy?kOWWljHe$V=D1e$5F+4#-310_DG! zmoM@K*RI7?#;T|Bw-@ZNC2491zxR$DJD2zaR^by;cu!szPUiT@!@f5*M3DWvGe)nv|7OIjlPV;7X{rlM}DCuJ}LSX9I<4YlJ9@ zi;IKdo|bWuae;8)&!F>wm*sy-Il;7nx4bz1)&u}=zeb_|Y+DE7RgxjINo1}_n$oDhy71r z?B)lP9TIQWfd~jsTx4Xdy6{j~Tem1EQi{~mW?BNW4sQbR1rWd?jj)3tFd-Bs6PeNd z`-_W6d8pmH{Z3+d-=BvQIrcAh6j2n;0?# z?Ax;mVBdUEu&RC!u5MM;7FeF})L^neh6VZ(v<66<+-bcmZugZ5A{vkd!NtYhiGr^s zz>#xuc7_C)&gOGWuw-!={mB>^6XqXj-Dbq5_6q>~QKFRx(bQY*sf7S~BV~7o&e>CX z0wKx^9y?5C10XJjhOynhAjt&gFFeN?{tj$lSag^GaA`2SHSXR8r3Oeq-D!SMMc0}&o!09RnzK$ZnQ0=U$XvRswk zI@WQeRIdpF%n(jJJv-h+I?k1V;|f*}FIa}3?Tdi3H(^6ILfZDwT5-3ixHz6cT7rcI z#z~Y1-w9yd8NW?wKfhxz0Z?_b-B|*7TKxERwY92gZQ+pr*P*NnIuAreY<0mj7&^mo zNt_*^3FojiY8GeEdI~!Yx;1G2s0ddRI7!giPTu*!`5EL1u5@H^MS7DPaf|4h0sI9h z8x|HOVBVt9M7`0|+v|%iN(28Nz5yT%DQsP>lgwvl!^P zA^WlIuZ*y|TKRC-Icv@aUD4`_9tcprD|! zlW@Ql@V?=52I5>(asup7aL-ftL*fPp2cg-6X7=TwAgZqFvXTyjCwE7pLAc7bzP8i6$Bb2F;`Sb{4P- zD@se512oddXJ*decas_XsP9A46m1wD08PK^M}Jc6NWf6hi3K_ zGO`yUJAY-|L@aZ9j`6--fp4giDUiw_Ct**66Q1W>HK_rfj_ z`$9GYKAcfLBFpzUJ=znS@$Q7g_3p~MQK&FgMwYI>)J8mp6>O)>mLI;?*n)NCe)`GZ z(sGVap!v2h@At=msgR_UR{n(LNF*DIk^Hf%r>CZd23x@U7fCN(bR9pn)DZ`ai0+KI zQn_KdH#8;=fD-Ty?2j;zVF7ByfUPM zbaXB!aD>P?E__LXSn)Np{`H?*d>3PuG3ex`!zd?wbOOy|4DM=nad8hQFFZ4OsT>MW>g8A(Lm>i-z0Rhq*3gchDx&j{qHw|*$g#^bSdeaZWcikfG;pWH-RPqe}JtA8+kGcBzHh&tG|C+{J_+B8HY{0P4dh1qcmDb;18D`^$7X*xOgv z)KEk#!q+3g%&^1YeMki+zZ|kPx_J@QZzwbZ0s`-B1I(7QJAc!mPb4we zAhy7CynZTejhQEa&j^03|5Ao6e3pbzhK#QDr#NOa98doiHvg`Hfwy3s)=fMB4Vc4> z5W$IFU4<_rfY6u98~-^eK9+@+3Sw(V=SVOkv{0~-K)_O?>9_`AMPJ|t%uyutEM_2z(gKVOhMqupU*HcUJoV@ z2jc?4@%Q(Szb1|XZ}&)2F*4@TI4cCkF7`qfwS_nz-&=5l>T7Eg&pweA{|*_eguIln z@QYnr^8YIYI(C%mY5|h~WaAH>>o7rC;dw&gV;v@Ui3fVCjdNf3r!Mj3eaLfwS`Rszy|V=u((if(EnjF!6zghPuDR2eIJ Date: Thu, 4 Aug 2022 15:45:03 +0200 Subject: [PATCH 0341/1204] Fix pages widths --- _layouts/default.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_layouts/default.html b/_layouts/default.html index 8fa354dc..b97cd792 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -15,7 +15,7 @@ src="//html5shiv.googlecode.com/svn/trunk/html5.js"> -
+

{{ site.title | default: site.github.repository_name }}

{% if site.logo %} @@ -58,7 +58,7 @@ Ball {% endif %}
-
+
{{ content }} From fb5f039991c53982287324a1c7d48422f4fe184d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 4 Aug 2022 15:48:35 +0200 Subject: [PATCH 0342/1204] Tweaks to layout --- _layouts/default.html | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/_layouts/default.html b/_layouts/default.html index b97cd792..879bb4a0 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -17,7 +17,6 @@ src="//html5shiv.googlecode.com/svn/trunk/html5.js">
-

{{ site.title | default: site.github.repository_name }}

{% if site.logo %} Logo {% endif %} @@ -26,14 +25,13 @@ src="//html5shiv.googlecode.com/svn/trunk/html5.js"> Home
- Releases -
- Javadoc + Releases +
+ Documentation
- Coverage + Javadoc +
+ Coverage
From a1eceaf8e10e2e0c0255057e99a6e84cb280e5f2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 8 Aug 2022 00:23:58 +0200 Subject: [PATCH 0343/1204] Add manpages, script to generate manpages --- .reuse/dep5 | 9 + .../packaging/man/pgpainless-cli-armor.1 | 138 ++++++++++++ .../packaging/man/pgpainless-cli-dearmor.1 | 132 ++++++++++++ .../packaging/man/pgpainless-cli-decrypt.1 | 201 +++++++++++++++++ .../packaging/man/pgpainless-cli-encrypt.1 | 170 +++++++++++++++ .../man/pgpainless-cli-extract-cert.1 | 138 ++++++++++++ .../man/pgpainless-cli-generate-completion.1 | 151 +++++++++++++ .../man/pgpainless-cli-generate-key.1 | 152 +++++++++++++ .../packaging/man/pgpainless-cli-help.1 | 146 +++++++++++++ .../man/pgpainless-cli-inline-detach.1 | 143 ++++++++++++ .../man/pgpainless-cli-inline-sign.1 | 165 ++++++++++++++ .../man/pgpainless-cli-inline-verify.1 | 165 ++++++++++++++ .../packaging/man/pgpainless-cli-sign.1 | 164 ++++++++++++++ .../packaging/man/pgpainless-cli-verify.1 | 153 +++++++++++++ .../packaging/man/pgpainless-cli-version.1 | 143 ++++++++++++ pgpainless-cli/packaging/man/pgpainless-cli.1 | 203 ++++++++++++++++++ pgpainless-cli/rewriteManPages.sh | 24 +++ 17 files changed, 2397 insertions(+) create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-armor.1 create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-help.1 create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-sign.1 create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-verify.1 create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-version.1 create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli.1 create mode 100755 pgpainless-cli/rewriteManPages.sh diff --git a/.reuse/dep5 b/.reuse/dep5 index 28dd7917..96efa937 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -60,3 +60,12 @@ License: CC0-1.0 Files: _layouts/* Copyright: 2022 Paul Schaub , 2017 Steve Smith License: CC-BY-SA-3.0 + +# Man Pages +Files: pgpainless-cli/rewriteManPages.sh +Copyright: 2022 Paul Schaub +License: Apache-2.0 + +Files: pgpainless-cli/packaging/man/* +Copyright: 2022 Paul Schaub +License: Apache-2.0 diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 new file mode 100644 index 00000000..bc0efe20 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 @@ -0,0 +1,138 @@ +'\" t +.\" Title: pgpainless-cli-armor +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-ARMOR" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-armor \- Add ASCII Armor to standard input +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli armor\fP [\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP +.RS 4 +Label to be used in the header and tail of the armoring +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 b/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 new file mode 100644 index 00000000..0dacf89c --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 @@ -0,0 +1,132 @@ +'\" t +.\" Title: pgpainless-cli-dearmor +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-DEARMOR" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-dearmor \- Remove ASCII Armor from standard input +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli dearmor\fP +.SH "DESCRIPTION" + +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 new file mode 100644 index 00000000..c97eb7e9 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 @@ -0,0 +1,201 @@ +'\" t +.\" Title: pgpainless-cli-decrypt +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-DECRYPT" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-decrypt \- Decrypt a message from standard input +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli decrypt\fP [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] +[\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP] [\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP] +[\fB\-\-verify\-with\fP=\fICERT\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... +[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP]... +[\fIKEY\fP...] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-not\-after\fP=\fIDATE\fP +.RS 4 +ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) +.sp +Reject signatures with a creation date not in range. +.sp +Defaults to current system time (\(aqnow\(aq). +.sp +Accepts special value \(aq\-\(aq for end of time. +.RE +.sp +\fB\-\-not\-before\fP=\fIDATE\fP +.RS 4 +ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) +.sp +Reject signatures with a creation date not in range. +.sp +Defaults to beginning of time (\(aq\-\(aq). +.RE +.sp +\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP +.RS 4 +Can be used to learn the session key on successful decryption +.RE +.sp +\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP +.RS 4 +Emits signature verification status to the designated output +.RE +.sp +\fB\-\-verify\-with\fP=\fICERT\fP +.RS 4 +Certificates for signature verification +.RE +.sp +\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +.RS 4 +Passphrase to unlock the secret key(s). +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.sp +\fB\-\-with\-password\fP=\fIPASSWORD\fP +.RS 4 +Symmetric passphrase to decrypt the message with. +.sp +Enables decryption based on any "SKESK" packets in the "CIPHERTEXT". +.RE +.sp +\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP +.RS 4 +Symmetric message key (session key). +.sp +Enables decryption of the "CIPHERTEXT" using the session key directly against the "SEIPD" packet. +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) +.RE +.SH "ARGUMENTS" +.sp +[\fIKEY\fP...] +.RS 4 +Secret keys to attempt decryption with +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 new file mode 100644 index 00000000..78c8e434 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 @@ -0,0 +1,170 @@ +'\" t +.\" Title: pgpainless-cli-encrypt +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-ENCRYPT" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-encrypt \- Encrypt a message from standard input +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli encrypt\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-as\fP=\fI{binary|text}\fP] [\fB\-\-sign\-with\fP=\fIKEY\fP]... +[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]... +[\fICERTS\fP...] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-as\fP=\fI{binary|text}\fP +.RS 4 +Type of the input data. Defaults to \(aqbinary\(aq +.RE +.sp +\fB\-\-[no\-]armor\fP +.RS 4 +ASCII armor the output +.RE +.sp +\fB\-\-sign\-with\fP=\fIKEY\fP +.RS 4 +Sign the output with a private key +.RE +.sp +\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +.RS 4 +Passphrase to unlock the secret key(s). +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.sp +\fB\-\-with\-password\fP=\fIPASSWORD\fP +.RS 4 +Encrypt the message with a password. +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) +.RE +.SH "ARGUMENTS" +.sp +[\fICERTS\fP...] +.RS 4 +Certificates the message gets encrypted to +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 b/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 new file mode 100644 index 00000000..7beaad14 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 @@ -0,0 +1,138 @@ +'\" t +.\" Title: pgpainless-cli-extract-cert +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-EXTRACT\-CERT" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-extract\-cert \- Extract a public key certificate from a secret key from standard input +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli extract\-cert\fP [\fB\-\-[no\-]armor\fP] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-[no\-]armor\fP +.RS 4 +ASCII armor the output +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 new file mode 100644 index 00000000..dfc5448a --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 @@ -0,0 +1,151 @@ +'\" t +.\" Title: pgpainless-cli-generate-completion +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: generate-completion 4.6.3 +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-GENERATE\-COMPLETION" "1" "2022-08-07" "generate\-completion 4.6.3" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-generate\-completion \- Generate bash/zsh completion script for pgpainless\-cli. +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli generate\-completion\fP [\fB\-hV\fP] +.SH "DESCRIPTION" +.sp +Generate bash/zsh completion script for pgpainless\-cli. +Run the following command to give \f(CRpgpainless\-cli\fP TAB completion in the current shell: +.sp +.if n .RS 4 +.nf +source <(pgpainless\-cli generate\-completion) +.fi +.if n .RE +.SH "OPTIONS" +.sp +\fB\-h\fP, \fB\-\-help\fP +.RS 4 +Show this help message and exit. +.RE +.sp +\fB\-V\fP, \fB\-\-version\fP +.RS 4 +Print version information and exit. +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 new file mode 100644 index 00000000..66d6d1b9 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 @@ -0,0 +1,152 @@ +'\" t +.\" Title: pgpainless-cli-generate-key +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-GENERATE\-KEY" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-generate\-key \- Generate a secret key +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli generate\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP] [\fI\fP...] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-[no\-]armor\fP +.RS 4 +ASCII armor the output +.RE +.sp +\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +.RS 4 +Password to protect the private key with +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.SH "ARGUMENTS" +.sp +[\fI\fP...] +.RS 4 +User\-ID, e.g. "Alice <\c +.MTO "alice\(atexample.com" "" ">"" +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-help.1 b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 new file mode 100644 index 00000000..8688615a --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 @@ -0,0 +1,146 @@ +'\" t +.\" Title: pgpainless-cli-help +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-HELP" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-help \- Display usage information for the specified subcommand +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli help\fP [\fB\-h\fP] [\fICOMMAND\fP] +.SH "DESCRIPTION" +.sp +When no COMMAND is given, the usage help for the main command is displayed. +If a COMMAND is specified, the help for that command is shown. +.SH "OPTIONS" +.sp +\fB\-h\fP, \fB\-\-help\fP +.RS 4 +Show usage help for the help command and exit. +.RE +.SH "ARGUMENTS" +.sp +[\fICOMMAND\fP] +.RS 4 +The COMMAND to display the usage help message for. +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 new file mode 100644 index 00000000..ccb4901a --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 @@ -0,0 +1,143 @@ +'\" t +.\" Title: pgpainless-cli-inline-detach +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-INLINE\-DETACH" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-inline\-detach \- Split signatures from a clearsigned message +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli inline\-detach\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-signatures\-out\fP=\fISIGNATURES\fP] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-[no\-]armor\fP +.RS 4 +ASCII armor the output +.RE +.sp +\fB\-\-signatures\-out\fP=\fISIGNATURES\fP +.RS 4 +Destination to which a detached signatures block will be written +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 new file mode 100644 index 00000000..90d8908f --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 @@ -0,0 +1,165 @@ +'\" t +.\" Title: pgpainless-cli-inline-sign +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-INLINE\-SIGN" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-inline\-sign \- Create an inline\-signed message from data on standard input +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli inline\-sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-as\fP=\fI{binary|text|cleartextsigned}\fP] +[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fIKEYS\fP...] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-as\fP=\fI{binary|text|cleartextsigned}\fP +.RS 4 +Specify the signature format of the signed message +.sp +\(aqtext\(aq and \(aqbinary\(aq will produce inline\-signed messages. +.sp +\(aqcleartextsigned\(aq will make use of the cleartext signature framework. +.sp +Defaults to \(aqbinary\(aq. +.sp +If \(aq\-\-as=text\(aq and the input data is not valid UTF\-8, inline\-sign fails with return code 53. +.RE +.sp +\fB\-\-[no\-]armor\fP +.RS 4 +ASCII armor the output +.RE +.sp +\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +.RS 4 +Passphrase to unlock the secret key(s). +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.SH "ARGUMENTS" +.sp +[\fIKEYS\fP...] +.RS 4 +Secret keys used for signing +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 new file mode 100644 index 00000000..2f989285 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 @@ -0,0 +1,165 @@ +'\" t +.\" Title: pgpainless-cli-inline-verify +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-INLINE\-VERIFY" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-inline\-verify \- Verify inline\-signed data from standard input +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli inline\-verify\fP [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] +[\fB\-\-verifications\-out\fP=\fI\fP] \fICERT\fP... +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-not\-after\fP=\fIDATE\fP +.RS 4 +ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) +.sp +Reject signatures with a creation date not in range. +.sp +Defaults to current system time ("now"). +.sp +Accepts special value "\-" for end of time. +.RE +.sp +\fB\-\-not\-before\fP=\fIDATE\fP +.RS 4 +ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) +.sp +Reject signatures with a creation date not in range. +.sp +Defaults to beginning of time ("\-"). +.RE +.sp +\fB\-\-verifications\-out\fP=\fI\fP +.RS 4 +File to write details over successful verifications to +.RE +.SH "ARGUMENTS" +.sp +\fICERT\fP... +.RS 4 +Public key certificates for signature verification +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 new file mode 100644 index 00000000..cc830e7f --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 @@ -0,0 +1,164 @@ +'\" t +.\" Title: pgpainless-cli-sign +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-SIGN" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-sign \- Create a detached signature on the data from standard input +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-as\fP=\fI{binary|text}\fP] [\fB\-\-micalg\-out\fP=\fIMICALG\fP] +[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fIKEYS\fP...] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-as\fP=\fI{binary|text}\fP +.RS 4 +Specify the output format of the signed message +.sp +Defaults to \(aqbinary\(aq. +.RE +.sp +\fB\-\-micalg\-out\fP=\fIMICALG\fP +.RS 4 +Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content\-Type (RFC3156) +.RE +.sp +\fB\-\-[no\-]armor\fP +.RS 4 +ASCII armor the output +.RE +.sp +\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +.RS 4 +Passphrase to unlock the secret key(s). +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +.RE +.SH "ARGUMENTS" +.sp +[\fIKEYS\fP...] +.RS 4 +Secret keys used for signing +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 new file mode 100644 index 00000000..297f8374 --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 @@ -0,0 +1,153 @@ +'\" t +.\" Title: pgpainless-cli-verify +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-VERIFY" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-verify \- Verify a detached signature over the data from standard input +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli verify\fP [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] \fISIGNATURE\fP \fICERT\fP... +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-not\-after\fP=\fIDATE\fP, \fB\-\-not\-before\fP=\fIDATE\fP +.RS 4 +ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) +.sp +Reject signatures with a creation date not in range. +.sp +Defaults to beginning of time ("\-"). +.RE +.SH "ARGUMENTS" +.sp +\fISIGNATURE\fP +.RS 4 +Detached signature +.RE +.sp +\fICERT\fP... +.RS 4 +Public key certificates for signature verification +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 new file mode 100644 index 00000000..0a235d7a --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 @@ -0,0 +1,143 @@ +'\" t +.\" Title: pgpainless-cli-version +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-VERSION" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-version \- Display version information about the tool +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli version\fP [\fB\-\-extended\fP | \fB\-\-backend\fP] +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-backend\fP +.RS 4 +Print information about the cryptographic backend +.RE +.sp +\fB\-\-extended\fP +.RS 4 +Print an extended version string +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli.1 b/pgpainless-cli/packaging/man/pgpainless-cli.1 new file mode 100644 index 00000000..171c394e --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli.1 @@ -0,0 +1,203 @@ +'\" t +.\" Title: pgpainless-cli +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Date: 2022-08-07 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli \- Stateless OpenPGP Protocol +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli\fP [COMMAND] +.SH "DESCRIPTION" + +.SH "COMMANDS" +.sp +\fBhelp\fP +.RS 4 +Display usage information for the specified subcommand +.RE +.sp +\fBarmor\fP +.RS 4 +Add ASCII Armor to standard input +.RE +.sp +\fBdearmor\fP +.RS 4 +Remove ASCII Armor from standard input +.RE +.sp +\fBdecrypt\fP +.RS 4 +Decrypt a message from standard input +.RE +.sp +\fBinline\-detach\fP +.RS 4 +Split signatures from a clearsigned message +.RE +.sp +\fBencrypt\fP +.RS 4 +Encrypt a message from standard input +.RE +.sp +\fBextract\-cert\fP +.RS 4 +Extract a public key certificate from a secret key from standard input +.RE +.sp +\fBgenerate\-key\fP +.RS 4 +Generate a secret key +.RE +.sp +\fBsign\fP +.RS 4 +Create a detached signature on the data from standard input +.RE +.sp +\fBverify\fP +.RS 4 +Verify a detached signature over the data from standard input +.RE +.sp +\fBinline\-sign\fP +.RS 4 +Create an inline\-signed message from data on standard input +.RE +.sp +\fBinline\-verify\fP +.RS 4 +Verify inline\-signed data from standard input +.RE +.sp +\fBversion\fP +.RS 4 +Display version information about the tool +.RE +.sp +\fBgenerate\-completion\fP +.RS 4 +Generate bash/zsh completion script for pgpainless\-cli. +.RE +.SH "EXIT CODES:" +.sp +\fB0\fP +.RS 4 +Successful program execution +.RE +.sp +\fB1\fP +.RS 4 +Generic program error +.RE +.sp +\fB3\fP +.RS 4 +Verification requested but no verifiable signature found +.RE +.sp +\fB13\fP +.RS 4 +Unsupported asymmetric algorithm +.RE +.sp +\fB17\fP +.RS 4 +Certificate is not encryption capable +.RE +.sp +\fB19\fP +.RS 4 +Usage error: Missing argument +.RE +.sp +\fB23\fP +.RS 4 +Incomplete verification instructions +.RE +.sp +\fB29\fP +.RS 4 +Unable to decrypt +.RE +.sp +\fB31\fP +.RS 4 +Password is not human\-readable +.RE +.sp +\fB37\fP +.RS 4 +Unsupported Option +.RE +.sp +\fB41\fP +.RS 4 +Invalid data or data of wrong type encountered +.RE +.sp +\fB53\fP +.RS 4 +Non\-text input received where text was expected +.RE +.sp +\fB59\fP +.RS 4 +Output file already exists +.RE +.sp +\fB61\fP +.RS 4 +Input file does not exist +.RE +.sp +\fB67\fP +.RS 4 +Cannot unlock password protected secret key +.RE +.sp +\fB69\fP +.RS 4 +Unsupported subcommand +.RE +.sp +\fB71\fP +.RS 4 +Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +.RE +.sp +\fB73\fP +.RS 4 +Ambiguous input (a filename matching the designator already exists) +.RE +.sp +\fB79\fP +.RS 4 +Key is not signing capable +.RE \ No newline at end of file diff --git a/pgpainless-cli/rewriteManPages.sh b/pgpainless-cli/rewriteManPages.sh new file mode 100755 index 00000000..321dbdde --- /dev/null +++ b/pgpainless-cli/rewriteManPages.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +SOP_DIR=$(realpath $SCRIPT_DIR/../../sop-java) +[ ! -d "$SOP_DIR" ] && echo "sop-java repository MUST be cloned next to pgpainless repo" && exit 1; +SRC_DIR=$SOP_DIR/sop-java-picocli/build/docs/manpage +[ ! -d "$SRC_DIR" ] && echo "No sop manpages found." && exit 1; +DEST_DIR=$SCRIPT_DIR/packaging/man +mkdir -p $DEST_DIR + +for page in $SRC_DIR/* +do + SRC="${page##*/}" + DEST="${SRC/sop/pgpainless-cli}" + sed \ + -e 's#.\\" Title: sop#.\\" Title: pgpainless-cli#g' \ + -e 's/Manual: Sop Manual/Manual: PGPainless-CLI Manual/g' \ + -e 's/.TH "SOP/.TH "PGPAINLESS\\-CLI/g' \ + -e 's/"Sop Manual"/"PGPainless\\-CLI Manual"/g' \ + -e 's/\\fBsop/\\fBpgpainless\\-cli/g' \ + -e 's/sop/pgpainless\\-cli/g' \ + $page > $DEST_DIR/$DEST +done + From e6b89e2c3b29caf4b98b43947eeb9a877f0e6edb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 8 Aug 2022 13:14:18 +0200 Subject: [PATCH 0344/1204] Add KeyRingReader.keyRing(*) mnethods to read either a public or secret key ring --- .../pgpainless/key/parsing/KeyRingReader.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java index 0021f74e..b5b8ea65 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java @@ -14,6 +14,7 @@ import java.util.List; import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPMarker; import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPPublicKeyRing; @@ -32,6 +33,42 @@ public class KeyRingReader { @SuppressWarnings("CharsetObjectCanBeUsed") public static final Charset UTF8 = Charset.forName("UTF-8"); + /** + * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing}) from the given + * {@link InputStream}. + * + * @param inputStream inputStream containing the OpenPGP key or certificate + * @return key ring + * @throws IOException in case of an IO error + */ + public PGPKeyRing keyRing(@Nonnull InputStream inputStream) throws IOException { + return readKeyRing(inputStream); + } + + /** + * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing}) from the given + * byte array. + * + * @param bytes byte array containing the OpenPGP key or certificate + * @return key ring + * @throws IOException in case of an IO error + */ + public PGPKeyRing keyRing(@Nonnull byte[] bytes) throws IOException { + return keyRing(new ByteArrayInputStream(bytes)); + } + + /** + * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing}) from the given + * ASCII armored string. + * + * @param asciiArmored ASCII armored OpenPGP key or certificate + * @return key ring + * @throws IOException in case of an IO error + */ + public PGPKeyRing keyRing(@Nonnull String asciiArmored) throws IOException { + return keyRing(asciiArmored.getBytes(UTF8)); + } + public PGPPublicKeyRing publicKeyRing(@Nonnull InputStream inputStream) throws IOException { return readPublicKeyRing(inputStream); } @@ -95,6 +132,55 @@ public class KeyRingReader { return keyRingCollection(asciiArmored.getBytes(UTF8), isSilent); } + /** + * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing}) from the given + * {@link InputStream}. + * This method will attempt to read at most {@link #MAX_ITERATIONS} objects from the stream before aborting. + * The first {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} will be returned. + * + * @param inputStream inputStream containing the OpenPGP key or certificate + * @return key ring + * @throws IOException in case of an IO error + */ + public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream) throws IOException { + return readKeyRing(inputStream, MAX_ITERATIONS); + } + + /** + * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing}) from the given + * {@link InputStream}. + * This method will attempt to read at most
maxIterations
objects from the stream before aborting. + * The first {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} will be returned. + * + * @param inputStream inputStream containing the OpenPGP key or certificate + * @param maxIterations maximum number of objects that are read before the method will abort + * @return key ring + * @throws IOException in case of an IO error + */ + public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( + ArmorUtils.getDecoderStream(inputStream)); + int i = 0; + Object next; + do { + next = objectFactory.nextObject(); + if (next == null) { + return null; + } + if (next instanceof PGPMarker) { + continue; + } + if (next instanceof PGPSecretKeyRing) { + return (PGPSecretKeyRing) next; + } + if (next instanceof PGPPublicKeyRing) { + return (PGPPublicKeyRing) next; + } + } while (++i < maxIterations); + + throw new IOException("Loop exceeded max iteration count."); + } + public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) throws IOException { return readPublicKeyRing(inputStream, MAX_ITERATIONS); } From b9845912ee1112a76158c2129b359056519c694e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 8 Aug 2022 13:20:28 +0200 Subject: [PATCH 0345/1204] Add tests for readKeyRing() --- .../key/parsing/KeyRingReaderTest.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java index 404440ca..8a634200 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/parsing/KeyRingReaderTest.java @@ -4,6 +4,7 @@ package org.pgpainless.key.parsing; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -23,6 +24,7 @@ import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.BCPGOutputStream; import org.bouncycastle.bcpg.MarkerPacket; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; @@ -562,4 +564,54 @@ class KeyRingReaderTest { assertThrows(IOException.class, () -> KeyRingReader.readPublicKeyRingCollection(new ByteArrayInputStream(bytes.toByteArray()), 512)); } + + @Test + public void testReadKeyRingWithBinaryPublicKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice "); + PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); + byte[] bytes = publicKeys.getEncoded(); + + PGPKeyRing keyRing = PGPainless.readKeyRing() + .keyRing(bytes); + + assertTrue(keyRing instanceof PGPPublicKeyRing); + assertArrayEquals(keyRing.getEncoded(), publicKeys.getEncoded()); + } + + @Test + public void testReadKeyRingWithBinarySecretKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice "); + byte[] bytes = secretKeys.getEncoded(); + + PGPKeyRing keyRing = PGPainless.readKeyRing() + .keyRing(bytes); + + assertTrue(keyRing instanceof PGPSecretKeyRing); + assertArrayEquals(keyRing.getEncoded(), secretKeys.getEncoded()); + } + + @Test + public void testReadKeyRingWithArmoredPublicKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice "); + PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); + String armored = PGPainless.asciiArmor(publicKeys); + + PGPKeyRing keyRing = PGPainless.readKeyRing() + .keyRing(armored); + + assertTrue(keyRing instanceof PGPPublicKeyRing); + assertArrayEquals(keyRing.getEncoded(), publicKeys.getEncoded()); + } + + @Test + public void testReadKeyRingWithArmoredSecretKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice "); + String armored = PGPainless.asciiArmor(secretKeys); + + PGPKeyRing keyRing = PGPainless.readKeyRing() + .keyRing(armored); + + assertTrue(keyRing instanceof PGPSecretKeyRing); + assertArrayEquals(keyRing.getEncoded(), secretKeys.getEncoded()); + } } From bc5dc50b788ec4826dde1ab32cb4fbc72df5eba3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 9 Aug 2022 15:08:59 +0200 Subject: [PATCH 0346/1204] Add KeyRingInfo.isSigningCapable() Fixes #307 --- .../org/pgpainless/key/info/KeyRingInfo.java | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 85cc8f28..cb328300 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -1051,10 +1051,36 @@ public class KeyRingInfo { return isKeyValidlyBound(getKeyId()) && !getEncryptionSubkeys(purpose).isEmpty(); } - public boolean isUsableForSigning() { + /** + * Returns true, if the key ring is capable of signing. + * Contrary to {@link #isUsableForSigning()}, this method also returns true, if this {@link KeyRingInfo} is based + * on a key ring which has at least one valid public key marked for signing. + * The secret key is not required for the key ring to qualify as signing capable. + * + * @return true if key corresponding to the cert is capable of signing + */ + public boolean isSigningCapable() { + // check if primary-key is revoked / expired if (!isKeyValidlyBound(getKeyId())) { return false; } + // check if it has signing-capable key + return !getSigningSubkeys().isEmpty(); + } + + /** + * Returns true, if this {@link KeyRingInfo} is based on a {@link PGPSecretKeyRing}, which has a valid signing key + * which is ready to be used (i.e. secret key is present and is not on a smart-card). + * + * If you just want to check, whether a key / certificate has signing capable subkeys, + * use {@link #isSigningCapable()} instead. + * + * @return true if key is ready to be used for signing + */ + public boolean isUsableForSigning() { + if (!isSigningCapable()) { + return false; + } List signingKeys = getSigningSubkeys(); for (PGPPublicKey pk : signingKeys) { From b3c6a33afe6de74043fcb796372a2f0a8bb72f7c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 11 Aug 2022 10:36:32 +0200 Subject: [PATCH 0347/1204] Update readme --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98324bea..32651964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.5-SNAPSHOT +- Add `KeyRingInfo.isCapableOfSigning()` +- Add `KeyRingReader.readKeyRing(*)` methods that can take both secret- and public keys +- Add manpages + - Add script to generate manpages from sop-java-picocli +- Build website from main branch + ## 1.3.4 - Fix `KeyRingInfo.isUsableForEncryption()`, `KeyRingInfo.isUsableForSigning()` not detecting revoked primary keys - Bump `sop-java` and `sop-java-picocli` to `4.0.1` From c361fecdd931900e61fe0e1683dadee64c402756 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 11 Aug 2022 15:56:32 +0200 Subject: [PATCH 0348/1204] PGPainless 1.3.5 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32651964..40035c0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.3.5-SNAPSHOT +## 1.3.5 - Add `KeyRingInfo.isCapableOfSigning()` - Add `KeyRingReader.readKeyRing(*)` methods that can take both secret- and public keys - Add manpages diff --git a/README.md b/README.md index c62cfc78..a733e426 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.4' + implementation 'org.pgpainless:pgpainless-core:1.3.5' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 3dc4e6c7..d5e22046 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.4" + implementation "org.pgpainless:pgpainless-sop:1.3.5" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.4 + 1.3.5 ... diff --git a/version.gradle b/version.gradle index bd645100..a0424908 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.5' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From bbcbba021d635c1e78567ae89e82b32e2dd0eca6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 11 Aug 2022 15:58:57 +0200 Subject: [PATCH 0349/1204] PGPainless 1.3.6-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index a0424908..3baa01be 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.5' - isSnapshot = false + shortVersion = '1.3.6' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From aeffcdd8ee77e97c4c6ef6badcbd672beea34f52 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 12 Aug 2022 13:05:17 +0200 Subject: [PATCH 0350/1204] Add repology packaging status badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a733e426..778f91c1 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ SPDX-License-Identifier: Apache-2.0 **PGPainless is an easy-to-use OpenPGP library for Java and Android applications** +[![Packaging status](https://repology.org/badge/vertical-allrepos/pgpainless.svg)](https://repology.org/project/pgpainless/versions) + ## About PGPainless aims to make using OpenPGP in Java projects as simple as possible. From 6b3d676531eaa53aba3a713b5d741a7cd8109311 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 12 Aug 2022 13:06:09 +0200 Subject: [PATCH 0351/1204] Move maven packaging badge next to repology badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 778f91c1..482b8ac9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless - Use OpenPGP Painlessly! [![Build Status](https://github.com/pgpainless/pgpainless/actions/workflows/gradle_push.yml/badge.svg)](https://github.com/pgpainless/pgpainless/actions/workflows/gradle_push.yml) -[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-core)](https://search.maven.org/artifact/org.pgpainless/pgpainless-core) [![Coverage Status](https://coveralls.io/repos/github/pgpainless/pgpainless/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/pgpainless?branch=main) [![Interoperability Test-Suite](https://badgen.net/badge/Sequoia%20Test%20Suite/%232/green)](https://tests.sequoia-pgp.org/) [![PGP](https://img.shields.io/badge/pgp-A027%20DB2F%203E1E%20118A-blue)](https://keyoxide.org/7F9116FEA90A5983936C7CFAA027DB2F3E1E118A) @@ -17,6 +16,7 @@ SPDX-License-Identifier: Apache-2.0 **PGPainless is an easy-to-use OpenPGP library for Java and Android applications** [![Packaging status](https://repology.org/badge/vertical-allrepos/pgpainless.svg)](https://repology.org/project/pgpainless/versions) +[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-core)](https://search.maven.org/artifact/org.pgpainless/pgpainless-core) ## About From 054828ef8c1fd1ff0c0bffa183fcd663b9e0adbc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 10:37:55 +0200 Subject: [PATCH 0352/1204] Remove deprecated EncryptionResult.getSymmetricKeyAlgorithm() Use getEncryptionAlgorithm() instead --- .../encryption_signing/EncryptionResult.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java index 10342a3c..4112092c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java @@ -46,19 +46,6 @@ public final class EncryptionResult { this.fileEncoding = encoding; } - /** - * Return the symmetric encryption algorithm used to encrypt the message. - * @return symmetric encryption algorithm - * - * @deprecated use {@link #getEncryptionAlgorithm()} instead. - * - * TODO: Remove in 1.2.X - */ - @Deprecated - public SymmetricKeyAlgorithm getSymmetricKeyAlgorithm() { - return getEncryptionAlgorithm(); - } - /** * Return the symmetric encryption algorithm used to encrypt the message. * From 7faa6c580ab1102faf43c1a9e68fd12ca6f49a1b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 10:38:44 +0200 Subject: [PATCH 0353/1204] Remove deprecated ArmorUtils.createArmoredOutputStream() --- .../java/org/pgpainless/util/ArmorUtils.java | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index 48bdc0cc..160d8d0a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -267,28 +267,6 @@ public final class ArmorUtils { return armoredOutputStream; } - /** - * Return an {@link ArmoredOutputStream} prepared with headers for the given key ring, which wraps the given - * {@link OutputStream}. - * - * The armored output stream can be used to encode the key ring by calling {@link PGPKeyRing#encode(OutputStream)} - * with the armored output stream as an argument. - * - * @param keyRing key ring - * @param outputStream wrapped output stream - * @return armored output stream - * - * @deprecated use {@link #toAsciiArmoredStream(PGPKeyRing, OutputStream)} instead - * - * TODO: Remove in 1.2.X - */ - @Deprecated - @Nonnull - public static ArmoredOutputStream createArmoredOutputStreamFor(@Nonnull PGPKeyRing keyRing, - @Nonnull OutputStream outputStream) { - return toAsciiArmoredStream(keyRing, outputStream); - } - /** * Generate a header map for ASCII armor from the given {@link PGPKeyRing}. * From 6f161c83363181f10c570baef2dc1f335c518e7f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 10:42:18 +0200 Subject: [PATCH 0354/1204] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40035c0b..b44e5502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.6-SNAPSHOT +- Remove deprecated methods + - `ArmorUtils.createArmoredOutputStreamFor()` -> use `ArmorUtils.toAsciiArmoredStream()` instead + - `EncryptionResult.getSymmetricKeyAlgorithm()` -> use `EncryptionResult.getEncryptionAlgorithm()` instead + ## 1.3.5 - Add `KeyRingInfo.isCapableOfSigning()` - Add `KeyRingReader.readKeyRing(*)` methods that can take both secret- and public keys From 405e67c0cbcd1448dbb87d7dadef589e65cced2c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 11:06:17 +0200 Subject: [PATCH 0355/1204] Add documentation to AlgorithmNegotiator classes --- .../negotiation/HashAlgorithmNegotiator.java | 33 +++++++++++++++++++ .../SymmetricKeyAlgorithmNegotiator.java | 21 ++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.java index f76a2c26..18fe53f9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.java @@ -9,18 +9,51 @@ import java.util.Set; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.policy.Policy; +/** + * Interface for a class that negotiates {@link HashAlgorithm HashAlgorithms}. + * + * You can provide your own implementation using custom logic by implementing the + * {@link #negotiateHashAlgorithm(Set)} method. + */ public interface HashAlgorithmNegotiator { + /** + * Pick one {@link HashAlgorithm} from the ordered set of acceptable algorithms. + * + * @param orderedHashAlgorithmPreferencesSet hash algorithm preferences + * @return picked algorithms + */ HashAlgorithm negotiateHashAlgorithm(Set orderedHashAlgorithmPreferencesSet); + /** + * Return an instance that negotiates {@link HashAlgorithm HashAlgorithms} used for non-revocation signatures + * based on the given {@link Policy}. + * + * @param policy algorithm policy + * @return negotiator + */ static HashAlgorithmNegotiator negotiateSignatureHashAlgorithm(Policy policy) { return negotiateByPolicy(policy.getSignatureHashAlgorithmPolicy()); } + /** + * Return an instance that negotiates {@link HashAlgorithm HashAlgorithms} used for revocation signatures + * based on the given {@link Policy}. + * + * @param policy algorithm policy + * @return negotiator + */ static HashAlgorithmNegotiator negotiateRevocationSignatureAlgorithm(Policy policy) { return negotiateByPolicy(policy.getRevocationSignatureHashAlgorithmPolicy()); } + /** + * Return an instance that negotiates {@link HashAlgorithm HashAlgorithms} based on the given + * {@link Policy.HashAlgorithmPolicy}. + * + * @param hashAlgorithmPolicy algorithm policy for hash algorithms + * @return negotiator + */ static HashAlgorithmNegotiator negotiateByPolicy(Policy.HashAlgorithmPolicy hashAlgorithmPolicy) { return new HashAlgorithmNegotiator() { @Override diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.java index 427bcc69..de8ced24 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.java @@ -22,18 +22,35 @@ public interface SymmetricKeyAlgorithmNegotiator { /** * Negotiate a symmetric encryption algorithm. + * If the override is non-null, it will be returned instead of performing an actual negotiation. + * Otherwise, the list of ordered sets containing the preferences of different recipient keys will be + * used to determine a suitable symmetric encryption algorithm. * * @param policy algorithm policy * @param override algorithm override (if not null, return this) * @param keyPreferences list of preferences per key * @return negotiated algorithm */ - SymmetricKeyAlgorithm negotiate(Policy.SymmetricKeyAlgorithmPolicy policy, SymmetricKeyAlgorithm override, List> keyPreferences); + SymmetricKeyAlgorithm negotiate( + Policy.SymmetricKeyAlgorithmPolicy policy, + SymmetricKeyAlgorithm override, + List> keyPreferences); + /** + * Return an instance that negotiates a {@link SymmetricKeyAlgorithm} by selecting the most popular acceptable + * algorithm from the list of preferences. + * + * This negotiator has the best chances to select an algorithm which is understood by all recipients. + * + * @return negotiator that selects by popularity + */ static SymmetricKeyAlgorithmNegotiator byPopularity() { return new SymmetricKeyAlgorithmNegotiator() { @Override - public SymmetricKeyAlgorithm negotiate(Policy.SymmetricKeyAlgorithmPolicy policy, SymmetricKeyAlgorithm override, List> preferences) { + public SymmetricKeyAlgorithm negotiate( + Policy.SymmetricKeyAlgorithmPolicy policy, + SymmetricKeyAlgorithm override, + List> preferences) { if (override == SymmetricKeyAlgorithm.NULL) { throw new IllegalArgumentException("Algorithm override cannot be NULL (plaintext)."); } From d019c0d5db2fd948e0e823b491322b2c9588ff3d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 11:09:32 +0200 Subject: [PATCH 0356/1204] Add RevocationState implementation from wot branch --- .../pgpainless/algorithm/RevocationState.java | 134 ++++++++++++++++-- .../algorithm/RevocationStateType.java | 23 +++ 2 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java index 5a0fa142..bf17ca2b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java @@ -4,20 +4,128 @@ package org.pgpainless.algorithm; -public enum RevocationState { +import org.pgpainless.util.DateUtil; - /** - * Certificate is not revoked. - */ - notRevoked, +import javax.annotation.Nonnull; +import java.util.Date; +import java.util.NoSuchElementException; - /** - * Certificate is revoked with a soft revocation. - */ - softRevoked, +public class RevocationState implements Comparable { - /** - * Certificate is revoked with a hard revocation. - */ - hardRevoked + private final RevocationStateType type; + private final Date date; + + private RevocationState(RevocationStateType type) { + this(type, null); + } + + private RevocationState(RevocationStateType type, Date date) { + this.type = type; + if (type == RevocationStateType.softRevoked && date == null) { + throw new NullPointerException("If type is 'softRevoked' then date cannot be null."); + } + this.date = date; + } + + public static RevocationState notRevoked() { + return new RevocationState(RevocationStateType.notRevoked); + } + + public static RevocationState softRevoked(@Nonnull Date date) { + return new RevocationState(RevocationStateType.softRevoked, date); + } + + public static RevocationState hardRevoked() { + return new RevocationState(RevocationStateType.hardRevoked); + } + + public RevocationStateType getType() { + return type; + } + + public @Nonnull Date getDate() { + if (!isSoftRevocation()) { + throw new NoSuchElementException("RevocationStateType is not equal to 'softRevoked'. Cannot extract date."); + } + return date; + } + + public boolean isHardRevocation() { + return getType() == RevocationStateType.hardRevoked; + } + + public boolean isSoftRevocation() { + return getType() == RevocationStateType.softRevoked; + } + + public boolean isNotRevoked() { + return getType() == RevocationStateType.notRevoked; + } + + @Override + public String toString() { + String out = getType().toString(); + if (isSoftRevocation()) { + out = out + " (" + DateUtil.formatUTCDate(date) + ")"; + } + return out; + } + + @Override + public int compareTo(@Nonnull RevocationState o) { + switch (getType()) { + case notRevoked: + if (o.isNotRevoked()) { + return 0; + } else { + return -1; + } + + case softRevoked: + if (o.isNotRevoked()) { + return 1; + } else if (o.isSoftRevocation()) { + // Compare soft dates in reverse + return o.getDate().compareTo(getDate()); + } else { + return -1; + } + + case hardRevoked: + if (o.isHardRevocation()) { + return 0; + } else { + return 1; + } + + default: + throw new AssertionError("Unknown type: " + type); + } + } + + @Override + public int hashCode() { + return type.hashCode() * 31 + (isSoftRevocation() ? getDate().hashCode() : 0); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (!(obj instanceof RevocationState)) { + return false; + } + RevocationState other = (RevocationState) obj; + if (getType() != other.getType()) { + return false; + } + if (isSoftRevocation()) { + return DateUtil.toSecondsPrecision(getDate()).getTime() == DateUtil.toSecondsPrecision(other.getDate()).getTime(); + } + return true; + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.java new file mode 100644 index 00000000..d1757255 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +public enum RevocationStateType { + + /** + * Certificate is not revoked. + */ + notRevoked, + + /** + * Certificate is revoked with a soft revocation. + */ + softRevoked, + + /** + * Certificate is revoked with a hard revocation. + */ + hardRevoked +} From c73905d179c77c7aca04bd6a067dc09d15ec8a52 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 11:12:42 +0200 Subject: [PATCH 0357/1204] Import RevocationStateTest from wot branch --- .../pgpainless/algorithm/RevocationState.java | 2 +- .../algorithm/RevocationStateTest.java | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java index bf17ca2b..8e4a60d3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java @@ -10,7 +10,7 @@ import javax.annotation.Nonnull; import java.util.Date; import java.util.NoSuchElementException; -public class RevocationState implements Comparable { +public final class RevocationState implements Comparable { private final RevocationStateType type; private final Date date; diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java new file mode 100644 index 00000000..c1e45413 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import org.junit.jupiter.api.Test; +import org.pgpainless.util.DateUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RevocationStateTest { + + @Test + public void testNotRevoked() { + RevocationState state = RevocationState.notRevoked(); + assertEquals(RevocationStateType.notRevoked, state.getType()); + assertTrue(state.isNotRevoked()); + assertFalse(state.isHardRevocation()); + assertFalse(state.isSoftRevocation()); + assertThrows(NoSuchElementException.class, state::getDate); + assertEquals("notRevoked", state.toString()); + } + + @Test + public void testHardRevoked() { + RevocationState state = RevocationState.hardRevoked(); + assertEquals(RevocationStateType.hardRevoked, state.getType()); + assertTrue(state.isHardRevocation()); + assertFalse(state.isSoftRevocation()); + assertFalse(state.isNotRevoked()); + + assertThrows(NoSuchElementException.class, state::getDate); + assertEquals("hardRevoked", state.toString()); + } + + @Test + public void testSoftRevoked() { + Date date = DateUtil.parseUTCDate("2022-08-03 18:26:35 UTC"); + assertNotNull(date); + + RevocationState state = RevocationState.softRevoked(date); + assertEquals(RevocationStateType.softRevoked, state.getType()); + assertTrue(state.isSoftRevocation()); + assertFalse(state.isHardRevocation()); + assertFalse(state.isNotRevoked()); + assertEquals(date, state.getDate()); + + assertEquals("softRevoked (2022-08-03 18:26:35 UTC)", state.toString()); + } + + @Test + public void testSoftRevokedNullDateThrows() { + assertThrows(NullPointerException.class, () -> RevocationState.softRevoked(null)); + } + + @Test + public void orderTest() { + assertEquals(RevocationState.notRevoked(), RevocationState.notRevoked()); + assertEquals(RevocationState.hardRevoked(), RevocationState.hardRevoked()); + Date now = new Date(); + assertEquals(RevocationState.softRevoked(now), RevocationState.softRevoked(now)); + + assertEquals(0, RevocationState.notRevoked().compareTo(RevocationState.notRevoked())); + assertEquals(0, RevocationState.hardRevoked().compareTo(RevocationState.hardRevoked())); + assertTrue(RevocationState.hardRevoked().compareTo(RevocationState.notRevoked()) > 0); + + List states = new ArrayList<>(); + RevocationState earlySoft = RevocationState.softRevoked(DateUtil.parseUTCDate("2000-05-12 10:44:01 UTC")); + RevocationState laterSoft = RevocationState.softRevoked(DateUtil.parseUTCDate("2022-08-03 18:26:35 UTC")); + RevocationState hard = RevocationState.hardRevoked(); + RevocationState not = RevocationState.notRevoked(); + RevocationState not2 = RevocationState.notRevoked(); + states.add(laterSoft); + states.add(not); + states.add(not2); + states.add(hard); + states.add(earlySoft); + + Collections.shuffle(states); + Collections.sort(states); + + assertEquals(states, Arrays.asList(not, not2, laterSoft, earlySoft, hard)); + } +} From 0cc884523ca79f04dbdb17f676dc1d8b4719061d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 11:30:10 +0200 Subject: [PATCH 0358/1204] Integrate RevocationState into KeyRingInfo class --- .../org/pgpainless/key/info/KeyRingInfo.java | 18 +++++++++++++++++- .../pgpainless/key/info/KeyRingInfoTest.java | 8 +++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index cb328300..995bded4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -36,6 +36,7 @@ import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.RevocationState; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.exception.KeyException; import org.pgpainless.key.OpenPgpFingerprint; @@ -58,6 +59,7 @@ public class KeyRingInfo { private final Signatures signatures; private final Date referenceDate; private final String primaryUserId; + private final RevocationState revocationState; /** * Evaluate the key ring at creation time of the given signature. @@ -101,6 +103,16 @@ public class KeyRingInfo { this.signatures = new Signatures(keys, validationDate, policy); this.referenceDate = validationDate; this.primaryUserId = findPrimaryUserId(); + this.revocationState = findRevocationState(); + } + + private RevocationState findRevocationState() { + PGPSignature revocation = signatures.primaryKeyRevocation; + if (revocation != null) { + return SignatureUtils.isHardRevocation(revocation) ? + RevocationState.hardRevoked() : RevocationState.softRevoked(revocation.getCreationTime()); + } + return RevocationState.notRevoked(); } /** @@ -650,13 +662,17 @@ public class KeyRingInfo { return mostRecent; } + public RevocationState getRevocationState() { + return revocationState; + } + /** * Return the date on which the primary key was revoked, or null if it has not yet been revoked. * * @return revocation date or null */ public @Nullable Date getRevocationDate() { - return getRevocationSelfSignature() == null ? null : getRevocationSelfSignature().getCreationTime(); + return getRevocationState().isSoftRevocation() ? getRevocationState().getDate() : null; } /** diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java index f49d5a46..61e09477 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java @@ -49,6 +49,7 @@ import org.pgpainless.key.generation.type.eddsa.EdDSACurve; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.key.util.UserId; import org.pgpainless.util.DateUtil; import org.pgpainless.util.Passphrase; @@ -105,7 +106,12 @@ public class KeyRingInfoTest { assertNull(sInfo.getRevocationDate()); assertNull(pInfo.getRevocationDate()); Date revocationDate = DateUtil.now(); - PGPSecretKeyRing revoked = PGPainless.modifyKeyRing(secretKeys).revoke(new UnprotectedKeysProtector()).done(); + PGPSecretKeyRing revoked = PGPainless.modifyKeyRing(secretKeys).revoke( + new UnprotectedKeysProtector(), + RevocationAttributes.createKeyRevocation() + .withReason(RevocationAttributes.Reason.KEY_RETIRED) + .withoutDescription() + ).done(); KeyRingInfo rInfo = PGPainless.inspectKeyRing(revoked); assertNotNull(rInfo.getRevocationDate()); assertEquals(revocationDate.getTime(), rInfo.getRevocationDate().getTime(), 5); From 1b04d67e1a768405bb6300fc51b676a51eff3450 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 11:30:26 +0200 Subject: [PATCH 0359/1204] Remove unused SignatureSubpacketGeneratorUtil class and tests --- .../SignatureSubpacketGeneratorUtil.java | 75 ------------------- .../SignatureSubpacketGeneratorUtilTest.java | 40 ---------- 2 files changed, 115 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorUtil.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/util/SignatureSubpacketGeneratorUtilTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorUtil.java deleted file mode 100644 index 8fc02e7f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorUtil.java +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -import java.util.Date; -import javax.annotation.Nonnull; - -import org.bouncycastle.bcpg.SignatureSubpacket; -import org.bouncycastle.bcpg.SignatureSubpacketTags; -import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; - -/** - * Utility class that helps to deal with BCs SignatureSubpacketGenerator class. - */ -public final class SignatureSubpacketGeneratorUtil { - - private SignatureSubpacketGeneratorUtil() { - - } - - /** - * Remove all packets of the given type from the {@link PGPSignatureSubpacketGenerator PGPSignatureSubpacketGenerators} - * internal set. - * - * @param subpacketType type of subpacket to remove - * @param subpacketGenerator subpacket generator - */ - public static void removeAllPacketsOfType(org.pgpainless.algorithm.SignatureSubpacket subpacketType, - PGPSignatureSubpacketGenerator subpacketGenerator) { - removeAllPacketsOfType(subpacketType.getCode(), subpacketGenerator); - } - - /** - * Remove all packets of the given type from the {@link PGPSignatureSubpacketGenerator PGPSignatureSubpacketGenerators} - * internal set. - * - * @param type type of subpacket to remove - * @param subpacketGenerator subpacket generator - */ - public static void removeAllPacketsOfType(int type, PGPSignatureSubpacketGenerator subpacketGenerator) { - for (SignatureSubpacket subpacket : subpacketGenerator.getSubpackets(type)) { - subpacketGenerator.removePacket(subpacket); - } - } - - /** - * Replace all occurrences of a signature creation time subpackets in the subpacket generator - * with a single new instance representing the provided date. - * - * @param date signature creation time - * @param subpacketGenerator subpacket generator - */ - public static void setSignatureCreationTimeInSubpacketGenerator(Date date, PGPSignatureSubpacketGenerator subpacketGenerator) { - removeAllPacketsOfType(SignatureSubpacketTags.CREATION_TIME, subpacketGenerator); - subpacketGenerator.setSignatureCreationTime(false, date); - } - - /** - * Replace all occurrences of key expiration time subpackets in the subpacket generator - * with a single instance representing the new expiration time. - * - * @param expirationDate expiration time as date or null for no expiration - * @param creationDate date on which the key was created - * @param subpacketGenerator subpacket generator - */ - public static void setKeyExpirationDateInSubpacketGenerator(Date expirationDate, - @Nonnull Date creationDate, - PGPSignatureSubpacketGenerator subpacketGenerator) { - removeAllPacketsOfType(SignatureSubpacketTags.KEY_EXPIRE_TIME, subpacketGenerator); - long secondsToExpire = SignatureSubpacketsUtil.getKeyLifetimeInSeconds(expirationDate, creationDate); - subpacketGenerator.setKeyExpirationTime(true, secondsToExpire); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/SignatureSubpacketGeneratorUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/SignatureSubpacketGeneratorUtilTest.java deleted file mode 100644 index fe10cf1e..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/util/SignatureSubpacketGeneratorUtilTest.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.util.Date; - -import org.bouncycastle.bcpg.SignatureSubpacketTags; -import org.bouncycastle.bcpg.sig.Features; -import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.junit.jupiter.api.Test; -import org.pgpainless.algorithm.SignatureSubpacket; -import org.pgpainless.signature.subpackets.SignatureSubpacketGeneratorUtil; - -public class SignatureSubpacketGeneratorUtilTest { - - @Test - public void testRemoveAllPacketsOfTypeRemovesAll() { - PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator(); - generator.setFeature(false, Features.FEATURE_MODIFICATION_DETECTION); - generator.setSignatureCreationTime(false, new Date()); - generator.setSignatureCreationTime(true, new Date()); - PGPSignatureSubpacketVector vector = generator.generate(); - - assertEquals(2, vector.getSubpackets(SignatureSubpacketTags.CREATION_TIME).length); - assertNotNull(vector.getSubpackets(SignatureSubpacketTags.FEATURES)); - - generator = new PGPSignatureSubpacketGenerator(vector); - SignatureSubpacketGeneratorUtil.removeAllPacketsOfType(SignatureSubpacket.signatureCreationTime, generator); - vector = generator.generate(); - - assertEquals(0, vector.getSubpackets(SignatureSubpacketTags.CREATION_TIME).length); - assertNotNull(vector.getSubpackets(SignatureSubpacketTags.FEATURES)); - } -} From 3f82bd31149321292b909a8e7e84573092ea7fee Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 12:03:01 +0200 Subject: [PATCH 0360/1204] Quickstart guide: Add section on ASCII armor --- docs/source/pgpainless-core/quickstart.md | 63 ++++++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/docs/source/pgpainless-core/quickstart.md b/docs/source/pgpainless-core/quickstart.md index f2a018ef..a965799a 100644 --- a/docs/source/pgpainless-core/quickstart.md +++ b/docs/source/pgpainless-core/quickstart.md @@ -91,15 +91,64 @@ PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); ``` ### Apply / Remove ASCII Armor +ASCII armor is a layer of radix64 encoding that can be used to wrap binary OpenPGP data in order to make it save to +transport via text-based channels (e.g. email bodies). + +The way in which ASCII armor can be applied depends on the type of data that you want to protect. +The easies way to ASCII armor an OpenPGP key or certificate is by using PGPainless' `asciiArmor()` method: + +```java +PGPPublicKey certificate = ...; +String asciiArmored = PGPainless.asciiArmor(certificate); +``` + +If you want to ASCII armor ciphertext, you can enable ASCII armoring during encrypting/signing by requesting +PGPainless to armor the result: + +```java +ProducerOptions producerOptions = ...; // prepare as usual (see next section) + +producerOptions.setAsciiArmor(true); // enable armoring + +EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(producerOptions); + +... +``` + +If you have an already encrypted / signed binary message and want to add ASCII armoring retrospectively, you need +to make use of BouncyCastle's `ArmoredOutputStream` as follows: + +```java +InputStream binaryOpenPgpIn = ...; // e.g. new ByteArrayInputStream(binaryMessage); + +OutputStream output = ...; // e.g. new ByteArrayOutputStream(); +ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(output); + +Streams.pipeAll(binaryOpenPgpIn, armorOut); +armorOut.close(); // important! +``` + +The output stream will now contain the ASCII armored representation of the binary data. + +To remove ASCII armor, you can make use of BouncyCastle's `ArmoredInputStream` as follows: + +```java +InputStream input = ...; // e.g. new ByteArrayInputStream(armoredString.getBytes(StandardCharsets.UTF8)); +OutputStream output = ...; + +ArmoredInputStream armorIn = new ArmoredInputStream(input); +Streams.pipeAll(armorIn, output); +armorIn.close(); +``` + +The output stream will now contain the binary OpenPGP data. + +### Encrypt and/or Sign a Message TODO -### Encrypt a Message -TODO - -### Decrypt a Message -TODO - -### Sign a Message +### Decrypt and/or Verify a Message TODO ### Verify a Signature From 39ff2bca73b81ee539a44962c5e96ed6aae5db62 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 12:35:51 +0200 Subject: [PATCH 0361/1204] Fix javadoc of SigningOptions methods --- .../encryption_signing/SigningOptions.java | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java index f81bf5aa..43db00d1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -104,6 +104,7 @@ public final class SigningOptions { * @param signingKey key ring containing the signing key * @return this * + * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be unlocked or a signing method cannot be created */ public SigningOptions addSignature(SecretKeyRingProtector signingKeyProtector, @@ -119,6 +120,7 @@ public final class SigningOptions { * @param signingKeys collection of signing keys * @param signatureType type of signature (binary, canonical text) * @return this + * * @throws KeyException if something is wrong with any of the keys * @throws PGPException if any of the keys cannot be unlocked or a signing method cannot be created */ @@ -140,9 +142,10 @@ public final class SigningOptions { * @param secretKeyDecryptor decryptor to unlock the signing secret key * @param secretKey signing key * @param signatureType type of signature (binary, canonical text) + * @return this + * * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be unlocked or the signing method cannot be created - * @return this */ public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, PGPSecretKeyRing secretKey, @@ -163,7 +166,8 @@ public final class SigningOptions { * @param userId user-id of the signer * @param signatureType signature type (binary, canonical text) * @return this - * @throws KeyException if the key is invalid + * + * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be unlocked or the signing method cannot be created */ public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, @@ -187,8 +191,8 @@ public final class SigningOptions { * @param signatureType signature type (binary, canonical text) * @param subpacketsCallback callback to modify the hashed and unhashed subpackets of the signature * @return this - * @throws KeyException - * if the key is invalid + * + * @throws KeyException if the key is invalid * @throws PGPException if the key cannot be unlocked or the signing method cannot be created */ public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, @@ -234,6 +238,8 @@ public final class SigningOptions { * @param signingKeys collection of signing key rings * @param signatureType type of the signature (binary, canonical text) * @return this + * + * @throws KeyException if something is wrong with any of the keys * @throws PGPException if any of the keys cannot be validated or unlocked, or if any signing method cannot be created */ public SigningOptions addDetachedSignatures(SecretKeyRingProtector secretKeyDecryptor, @@ -255,8 +261,10 @@ public final class SigningOptions { * @param secretKeyDecryptor decryptor to unlock the secret signing key * @param secretKey signing key * @param signatureType type of data that is signed (binary, canonical text) - * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created * @return this + * + * @throws KeyException if something is wrong with the key + * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created */ public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, PGPSecretKeyRing secretKey, @@ -277,8 +285,10 @@ public final class SigningOptions { * @param secretKey signing key * @param userId user-id * @param signatureType type of data that is signed (binary, canonical text) - * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created * @return this + * + * @throws KeyException if something is wrong with the key + * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created */ public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, PGPSecretKeyRing secretKey, @@ -301,8 +311,10 @@ public final class SigningOptions { * @param userId user-id * @param signatureType type of data that is signed (binary, canonical text) * @param subpacketCallback callback to modify hashed and unhashed subpackets of the signature - * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created * @return this + * + * @throws KeyException if something is wrong with the key + * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created */ public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, PGPSecretKeyRing secretKey, From d1001412a15c83313f3bd3f35e6602fab9f38daf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 12:36:16 +0200 Subject: [PATCH 0362/1204] Add SigningOptions.addDetachedSignature(protector, key) shortcut method --- .../encryption_signing/SigningOptions.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java index 43db00d1..0a7c47c4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -252,6 +252,23 @@ public final class SigningOptions { return this; } + /** + * Create a detached signature. + * The signature will be of type {@link DocumentSignatureType#BINARY_DOCUMENT}. + * + * @param secretKeyDecryptor decryptor to unlock the secret signing key + * @param signingKey signing key + * @return this + * + * @throws KeyException if something is wrong with the key + * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created + */ + public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, + PGPSecretKeyRing signingKey) + throws PGPException { + return addDetachedSignature(secretKeyDecryptor, signingKey, DocumentSignatureType.BINARY_DOCUMENT); + } + /** * Create a detached signature. * Detached signatures are not being added into the PGP message itself. From 5746985bb77e145db569b4c6f9ab9f5ffeaec8ce Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 12:46:36 +0200 Subject: [PATCH 0363/1204] Add EncryptionOptions.get() factory method --- .../encryption_signing/EncryptionOptions.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 2a75ee4e..6d2ca642 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -80,6 +80,19 @@ public class EncryptionOptions { this.purpose = purpose; } + /** + * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys + * which carry either the {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS} or + * {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE} flag. + * + * Use this if you are not sure. + * + * @return encryption options + */ + public static EncryptionOptions get() { + return new EncryptionOptions(); + } + /** * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys * which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}. From bc24c4626ab1af839977ce8750c50a63420c8f73 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 13:00:50 +0200 Subject: [PATCH 0364/1204] Add ConsumerOptions.get() factory method --- .../pgpainless/decryption_verification/ConsumerOptions.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index 7b8d1e70..b6117a60 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -55,6 +55,10 @@ public class ConsumerOptions { private MultiPassStrategy multiPassStrategy = new InMemoryMultiPassStrategy(); + public static ConsumerOptions get() { + return new ConsumerOptions(); + } + /** * Consider signatures on the message made before the given timestamp invalid. * Null means no limitation. From 4efe8fb468cafb1ba478d1d8bdb332657d968565 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 13:18:02 +0200 Subject: [PATCH 0365/1204] Quickstart guide: Add sections on encrypting, signing, decryption, verification --- docs/source/pgpainless-core/quickstart.md | 179 +++++++++++++++++++++- 1 file changed, 176 insertions(+), 3 deletions(-) diff --git a/docs/source/pgpainless-core/quickstart.md b/docs/source/pgpainless-core/quickstart.md index a965799a..371a5f6f 100644 --- a/docs/source/pgpainless-core/quickstart.md +++ b/docs/source/pgpainless-core/quickstart.md @@ -132,6 +132,22 @@ armorOut.close(); // important! The output stream will now contain the ASCII armored representation of the binary data. +If the data you want to wrap in ASCII armor is non-OpenPGP data (e.g. the String "Hello World!"), +you need to use the following code: + +```java +InputStream inputStream = ...; +OutputStream output = ...; + +EncryptionStream armorStream = PGPainless.encryptAndOrSign() + .onOutputStream(output) + .withOptions(ProducerOptions.noEncryptionNoSigning() + .setAsciiArmor(true)); + +Streams.pipeAll(inputStream, armorStream); +armorStream.close(); +``` + To remove ASCII armor, you can make use of BouncyCastle's `ArmoredInputStream` as follows: ```java @@ -146,10 +162,167 @@ armorIn.close(); The output stream will now contain the binary OpenPGP data. ### Encrypt and/or Sign a Message -TODO +Encrypting and signing messages is done using the same API in PGPainless. +The type of action depends on the configuration of the `ProducerOptions` class, which in term accepts +`SigningOptions` and `EncryptionOptions` objects: + +```java +// Encrypt only +ProducerOptions options = ProducerOptions.encrypt(encryptionOptions); + +// Sign only +ProducerOptions options = ProducerOptions.sign(signingOptions); + +// Sign and encrypt +ProducerOptions options = ProducerOptions.signAndEncrypt(signingOptions, encryptionOptions); +``` + +The `ProducerOptions` object can then be passed into the `encryptAndOrSign()` API: + +```java +InputStream plaintext = ...; // The data that shall be encrypted and/or signed +OutputStream ciphertext = ...; // Destination for the ciphertext + +EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertext) + .withOptions(options); // pass in the options object + +Streams.pipeAll(plaintext, encryptionStream); // pipe the data through +encryptionStream.close(); // important! Close the stream to finish encryption/signing + +EncryptionResult result = encryptionStream.getResult(); // metadata +``` + +The `ciphertext` output stream now contains the encrypted and/or signed data. + +Now lets take a look at the configuration of the `SigningOptions` object and how to instruct PGPainless to add a simple +signature to the message: + +```java +PGPSecretKeyRing signingKey = ...; // Key used for signing +SecretKeyRingProtector protector = ...; // Protector to unlock the signing key + +SigningOptions signOptions = SigningOptions.get() + .addSignature(protector, signingKey); +``` +This will add an inline signature to the message. + +It is possible to add multiple signatures from different keys by repeating the `addSignature()` method call. + +If instead of an inline signature, you want to create a detached signature instead (e.g. because you do not want +to alter the data you are signing), you can add the signature as follows: + +```java +signOptions.addDetachedSignature(protector, signingKey); +``` + +Passing in the `SigningOptions` object like this will result in the signature not being added to the message itself. +Instead, the signature can later be acquired from the `EncryptionResult` object via `EncryptionResult.getDetachedSignatures()`. +That way, it can be distributed independent of the message. + +The `EncryptionOptions` object can be configured in a similar way: + +```java +PGPPublicKey certificate = ...; + +EncryptionOptions encOptions = EncryptionOptions.get() + .addRecipient(certificate); +``` + +Once again, it is possible to add multiple recipients by repeating the `addRecipient()` method call. + +You can also encrypt a message to a password like this: +```java +encOptions.addPassphrase(Passphrase.fromPassword("sw0rdf1sh")); +``` + +Both methods can be used in combination to create a message which can be decrypted with either a recipients secret key +or the passphrase. ### Decrypt and/or Verify a Message -TODO +Decryption and verification of a message is both done using the same API. +Whether a message was actually signed / encrypted can be determined after the message has been processed by checking +the `OpenPgpMetadata` object which can be obtained from the `DecryptionStream`. + +To configure the decryption / verification process, the `ConsumerOptions` object is used: + +```java +PGPPublicKeyRing verificationCert = ...; // optional, signers certificate for signature verification +PGPSecretKeyRing decryptionKey = ...; // optional, decryption key + +ConsumerOptions options = ConsumerOptions.get() + .addVerificationCert(verificationCert) // add a verification cert for signature verification + .addDecryptionKey(decryptionKey); // add a secret key for message decryption +``` + +Both verification certificates and decryption keys are optional. +If you know the message is signed, but not encrypted you can omit providing a decryption key. +Same goes for if you know that the message is encrypted, but not signed. +In this case you can omit the verification certificate. + +On the other hand, providing these parameters does not hurt. +PGPainless will ignore unused keys / certificates, so if you provide a decryption key and the message is not encrypted, +nothing bad will happen. + +It is possible to provide multiple verification certs and decryption keys. PGPainless will pick suitable ones on the fly. +If the message is signed with key `0xAAAA` and you provide certificates `0xAAAA` and `0xBBBB`, it will verify +with cert `0xAAAA` and ignore `0xBBBB`. + +To do the actual decryption / verification of the message, do the following: + +```java +InputStream ciphertext = ...; // encrypted and/or signed message +OutputStream plaintext = ...; // destination for the plaintext + +ConsumerOptions options = ...; // see above +DecryptionStream consumerStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertext) + .withOptions(options); + +Streams.pipeAll(consumerStream, plaintext); +consumerStream.close(); // important! + +// The result will contain metadata of the message +OpenPgpMetadata result = consumerStream.getResult(); +``` + +After the message has been processed, you can consult the `OpenPgpMetadata` object to determine the nature of the message: + +```java +boolean wasEncrypted = result.isEncrypted(); +SubkeyIdentifier decryptionKey = result.getDecryptionKey(); +Map validSignatures = result.getVerifiedSignatures(); +boolean wasSignedByCert = result.containsVerifiedSignatureFrom(certificate); + +// For files: +String fileName = result.getFileName(); +Date modificationData = result.getModificationDate(); +``` ### Verify a Signature -TODO +In some cases, detached signatures are distributed alongside the message. +This is the case for example with Debians `Release` and `Release.gpg` files. +Here, `Release` is the plaintext message, which is unaltered by the signing process while `Release.gpg` contains +the detached OpenPGP signature. + +To verify a detached signature, you need to call the PGPainless API like this: + +```java +InputStream plaintext = ...; // e.g. new FileInputStream(releaseFile); +InputStream detachedSignature = ...; // e.g. new FileInputStream(releaseGpgFile); +PGPPublicKeyRing certificate = ...; // e.g. debians public signing key + +ConsumerOptions options = ConsumerOptions.get() + .addVerificationCert(certificate) // provide certificate for verification + .addVerificationOfDetachedSignatures(detachedSignature) // provide detached signature + +DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() + .onInputStream(plaintext) + .withOptions(options); + +Streams.drain(verificationStream); // push all the data through the stream +verificationStream.close(); // finish verification + +OpenPgpMetadata result = verificationStream.getResult(); // get metadata of signed message +assertTrue(result.containsVerifiedSignatureFrom(certificate)); // check if message was in fact signed +``` \ No newline at end of file From a2bfb55d87c59b0273dcd84195cb8499eddcda1a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 13:22:44 +0200 Subject: [PATCH 0366/1204] Use *Options.get() factory methods in SOP module --- .../src/main/java/org/pgpainless/sop/DecryptImpl.java | 2 +- .../src/main/java/org/pgpainless/sop/DetachedSignImpl.java | 2 +- .../src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java | 2 +- .../src/main/java/org/pgpainless/sop/EncryptImpl.java | 2 +- .../src/main/java/org/pgpainless/sop/InlineSignImpl.java | 2 +- .../src/main/java/org/pgpainless/sop/InlineVerifyImpl.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 7d8481e3..4957f748 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -35,7 +35,7 @@ import sop.operation.Decrypt; public class DecryptImpl implements Decrypt { - private final ConsumerOptions consumerOptions = new ConsumerOptions(); + private final ConsumerOptions consumerOptions = ConsumerOptions.get(); private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); @Override diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java index 704b5af5..0ce326a9 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java @@ -39,7 +39,7 @@ public class DetachedSignImpl implements DetachedSign { private boolean armor = true; private SignAs mode = SignAs.Binary; - private final SigningOptions signingOptions = new SigningOptions(); + private final SigningOptions signingOptions = SigningOptions.get(); private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); @Override diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index 81cb08ff..e6e2768e 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -24,7 +24,7 @@ import sop.operation.DetachedVerify; public class DetachedVerifyImpl implements DetachedVerify { - private final ConsumerOptions options = new ConsumerOptions(); + private final ConsumerOptions options = ConsumerOptions.get(); @Override public DetachedVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 0843ae08..1b95d87c 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -36,7 +36,7 @@ import sop.util.ProxyOutputStream; public class EncryptImpl implements Encrypt { - EncryptionOptions encryptionOptions = new EncryptionOptions(); + EncryptionOptions encryptionOptions = EncryptionOptions.get(); SigningOptions signingOptions = null; MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); private final Set signingKeys = new HashSet<>(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index 639d8c6e..3bdc8fc3 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -80,7 +80,7 @@ public class InlineSignImpl implements InlineSign { for (PGPSecretKeyRing key : signingKeys) { try { if (mode == InlineSignAs.CleartextSigned) { - signingOptions.addDetachedSignature(protector, key, DocumentSignatureType.BINARY_DOCUMENT); + signingOptions.addDetachedSignature(protector, key); } else { signingOptions.addInlineSignature(protector, key, modeToSigType(mode)); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 7c31f44b..1e8c4fee 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -26,7 +26,7 @@ import sop.operation.InlineVerify; public class InlineVerifyImpl implements InlineVerify { - private final ConsumerOptions options = new ConsumerOptions(); + private final ConsumerOptions options = ConsumerOptions.get(); @Override public InlineVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { From 76905cc1e828bfad386d343683d8405fbc9654d1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 13:28:08 +0200 Subject: [PATCH 0367/1204] Add installation hint to cli usage guide --- docs/source/pgpainless-cli/usage.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/source/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md index d1694ea7..967bfc11 100644 --- a/docs/source/pgpainless-cli/usage.md +++ b/docs/source/pgpainless-cli/usage.md @@ -10,6 +10,18 @@ You can use it to generate keys, encrypt, sign and decrypt messages, as well as Essentially, `pgpainless-cli` is just a very small composing module, which injects `pgpainless-sop` as a concrete implementation of `sop-java` into `sop-java-picocli`. +## Install + +The `pgpainless-cli` command line application is available in Debian unstable / Ubuntu 22.10 and can be installed via APT: +```shell +$ sudo apt install pgpainless-cli +``` + +This method comes with man-pages: +```shell +$ man pgpainless-cli +``` + ## Build To build a standalone *fat*-jar: From c6676d3c91583e5b66ea1c774d71ef55eff26e59 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 14:12:02 +0200 Subject: [PATCH 0368/1204] Add support for generating keys without user-ids Fixes #296 --- .../key/generation/KeyRingBuilder.java | 44 ++++----- .../consumer/CertificateValidator.java | 3 +- .../GenerateKeyWithoutUserIdTest.java | 93 +++++++++++++++++++ 3 files changed, 118 insertions(+), 22 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java index 88ed6ecd..10970fc2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java @@ -143,9 +143,6 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { @Override public PGPSecretKeyRing build() throws NoSuchAlgorithmException, PGPException, InvalidAlgorithmParameterException { - if (userIds.isEmpty()) { - throw new IllegalStateException("At least one user-id is required."); - } PGPDigestCalculator keyFingerprintCalculator = ImplementationFactory.getInstance().getV4FingerprintCalculator(); PBESecretKeyEncryptor secretKeyEncryptor = buildSecretKeyEncryptor(keyFingerprintCalculator); PBESecretKeyDecryptor secretKeyDecryptor = buildSecretKeyDecryptor(); @@ -157,19 +154,35 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { PGPContentSignerBuilder signer = buildContentSigner(certKey); PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(signer); - // Prepare primary user-id sig SignatureSubpackets hashedSubPacketGenerator = primaryKeySpec.getSubpacketGenerator(); hashedSubPacketGenerator.setIssuerFingerprintAndKeyId(certKey.getPublicKey()); - hashedSubPacketGenerator.setPrimaryUserId(); if (expirationDate != null) { hashedSubPacketGenerator.setKeyExpirationTime(certKey.getPublicKey(), expirationDate); } + if (!userIds.isEmpty()) { + hashedSubPacketGenerator.setPrimaryUserId(); + } + PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator(); SignatureSubpacketsHelper.applyTo(hashedSubPacketGenerator, generator); PGPSignatureSubpacketVector hashedSubPackets = generator.generate(); + PGPKeyRingGenerator ringGenerator; + if (userIds.isEmpty()) { + ringGenerator = new PGPKeyRingGenerator( + certKey, + keyFingerprintCalculator, + hashedSubPackets, + null, + signer, + secretKeyEncryptor); + } else { + String primaryUserId = userIds.entrySet().iterator().next().getKey(); + ringGenerator = new PGPKeyRingGenerator( + SignatureType.POSITIVE_CERTIFICATION.getCode(), certKey, + primaryUserId, keyFingerprintCalculator, + hashedSubPackets, null, signer, secretKeyEncryptor); + } - PGPKeyRingGenerator ringGenerator = buildRingGenerator( - certKey, signer, keyFingerprintCalculator, hashedSubPackets, secretKeyEncryptor); addSubKeys(certKey, ringGenerator); // Generate secret key ring with only primary user id @@ -182,7 +195,9 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKeyRing.getSecretKey(), secretKeyDecryptor); Iterator> userIdIterator = this.userIds.entrySet().iterator(); - userIdIterator.next(); // Skip primary user id + if (userIdIterator.hasNext()) { + userIdIterator.next(); // Skip primary user id + } while (userIdIterator.hasNext()) { Map.Entry additionalUserId = userIdIterator.next(); String userIdString = additionalUserId.getKey(); @@ -217,19 +232,6 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { return secretKeyRing; } - private PGPKeyRingGenerator buildRingGenerator(PGPKeyPair certKey, - PGPContentSignerBuilder signer, - PGPDigestCalculator keyFingerprintCalculator, - PGPSignatureSubpacketVector hashedSubPackets, - PBESecretKeyEncryptor secretKeyEncryptor) - throws PGPException { - String primaryUserId = userIds.entrySet().iterator().next().getKey(); - return new PGPKeyRingGenerator( - SignatureType.POSITIVE_CERTIFICATION.getCode(), certKey, - primaryUserId, keyFingerprintCalculator, - hashedSubPackets, null, signer, secretKeyEncryptor); - } - private void addSubKeys(PGPKeyPair primaryKey, PGPKeyRingGenerator ringGenerator) throws NoSuchAlgorithmException, PGPException, InvalidAlgorithmParameterException { for (KeySpec subKeySpec : subkeySpecs) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java index 809ea003..4c4c6689 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java @@ -138,6 +138,7 @@ public final class CertificateValidator { } boolean anyUserIdValid = false; + boolean hasAnyUserIds = !userIdSignatures.keySet().isEmpty(); for (String userId : userIdSignatures.keySet()) { if (!userIdSignatures.get(userId).isEmpty()) { PGPSignature current = userIdSignatures.get(userId).get(0); @@ -149,7 +150,7 @@ public final class CertificateValidator { } } - if (!anyUserIdValid) { + if (hasAnyUserIds && !anyUserIdValid) { throw new SignatureValidationException("No valid user-id found.", rejections); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java new file mode 100644 index 00000000..8b022a21 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.DateUtil; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GenerateKeyWithoutUserIdTest { + + @Test + public void generateKeyWithoutUserId() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + Date expirationDate = DateUtil.toSecondsPrecision(new Date(DateUtil.now().getTime() + 1000 * 6000)); + PGPSecretKeyRing secretKey = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .setExpirationDate(expirationDate) + .build(); + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + assertNull(info.getPrimaryUserId()); + assertTrue(info.getUserIds().isEmpty()); + JUtils.assertDateEquals(expirationDate, info.getPrimaryKeyExpirationDate()); + + InputStream plaintextIn = new ByteArrayInputStream("Hello, World!\n".getBytes()); + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); + + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions(ProducerOptions.signAndEncrypt( + EncryptionOptions.get() + .addRecipient(certificate), + SigningOptions.get() + .addSignature(protector, secretKey) + )); + Streams.pipeAll(plaintextIn, encryptionStream); + encryptionStream.close(); + + EncryptionResult result = encryptionStream.getResult(); + assertTrue(result.isEncryptedFor(certificate)); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray()); + ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addDecryptionKey(secretKey) + .addVerificationCert(certificate)); + + Streams.pipeAll(decryptionStream, plaintextOut); + decryptionStream.close(); + + OpenPgpMetadata metadata = decryptionStream.getResult(); + + assertTrue(metadata.containsVerifiedSignatureFrom(certificate)); + assertTrue(metadata.isEncrypted()); + } +} From cf43c5f83b46faad373e2072fbaa605a64b41808 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 14:43:41 +0200 Subject: [PATCH 0369/1204] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b44e5502..0421a2c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ SPDX-License-Identifier: CC0-1.0 - Remove deprecated methods - `ArmorUtils.createArmoredOutputStreamFor()` -> use `ArmorUtils.toAsciiArmoredStream()` instead - `EncryptionResult.getSymmetricKeyAlgorithm()` -> use `EncryptionResult.getEncryptionAlgorithm()` instead +- Add `KeyRingInfo.getRevocationState()` + - Better way to determine whether a key is revoked +- Add `SigningOptions.addDetachedSignature(protector, key)` shortcut method +- Add `EncryptionOptions.get()`, `ConsumerOptions.get()` factory methods +- Add support for generating keys without user-id (only using `PGPainless.buildKeyRing()` for now) ## 1.3.5 - Add `KeyRingInfo.isCapableOfSigning()` From 251bbaeaa7a416360cd4d151d9970827ceef84cb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 30 Aug 2022 22:35:50 +0200 Subject: [PATCH 0370/1204] Remove branches section from README --- README.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/README.md b/README.md index 482b8ac9..2e14efc1 100644 --- a/README.md +++ b/README.md @@ -212,18 +212,6 @@ We are using SemVer (MAJOR.MINOR.PATCH) versioning, although MINOR releases coul If you want to contribute a bug fix, please check the `release/X.Y` branches first to see, what the oldest release is which contains the bug you are fixing. That way we can update older revisions of the library easily. -### Branches -* `release/X.Y` contains the state of the latest `X.Y.Z` PATCH release + next PATCH snapshot definition. -* `master` contains the state of the latest MINOR release + some smaller changes that will make it into the next PATCH release. -* `development` contains new features that will make it into the next MINOR release. - -#### Example: -Latest release: 1.1.4 -* `release/1.0` contains the state of `1.0.5-SNAPSHOT` -* `release/1.1` contains the state of `1.1.5-SNAPSHOT` -* `master` contains the state `release/1.1` plus patch level changes that will make it into `1.1.5`. -* `development` contains the state which will at some point become `1.2.0`. - Please follow the [code of conduct](CODE_OF_CONDUCT.md) if you want to be part of the project. ## Acknowledgements From 15046cdc32d0fe0eb14910892bc0742dc04f2954 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 31 Aug 2022 21:37:31 +0200 Subject: [PATCH 0371/1204] Switch default S2K for secret key protection over to use SHA256 and add documentation --- .../protection/KeyRingProtectionSettings.java | 61 +++++++++++++++++-- .../InvalidProtectionSettingsTest.java | 20 ++++++ 2 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/protection/InvalidProtectionSettingsTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/KeyRingProtectionSettings.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/KeyRingProtectionSettings.java index ede286c6..a93534ab 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/KeyRingProtectionSettings.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/KeyRingProtectionSettings.java @@ -9,18 +9,39 @@ import javax.annotation.Nonnull; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +/** + * Secret key protection settings for iterated and salted S2K. + */ public class KeyRingProtectionSettings { private final SymmetricKeyAlgorithm encryptionAlgorithm; private final HashAlgorithm hashAlgorithm; private final int s2kCount; + /** + * Create a {@link KeyRingProtectionSettings} object using the given encryption algorithm, SHA1 and + * 65536 iterations. + * + * @param encryptionAlgorithm encryption algorithm + */ public KeyRingProtectionSettings(@Nonnull SymmetricKeyAlgorithm encryptionAlgorithm) { - this(encryptionAlgorithm, HashAlgorithm.SHA1, 0x60); // Same s2kCount as used in BC. + this(encryptionAlgorithm, HashAlgorithm.SHA1, 0x60); // Same s2kCount (encoded) as used in BC. } + /** + * Constructor for custom salted and iterated S2K protection settings. + * The salt gets randomly chosen by the library each time. + * + * Note, that the s2kCount is the already encoded single-octet number. + * + * @see Encoding Formula + * + * @param encryptionAlgorithm encryption algorithm + * @param hashAlgorithm hash algorithm + * @param s2kCount encoded s2k iteration count + */ public KeyRingProtectionSettings(@Nonnull SymmetricKeyAlgorithm encryptionAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, int s2kCount) { - this.encryptionAlgorithm = encryptionAlgorithm; + this.encryptionAlgorithm = validateEncryptionAlgorithm(encryptionAlgorithm); this.hashAlgorithm = hashAlgorithm; if (s2kCount < 1) { throw new IllegalArgumentException("s2kCount cannot be less than 1."); @@ -28,18 +49,50 @@ public class KeyRingProtectionSettings { this.s2kCount = s2kCount; } - public static KeyRingProtectionSettings secureDefaultSettings() { - return new KeyRingProtectionSettings(SymmetricKeyAlgorithm.AES_256); + private static SymmetricKeyAlgorithm validateEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) { + switch (encryptionAlgorithm) { + case NULL: + throw new IllegalArgumentException("Unencrypted is not allowed here!"); + default: + return encryptionAlgorithm; + } } + /** + * Secure default settings using {@link SymmetricKeyAlgorithm#AES_256}, {@link HashAlgorithm#SHA256} + * and an iteration count of 65536. + * + * @return secure protection settings + */ + public static KeyRingProtectionSettings secureDefaultSettings() { + return new KeyRingProtectionSettings(SymmetricKeyAlgorithm.AES_256, HashAlgorithm.SHA256, 0x60); + } + + /** + * Return the encryption algorithm. + * + * @return encryption algorithm + */ public @Nonnull SymmetricKeyAlgorithm getEncryptionAlgorithm() { return encryptionAlgorithm; } + /** + * Return the hash algorithm. + * + * @return hash algorithm + */ public @Nonnull HashAlgorithm getHashAlgorithm() { return hashAlgorithm; } + /** + * Return the (encoded!) s2k iteration count. + * + * @see Encoding Formula + * + * @return encoded s2k count + */ public int getS2kCount() { return s2kCount; } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/InvalidProtectionSettingsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/InvalidProtectionSettingsTest.java new file mode 100644 index 00000000..b0746398 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/InvalidProtectionSettingsTest.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection; + +import org.junit.jupiter.api.Test; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class InvalidProtectionSettingsTest { + + @Test + public void unencryptedKeyRingProtectionSettingsThrows() { + assertThrows(IllegalArgumentException.class, () -> + new KeyRingProtectionSettings(SymmetricKeyAlgorithm.NULL, HashAlgorithm.SHA256, 0x60)); + } +} From 328b8ccf8aa7f1a58bbce90f20648e8158eb6a2c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 31 Aug 2022 21:38:09 +0200 Subject: [PATCH 0372/1204] Add information about KeyRingProtectionSettings to documentation --- docs/source/pgpainless-core/passphrase.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/source/pgpainless-core/passphrase.md b/docs/source/pgpainless-core/passphrase.md index 27769aad..4a6e4393 100644 --- a/docs/source/pgpainless-core/passphrase.md +++ b/docs/source/pgpainless-core/passphrase.md @@ -57,4 +57,14 @@ SecretKeyRingProtector singlePassphrase = SecretKeyRingProtector // If you want to be flexible, use this: CachingSecretKeyRingProtector flexible = SecretKeyRingProtector .defaultSecretKeyRingProtector(passphraseCallback); -``` \ No newline at end of file +``` + +The last example shows how to instantiate the `CachingSecretKeyRingProtector` with a `SecretKeyPassphraseProvider`. +As the name suggests, the `CachingSecretKeyRingProtector` caches passphrases in a map. +If you try to unlock a protected secret key for which no passphrase is cached, the `getPassphraseFor()` method of +the `SecretKeyPassphraseProvider` will be called to interactively ask for the missing passphrase. Afterwards, the +acquired passphrase will be cached for future use. + +Most `SecretKeyRingProtector` implementations can be instantiated with custom `KeyRingProtectionSettings`. +By default, most implementations use `KeyRingProtectionSettings.secureDefaultSettings()` which corresponds to iterated +and salted S2K using AES256 and SHA256 with an iteration count of 65536. From 3030de7f3f15b9768de8d21861f3064d84161830 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 31 Aug 2022 21:59:40 +0200 Subject: [PATCH 0373/1204] Add further information about key protectors to documentation --- docs/source/pgpainless-core/passphrase.md | 28 +++++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/source/pgpainless-core/passphrase.md b/docs/source/pgpainless-core/passphrase.md index 4a6e4393..2c370006 100644 --- a/docs/source/pgpainless-core/passphrase.md +++ b/docs/source/pgpainless-core/passphrase.md @@ -42,7 +42,7 @@ There are certain operations that require you to provide the passphrase for a ke Examples are decryption of messages, or creating signatures / certifications. The primary way of telling PGPainless, which password to use for a certain key is the `SecretKeyRingProtector` -interface. +interface which maps `Passphrases` to (sub-)keys. There are multiple implementations of this interface, which may or may not suite your needs: ```java @@ -59,11 +59,29 @@ CachingSecretKeyRingProtector flexible = SecretKeyRingProtector .defaultSecretKeyRingProtector(passphraseCallback); ``` -The last example shows how to instantiate the `CachingSecretKeyRingProtector` with a `SecretKeyPassphraseProvider`. -As the name suggests, the `CachingSecretKeyRingProtector` caches passphrases in a map. +`SecretKeyRingProtector.unprotectedKeys()` will return an empty passphrase for any key. +It is best used when dealing with unencrypted secret keys. + +`SecretKeyRingProtector.unlockAnyKeyWith(passphrase)` will return the same exact passphrase for any given key. +You should use this if you have a single key with a static passphrase. + +The last example shows how to instantiate the `CachingSecretKeyRingProtector` with a `SecretKeyPassphraseProvider` +as argument. +As the name suggests, the `CachingSecretKeyRingProtector` caches passphrases it knows about in a map. +That way, you only have to provide the passphrase for a certain key only once, after which it will be remembered. If you try to unlock a protected secret key for which no passphrase is cached, the `getPassphraseFor()` method of -the `SecretKeyPassphraseProvider` will be called to interactively ask for the missing passphrase. Afterwards, the -acquired passphrase will be cached for future use. +the `SecretKeyPassphraseProvider` callback will be called to interactively ask for the missing passphrase. +Afterwards, the acquired passphrase will be cached for future use. + +:::{note} +While especially the `CachingSecretKeyRingProtector` can handle multiple keys without problems, it is advised +to use individual `SecretKeyRingProtector` objects per key. +The reason for this is, that internally the 64bit key-id is used to resolve `Passphrase` objects and collisions are not +unlikely in this key-space. +Furthermore, multiple OpenPGP keys could contain the same subkey, but with different passphrases set. +If the same `SecretKeyRingProtector` is used for two OpenPGP keys with the same subkey, but different passwords, +the key-id collision will cause the password to be overwritten for one of the keys, which might result in issues. +::: Most `SecretKeyRingProtector` implementations can be instantiated with custom `KeyRingProtectionSettings`. By default, most implementations use `KeyRingProtectionSettings.secureDefaultSettings()` which corresponds to iterated From c3dc3c9d8734894cc7bb0c6837a933fcc3a6216f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 3 Sep 2022 12:19:34 +0200 Subject: [PATCH 0374/1204] Allow modification of keys with custom reference date Also, bind subkeys using SubkeyBindingSignatureBuilder --- .../main/java/org/pgpainless/PGPainless.java | 6 +- .../org/pgpainless/key/info/KeyRingInfo.java | 14 +- .../secretkeyring/SecretKeyRingEditor.java | 124 ++++++++-------- .../PrimaryKeyBindingSignatureBuilder.java | 11 ++ .../SubkeyBindingSignatureBuilder.java | 9 ++ .../KeyGenerationSubpacketsTest.java | 12 +- .../modification/ChangeExpirationTest.java | 33 ++--- ...gePrimaryUserIdAndExpirationDatesTest.java | 140 ++++++++++-------- ...reSubpacketsArePreservedOnNewSigTest.java} | 18 +-- ...irdPartyDirectKeySignatureBuilderTest.java | 10 +- 10 files changed, 207 insertions(+), 170 deletions(-) rename pgpainless-core/src/test/java/org/pgpainless/key/modification/{OldSignatureSubpacketsArePreservedOnNewSig.java => OldSignatureSubpacketsArePreservedOnNewSigTest.java} (80%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 74a1f239..fd670fc9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -127,7 +127,11 @@ public final class PGPainless { * @return builder */ public static SecretKeyRingEditorInterface modifyKeyRing(PGPSecretKeyRing secretKeys) { - return new SecretKeyRingEditor(secretKeys); + return modifyKeyRing(secretKeys, null); + } + + public static SecretKeyRingEditorInterface modifyKeyRing(PGPSecretKeyRing secretKeys, Date referenceTime) { + return new SecretKeyRingEditor(secretKeys, referenceTime); } /** diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 995bded4..b818290b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -85,10 +85,10 @@ public class KeyRingInfo { * Evaluate the key ring at the provided validation date. * * @param keys key ring - * @param validationDate date of validation + * @param referenceDate date of validation */ - public KeyRingInfo(PGPKeyRing keys, Date validationDate) { - this(keys, PGPainless.getPolicy(), validationDate); + public KeyRingInfo(PGPKeyRing keys, Date referenceDate) { + this(keys, PGPainless.getPolicy(), referenceDate); } /** @@ -96,12 +96,12 @@ public class KeyRingInfo { * * @param keys key ring * @param policy policy - * @param validationDate validation date + * @param referenceDate validation date */ - public KeyRingInfo(PGPKeyRing keys, Policy policy, Date validationDate) { + public KeyRingInfo(PGPKeyRing keys, Policy policy, Date referenceDate) { + this.referenceDate = referenceDate != null ? referenceDate : new Date(); this.keys = keys; - this.signatures = new Signatures(keys, validationDate, policy); - this.referenceDate = validationDate; + this.signatures = new Signatures(keys, this.referenceDate, policy); this.primaryUserId = findPrimaryUserId(); this.revocationState = findRevocationState(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index e1b1b23e..20876c36 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -4,35 +4,17 @@ package org.pgpainless.key.modification.secretkeyring; -import static org.pgpainless.util.CollectionUtils.concat; - -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - import org.bouncycastle.bcpg.S2K; import org.bouncycastle.bcpg.SecretKeyPacket; import org.bouncycastle.bcpg.sig.KeyExpirationTime; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPKeyRingGenerator; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.AlgorithmSuite; import org.pgpainless.algorithm.CompressionAlgorithm; @@ -57,26 +39,49 @@ import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvi import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.signature.builder.DirectKeySelfSignatureBuilder; +import org.pgpainless.signature.builder.PrimaryKeyBindingSignatureBuilder; import org.pgpainless.signature.builder.RevocationSignatureBuilder; import org.pgpainless.signature.builder.SelfSignatureBuilder; +import org.pgpainless.signature.builder.SubkeyBindingSignatureBuilder; import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets; import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.CollectionUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.selection.userid.SelectUserId; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import static org.pgpainless.util.CollectionUtils.concat; + public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { private PGPSecretKeyRing secretKeyRing; + private final Date referenceTime; public SecretKeyRingEditor(PGPSecretKeyRing secretKeyRing) { + this(secretKeyRing, null); + } + + public SecretKeyRingEditor(PGPSecretKeyRing secretKeyRing, Date referenceTime) { if (secretKeyRing == null) { throw new NullPointerException("SecretKeyRing MUST NOT be null."); } this.secretKeyRing = secretKeyRing; + this.referenceTime = referenceTime; } @Override @@ -99,7 +104,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); // retain key flags from previous signature - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); if (info.isHardRevoked(userId.toString())) { throw new IllegalArgumentException("User-ID " + userId + " is hard revoked and cannot be re-certified."); } @@ -121,6 +126,9 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { } SelfSignatureBuilder builder = new SelfSignatureBuilder(primaryKey, protector); + if (referenceTime != null) { + builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); + } builder.setSignatureType(SignatureType.POSITIVE_CERTIFICATION); // Retain signature subpackets of previous signatures @@ -145,7 +153,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { // Determine previous key expiration date PGPPublicKey primaryKey = secretKeyRing.getSecretKey().getPublicKey(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); String primaryUserId = info.getPrimaryUserId(); PGPSignature signature = primaryUserId == null ? info.getLatestDirectKeySelfSignature() : info.getLatestUserIdCertification(primaryUserId); @@ -169,7 +177,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { protector); // unmark previous primary user-ids to be non-primary - info = PGPainless.inspectKeyRing(secretKeyRing); + info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); for (String otherUserId : info.getValidAndExpiredUserIds()) { if (userId.toString().equals(otherUserId)) { continue; @@ -227,7 +235,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { throw new IllegalArgumentException("New user-id cannot be empty."); } - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); if (!info.isUserIdValid(oldUID)) { throw new NoSuchElementException("Key does not carry user-id '" + oldUID + "', or it is not valid."); } @@ -333,46 +341,34 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { } PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing); - PublicKeyAlgorithm signingKeyAlgorithm = PublicKeyAlgorithm.requireFromId(primaryKey.getPublicKey().getAlgorithm()); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); HashAlgorithm hashAlgorithm = HashAlgorithmNegotiator .negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) .negotiateHashAlgorithm(info.getPreferredHashAlgorithms()); - // While we'd like to rely on our own BindingSignatureBuilder implementation, - // unfortunately we have to use BCs PGPKeyRingGenerator class since there is no public constructor - // for subkeys. See https://github.com/bcgit/bc-java/pull/1063 - PGPKeyRingGenerator ringGenerator = new PGPKeyRingGenerator( - secretKeyRing, - primaryKeyProtector.getDecryptor(primaryKey.getKeyID()), - ImplementationFactory.getInstance().getV4FingerprintCalculator(), - ImplementationFactory.getInstance().getPGPContentSignerBuilder( - signingKeyAlgorithm, hashAlgorithm), - subkeyProtector.getEncryptor(subkey.getKeyID())); + PGPSecretKey secretSubkey = new PGPSecretKey(subkey.getPrivateKey(), subkey.getPublicKey(), ImplementationFactory.getInstance() + .getV4FingerprintCalculator(), false, subkeyProtector.getEncryptor(subkey.getKeyID())); - SelfSignatureSubpackets hashedSubpackets = SignatureSubpackets.createHashedSubpackets(primaryKey.getPublicKey()); - SelfSignatureSubpackets unhashedSubpackets = SignatureSubpackets.createEmptySubpackets(); - hashedSubpackets.setKeyFlags(flags); + SubkeyBindingSignatureBuilder skBindingBuilder = new SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector, hashAlgorithm); + if (referenceTime != null) { + skBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); + } + skBindingBuilder.getHashedSubpackets().setKeyFlags(flags); - if (bindingSignatureCallback != null) { - bindingSignatureCallback.modifyHashedSubpackets(hashedSubpackets); - bindingSignatureCallback.modifyUnhashedSubpackets(unhashedSubpackets); + if (subkeyAlgorithm.isSigningCapable()) { + PrimaryKeyBindingSignatureBuilder pkBindingBuilder = new PrimaryKeyBindingSignatureBuilder(secretSubkey, subkeyProtector, hashAlgorithm); + if (referenceTime != null) { + pkBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); + } + PGPSignature pkBinding = pkBindingBuilder.build(primaryKey.getPublicKey()); + skBindingBuilder.getHashedSubpackets().addEmbeddedSignature(pkBinding); } - boolean isSigningKey = CollectionUtils.contains(flags, KeyFlag.SIGN_DATA) || - CollectionUtils.contains(flags, KeyFlag.CERTIFY_OTHER); - PGPContentSignerBuilder primaryKeyBindingSigner = null; - if (isSigningKey) { - primaryKeyBindingSigner = ImplementationFactory.getInstance().getPGPContentSignerBuilder(subkeyAlgorithm, hashAlgorithm); - } - - ringGenerator.addSubKey(subkey, - SignatureSubpacketsHelper.toVector((SignatureSubpackets) hashedSubpackets), - SignatureSubpacketsHelper.toVector((SignatureSubpackets) unhashedSubpackets), - primaryKeyBindingSigner); - - secretKeyRing = ringGenerator.generateSecretKeyRing(); + skBindingBuilder.applyCallback(bindingSignatureCallback); + PGPSignature skBinding = skBindingBuilder.build(secretSubkey.getPublicKey()); + secretSubkey = KeyRingUtils.secretKeyPlusSignature(secretSubkey, skBinding); + secretKeyRing = KeyRingUtils.keysPlusSecretKey(secretKeyRing, secretSubkey); return this; } @@ -558,6 +554,9 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { SignatureType.CERTIFICATION_REVOCATION, primarySecretKey, protector); + if (referenceTime != null) { + signatureBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); + } signatureBuilder.applyCallback(callback); @@ -585,14 +584,14 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { } // reissue primary user-id sig - String primaryUserId = PGPainless.inspectKeyRing(secretKeyRing).getPossiblyExpiredPrimaryUserId(); + String primaryUserId = PGPainless.inspectKeyRing(secretKeyRing, referenceTime).getPossiblyExpiredPrimaryUserId(); if (primaryUserId != null) { PGPSignature prevUserIdSig = getPreviousUserIdSignatures(primaryUserId); PGPSignature userIdSig = reissuePrimaryUserIdSig(expiration, secretKeyRingProtector, primaryUserId, prevUserIdSig); secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryUserId, userIdSig); } - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); for (String userId : info.getValidUserIds()) { if (userId.equals(primaryUserId)) { continue; @@ -618,6 +617,9 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { PGPSignature prevUserIdSig) throws PGPException { SelfSignatureBuilder builder = new SelfSignatureBuilder(secretKeyRing.getSecretKey(), secretKeyRingProtector, prevUserIdSig); + if (referenceTime != null) { + builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); + } builder.applyCallback(new SelfSignatureSubpackets.Callback() { @Override public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { @@ -638,6 +640,9 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { PGPPublicKey publicKey = primaryKey.getPublicKey(); SelfSignatureBuilder builder = new SelfSignatureBuilder(primaryKey, secretKeyRingProtector, prevUserIdSig); + if (referenceTime != null) { + builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); + } builder.applyCallback(new SelfSignatureSubpackets.Callback() { @Override public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { @@ -662,6 +667,9 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { final Date keyCreationTime = publicKey.getCreationTime(); DirectKeySelfSignatureBuilder builder = new DirectKeySelfSignatureBuilder(primaryKey, secretKeyRingProtector, prevDirectKeySig); + if (referenceTime != null) { + builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); + } builder.applyCallback(new SelfSignatureSubpackets.Callback() { @Override public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { @@ -677,12 +685,12 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { } private PGPSignature getPreviousDirectKeySignature() { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); return info.getLatestDirectKeySelfSignature(); } private PGPSignature getPreviousUserIdSignatures(String userId) { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); return info.getLatestUserIdCertification(userId); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.java index 93339f86..156e739f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.java @@ -10,9 +10,11 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; +import org.pgpainless.signature.subpackets.SignatureSubpackets; public class PrimaryKeyBindingSignatureBuilder extends AbstractSignatureBuilder { @@ -21,6 +23,15 @@ public class PrimaryKeyBindingSignatureBuilder extends AbstractSignatureBuilder< super(SignatureType.PRIMARYKEY_BINDING, subkey, subkeyProtector); } + public PrimaryKeyBindingSignatureBuilder(PGPSecretKey secretSubKey, + SecretKeyRingProtector subkeyProtector, + HashAlgorithm hashAlgorithm) + throws PGPException { + super(SignatureType.PRIMARYKEY_BINDING, secretSubKey, subkeyProtector, hashAlgorithm, + SignatureSubpackets.createHashedSubpackets(secretSubKey.getPublicKey()), + SignatureSubpackets.createEmptySubpackets()); + } + public SelfSignatureSubpackets getHashedSubpackets() { return hashedSubpackets; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java index 9c51955d..c15e219e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java @@ -10,9 +10,11 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; +import org.pgpainless.signature.subpackets.SignatureSubpackets; public class SubkeyBindingSignatureBuilder extends AbstractSignatureBuilder { @@ -21,6 +23,13 @@ public class SubkeyBindingSignatureBuilder extends AbstractSignatureBuilder - PGPainless.modifyKeyRing(finalSecretKeys).addUserId("A", protector)); + PGPainless.modifyKeyRing(finalSecretKeys, threeHoursLater).addUserId("A", protector)); } @Test public void generateA_primaryExpire_isExpired() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InterruptedException { + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .modernKeyRing("A"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); @@ -76,71 +81,77 @@ public class ChangePrimaryUserIdAndExpirationDatesTest { KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); assertIsPrimaryUserId("A", info); - Thread.sleep(1000); + Date now = new Date(); + Date later = new Date(now.getTime() + millisInHour); - secretKeys = PGPainless.modifyKeyRing(secretKeys) - .setExpirationDate(new Date(), protector) // expire the whole key + secretKeys = PGPainless.modifyKeyRing(secretKeys, now) + .setExpirationDate(later, protector) // expire the whole key .done(); - Thread.sleep(1000); + Date evenLater = new Date(now.getTime() + 2 * millisInHour); - info = PGPainless.inspectKeyRing(secretKeys); + info = PGPainless.inspectKeyRing(secretKeys, evenLater); assertFalse(info.isUserIdValid("A")); // is expired by now } @Test public void generateA_primaryB_primaryExpire_bIsStillPrimary() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InterruptedException { + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .modernKeyRing("A"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + Date now = new Date(); + // Generate key with primary user-id A KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); assertIsPrimaryUserId("A", info); - Thread.sleep(1000); - - secretKeys = PGPainless.modifyKeyRing(secretKeys) + // later set primary user-id to B + Date t1 = new Date(now.getTime() + millisInHour); + secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) .addPrimaryUserId("B", protector) .done(); - info = PGPainless.inspectKeyRing(secretKeys); - + info = PGPainless.inspectKeyRing(secretKeys, t1); assertIsPrimaryUserId("B", info); assertIsNotPrimaryUserId("A", info); - Thread.sleep(1000); - - secretKeys = PGPainless.modifyKeyRing(secretKeys) - .setExpirationDate(new Date(new Date().getTime() + 1000), protector) // expire the whole key in 1 sec + // Even later expire the whole key + Date t2 = new Date(now.getTime() + 2 * millisInHour); + Date expiration = new Date(now.getTime() + 10 * millisInHour); + secretKeys = PGPainless.modifyKeyRing(secretKeys, t2) + .setExpirationDate(expiration, protector) // expire the whole key in 1 hour .done(); - info = PGPainless.inspectKeyRing(secretKeys); + Date t3 = new Date(now.getTime() + 3 * millisInHour); + + info = PGPainless.inspectKeyRing(secretKeys, t3); assertIsValid("A", info); assertIsValid("B", info); assertIsPrimaryUserId("B", info); assertIsNotPrimaryUserId("A", info); - Thread.sleep(2000); - - info = PGPainless.inspectKeyRing(secretKeys); + info = PGPainless.inspectKeyRing(secretKeys, expiration); assertIsPrimaryUserId("B", info); // B is still primary, even though assertFalse(info.isUserIdValid("A")); // key is expired by now assertFalse(info.isUserIdValid("B")); } @Test - public void generateA_expire_certify() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InterruptedException { + public void generateA_expire_certify() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("A"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - secretKeys = PGPainless.modifyKeyRing(secretKeys) - .setExpirationDate(new Date(new Date().getTime() + 1000), protector) + Date now = new Date(); + Date t1 = new Date(now.getTime() + millisInHour); + secretKeys = PGPainless.modifyKeyRing(secretKeys, now) + .setExpirationDate(t1, protector) .done(); - Thread.sleep(2000); - - secretKeys = PGPainless.modifyKeyRing(secretKeys) - .setExpirationDate(new Date(new Date().getTime() + 2000), protector) + Date t2 = new Date(now.getTime() + 2 * millisInHour); + Date t4 = new Date(now.getTime() + 4 * millisInHour); + secretKeys = PGPainless.modifyKeyRing(secretKeys, t2) + .setExpirationDate(t4, protector) .done(); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); @@ -150,49 +161,48 @@ public class ChangePrimaryUserIdAndExpirationDatesTest { @Test public void generateA_expire_primaryB_expire_isPrimaryB() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InterruptedException { + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("A"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - Thread.sleep(1000); - - secretKeys = PGPainless.modifyKeyRing(secretKeys) - .setExpirationDate(new Date(), protector) + Date now = new Date(); + Date t1 = new Date(now.getTime() + millisInHour); + secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) + .setExpirationDate(t1, protector) .done(); - Thread.sleep(2000); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + Date t2 = new Date(now.getTime() + 2 * millisInHour); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys, t2); assertIsPrimaryUserId("A", info); assertIsNotValid("A", info); // A is expired - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = PGPainless.modifyKeyRing(secretKeys, t2) .addPrimaryUserId("B", protector) .done(); - info = PGPainless.inspectKeyRing(secretKeys); + Date t3 = new Date(now.getTime() + 3 * millisInHour); + info = PGPainless.inspectKeyRing(secretKeys, t3); assertIsPrimaryUserId("B", info); assertIsNotValid("B", info); // A and B are still expired assertIsNotValid("A", info); - Thread.sleep(1000); - - secretKeys = PGPainless.modifyKeyRing(secretKeys) - .setExpirationDate(new Date(new Date().getTime() + 10000), protector) + Date t4 = new Date(now.getTime() + 4 * millisInHour); + Date t5 = new Date(now.getTime() + 5 * millisInHour); + secretKeys = PGPainless.modifyKeyRing(secretKeys, t3) + .setExpirationDate(t5, protector) .done(); - Thread.sleep(1000); - info = PGPainless.inspectKeyRing(secretKeys); - + info = PGPainless.inspectKeyRing(secretKeys, t4); assertIsValid("B", info); assertIsValid("A", info); // A got re-validated when changing exp date assertIsPrimaryUserId("B", info); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = PGPainless.modifyKeyRing(secretKeys, t4) .addUserId("A", protector) // re-certify A as non-primary user-id .done(); - info = PGPainless.inspectKeyRing(secretKeys); + info = PGPainless.inspectKeyRing(secretKeys, t4); assertIsValid("B", info); assertIsValid("A", info); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSig.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSigTest.java similarity index 80% rename from pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSig.java rename to pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSigTest.java index f7d4d181..674cd6e5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSig.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSigTest.java @@ -10,7 +10,6 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import java.util.Calendar; import java.util.Date; import org.bouncycastle.openpgp.PGPException; @@ -23,12 +22,14 @@ import org.pgpainless.PGPainless; import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.util.TestAllImplementations; -public class OldSignatureSubpacketsArePreservedOnNewSig { +public class OldSignatureSubpacketsArePreservedOnNewSigTest { + + private static final long millisInHour = 1000 * 60 * 60; @TestTemplate @ExtendWith(TestAllImplementations.class) public void verifyOldSignatureSubpacketsArePreservedOnNewExpirationDateSig() - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, InterruptedException { + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .simpleEcKeyRing("Alice "); @@ -37,17 +38,14 @@ public class OldSignatureSubpacketsArePreservedOnNewSig { assertEquals(0, oldPackets.getKeyExpirationTime()); - Thread.sleep(1000); Date now = new Date(); - Calendar calendar = Calendar.getInstance(); - calendar.setTime(now); - calendar.add(Calendar.DATE, 5); - Date expiration = calendar.getTime(); // in 5 days + Date t1 = new Date(now.getTime() + millisInHour); + Date expiration = new Date(now.getTime() + 5 * 24 * millisInHour); // in 5 days - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) .setExpirationDate(expiration, new UnprotectedKeysProtector()) .done(); - PGPSignature newSignature = PGPainless.inspectKeyRing(secretKeys).getLatestUserIdCertification("Alice "); + PGPSignature newSignature = PGPainless.inspectKeyRing(secretKeys, t1).getLatestUserIdCertification("Alice "); PGPSignatureSubpacketVector newPackets = newSignature.getHashedSubPackets(); assertNotEquals(0, newPackets.getKeyExpirationTime()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java index 46dc1ac5..72acc125 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Collections; +import java.util.Date; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; @@ -32,7 +33,7 @@ import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; public class ThirdPartyDirectKeySignatureBuilderTest { @Test - public void testDirectKeySignatureBuilding() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InterruptedException { + public void testDirectKeySignatureBuilding() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .modernKeyRing("Alice"); @@ -40,9 +41,12 @@ public class ThirdPartyDirectKeySignatureBuilderTest { secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); + Date now = new Date(); + Date t1 = new Date(now.getTime() + 1000 * 60 * 60); dsb.applyCallback(new SelfSignatureSubpackets.Callback() { @Override public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { + hashedSubpackets.setSignatureCreationTime(t1); hashedSubpackets.setKeyFlags(KeyFlag.CERTIFY_OTHER); hashedSubpackets.setPreferredHashAlgorithms(HashAlgorithm.SHA512); hashedSubpackets.setPreferredCompressionAlgorithms(CompressionAlgorithm.ZIP); @@ -51,13 +55,11 @@ public class ThirdPartyDirectKeySignatureBuilderTest { } }); - Thread.sleep(1000); - PGPSignature directKeySig = dsb.build(secretKeys.getPublicKey()); assertNotNull(directKeySig); secretKeys = KeyRingUtils.injectCertification(secretKeys, secretKeys.getPublicKey(), directKeySig); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys, t1); PGPSignature signature = info.getLatestDirectKeySelfSignature(); assertNotNull(signature); From 7189516dd4cc4b0022d9651881de22ca384b3815 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 3 Sep 2022 13:46:32 +0200 Subject: [PATCH 0375/1204] Add documentation for modifyKeyRing(keys, date) --- .../src/main/java/org/pgpainless/PGPainless.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index fd670fc9..3f71051f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -118,8 +118,8 @@ public final class PGPainless { } /** - * Make changes to a key ring. - * This method can be used to change key expiration dates and passphrases, or add/remove/revoke subkeys. + * Make changes to a secret key. + * This method can be used to change key expiration dates and passphrases, or add/revoke subkeys. * * After making the desired changes in the builder, the modified key ring can be extracted using {@link SecretKeyRingEditorInterface#done()}. * @@ -130,6 +130,16 @@ public final class PGPainless { return modifyKeyRing(secretKeys, null); } + /** + * Make changes to a secret key at the given reference time. + * This method can be used to change key expiration dates and passphrases, or add/revoke user-ids and subkeys. + * + * After making the desired changes in the builder, the modified key can be extracted using {@link SecretKeyRingEditorInterface#done()}. + * + * @param secretKeys secret key ring + * @param referenceTime reference time used as signature creation date + * @return builder + */ public static SecretKeyRingEditorInterface modifyKeyRing(PGPSecretKeyRing secretKeys, Date referenceTime) { return new SecretKeyRingEditor(secretKeys, referenceTime); } From 3cd5a95d898703875c500391a14019e3eff14668 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 3 Sep 2022 13:48:02 +0200 Subject: [PATCH 0376/1204] Rename inspectionDate to referenceTime --- .../src/main/java/org/pgpainless/PGPainless.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 3f71051f..9ff2a3b0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -163,11 +163,11 @@ public final class PGPainless { * This method can be used to determine expiration dates, key flags and other information about a key at a specific time. * * @param keyRing key ring - * @param inspectionDate date of inspection + * @param referenceTime date of inspection * @return access object */ - public static KeyRingInfo inspectKeyRing(PGPKeyRing keyRing, Date inspectionDate) { - return new KeyRingInfo(keyRing, inspectionDate); + public static KeyRingInfo inspectKeyRing(PGPKeyRing keyRing, Date referenceTime) { + return new KeyRingInfo(keyRing, referenceTime); } /** From 3a33bb126a6f619ba662c52cf8b892bafc1c1d1a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 3 Sep 2022 14:24:37 +0200 Subject: [PATCH 0377/1204] Add RNGPerformanceTest to help diagnose performance bottlenecks Related to https://github.com/pgpainless/pgpainless/issues/309 --- .../investigations/RNGPerformanceTest.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 pgpainless-core/src/test/java/investigations/RNGPerformanceTest.java diff --git a/pgpainless-core/src/test/java/investigations/RNGPerformanceTest.java b/pgpainless-core/src/test/java/investigations/RNGPerformanceTest.java new file mode 100644 index 00000000..e583089d --- /dev/null +++ b/pgpainless-core/src/test/java/investigations/RNGPerformanceTest.java @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package investigations; + +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.prng.DigestRandomGenerator; +import org.junit.jupiter.api.Test; +import org.junit.platform.commons.logging.Logger; +import org.junit.platform.commons.logging.LoggerFactory; + +import java.security.SecureRandom; +import java.time.Duration; +import java.time.Instant; +import java.util.Random; + +/** + * Evaluate performance of random number generators. + */ +public class RNGPerformanceTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(RNGPerformanceTest.class); + private static final int bytesInMebiByte = 1024 * 1024; + + @Test + public void evaluateRandomPerformance() { + Random random = new Random(); + byte[] bytes = new byte[bytesInMebiByte]; + + Instant start = Instant.now(); + random.nextBytes(bytes); + Instant end = Instant.now(); + + Duration duration = Duration.between(start, end); + LOGGER.info(() -> String.format( + "Random.nextBytes() took %s milliseconds to generate 1 MiB of data", + duration.toMillis())); + } + + @Test + public void evaluateSecureRandomPerformance() { + SecureRandom random = new SecureRandom(); + byte[] bytes = new byte[bytesInMebiByte]; + + Instant start = Instant.now(); + random.nextBytes(bytes); + Instant end = Instant.now(); + + Duration duration = Duration.between(start, end); + LOGGER.info(() -> String.format( + "SecureRandom.nextBytes() took %s milliseconds to generate 1 MiB of data", + duration.toMillis())); + } + + @Test + public void evaluateSHA256BasedDigestRandomGeneratorPerformance() { + SHA256Digest digest = new SHA256Digest(); + DigestRandomGenerator random = new DigestRandomGenerator(digest); + byte[] bytes = new byte[bytesInMebiByte]; + + Instant start = Instant.now(); + random.nextBytes(bytes); + Instant end = Instant.now(); + + Duration duration = Duration.between(start, end); + LOGGER.info(() -> String.format( + "SHA256-based DigestRandomGenerator.nextBytes() took %s milliseconds to generate 1 MiB of data", + duration.toMillis())); + } + + @Test + public void evaluateSHA1BasedDigestRandomGeneratorPerformance() { + SHA1Digest digest = new SHA1Digest(); + DigestRandomGenerator random = new DigestRandomGenerator(digest); + byte[] bytes = new byte[bytesInMebiByte]; + + Instant start = Instant.now(); + random.nextBytes(bytes); + Instant end = Instant.now(); + + Duration duration = Duration.between(start, end); + LOGGER.info(() -> String.format( + "SHA1-based DigestRandomGenerator.nextBytes() took %s milliseconds to generate 1 MiB of data", + duration.toMillis())); + } +} From f2903515e7d10bcbd107bc495aa5d294658631b1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 3 Sep 2022 18:04:04 +0200 Subject: [PATCH 0378/1204] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0421a2c8..8cc9f494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ SPDX-License-Identifier: CC0-1.0 - Add `SigningOptions.addDetachedSignature(protector, key)` shortcut method - Add `EncryptionOptions.get()`, `ConsumerOptions.get()` factory methods - Add support for generating keys without user-id (only using `PGPainless.buildKeyRing()` for now) +- Switch to `SHA256` as default `S2K` hash algorithm for secret key protection +- Allow to set custom reference time when modifying secret keys +- Add diagnostic test to explore system PRNG performance ## 1.3.5 - Add `KeyRingInfo.isCapableOfSigning()` From f6f9df6e7fc1a67587107b540eae6950712d896d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 3 Sep 2022 18:04:38 +0200 Subject: [PATCH 0379/1204] PGPainless 1.3.6 --- CHANGELOG.md | 2 +- version.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cc9f494..f558b3b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.3.6-SNAPSHOT +## 1.3.6 - Remove deprecated methods - `ArmorUtils.createArmoredOutputStreamFor()` -> use `ArmorUtils.toAsciiArmoredStream()` instead - `EncryptionResult.getSymmetricKeyAlgorithm()` -> use `EncryptionResult.getEncryptionAlgorithm()` instead diff --git a/version.gradle b/version.gradle index 3baa01be..cfe82d63 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.6' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From a188b62170f983838fc0b398ddbc5562b3c827c3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 3 Sep 2022 18:06:28 +0200 Subject: [PATCH 0380/1204] PGPainless 1.3.7-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index cfe82d63..f361dc8d 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.6' - isSnapshot = false + shortVersion = '1.3.7' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From 7bff3128cbd3ab6007ed539d74634cf627ccfd16 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 3 Sep 2022 18:07:32 +0200 Subject: [PATCH 0381/1204] Update READMEs --- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2e14efc1..b8bd3c51 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.5' + implementation 'org.pgpainless:pgpainless-core:1.3.6' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index d5e22046..ad96b8dc 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.5" + implementation "org.pgpainless:pgpainless-sop:1.3.6" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.5 + 1.3.6 ... From 0d23809524cafe74b4f0777378e98b37987b87e6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Sep 2022 13:28:00 +0200 Subject: [PATCH 0382/1204] Update README of docs directory --- docs/README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 73f97fab..f3e156ef 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,7 +17,15 @@ To build: $ make {html|epub|latexpdf} ``` -Note: Building diagrams from source requires `mermaid-cli` to be installed. +Note: Diagrams are currently not built from source. +Instead, pre-built image files are used directly, because there are issues with mermaid in CLI systems. + +If you want to build the diagrams from source, you need `mermaid-cli` to be installed on your system. ```shell $ npm install -g @mermaid-js/mermaid-cli ``` + +You can then use `mmdc` to build/update single diagram files like this: +```shell +mmdc --theme default --width 1600 --backgroundColor transparent -i ecosystem_dia.md -o ecosystem_dia.svg +``` From fb0908ffd10760f04a7d7b55ca4b82110ebe66c5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Sep 2022 13:46:12 +0200 Subject: [PATCH 0383/1204] Add explanation for secret key protector hint to documentation --- docs/source/pgpainless-core/passphrase.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/pgpainless-core/passphrase.md b/docs/source/pgpainless-core/passphrase.md index 2c370006..3127ab25 100644 --- a/docs/source/pgpainless-core/passphrase.md +++ b/docs/source/pgpainless-core/passphrase.md @@ -81,6 +81,7 @@ unlikely in this key-space. Furthermore, multiple OpenPGP keys could contain the same subkey, but with different passphrases set. If the same `SecretKeyRingProtector` is used for two OpenPGP keys with the same subkey, but different passwords, the key-id collision will cause the password to be overwritten for one of the keys, which might result in issues. +See `FLO-04-004 WP2` of the [2021 security audit](https://cure53.de/pentest-report_pgpainless.pdf) for more details. ::: Most `SecretKeyRingProtector` implementations can be instantiated with custom `KeyRingProtectionSettings`. From c80e0dd0d5cad7e6094656755a4660dbeac4b36b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Sep 2022 13:56:29 +0200 Subject: [PATCH 0384/1204] Add further information to docs index --- docs/source/index.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index a29e6c03..06c115ec 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -20,6 +20,11 @@ PGPainless' goal is to empower you to use OpenPGP without needing to write all t Bouncy Castle. It aims to be secure by default while allowing customization if required. +From its inception in 2018 as part of a `Google Summer of Code project `_, +the library was steadily advanced. +Since 2020, FlowCrypt is the primary sponsor of its development. +In 2022, PGPainless received a `grant from NLnet for creating a Web-of-Trust implementation `_ as part of NGI Assure. + Contents -------- From 70ce4d45f452aa22d7e6fad3184b296490786b31 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Sep 2022 14:20:11 +0200 Subject: [PATCH 0385/1204] Remove unused CRCinArmoredInputStreamWrapper.possiblyWrap() --- .../util/CRCingArmoredInputStreamWrapper.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java b/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java index ff97013a..2c43339b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java @@ -27,18 +27,6 @@ public class CRCingArmoredInputStreamWrapper extends ArmoredInputStream { this.inputStream = inputStream; } - public static InputStream possiblyWrap(InputStream inputStream) throws IOException { - if (inputStream instanceof CRCingArmoredInputStreamWrapper) { - return inputStream; - } - - if (inputStream instanceof ArmoredInputStream) { - return new CRCingArmoredInputStreamWrapper((ArmoredInputStream) inputStream); - } - - return inputStream; - } - @Override public boolean isClearText() { return inputStream.isClearText(); From 4ec38bb63b9102d94a504e388cd0ce5358049331 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Sep 2022 14:37:23 +0200 Subject: [PATCH 0386/1204] Add tests for ArmoredInputStreamFactory --- .../util/ArmoredInputStreamFactoryTest.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java new file mode 100644 index 00000000..147b957d --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ArmoredInputStreamFactoryTest { + + // Hello World!\n + String armored = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "owE7LZzEAAIeqTk5+Qrh+UU5KYpcAA==\n" + + "=g3nV\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void testGet() throws IOException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(armored.getBytes()); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(inputStream); + assertNotNull(armorIn); + } + + @Test + public void testGet_willWrapArmoredInputStreamWithCRC() throws IOException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(armored.getBytes()); + ArmoredInputStream plainArmor = new ArmoredInputStream(inputStream); + + ArmoredInputStream armor = ArmoredInputStreamFactory.get(plainArmor); + assertTrue(armor instanceof CRCingArmoredInputStreamWrapper); + } + + @Test + public void testGet_onCRCinArmoredInputStream() throws IOException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(armored.getBytes()); + CRCingArmoredInputStreamWrapper crc = new CRCingArmoredInputStreamWrapper(new ArmoredInputStream(inputStream)); + + ArmoredInputStream armor = ArmoredInputStreamFactory.get(crc); + assertSame(crc, armor); + } +} From 5be42b22bd45c8f087faf811c89824084e9d51cc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Sep 2022 14:45:22 +0200 Subject: [PATCH 0387/1204] Add test for KeyRingUtils.keysPlusPublicKey --- .../pgpainless/key/util/KeyRingUtilTest.java | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyRingUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyRingUtilTest.java index b75969fc..11fd5cd3 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyRingUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyRingUtilTest.java @@ -4,16 +4,11 @@ package org.pgpainless.key.util; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Random; - import org.bouncycastle.bcpg.attr.ImageAttribute; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; @@ -22,12 +17,26 @@ import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVectorGenerator; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.generation.KeyRingBuilder; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.ecc.EllipticCurve; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.CollectionUtils; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class KeyRingUtilTest { @Test @@ -62,4 +71,21 @@ public class KeyRingUtilTest { assertEquals(userAttr, secretKeys.getPublicKey().getUserAttributes().next()); assertEquals(sigCount + 1, CollectionUtils.iteratorToList(secretKeys.getPublicKey().getSignatures()).size()); } + + @Test + public void testKeysPlusPublicKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); + PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); + + PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(KeySpec.getBuilder( + KeyType.ECDH(EllipticCurve._P256), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE).build()); + PGPPublicKey pubkey = keyPair.getPublicKey(); + assertFalse(pubkey.isMasterKey()); + + PGPSecretKeyRing secretKeysPlus = KeyRingUtils.keysPlusPublicKey(secretKeys, pubkey); + assertNotNull(secretKeysPlus.getPublicKey(pubkey.getKeyID())); + + PGPPublicKeyRing publicKeysPlus = KeyRingUtils.keysPlusPublicKey(publicKeys, pubkey); + assertNotNull(publicKeysPlus.getPublicKey(pubkey.getKeyID())); + } } From cd0b9603e7b80e84cb94c91644ebe86094cfed0b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Sep 2022 15:15:58 +0200 Subject: [PATCH 0388/1204] Add KeyRingUtils.injectCertification(keys, certification) --- .../org/pgpainless/key/util/KeyRingUtils.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 66536b88..0bcdf8d1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -238,7 +238,21 @@ public final class KeyRingUtils { } /** - * Inject a key certification into the given key ring. + * Inject a key certification for the primary key into the given key ring. + * + * @param keyRing key ring + * @param certification key signature + * @return key ring with injected signature + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + */ + @Nonnull + public static T injectCertification(@Nonnull T keyRing, + @Nonnull PGPSignature certification) { + return injectCertification(keyRing, keyRing.getPublicKey(), certification); + } + + /** + * Inject a key certification for the given key into the given key ring. * * @param keyRing key ring * @param certifiedKey signed public key From 9106d98449ae28164c6ead62047156e1533d73e1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Sep 2022 15:25:29 +0200 Subject: [PATCH 0389/1204] Add tests for Certificate merging --- .../pgpainless/key/TestMergeCertificate.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/TestMergeCertificate.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/TestMergeCertificate.java b/pgpainless-core/src/test/java/org/pgpainless/key/TestMergeCertificate.java new file mode 100644 index 00000000..34861a5c --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/TestMergeCertificate.java @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.signature.SignatureUtils; +import org.pgpainless.util.DateUtil; + +import java.io.IOException; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestMergeCertificate { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9F3E C7B3 3FCF 807E 516D 5DA1 C102 B0FC 9A1C 69E9\n" + + "Comment: Revik Okemi \n" + + "\n" + + "lFgEYxXwbRYJKwYBBAHaRw8BAQdAtAWpi1+uUUpe37nSQqybiLpcAoa5KhlpLZmk\n" + + "IkqLXn8AAP4s+6jp7OInR4PqasuH0YefMEfPu9ZY5ZHjq3HFoaqEpxTxtBhSZXZp\n" + + "ayBPa2VtaSA8cmV2QG9rZS5taT6IjwQTFgoAQQUCYxXwbQkQwQKw/JocaekWIQSf\n" + + "PsezP8+AflFtXaHBArD8mhxp6QKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAAi\n" + + "SAEApd8RdhvF33eiUgXlMBU3/ob1/NdMIbVJCBUXj7URYzUBAKxH+BwesiSagsXO\n" + + "KbQEOjzu1R7Nd2Hmf+gue9AVQQ0BnF0EYxXwbRIKKwYBBAGXVQEFAQEHQPLc0OH8\n" + + "8v+govDgUQs7gnM5NK3H+haFCsq/ILMBb48YAwEIBwAA/2CXgEXUIi4s38GaVbDK\n" + + "ts7nj3CWwEOAqtLsO8+QcXmoEyuIdQQYFgoAHQUCYxXwbQKeAQKbDAUWAgMBAAQL\n" + + "CQgHBRUKCQgLAAoJEMECsPyaHGnpO7AA/2zF7j5cgxCZ+Ws+ENj6Uzgq47kqsRxa\n" + + "Ii4kPjW1HmCtAP4rie2Z0ra/1alG/wu2bUtxHgEkeTBsHP8pOM5Xz4JVDZxYBGMV\n" + + "8G0WCSsGAQQB2kcPAQEHQHofxjdBzpFaLsiyEDRaotbB5/New7vdtAHV7t5rv1BU\n" + + "AAD/fnI4ilbhsRYaGSGX5ma7VfkgWiK7UQi04YpJVV3HOEYO/ojVBBgWCgB9BQJj\n" + + "FfBtAp4BApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUCYxXwbQAKCRCUM1S1\n" + + "VUVbouF5AQDQUJIkFikWriyhSMWEUS52l0i3SlllmPCJuDc1dy389AD9FXCU5+W0\n" + + "GT2N1hRb8eIf+0aDiVLCdV3folVbuPaNvgcACgkQwQKw/Jocaem+GwD+NJD8EIdP\n" + + "Nf4Q3IvT9YFXEbilk+mKw3IdV68DsQxEtQoBAPkugEJxuI2XNEdl6sigtGF94q3u\n" + + "IzX9xT12kqD4GtgO\n" + + "=slQ4\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final String SOFT_REVOCATION = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: PGPainless\n" + + "\n" + + "iHsEIBYKAC0FAmMV8kcJEMECsPyaHGnpFiEEnz7Hsz/PgH5RbV2hwQKw/JocaekC\n" + + "hwACHQMAAMTqAP9XbUer/yjcAUOpbggqC35zrhzXi4/zc6QuuM9NSLnePwD/YZCn\n" + + "NoE+7B24C/SZVr7d4U0ryB2gNWJdvfMfQnGLaQA=\n" + + "=d2pq\n" + + "-----END PGP SIGNATURE-----"; + private static final Date SOFT_REVOCATION_DATE = DateUtil.parseUTCDate("2022-09-05 12:57:43 UTC"); + + private static final String HARD_REVOCATION = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: PGPainless\n" + + "\n" + + "iHsEIBYKAC0FAmMV8pUJEMECsPyaHGnpFiEEnz7Hsz/PgH5RbV2hwQKw/JocaekC\n" + + "hwACHQIAAFaCAQCZPxqJHe87GqLjaDuMdTPdI1dT8kuHvBC4LfhMP2VobQEAiCgQ\n" + + "WMqWZTfJmbhubnUhEnTu/+qPFiHChgDnaJmoMAk=\n" + + "=pl4A\n" + + "-----END PGP SIGNATURE-----"; + + @Test + public void testRevocationStateWithDifferentRevocationsMerged() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + KeyRingInfo info = PGPainless.inspectKeyRing(certificate); + assertTrue(info.getRevocationState().isNotRevoked()); + + PGPSignature softRevocation = SignatureUtils.readSignatures(SOFT_REVOCATION).get(0); + PGPPublicKeyRing softRevoked = KeyRingUtils.injectCertification(certificate, softRevocation); + + info = PGPainless.inspectKeyRing(softRevoked, softRevoked.getPublicKey().getCreationTime()); + assertTrue(info.getRevocationState().isNotRevoked(), + "Expect: Cert is not revoked at creation time, although we already added soft revocation"); + + info = KeyRingInfo.evaluateForSignature(softRevoked, softRevocation); + assertTrue(info.getRevocationState().isSoftRevocation(), "Expect: Cert is now revoked, since now is after soft revocation creation"); + JUtils.assertDateEquals(SOFT_REVOCATION_DATE, info.getRevocationDate()); + + PGPSignature hardRevocation = SignatureUtils.readSignatures(HARD_REVOCATION).get(0); + PGPPublicKeyRing hardRevoked = KeyRingUtils.injectCertification(certificate, hardRevocation); + + info = PGPainless.inspectKeyRing(hardRevoked); + assertTrue(info.getRevocationState().isHardRevocation()); + + info = PGPainless.inspectKeyRing(hardRevoked, hardRevoked.getPublicKey().getCreationTime()); + assertTrue(info.getRevocationState().isHardRevocation(), "Expect: Key is hard revoked, no matter reference time"); + + PGPPublicKeyRing merged = PGPainless.mergeCertificate(certificate, softRevoked); + info = PGPainless.inspectKeyRing(merged); + assertTrue(info.getRevocationState().isSoftRevocation()); + + merged = PGPainless.mergeCertificate(merged, hardRevoked); + info = PGPainless.inspectKeyRing(merged); + assertTrue(info.getRevocationState().isHardRevocation()); + } +} From 0bafc410a02f1a6b807292654bc09108206b0004 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Sep 2022 15:41:58 +0200 Subject: [PATCH 0390/1204] Add missing parseAndCombineSignatures call For some reason this was missing from the single-byte read() method of the SignatureInputStream, causing issues if draining the stream byte by byte --- .../pgpainless/decryption_verification/SignatureInputStream.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java index 33e6139b..42ca4dc9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java @@ -67,6 +67,7 @@ public abstract class SignatureInputStream extends FilterInputStream { final int data = super.read(); final boolean endOfStream = data == -1; if (endOfStream) { + parseAndCombineSignatures(); verifyOnePassSignatures(); verifyDetachedSignatures(); } else { From 0dd54f27b7eeab8ce78c4dc52af3c8d6dc256c38 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Sep 2022 15:43:32 +0200 Subject: [PATCH 0391/1204] Add test for processing message byte by byte --- .../DecryptAndVerifyMessageTest.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java index 152f4c33..f7402238 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java @@ -78,4 +78,44 @@ public class DecryptAndVerifyMessageTest { assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.JULIET_FINGERPRINT)); assertEquals(new SubkeyIdentifier(TestKeys.JULIET_FINGERPRINT), metadata.getDecryptionKey()); } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void decryptMessageAndVerifySignatureByteByByteTest() throws Exception { + String encryptedMessage = TestKeys.MSG_SIGN_CRYPT_JULIET_JULIET; + + ConsumerOptions options = new ConsumerOptions() + .addDecryptionKey(juliet) + .addVerificationCert(KeyRingUtils.publicKeyRingFrom(juliet)); + + DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(encryptedMessage.getBytes())) + .withOptions(options); + + ByteArrayOutputStream toPlain = new ByteArrayOutputStream(); + int r; + while ((r = decryptor.read()) != -1) { + toPlain.write(r); + } + + decryptor.close(); + toPlain.close(); + OpenPgpMetadata metadata = decryptor.getResult(); + + byte[] expected = TestKeys.TEST_MESSAGE_01_PLAIN.getBytes(UTF8); + byte[] actual = toPlain.toByteArray(); + + assertArrayEquals(expected, actual); + + assertTrue(metadata.isEncrypted()); + assertTrue(metadata.isSigned()); + assertFalse(metadata.isCleartextSigned()); + assertTrue(metadata.isVerified()); + assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getSymmetricKeyAlgorithm()); + assertEquals(1, metadata.getSignatures().size()); + assertEquals(1, metadata.getVerifiedSignatures().size()); + assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.JULIET_FINGERPRINT)); + assertEquals(new SubkeyIdentifier(TestKeys.JULIET_FINGERPRINT), metadata.getDecryptionKey()); + } } From bed18dc0adeda70fa4fd2f85398107842eccc846 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Sep 2022 15:51:19 +0200 Subject: [PATCH 0392/1204] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f558b3b3..250a80c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.7-SNAPSHOT +- Add `KeyRingUtils.injectCertification(keys, certification)` +- Bugfix: Fix signature verification when `DecryptionStream` is drained byte-by-byte using `read()` call + ## 1.3.6 - Remove deprecated methods - `ArmorUtils.createArmoredOutputStreamFor()` -> use `ArmorUtils.toAsciiArmoredStream()` instead From 31c4570d10a3252dd5e596440af4410e789907fb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 7 Sep 2022 13:48:59 +0200 Subject: [PATCH 0393/1204] Move finalization of signatures into own method --- .../SignatureInputStream.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java index 42ca4dc9..275acc17 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java @@ -67,9 +67,7 @@ public abstract class SignatureInputStream extends FilterInputStream { final int data = super.read(); final boolean endOfStream = data == -1; if (endOfStream) { - parseAndCombineSignatures(); - verifyOnePassSignatures(); - verifyDetachedSignatures(); + finalizeSignatures(); } else { byte b = (byte) data; updateOnePassSignatures(b); @@ -84,9 +82,7 @@ public abstract class SignatureInputStream extends FilterInputStream { final boolean endOfStream = read == -1; if (endOfStream) { - parseAndCombineSignatures(); - verifyOnePassSignatures(); - verifyDetachedSignatures(); + finalizeSignatures(); } else { updateOnePassSignatures(b, off, read); updateDetachedSignatures(b, off, read); @@ -94,6 +90,12 @@ public abstract class SignatureInputStream extends FilterInputStream { return read; } + private void finalizeSignatures() { + parseAndCombineSignatures(); + verifyOnePassSignatures(); + verifyDetachedSignatures(); + } + public void parseAndCombineSignatures() { if (objectFactory == null) { return; From e78880602a002db5269e5416ae1d1d01c4dfdfe8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 7 Sep 2022 17:29:04 +0200 Subject: [PATCH 0394/1204] Add diagram of pushdown automaton for the OpenPGP Message Format --- misc/OpenPGPMessageFormat.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 misc/OpenPGPMessageFormat.md diff --git a/misc/OpenPGPMessageFormat.md b/misc/OpenPGPMessageFormat.md new file mode 100644 index 00000000..daaa715e --- /dev/null +++ b/misc/OpenPGPMessageFormat.md @@ -0,0 +1,30 @@ + + +# Pushdown Automaton for the OpenPGP Message Format + +See [RFC4880 §11.3. OpenPGP Messages](https://www.rfc-editor.org/rfc/rfc4880#section-11.3) for the formal definition. + +A simulation of the automaton can be found [here](https://automatonsimulator.com/#%7B%22type%22%3A%22PDA%22%2C%22pda%22%3A%7B%22transitions%22%3A%7B%22start%22%3A%7B%22%22%3A%7B%22%22%3A%5B%7B%22state%22%3A%22s12%22%2C%22stackPushChar%22%3A%22%23%22%7D%5D%2C%22%23%22%3A%5B%5D%7D%7D%2C%22s0%22%3A%7B%22C%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%2C%22L%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s1%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%2C%22S%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%2C%22o%22%3A%5B%5D%7D%2C%22O%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s9%22%2C%22stackPushChar%22%3A%22o%22%7D%5D%7D%2C%22E%22%3A%7B%22M%22%3A%5B%5D%7D%2C%22p%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22s%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22I%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%2C%22J%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%7D%2C%22s1%22%3A%7B%22%22%3A%7B%22%22%3A%5B%5D%2C%22%23%22%3A%5B%7B%22state%22%3A%22s4%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%2C%22S%22%3A%7B%22o%22%3A%5B%7B%22state%22%3A%22s10%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%7D%2C%22s6%22%3A%7B%22p%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22s%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22I%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%2C%22J%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%7D%2C%22s8%22%3A%7B%22%22%3A%7B%22E%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%7D%2C%22s9%22%3A%7B%22%22%3A%7B%22%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%7D%2C%22s10%22%3A%7B%22%22%3A%7B%22%22%3A%5B%5D%2C%22%23%22%3A%5B%7B%22state%22%3A%22s4%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%2C%22S%22%3A%7B%22o%22%3A%5B%7B%22state%22%3A%22s10%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%7D%2C%22s4%22%3A%7B%22%22%3A%7B%22o%22%3A%5B%5D%7D%7D%2C%22s12%22%3A%7B%22%22%3A%7B%22%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%7D%7D%2C%22startState%22%3A%22start%22%2C%22acceptStates%22%3A%5B%22s4%22%5D%7D%2C%22states%22%3A%7B%22start%22%3A%7B%7D%2C%22s12%22%3A%7B%22top%22%3A395.00001525878906%2C%22left%22%3A99%2C%22displayId%22%3A%22Add%20Terminal%22%7D%2C%22s0%22%3A%7B%22top%22%3A259.00001525878906%2C%22left%22%3A162%2C%22displayId%22%3A%22OpenPGP%20Message%22%7D%2C%22s1%22%3A%7B%22top%22%3A304.00001525878906%2C%22left%22%3A524%2C%22displayId%22%3A%22Literal%20Message%22%7D%2C%22s9%22%3A%7B%22top%22%3A476.00001525878906%2C%22left%22%3A282%2C%22displayId%22%3A%22One%20Pass%20Signatures%22%7D%2C%22s6%22%3A%7B%22top%22%3A100%2C%22left%22%3A324%2C%22displayId%22%3A%22ESKs%22%7D%2C%22s8%22%3A%7B%22top%22%3A202%2C%22left%22%3A471%2C%22displayId%22%3A%22Encrypted%20Data%22%7D%2C%22s4%22%3A%7B%22isAccept%22%3Atrue%2C%22top%22%3A381.00001525878906%2C%22left%22%3A832%2C%22displayId%22%3A%22Accept%22%7D%2C%22s10%22%3A%7B%22top%22%3A237.00001525878906%2C%22left%22%3A809%2C%22displayId%22%3A%22Corresponding%20Signatures%22%7D%7D%2C%22transitions%22%3A%5B%7B%22stateA%22%3A%22start%22%2C%22label%22%3A%22%CF%B5%2C%CF%B5%2C%23%22%2C%22stateB%22%3A%22s12%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22C%2CM%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22L%2CM%2C%CF%B5%22%2C%22stateB%22%3A%22s1%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22S%2CM%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22O%2CM%2Co%22%2C%22stateB%22%3A%22s9%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22p%2CM%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22s%2CM%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22I%2CM%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22J%2CM%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s1%22%2C%22label%22%3A%22%CF%B5%2C%23%2C%CF%B5%22%2C%22stateB%22%3A%22s4%22%7D%2C%7B%22stateA%22%3A%22s1%22%2C%22label%22%3A%22S%2Co%2C%CF%B5%22%2C%22stateB%22%3A%22s10%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22p%2CX%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22s%2CX%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22I%2CX%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22J%2CX%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s8%22%2C%22label%22%3A%22%CF%B5%2CE%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s9%22%2C%22label%22%3A%22%CF%B5%2C%CF%B5%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s10%22%2C%22label%22%3A%22%CF%B5%2C%23%2C%CF%B5%22%2C%22stateB%22%3A%22s4%22%7D%2C%7B%22stateA%22%3A%22s10%22%2C%22label%22%3A%22S%2Co%2C%CF%B5%22%2C%22stateB%22%3A%22s10%22%7D%2C%7B%22stateA%22%3A%22s12%22%2C%22label%22%3A%22%CF%B5%2C%CF%B5%2CM%22%2C%22stateB%22%3A%22s0%22%7D%5D%2C%22bulkTests%22%3A%7B%22accept%22%3A%22L%5CnCL%5CnCCL%5CnSL%5CnSSL%5CnSCL%5CnSpICL%5CnOLS%5CnOOLSS%5CnCspIL%5CnppppJCOLS%5CnOspILS%5CnOpJCLS%5CnOCLS%5CnCOCLS%22%2C%22reject%22%3A%22C%5CnO%5CnOL%5CnLS%5CnLL%5CnS%5Cns%5Cnp%5CnOOLS%22%7D%7D). + +```mermaid +graph LR + start((start)) -- "Îĩ,Îĩ/m#" --> pgpmsg((OpenPGP Message)) + pgpmsg -- "Literal Data,m/Îĩ" --> literal((Literal Message)) + literal -- "Îĩ,#/Îĩ" --> accept((Valid)) + literal -- "Signature,o/Îĩ" --> sig4ops((Corresponding Signature)) + sig4ops -- "Signature,o/Îĩ" --> sig4ops + sig4ops -- "Îĩ,#/Îĩ" --> accept + pgpmsg -- "OnePassSignature,m/o" --> ops((One-Pass-Signed Message)) + ops -- "Îĩ,Îĩ/m" --> pgpmsg + pgpmsg -- "Signature,m/m" --> pgpmsg + pgpmsg -. "Compressed Data,m/m" .-> pgpmsg + pgpmsg -- "SKESK|PKESK,m/k" --> esks((ESKs)) + pgpmsg -- "Sym. Enc. (Int. Prot.) Data,m/e" --> enc + esks -- "SKESK|PKESK,k/k" --> esks + esks -- "Sym. Enc. (Int. Prot.) Data,k/e" --> enc((Encrypted Data)) + enc -. "Îĩ,e/m" .-> pgpmsg +``` \ No newline at end of file From 27476831d927f35879dd49fba726a0c6526821fb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 7 Sep 2022 17:56:46 +0200 Subject: [PATCH 0395/1204] Add hint about dotted line in message format diagram --- misc/OpenPGPMessageFormat.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/misc/OpenPGPMessageFormat.md b/misc/OpenPGPMessageFormat.md index daaa715e..524798df 100644 --- a/misc/OpenPGPMessageFormat.md +++ b/misc/OpenPGPMessageFormat.md @@ -27,4 +27,8 @@ graph LR esks -- "SKESK|PKESK,k/k" --> esks esks -- "Sym. Enc. (Int. Prot.) Data,k/e" --> enc((Encrypted Data)) enc -. "Îĩ,e/m" .-> pgpmsg -``` \ No newline at end of file +``` + +The dotted line indicates a nested transition. +For example the arrow `Compressed Data,m/m` indicates, that the content of the Compressed Data packet itself +is an OpenPGP Message. \ No newline at end of file From 36cb6918a4a127e3cac017e275c6ce5901d472ed Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 7 Sep 2022 18:16:11 +0200 Subject: [PATCH 0396/1204] Refine PDA diagram --- misc/OpenPGPMessageFormat.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/misc/OpenPGPMessageFormat.md b/misc/OpenPGPMessageFormat.md index 524798df..d713d295 100644 --- a/misc/OpenPGPMessageFormat.md +++ b/misc/OpenPGPMessageFormat.md @@ -21,12 +21,17 @@ graph LR pgpmsg -- "OnePassSignature,m/o" --> ops((One-Pass-Signed Message)) ops -- "Îĩ,Îĩ/m" --> pgpmsg pgpmsg -- "Signature,m/m" --> pgpmsg - pgpmsg -. "Compressed Data,m/m" .-> pgpmsg + pgpmsg -- "Compressed Data,m/Îĩ" --> comp((Compressed Message)) + comp -. "Îĩ,Îĩ/m" .-> pgpmsg + comp -- "Îĩ,#/Îĩ" --> accept + comp -- "Signature,o/Îĩ" --> sig4ops pgpmsg -- "SKESK|PKESK,m/k" --> esks((ESKs)) - pgpmsg -- "Sym. Enc. (Int. Prot.) Data,m/e" --> enc + pgpmsg -- "Sym. Enc. (Int. Prot.) Data,m/Îĩ" --> enc esks -- "SKESK|PKESK,k/k" --> esks - esks -- "Sym. Enc. (Int. Prot.) Data,k/e" --> enc((Encrypted Data)) - enc -. "Îĩ,e/m" .-> pgpmsg + esks -- "Sym. Enc. (Int. Prot.) Data,k/Îĩ" --> enc((Encrypted Message)) + enc -. "Îĩ,Îĩ/m" .-> pgpmsg + enc -- "Îĩ,#/Îĩ" --> accept + enc -- "Signature,o/Îĩ" --> sig4ops ``` The dotted line indicates a nested transition. From ba5eea8b9c03bff824172d120415b67ec1dfd5e8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 7 Sep 2022 18:27:54 +0200 Subject: [PATCH 0397/1204] Add alphabet description --- misc/OpenPGPMessageFormat.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/misc/OpenPGPMessageFormat.md b/misc/OpenPGPMessageFormat.md index d713d295..36b787ce 100644 --- a/misc/OpenPGPMessageFormat.md +++ b/misc/OpenPGPMessageFormat.md @@ -34,6 +34,27 @@ graph LR enc -- "Signature,o/Îĩ" --> sig4ops ``` +The input alphabet consists of the following OpenPGP packets: +* `Literal Data`: Literal Data Packet +* `Signature`: Signature Packet +* `OnePassSignature`: One-Pass-Signature Packet +* `Compressed Data`: Compressed Data Packet +* `SKESK`: Symmetric-Key Encrypted Session Key Packet +* `PKESK`: Public-Key Encrypted Session Key Packet +* `Sym. Enc. Data`: Symmetrically Encrypted Data Packet +* `Sym. Enc. Int. Prot. Data`: Symmetrically Encrypted Integrity Protected Data Packet + +Additionally, `Îĩ` is used to transition without reading OpenPGP packets. + +The following stack alphabet is used: +* `m`: OpenPGP Message +* `o`: One-Pass-Signature packet. +* `k`: Encrypted Session Key +* `#`: Terminal for valid OpenPGP messages + +Note: The standards document states, that Marker Packets shall be ignored as well. +For the sake of readability, those transitions are omitted here. + The dotted line indicates a nested transition. For example the arrow `Compressed Data,m/m` indicates, that the content of the Compressed Data packet itself is an OpenPGP Message. \ No newline at end of file From 21cadcb8eb6e6f4d2eb5d0c8ee3c2d5987ca0a84 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 7 Sep 2022 18:33:36 +0200 Subject: [PATCH 0398/1204] Introduce dedicated state for Signed Message --- misc/OpenPGPMessageFormat.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/misc/OpenPGPMessageFormat.md b/misc/OpenPGPMessageFormat.md index 36b787ce..c3631109 100644 --- a/misc/OpenPGPMessageFormat.md +++ b/misc/OpenPGPMessageFormat.md @@ -20,7 +20,8 @@ graph LR sig4ops -- "Îĩ,#/Îĩ" --> accept pgpmsg -- "OnePassSignature,m/o" --> ops((One-Pass-Signed Message)) ops -- "Îĩ,Îĩ/m" --> pgpmsg - pgpmsg -- "Signature,m/m" --> pgpmsg + pgpmsg -- "Signature,m/Îĩ" --> signed((Signed Message)) + signed -- "Îĩ,Îĩ/m" --> pgpmsg pgpmsg -- "Compressed Data,m/Îĩ" --> comp((Compressed Message)) comp -. "Îĩ,Îĩ/m" .-> pgpmsg comp -- "Îĩ,#/Îĩ" --> accept From c01f2db5ef32ac8e5b5078d7c73bdcae8bb3af01 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 7 Sep 2022 19:35:41 +0200 Subject: [PATCH 0399/1204] Add formal definition of PDA --- misc/OpenPGPMessageFormat.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/misc/OpenPGPMessageFormat.md b/misc/OpenPGPMessageFormat.md index c3631109..32943eff 100644 --- a/misc/OpenPGPMessageFormat.md +++ b/misc/OpenPGPMessageFormat.md @@ -10,6 +10,8 @@ See [RFC4880 §11.3. OpenPGP Messages](https://www.rfc-editor.org/rfc/rfc4880#se A simulation of the automaton can be found [here](https://automatonsimulator.com/#%7B%22type%22%3A%22PDA%22%2C%22pda%22%3A%7B%22transitions%22%3A%7B%22start%22%3A%7B%22%22%3A%7B%22%22%3A%5B%7B%22state%22%3A%22s12%22%2C%22stackPushChar%22%3A%22%23%22%7D%5D%2C%22%23%22%3A%5B%5D%7D%7D%2C%22s0%22%3A%7B%22C%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%2C%22L%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s1%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%2C%22S%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%2C%22o%22%3A%5B%5D%7D%2C%22O%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s9%22%2C%22stackPushChar%22%3A%22o%22%7D%5D%7D%2C%22E%22%3A%7B%22M%22%3A%5B%5D%7D%2C%22p%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22s%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22I%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%2C%22J%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%7D%2C%22s1%22%3A%7B%22%22%3A%7B%22%22%3A%5B%5D%2C%22%23%22%3A%5B%7B%22state%22%3A%22s4%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%2C%22S%22%3A%7B%22o%22%3A%5B%7B%22state%22%3A%22s10%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%7D%2C%22s6%22%3A%7B%22p%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22s%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22I%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%2C%22J%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%7D%2C%22s8%22%3A%7B%22%22%3A%7B%22E%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%7D%2C%22s9%22%3A%7B%22%22%3A%7B%22%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%7D%2C%22s10%22%3A%7B%22%22%3A%7B%22%22%3A%5B%5D%2C%22%23%22%3A%5B%7B%22state%22%3A%22s4%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%2C%22S%22%3A%7B%22o%22%3A%5B%7B%22state%22%3A%22s10%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%7D%2C%22s4%22%3A%7B%22%22%3A%7B%22o%22%3A%5B%5D%7D%7D%2C%22s12%22%3A%7B%22%22%3A%7B%22%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%7D%7D%2C%22startState%22%3A%22start%22%2C%22acceptStates%22%3A%5B%22s4%22%5D%7D%2C%22states%22%3A%7B%22start%22%3A%7B%7D%2C%22s12%22%3A%7B%22top%22%3A395.00001525878906%2C%22left%22%3A99%2C%22displayId%22%3A%22Add%20Terminal%22%7D%2C%22s0%22%3A%7B%22top%22%3A259.00001525878906%2C%22left%22%3A162%2C%22displayId%22%3A%22OpenPGP%20Message%22%7D%2C%22s1%22%3A%7B%22top%22%3A304.00001525878906%2C%22left%22%3A524%2C%22displayId%22%3A%22Literal%20Message%22%7D%2C%22s9%22%3A%7B%22top%22%3A476.00001525878906%2C%22left%22%3A282%2C%22displayId%22%3A%22One%20Pass%20Signatures%22%7D%2C%22s6%22%3A%7B%22top%22%3A100%2C%22left%22%3A324%2C%22displayId%22%3A%22ESKs%22%7D%2C%22s8%22%3A%7B%22top%22%3A202%2C%22left%22%3A471%2C%22displayId%22%3A%22Encrypted%20Data%22%7D%2C%22s4%22%3A%7B%22isAccept%22%3Atrue%2C%22top%22%3A381.00001525878906%2C%22left%22%3A832%2C%22displayId%22%3A%22Accept%22%7D%2C%22s10%22%3A%7B%22top%22%3A237.00001525878906%2C%22left%22%3A809%2C%22displayId%22%3A%22Corresponding%20Signatures%22%7D%7D%2C%22transitions%22%3A%5B%7B%22stateA%22%3A%22start%22%2C%22label%22%3A%22%CF%B5%2C%CF%B5%2C%23%22%2C%22stateB%22%3A%22s12%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22C%2CM%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22L%2CM%2C%CF%B5%22%2C%22stateB%22%3A%22s1%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22S%2CM%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22O%2CM%2Co%22%2C%22stateB%22%3A%22s9%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22p%2CM%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22s%2CM%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22I%2CM%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22J%2CM%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s1%22%2C%22label%22%3A%22%CF%B5%2C%23%2C%CF%B5%22%2C%22stateB%22%3A%22s4%22%7D%2C%7B%22stateA%22%3A%22s1%22%2C%22label%22%3A%22S%2Co%2C%CF%B5%22%2C%22stateB%22%3A%22s10%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22p%2CX%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22s%2CX%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22I%2CX%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22J%2CX%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s8%22%2C%22label%22%3A%22%CF%B5%2CE%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s9%22%2C%22label%22%3A%22%CF%B5%2C%CF%B5%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s10%22%2C%22label%22%3A%22%CF%B5%2C%23%2C%CF%B5%22%2C%22stateB%22%3A%22s4%22%7D%2C%7B%22stateA%22%3A%22s10%22%2C%22label%22%3A%22S%2Co%2C%CF%B5%22%2C%22stateB%22%3A%22s10%22%7D%2C%7B%22stateA%22%3A%22s12%22%2C%22label%22%3A%22%CF%B5%2C%CF%B5%2CM%22%2C%22stateB%22%3A%22s0%22%7D%5D%2C%22bulkTests%22%3A%7B%22accept%22%3A%22L%5CnCL%5CnCCL%5CnSL%5CnSSL%5CnSCL%5CnSpICL%5CnOLS%5CnOOLSS%5CnCspIL%5CnppppJCOLS%5CnOspILS%5CnOpJCLS%5CnOCLS%5CnCOCLS%22%2C%22reject%22%3A%22C%5CnO%5CnOL%5CnLS%5CnLL%5CnS%5Cns%5Cnp%5CnOOLS%22%7D%7D). +The graph representation of the [Pushdown Automaton](https://en.wikipedia.org/wiki/Pushdown_automaton) looks like the following: + ```mermaid graph LR start((start)) -- "Îĩ,Îĩ/m#" --> pgpmsg((OpenPGP Message)) @@ -35,6 +37,20 @@ graph LR enc -- "Signature,o/Îĩ" --> sig4ops ``` +Formally, the PDA is defined as $M = (\mathcal{Q}, \Sigma, \Upgamma, \delta, q_0, Z, F)$, where +* $\mathcal{Q}$ is a finite set of states +* $\Sigma$ is a finite set which is called the input alphabet +* $\Upgamma$ is a finite set which is called the stack alphabet +* $\delta$ is a finite set of $\mathcal{Q}\times(\Sigma\cup\{\epsilon\})\times\Upgamma\times\mathcal{Q}\times\Upgamma^*$, the transition relation +* $q_0\in\mathcal{Q}$ is the start state +* $Z\in\Upgamma$ is the initial stack symbol +* $F\subseteq\mathcal{Q}$ is the set of accepting states + +In our diagram, the initial state $q_0$ is called `start`. +The initial stack symbol $Z$ is `Îĩ` (TODO: Make it `#`?). +The set of accepting states is $F=\text{valid}$. +$\delta$ is defined by the transitions shown in the graph diagram. + The input alphabet consists of the following OpenPGP packets: * `Literal Data`: Literal Data Packet * `Signature`: Signature Packet @@ -57,5 +73,5 @@ Note: The standards document states, that Marker Packets shall be ignored as wel For the sake of readability, those transitions are omitted here. The dotted line indicates a nested transition. -For example the arrow `Compressed Data,m/m` indicates, that the content of the Compressed Data packet itself -is an OpenPGP Message. \ No newline at end of file +For example, the transition `(Compressed Message, Îĩ, Îĩ, OpenPGP Message, m)` indicates, that the content of the +Compressed Data packet itself is an OpenPGP Message. \ No newline at end of file From acb845d28060698c3b0e200ee0b24f6898928fa4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 7 Sep 2022 19:37:36 +0200 Subject: [PATCH 0400/1204] Change transition to latex --- misc/OpenPGPMessageFormat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/OpenPGPMessageFormat.md b/misc/OpenPGPMessageFormat.md index 32943eff..6643de33 100644 --- a/misc/OpenPGPMessageFormat.md +++ b/misc/OpenPGPMessageFormat.md @@ -73,5 +73,5 @@ Note: The standards document states, that Marker Packets shall be ignored as wel For the sake of readability, those transitions are omitted here. The dotted line indicates a nested transition. -For example, the transition `(Compressed Message, Îĩ, Îĩ, OpenPGP Message, m)` indicates, that the content of the +For example, the transition $(\text{Compressed Message}, \epsilon, \epsilon, \text{OpenPGP Message}, m)$ indicates, that the content of the Compressed Data packet itself is an OpenPGP Message. \ No newline at end of file From 14941e77b34f3c5f95211dc4785234a3c4b2d341 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 7 Sep 2022 19:54:50 +0200 Subject: [PATCH 0401/1204] More latex, less markdown --- misc/OpenPGPMessageFormat.md | 38 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/misc/OpenPGPMessageFormat.md b/misc/OpenPGPMessageFormat.md index 6643de33..94412958 100644 --- a/misc/OpenPGPMessageFormat.md +++ b/misc/OpenPGPMessageFormat.md @@ -41,32 +41,32 @@ Formally, the PDA is defined as $M = (\mathcal{Q}, \Sigma, \Upgamma, \delta, q_0 * $\mathcal{Q}$ is a finite set of states * $\Sigma$ is a finite set which is called the input alphabet * $\Upgamma$ is a finite set which is called the stack alphabet -* $\delta$ is a finite set of $\mathcal{Q}\times(\Sigma\cup\{\epsilon\})\times\Upgamma\times\mathcal{Q}\times\Upgamma^*$, the transition relation +* $\delta$ is a finite set of $\mathcal{Q}\times(\Sigma\cup\textbraceleft\epsilon\textbraceright)\times\Upgamma\times\mathcal{Q}\times\Upgamma^*$, the transition relation * $q_0\in\mathcal{Q}$ is the start state * $Z\in\Upgamma$ is the initial stack symbol * $F\subseteq\mathcal{Q}$ is the set of accepting states -In our diagram, the initial state $q_0$ is called `start`. -The initial stack symbol $Z$ is `Îĩ` (TODO: Make it `#`?). -The set of accepting states is $F=\text{valid}$. +In our diagram, the initial state is $q_0 = \text{start}$. +The initial stack symbol is $Z=\epsilon$ (TODO: Make it `#`?). +The set of accepting states is $F=\textbraceleft\text{valid}\textbraceright$. $\delta$ is defined by the transitions shown in the graph diagram. -The input alphabet consists of the following OpenPGP packets: -* `Literal Data`: Literal Data Packet -* `Signature`: Signature Packet -* `OnePassSignature`: One-Pass-Signature Packet -* `Compressed Data`: Compressed Data Packet -* `SKESK`: Symmetric-Key Encrypted Session Key Packet -* `PKESK`: Public-Key Encrypted Session Key Packet -* `Sym. Enc. Data`: Symmetrically Encrypted Data Packet -* `Sym. Enc. Int. Prot. Data`: Symmetrically Encrypted Integrity Protected Data Packet +The input alphabet $\Sigma$ consists of the following OpenPGP packets: +* $\text{Literal Data}$: Literal Data Packet +* $\text{Signature}$: Signature Packet +* $\text{OnePassSignature}$: One-Pass-Signature Packet +* $\text{Compressed Data}$: Compressed Data Packet +* $\text{SKESK}$: Symmetric-Key Encrypted Session Key Packet +* $\text{PKESK}$: Public-Key Encrypted Session Key Packet +* $\text{Sym. Enc. Data}$: Symmetrically Encrypted Data Packet +* $\text{Sym. Enc. Int. Prot. Data}$: Symmetrically Encrypted Integrity Protected Data Packet -Additionally, `Îĩ` is used to transition without reading OpenPGP packets. +Additionally, $\epsilon$ is used to transition without reading OpenPGP packets or popping the stack. -The following stack alphabet is used: -* `m`: OpenPGP Message -* `o`: One-Pass-Signature packet. -* `k`: Encrypted Session Key +The following stack alphabet $\Upgamma$ is used: +* $m$: OpenPGP Message +* $o$: One-Pass-Signature packet. +* $k$: Encrypted Session Key * `#`: Terminal for valid OpenPGP messages Note: The standards document states, that Marker Packets shall be ignored as well. @@ -74,4 +74,4 @@ For the sake of readability, those transitions are omitted here. The dotted line indicates a nested transition. For example, the transition $(\text{Compressed Message}, \epsilon, \epsilon, \text{OpenPGP Message}, m)$ indicates, that the content of the -Compressed Data packet itself is an OpenPGP Message. \ No newline at end of file +Compressed Data packet itself is an OpenPGP Message. From bf0370fdc1ba22e5c6a25a894b3260f1a4baed1d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 7 Sep 2022 20:32:47 +0200 Subject: [PATCH 0402/1204] Add grammar from RFC to diagram --- misc/OpenPGPMessageFormat.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/misc/OpenPGPMessageFormat.md b/misc/OpenPGPMessageFormat.md index 94412958..d04160cc 100644 --- a/misc/OpenPGPMessageFormat.md +++ b/misc/OpenPGPMessageFormat.md @@ -10,7 +10,39 @@ See [RFC4880 §11.3. OpenPGP Messages](https://www.rfc-editor.org/rfc/rfc4880#se A simulation of the automaton can be found [here](https://automatonsimulator.com/#%7B%22type%22%3A%22PDA%22%2C%22pda%22%3A%7B%22transitions%22%3A%7B%22start%22%3A%7B%22%22%3A%7B%22%22%3A%5B%7B%22state%22%3A%22s12%22%2C%22stackPushChar%22%3A%22%23%22%7D%5D%2C%22%23%22%3A%5B%5D%7D%7D%2C%22s0%22%3A%7B%22C%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%2C%22L%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s1%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%2C%22S%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%2C%22o%22%3A%5B%5D%7D%2C%22O%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s9%22%2C%22stackPushChar%22%3A%22o%22%7D%5D%7D%2C%22E%22%3A%7B%22M%22%3A%5B%5D%7D%2C%22p%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22s%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22I%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%2C%22J%22%3A%7B%22M%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%7D%2C%22s1%22%3A%7B%22%22%3A%7B%22%22%3A%5B%5D%2C%22%23%22%3A%5B%7B%22state%22%3A%22s4%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%2C%22S%22%3A%7B%22o%22%3A%5B%7B%22state%22%3A%22s10%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%7D%2C%22s6%22%3A%7B%22p%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22s%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s6%22%2C%22stackPushChar%22%3A%22X%22%7D%5D%7D%2C%22I%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%2C%22J%22%3A%7B%22X%22%3A%5B%7B%22state%22%3A%22s8%22%2C%22stackPushChar%22%3A%22E%22%7D%5D%7D%7D%2C%22s8%22%3A%7B%22%22%3A%7B%22E%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%7D%2C%22s9%22%3A%7B%22%22%3A%7B%22%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%7D%2C%22s10%22%3A%7B%22%22%3A%7B%22%22%3A%5B%5D%2C%22%23%22%3A%5B%7B%22state%22%3A%22s4%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%2C%22S%22%3A%7B%22o%22%3A%5B%7B%22state%22%3A%22s10%22%2C%22stackPushChar%22%3A%22%22%7D%5D%7D%7D%2C%22s4%22%3A%7B%22%22%3A%7B%22o%22%3A%5B%5D%7D%7D%2C%22s12%22%3A%7B%22%22%3A%7B%22%22%3A%5B%7B%22state%22%3A%22s0%22%2C%22stackPushChar%22%3A%22M%22%7D%5D%7D%7D%7D%2C%22startState%22%3A%22start%22%2C%22acceptStates%22%3A%5B%22s4%22%5D%7D%2C%22states%22%3A%7B%22start%22%3A%7B%7D%2C%22s12%22%3A%7B%22top%22%3A395.00001525878906%2C%22left%22%3A99%2C%22displayId%22%3A%22Add%20Terminal%22%7D%2C%22s0%22%3A%7B%22top%22%3A259.00001525878906%2C%22left%22%3A162%2C%22displayId%22%3A%22OpenPGP%20Message%22%7D%2C%22s1%22%3A%7B%22top%22%3A304.00001525878906%2C%22left%22%3A524%2C%22displayId%22%3A%22Literal%20Message%22%7D%2C%22s9%22%3A%7B%22top%22%3A476.00001525878906%2C%22left%22%3A282%2C%22displayId%22%3A%22One%20Pass%20Signatures%22%7D%2C%22s6%22%3A%7B%22top%22%3A100%2C%22left%22%3A324%2C%22displayId%22%3A%22ESKs%22%7D%2C%22s8%22%3A%7B%22top%22%3A202%2C%22left%22%3A471%2C%22displayId%22%3A%22Encrypted%20Data%22%7D%2C%22s4%22%3A%7B%22isAccept%22%3Atrue%2C%22top%22%3A381.00001525878906%2C%22left%22%3A832%2C%22displayId%22%3A%22Accept%22%7D%2C%22s10%22%3A%7B%22top%22%3A237.00001525878906%2C%22left%22%3A809%2C%22displayId%22%3A%22Corresponding%20Signatures%22%7D%7D%2C%22transitions%22%3A%5B%7B%22stateA%22%3A%22start%22%2C%22label%22%3A%22%CF%B5%2C%CF%B5%2C%23%22%2C%22stateB%22%3A%22s12%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22C%2CM%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22L%2CM%2C%CF%B5%22%2C%22stateB%22%3A%22s1%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22S%2CM%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22O%2CM%2Co%22%2C%22stateB%22%3A%22s9%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22p%2CM%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22s%2CM%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22I%2CM%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s0%22%2C%22label%22%3A%22J%2CM%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s1%22%2C%22label%22%3A%22%CF%B5%2C%23%2C%CF%B5%22%2C%22stateB%22%3A%22s4%22%7D%2C%7B%22stateA%22%3A%22s1%22%2C%22label%22%3A%22S%2Co%2C%CF%B5%22%2C%22stateB%22%3A%22s10%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22p%2CX%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22s%2CX%2CX%22%2C%22stateB%22%3A%22s6%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22I%2CX%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s6%22%2C%22label%22%3A%22J%2CX%2CE%22%2C%22stateB%22%3A%22s8%22%7D%2C%7B%22stateA%22%3A%22s8%22%2C%22label%22%3A%22%CF%B5%2CE%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s9%22%2C%22label%22%3A%22%CF%B5%2C%CF%B5%2CM%22%2C%22stateB%22%3A%22s0%22%7D%2C%7B%22stateA%22%3A%22s10%22%2C%22label%22%3A%22%CF%B5%2C%23%2C%CF%B5%22%2C%22stateB%22%3A%22s4%22%7D%2C%7B%22stateA%22%3A%22s10%22%2C%22label%22%3A%22S%2Co%2C%CF%B5%22%2C%22stateB%22%3A%22s10%22%7D%2C%7B%22stateA%22%3A%22s12%22%2C%22label%22%3A%22%CF%B5%2C%CF%B5%2CM%22%2C%22stateB%22%3A%22s0%22%7D%5D%2C%22bulkTests%22%3A%7B%22accept%22%3A%22L%5CnCL%5CnCCL%5CnSL%5CnSSL%5CnSCL%5CnSpICL%5CnOLS%5CnOOLSS%5CnCspIL%5CnppppJCOLS%5CnOspILS%5CnOpJCLS%5CnOCLS%5CnCOCLS%22%2C%22reject%22%3A%22C%5CnO%5CnOL%5CnLS%5CnLL%5CnS%5Cns%5Cnp%5CnOOLS%22%7D%7D). -The graph representation of the [Pushdown Automaton](https://en.wikipedia.org/wiki/Pushdown_automaton) looks like the following: +RFC4880 defines the grammar of OpenPGP messages as follows: +``` + OpenPGP Message :- Encrypted Message | Signed Message | + Compressed Message | Literal Message. + + Compressed Message :- Compressed Data Packet. + + Literal Message :- Literal Data Packet. + + ESK :- Public-Key Encrypted Session Key Packet | + Symmetric-Key Encrypted Session Key Packet. + + ESK Sequence :- ESK | ESK Sequence, ESK. + + Encrypted Data :- Symmetrically Encrypted Data Packet | + Symmetrically Encrypted Integrity Protected Data Packet + + Encrypted Message :- Encrypted Data | ESK Sequence, Encrypted Data. + + One-Pass Signed Message :- One-Pass Signature Packet, + OpenPGP Message, Corresponding Signature Packet. + + Signed Message :- Signature Packet, OpenPGP Message | + One-Pass Signed Message. + + In addition, decrypting a Symmetrically Encrypted Data packet or a + Symmetrically Encrypted Integrity Protected Data packet as well as + decompressing a Compressed Data packet must yield a valid OpenPGP + Message. +``` + +This grammar can be translated into a [pushdown automaton](https://en.wikipedia.org/wiki/Pushdown_automaton) with +the following graphical representation: ```mermaid graph LR From 7480c47fa7c9fe9eac5029c02617ac0b6c79840a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 8 Sep 2022 18:15:52 +0200 Subject: [PATCH 0403/1204] Add behavior test to ensure that ArmoredInputStream cuts away any data outside of the armor --- ...rArmoredDataWithAppendedCleartextTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/bouncycastle/ArmoredInputStreamOverArmoredDataWithAppendedCleartextTest.java diff --git a/pgpainless-core/src/test/java/org/bouncycastle/ArmoredInputStreamOverArmoredDataWithAppendedCleartextTest.java b/pgpainless-core/src/test/java/org/bouncycastle/ArmoredInputStreamOverArmoredDataWithAppendedCleartextTest.java new file mode 100644 index 00000000..06921294 --- /dev/null +++ b/pgpainless-core/src/test/java/org/bouncycastle/ArmoredInputStreamOverArmoredDataWithAppendedCleartextTest.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ArmoredInputStreamOverArmoredDataWithAppendedCleartextTest { + + private static final String ASCII_ARMORED_WITH_APPENDED_GARBAGE = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCg==\n" + + "=WGju\n" + + "-----END PGP MESSAGE-----\n" + + "This is a bunch of crap that we appended."; + @Test + public void testArmoredInputStreamCutsOffAnyDataAfterTheAsciiArmor() throws IOException { + InputStream inputStream = new ByteArrayInputStream(ASCII_ARMORED_WITH_APPENDED_GARBAGE.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = new ArmoredInputStream(inputStream); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(armorIn, out); + armorIn.close(); + + assertEquals(22, out.size(), "ArmoredInputStream cuts off any appended data outside the ASCII armor."); + } +} From 8dfabf184244df4fd2e550cb9dcc4654f48a7d7c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 12 Sep 2022 15:25:43 +0200 Subject: [PATCH 0404/1204] Test decryption of messages using Session Key --- ...ionOfMessageWithoutESKUsingSessionKey.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java new file mode 100644 index 00000000..a7904c34 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSessionKey; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.util.SessionKey; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestDecryptionOfMessageWithoutESKUsingSessionKey { + + private static final String encryptedMessageWithSKESK = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0ECQMCc7jNEadAMZJg0j8BNtJwO2PLoRdG+VynivV7XpHp2Nw/S489vksUKct6\n" + + "7CYTFpVTzB4IcJwmUGMmre/N1KMTznEBzy3Txa1QVBc=\n" + + "=3M8l\n" + + "-----END PGP MESSAGE-----"; + + private static final String encryptedMessageWithoutESK = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "0j8BNtJwO2PLoRdG+VynivV7XpHp2Nw/S489vksUKct67CYTFpVTzB4IcJwmUGMm\n" + + "re/N1KMTznEBzy3Txa1QVBc=\n" + + "=t+pk\n" + + "-----END PGP MESSAGE-----"; + + private static final SessionKey sessionKey = new SessionKey( + PGPSessionKey.fromAsciiRepresentation("9:26be99bc478520fbc8ab8fb84991dace4b82cfb9b00f7d05c051d69b8cea8a7f")); + + @Test + public void decryptMessageWithSKESK() throws PGPException, IOException { + ByteArrayInputStream in = new ByteArrayInputStream(encryptedMessageWithSKESK.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(ConsumerOptions.get() + .setSessionKey(sessionKey)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + assertEquals("Hello, World!\n", out.toString()); + } + + // TODO: Enable when BC 172 gets released with our fix + @Disabled("Bug in BC 171. See https://github.com/bcgit/bc-java/pull/1228") + @Test + public void decryptMessageWithoutSKESK() throws PGPException, IOException { + ByteArrayInputStream in = new ByteArrayInputStream(encryptedMessageWithoutESK.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(ConsumerOptions.get() + .setSessionKey(sessionKey)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + assertEquals("Hello, World!\n", out.toString()); + } +} From 9e403c11248086268d8ef391226bb2218a06f76f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Sep 2022 20:22:12 +0200 Subject: [PATCH 0405/1204] Add ImplementationFactory.getSessionKeyDataDecryptorFactory() and impls --- .../implementation/BcImplementationFactory.java | 5 +++++ .../implementation/ImplementationFactory.java | 11 +++++++++++ .../implementation/JceImplementationFactory.java | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java index c8e20521..b7bbd5f9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java @@ -113,6 +113,11 @@ public class BcImplementationFactory extends ImplementationFactory { return new BcPublicKeyDataDecryptorFactory(privateKey); } + @Override + public SessionKeyDataDecryptorFactory getSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey) { + return new BcSessionKeyDataDecryptorFactory(sessionKey); + } + @Override public PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key) { return new BcPublicKeyKeyEncryptionMethodGenerator(key); diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java index 94967dee..ebd776a6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java @@ -32,6 +32,7 @@ import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.SessionKey; public abstract class ImplementationFactory { @@ -91,6 +92,16 @@ public abstract class ImplementationFactory { public abstract PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey); + public SessionKeyDataDecryptorFactory getSessionKeyDataDecryptorFactory(SessionKey sessionKey) { + PGPSessionKey pgpSessionKey = new PGPSessionKey( + sessionKey.getAlgorithm().getAlgorithmId(), + sessionKey.getKey() + ); + return getSessionKeyDataDecryptorFactory(pgpSessionKey); + } + + public abstract SessionKeyDataDecryptorFactory getSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey); + public abstract PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key); public abstract PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase); diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java index 504b197e..f4efd11e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java @@ -103,6 +103,12 @@ public class JceImplementationFactory extends ImplementationFactory { .build(privateKey); } + @Override + public SessionKeyDataDecryptorFactory getSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey) { + return new JceSessionKeyDataDecryptorFactoryBuilder() + .build(sessionKey); + } + public PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key) { return new JcePublicKeyKeyEncryptionMethodGenerator(key) .setProvider(ProviderFactory.getProvider()); From 0e45de9b4a0d0dc2a99429bc5ae1445ed2160ff4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Sep 2022 20:22:22 +0200 Subject: [PATCH 0406/1204] Formatting --- .../pgpainless/implementation/ImplementationFactory.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java index ebd776a6..51c76c6f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java @@ -50,7 +50,7 @@ public abstract class ImplementationFactory { } public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, - Passphrase passphrase) + Passphrase passphrase) throws PGPException { return getPBESecretKeyEncryptor(symmetricKeyAlgorithm, getPGPDigestCalculator(HashAlgorithm.SHA1), passphrase); @@ -59,8 +59,8 @@ public abstract class ImplementationFactory { public abstract PBESecretKeyEncryptor getPBESecretKeyEncryptor(PGPSecretKey secretKey, Passphrase passphrase) throws PGPException; public abstract PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, - PGPDigestCalculator digestCalculator, - Passphrase passphrase); + PGPDigestCalculator digestCalculator, + Passphrase passphrase); public abstract PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) throws PGPException; From 609bb4556aba7f005eba3226b2c5dc1aeb4c0d43 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Sep 2022 20:26:13 +0200 Subject: [PATCH 0407/1204] Use ImplementationFactory.getSessionKeyDataDecryptorFactory() method --- .../decryption_verification/DecryptionStreamFactory.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 77ad92bb..a787280a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -256,9 +256,8 @@ public final class DecryptionStreamFactory { PGPEncryptedDataList pgpEncryptedDataList, SessionKey sessionKey) throws PGPException { - PGPSessionKey pgpSessionKey = new PGPSessionKey(sessionKey.getAlgorithm().getAlgorithmId(), sessionKey.getKey()); - SessionKeyDataDecryptorFactory decryptorFactory = - ImplementationFactory.getInstance().provideSessionKeyDataDecryptorFactory(pgpSessionKey); + SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getSessionKeyDataDecryptorFactory(sessionKey); InputStream decryptedDataStream = null; PGPEncryptedData encryptedData = null; for (PGPEncryptedData pgpEncryptedData : pgpEncryptedDataList) { From 639d2a19f87127e2baef357f0bba102d27a80e1f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Sep 2022 20:27:16 +0200 Subject: [PATCH 0408/1204] Remove unused provideSessionKeyDataDecryptorFactory() methods --- .../pgpainless/implementation/BcImplementationFactory.java | 5 ----- .../org/pgpainless/implementation/ImplementationFactory.java | 2 -- .../pgpainless/implementation/JceImplementationFactory.java | 5 ----- 3 files changed, 12 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java index b7bbd5f9..c8e86311 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java @@ -148,11 +148,6 @@ public class BcImplementationFactory extends ImplementationFactory { .build(passphrase.getChars()); } - @Override - public SessionKeyDataDecryptorFactory provideSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey) { - return new BcSessionKeyDataDecryptorFactory(sessionKey); - } - @Override public PGPObjectFactory getPGPObjectFactory(byte[] bytes) { return new BcPGPObjectFactory(bytes); diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java index 51c76c6f..06065838 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java @@ -118,8 +118,6 @@ public abstract class ImplementationFactory { HashAlgorithm hashAlgorithm, int s2kCount, Passphrase passphrase) throws PGPException; - public abstract SessionKeyDataDecryptorFactory provideSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey); - public abstract PGPObjectFactory getPGPObjectFactory(InputStream inputStream); public abstract PGPObjectFactory getPGPObjectFactory(byte[] bytes); diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java index f4efd11e..31a7f128 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java @@ -137,11 +137,6 @@ public class JceImplementationFactory extends ImplementationFactory { .build(passphrase.getChars()); } - @Override - public SessionKeyDataDecryptorFactory provideSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey) { - return new JceSessionKeyDataDecryptorFactoryBuilder().build(sessionKey); - } - @Override public PGPObjectFactory getPGPObjectFactory(InputStream inputStream) { return new JcaPGPObjectFactory(inputStream); From 5bccc1960e51a1cdcc085a780a3bd663e0b7897f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 27 Sep 2022 16:11:45 +0200 Subject: [PATCH 0409/1204] Add PGPainless.asciiArmor(key, outputStream) --- .../main/java/org/pgpainless/PGPainless.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 9ff2a3b0..e41304b7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -5,9 +5,11 @@ package org.pgpainless; import java.io.IOException; +import java.io.OutputStream; import java.util.Date; import javax.annotation.Nonnull; +import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRing; @@ -89,9 +91,10 @@ public final class PGPainless { * @param key key or certificate * @return ascii armored string * - * @throws IOException in case of an error in the {@link org.bouncycastle.bcpg.ArmoredOutputStream} + * @throws IOException in case of an error in the {@link ArmoredOutputStream} */ - public static String asciiArmor(@Nonnull PGPKeyRing key) throws IOException { + public static String asciiArmor(@Nonnull PGPKeyRing key) + throws IOException { if (key instanceof PGPSecretKeyRing) { return ArmorUtils.toAsciiArmoredString((PGPSecretKeyRing) key); } else { @@ -99,6 +102,21 @@ public final class PGPainless { } } + /** + * Wrap a key of certificate in ASCII armor and write the result into the given {@link OutputStream}. + * + * @param key key or certificate + * @param outputStream output stream + * + * @throws IOException in case of an error ion the {@link ArmoredOutputStream} + */ + public static void asciiArmor(@Nonnull PGPKeyRing key, @Nonnull OutputStream outputStream) + throws IOException { + ArmoredOutputStream armorOut = ArmorUtils.toAsciiArmoredStream(key, outputStream); + key.encode(armorOut); + armorOut.close(); + } + /** * Create an {@link EncryptionStream}, which can be used to encrypt and/or sign data using OpenPGP. * From dac059c70280d77763ccec1bea7770363c596142 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 27 Sep 2022 16:17:22 +0200 Subject: [PATCH 0410/1204] Add test for PGPainless.asciiArmor(key, stream) --- .../src/test/java/org/pgpainless/util/ArmorUtilsTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java index c320c649..5eb32179 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java @@ -128,6 +128,14 @@ public class ArmorUtilsTest { assertTrue(ascii.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\n")); } + @Test + public void testAsciiArmorToStream() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + PGPainless.asciiArmor(secretKeys, bytes); + assertTrue(bytes.toString().startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\n")); + } + @Test public void testSetCustomVersionHeader() throws IOException { ArmoredOutputStreamFactory.setVersionInfo("MyVeryFirstOpenPGPProgram 1.0"); From d74a8d040801f4f80f359865c7c3c41ac1d69f9e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 27 Sep 2022 16:28:31 +0200 Subject: [PATCH 0411/1204] Add PGPainless.asciiArmor(PGPSignature) --- .../main/java/org/pgpainless/PGPainless.java | 15 +++++++++ .../java/org/pgpainless/util/ArmorUtils.java | 32 +++++++++++++++++++ .../org/pgpainless/util/ArmorUtilsTest.java | 17 ++++++++++ 3 files changed, 64 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index e41304b7..33cf7faa 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -14,6 +14,7 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.decryption_verification.DecryptionBuilder; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.encryption_signing.EncryptionBuilder; @@ -27,6 +28,7 @@ import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterfac import org.pgpainless.key.parsing.KeyRingReader; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.policy.Policy; +import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.ArmorUtils; public final class PGPainless { @@ -102,6 +104,19 @@ public final class PGPainless { } } + /** + * Wrap the detached signature in ASCII armor. + * + * @param signature detached signature + * @return ascii armored string + * + * @throws IOException in case of an error in the {@link ArmoredOutputStream} + */ + public static String asciiArmor(@Nonnull PGPSignature signature) + throws IOException { + return ArmorUtils.toAsciiArmoredString(signature); + } + /** * Wrap a key of certificate in ASCII armor and write the result into the given {@link OutputStream}. * diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index 160d8d0a..8a51ff3d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -156,6 +156,38 @@ public final class ArmorUtils { return sb.toString(); } + /** + * Return the ASCII armored representation of the given detached signature. + * The signature will not be stripped of non-exportable subpackets or trust-packets. + * If you need to strip those (e.g. because the signature is intended to be sent to a third party), use + * {@link #toAsciiArmoredString(PGPSignature, boolean)} and provide
true
as boolean value. + * + * @param signature signature + * @return ascii armored string + * + * @throws IOException in case of an error in the {@link ArmoredOutputStream} + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull PGPSignature signature) throws IOException { + return toAsciiArmoredString(signature, false); + } + + /** + * Return the ASCII armored representation of the given detached signature. + * If
export
is true, the signature will be stripped of non-exportable subpackets or trust-packets. + * If it is
false
, the signature will be encoded as-is. + * + * @param signature signature + * @return ascii armored string + * + * @throws IOException in case of an error in the {@link ArmoredOutputStream} + */ + @Nonnull + public static String toAsciiArmoredString(@Nonnull PGPSignature signature, boolean export) + throws IOException { + return toAsciiArmoredString(signature.getEncoded(export)); + } + /** * Return the ASCII armored encoding of the given OpenPGP data bytes. * diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java index 5eb32179..b83c0d03 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java @@ -24,6 +24,7 @@ import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.AfterAll; @@ -39,6 +40,7 @@ import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.ecc.EllipticCurve; import org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA; +import org.pgpainless.signature.SignatureUtils; public class ArmorUtilsTest { @@ -128,6 +130,21 @@ public class ArmorUtilsTest { assertTrue(ascii.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\n")); } + @Test + public void signatureToAsciiArmoredString() throws PGPException, IOException { + String SIG = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: PGPainless\n" + + "\n" + + "iHUEARMKAB0WIQRPZlxNwsRmC8ZCXkFXNuaTGs83DAUCYJ/x5gAKCRBXNuaTGs83\n" + + "DFRwAP9/4wMvV3WcX59Clo7mkRce6iwW3VBdiN+yMu3tjmHB2wD/RfE28Q1v4+eo\n" + + "ySNgbyvqYYsNr0fnBwaG3aaj+u5ExiE=\n" + + "=Z2SO\n" + + "-----END PGP SIGNATURE-----\n"; + PGPSignature signature = SignatureUtils.readSignatures(SIG).get(0); + String armored = PGPainless.asciiArmor(signature); + assertEquals(SIG, armored); + } + @Test public void testAsciiArmorToStream() throws IOException, PGPException { PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); From 6a2a604ba408f93fcb6c50e2eb01419afbb3bca1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 27 Sep 2022 16:47:23 +0200 Subject: [PATCH 0412/1204] Update TODO for BC 173 --- .../TestDecryptionOfMessageWithoutESKUsingSessionKey.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java index a7904c34..c9778426 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java @@ -55,8 +55,8 @@ public class TestDecryptionOfMessageWithoutESKUsingSessionKey { assertEquals("Hello, World!\n", out.toString()); } - // TODO: Enable when BC 172 gets released with our fix - @Disabled("Bug in BC 171. See https://github.com/bcgit/bc-java/pull/1228") + // TODO: Enable when BC 173 gets released with our fix + @Disabled("Bug in BC 172. See https://github.com/bcgit/bc-java/pull/1228") @Test public void decryptMessageWithoutSKESK() throws PGPException, IOException { ByteArrayInputStream in = new ByteArrayInputStream(encryptedMessageWithoutESK.getBytes(StandardCharsets.UTF_8)); From f94917d01f11308e5fc5d0d4ef690f65151d7e08 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 28 Sep 2022 13:18:34 +0200 Subject: [PATCH 0413/1204] Fix checkstyle issue --- pgpainless-core/src/main/java/org/pgpainless/PGPainless.java | 1 - 1 file changed, 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 33cf7faa..6da77c80 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -28,7 +28,6 @@ import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterfac import org.pgpainless.key.parsing.KeyRingReader; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.policy.Policy; -import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.ArmorUtils; public final class PGPainless { From 2c76f66987d03bfc0738e617a0fe8a99525718ab Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 4 Oct 2022 19:13:01 +0200 Subject: [PATCH 0414/1204] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 250a80c2..f933f760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ SPDX-License-Identifier: CC0-1.0 ## 1.3.7-SNAPSHOT - Add `KeyRingUtils.injectCertification(keys, certification)` - Bugfix: Fix signature verification when `DecryptionStream` is drained byte-by-byte using `read()` call +- Add `PGPainless.asciiArmor(key, outputStream)` +- Add `PGPainless.asciiArmor(signature)` ## 1.3.6 - Remove deprecated methods From 19ee2ebacf7ab26dfed102180b511a6058f3eaa0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 5 Oct 2022 12:27:38 +0200 Subject: [PATCH 0415/1204] PGPainless 1.3.7 --- CHANGELOG.md | 4 ++-- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f933f760..1b5cc621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.3.7-SNAPSHOT -- Add `KeyRingUtils.injectCertification(keys, certification)` +## 1.3.7 - Bugfix: Fix signature verification when `DecryptionStream` is drained byte-by-byte using `read()` call +- Add `KeyRingUtils.injectCertification(keys, certification)` - Add `PGPainless.asciiArmor(key, outputStream)` - Add `PGPainless.asciiArmor(signature)` diff --git a/README.md b/README.md index b8bd3c51..7cdd25e4 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.6' + implementation 'org.pgpainless:pgpainless-core:1.3.7' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index ad96b8dc..ab074514 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.6" + implementation "org.pgpainless:pgpainless-sop:1.3.7" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.6 + 1.3.7 ... diff --git a/version.gradle b/version.gradle index f361dc8d..cd80793d 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.7' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From 00eafc3957df02f2439bfaa304168725437e4e7a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 5 Oct 2022 12:31:20 +0200 Subject: [PATCH 0416/1204] PGPainless 1.3.8-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index cd80793d..1352ec44 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.7' - isSnapshot = false + shortVersion = '1.3.8' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.71' From 8834d8ad1047c9f593af82eba5c3b388db1f80f0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Oct 2022 15:13:49 +0200 Subject: [PATCH 0417/1204] Increase timeframe for some tests which check expiration dates --- .../GenerateKeyWithAdditionalUserIdTest.java | 10 ++++--- .../GenerateKeyWithoutUserIdTest.java | 24 ++++++++++++---- .../timeframe/TestTimeFrameProvider.java | 28 +++++++++++++++++++ .../pgpainless/timeframe/package-info.java | 8 ++++++ 4 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/timeframe/TestTimeFrameProvider.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/timeframe/package-info.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java index b05dbe1e..2e2ffc8d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java @@ -25,7 +25,7 @@ import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.UserId; -import org.pgpainless.util.DateUtil; +import org.pgpainless.timeframe.TestTimeFrameProvider; import org.pgpainless.util.TestAllImplementations; public class GenerateKeyWithAdditionalUserIdTest { @@ -33,11 +33,13 @@ public class GenerateKeyWithAdditionalUserIdTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { - Date expiration = new Date(DateUtil.now().getTime() + 60 * 1000); + Date now = new Date(); + Date expiration = TestTimeFrameProvider.defaultExpirationForCreationDate(now); PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.RSA(RsaLength._3072), - KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) + KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS) + .setKeyCreationDate(now)) .addUserId(UserId.onlyEmail("primary@user.id")) .addUserId(UserId.onlyEmail("additional@user.id")) .addUserId(UserId.onlyEmail("additional2@user.id")) @@ -46,7 +48,7 @@ public class GenerateKeyWithAdditionalUserIdTest { .build(); PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys); - JUtils.assertEquals(expiration.getTime(), PGPainless.inspectKeyRing(publicKeys).getPrimaryKeyExpirationDate().getTime(), 2000); + JUtils.assertDateEquals(expiration, PGPainless.inspectKeyRing(publicKeys).getPrimaryKeyExpirationDate()); Iterator userIds = publicKeys.getPublicKey().getUserIDs(); assertEquals("primary@user.id", userIds.next()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java index 8b022a21..24484cd0 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java @@ -15,6 +15,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; @@ -25,7 +26,7 @@ import org.pgpainless.key.generation.type.eddsa.EdDSACurve; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.util.DateUtil; +import org.pgpainless.timeframe.TestTimeFrameProvider; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -34,6 +35,7 @@ import java.io.InputStream; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Date; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -42,11 +44,12 @@ public class GenerateKeyWithoutUserIdTest { @Test public void generateKeyWithoutUserId() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - Date expirationDate = DateUtil.toSecondsPrecision(new Date(DateUtil.now().getTime() + 1000 * 6000)); + Date now = new Date(); + Date expirationDate = TestTimeFrameProvider.defaultExpirationForCreationDate(now); PGPSecretKeyRing secretKey = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER).setKeyCreationDate(now)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA).setKeyCreationDate(now)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE).setKeyCreationDate(now)) .setExpirationDate(expirationDate) .build(); @@ -87,7 +90,16 @@ public class GenerateKeyWithoutUserIdTest { OpenPgpMetadata metadata = decryptionStream.getResult(); - assertTrue(metadata.containsVerifiedSignatureFrom(certificate)); + assertTrue(metadata.containsVerifiedSignatureFrom(certificate), + failuresToString(metadata.getInvalidInbandSignatures())); assertTrue(metadata.isEncrypted()); } + + private static String failuresToString(List failureList) { + StringBuilder sb = new StringBuilder(); + for (SignatureVerification.Failure failure : failureList) { + sb.append(failure.toString()).append('\n'); + } + return sb.toString(); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/timeframe/TestTimeFrameProvider.java b/pgpainless-core/src/test/java/org/pgpainless/timeframe/TestTimeFrameProvider.java new file mode 100644 index 00000000..9d32a405 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/timeframe/TestTimeFrameProvider.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.timeframe; + +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +public class TestTimeFrameProvider { + + /** + * Return an expiration date which is 7h 13m and 31s from the given date. + * + * @param now t0 + * @return t1 which is t0 +7h13m31s + */ + public static Date defaultExpirationForCreationDate(Date now) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); + calendar.setTime(now); + calendar.add(Calendar.HOUR, 7); + calendar.add(Calendar.MINUTE, 13); + calendar.add(Calendar.SECOND, 31); + return calendar.getTime(); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/timeframe/package-info.java b/pgpainless-core/src/test/java/org/pgpainless/timeframe/package-info.java new file mode 100644 index 00000000..dfada70c --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/timeframe/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Test classes for timeframes. + */ +package org.pgpainless.timeframe; From 754fcf72a18c930c42da70ce8905cdf3023f11bf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 31 Oct 2022 11:43:24 +0100 Subject: [PATCH 0418/1204] Implement ProducerOptions.setHideArmorHeaders() Fixes #328 --- .../encryption_signing/EncryptionStream.java | 2 +- .../encryption_signing/ProducerOptions.java | 19 +++++++++++ .../util/ArmoredOutputStreamFactory.java | 11 ++++++ .../HideArmorHeadersTest.java | 34 +++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HideArmorHeadersTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 50f5cb5f..27d35ea5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -86,7 +86,7 @@ public final class EncryptionStream extends OutputStream { outermostStream = new BufferedOutputStream(outermostStream); LOGGER.debug("Wrap encryption output in ASCII armor"); - armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream); + armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream, options); if (options.hasComment()) { String[] commentLines = options.getComment().split("\n"); for (String commentLine : commentLines) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java index a015a581..4948a7fe 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java @@ -22,6 +22,7 @@ public final class ProducerOptions { private StreamEncoding encodingField = StreamEncoding.BINARY; private boolean applyCRLFEncoding = false; private boolean cleartextSigned = false; + private boolean hideArmorHeaders = false; private CompressionAlgorithm compressionAlgorithmOverride = PGPainless.getPolicy().getCompressionAlgorithmPolicy() .defaultCompressionAlgorithm(); @@ -302,4 +303,22 @@ public final class ProducerOptions { public @Nullable SigningOptions getSigningOptions() { return signingOptions; } + + public boolean isHideArmorHeaders() { + return hideArmorHeaders; + } + + /** + * If set to
true
, armor headers like version or comments will be omitted from armored output. + * By default, armor headers are not hidden. + * Note: If comments are added via {@link #setComment(String)}, those are not omitted, even if + * {@link #hideArmorHeaders} is set to
true
. + * + * @param hideArmorHeaders true or false + * @return this + */ + public ProducerOptions setHideArmorHeaders(boolean hideArmorHeaders) { + this.hideArmorHeaders = hideArmorHeaders; + return this; + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java index 2e1377a0..403115ba 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java @@ -7,6 +7,7 @@ package org.pgpainless.util; import java.io.OutputStream; import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.pgpainless.encryption_signing.ProducerOptions; /** * Factory to create configured {@link ArmoredOutputStream ArmoredOutputStreams}. @@ -37,6 +38,16 @@ public final class ArmoredOutputStreamFactory { return armoredOutputStream; } + public static ArmoredOutputStream get(OutputStream outputStream, ProducerOptions options) { + if (options.isHideArmorHeaders()) { + ArmoredOutputStream armorOut = new ArmoredOutputStream(outputStream); + armorOut.clearHeaders(); + return armorOut; + } else { + return get(outputStream); + } + } + /** * Overwrite the version header of ASCII armors with a custom value. * Newlines in the version info string result in multiple version header entries. diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HideArmorHeadersTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HideArmorHeadersTest.java new file mode 100644 index 00000000..242b430b --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HideArmorHeadersTest.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import org.bouncycastle.openpgp.PGPException; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.util.Passphrase; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HideArmorHeadersTest { + + @Test + public void testVersionHeaderIsOmitted() throws PGPException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt( + EncryptionOptions.get() + .addPassphrase(Passphrase.fromPassword("sw0rdf1sh"))) + .setHideArmorHeaders(true)); + + encryptionStream.write("Hello, World!\n".getBytes()); + encryptionStream.close(); + + assertTrue(out.toString().startsWith("-----BEGIN PGP MESSAGE-----\n\n")); // No "Version: PGPainless" + } +} From f5e4c7571c86a2272798f6c07aa499112e620502 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 2 Nov 2022 10:37:06 +0100 Subject: [PATCH 0419/1204] Bump BC to 1.72, BCPG to 1.72.1 --- pgpainless-core/build.gradle | 3 ++- version.gradle | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index f1b852ca..3c73121f 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -21,7 +21,8 @@ dependencies { // Bouncy Castle api "org.bouncycastle:bcprov-jdk15to18:$bouncyCastleVersion" - api "org.bouncycastle:bcpg-jdk15to18:$bouncyCastleVersion" + api "org.bouncycastle:bcpg-jdk15to18:$bouncyPgVersion" + // api(files("../libs/bcpg-jdk18on-1.70.jar")) // @Nullable, @Nonnull annotations implementation "com.google.code.findbugs:jsr305:3.0.2" diff --git a/version.gradle b/version.gradle index 1352ec44..9b1dc318 100644 --- a/version.gradle +++ b/version.gradle @@ -8,7 +8,10 @@ allprojects { isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 - bouncyCastleVersion = '1.71' + bouncyCastleVersion = '1.72' + // unfortunately we rely on 1.72.1 for a patch for https://github.com/bcgit/bc-java/issues/1257 + // which is a bug we introduced with a PR against BC :/ oops + bouncyPgVersion = '1.72.1' junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' From b0258f8c5b5189434556670ac9c76cb131f7debb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 2 Nov 2022 10:57:53 +0100 Subject: [PATCH 0420/1204] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b5cc621..8a31584c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.8-SNAPSHOT +- Bump `bcprov` to `1.72` +- Bump `bcpg` to `1.72.1` +- Add `ProducerOptions.setHideArmorHeaders(boolean)` to hide automatically added armor headers + in encrypted messages + ## 1.3.7 - Bugfix: Fix signature verification when `DecryptionStream` is drained byte-by-byte using `read()` call - Add `KeyRingUtils.injectCertification(keys, certification)` From df258b46c1fe590f60b6b267e48b2dde1ca62f25 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 11:46:07 +0100 Subject: [PATCH 0421/1204] PGPainless 1.3.8 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a31584c..89adc4a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.3.8-SNAPSHOT +## 1.3.8 - Bump `bcprov` to `1.72` - Bump `bcpg` to `1.72.1` - Add `ProducerOptions.setHideArmorHeaders(boolean)` to hide automatically added armor headers diff --git a/README.md b/README.md index 7cdd25e4..305e5cea 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.7' + implementation 'org.pgpainless:pgpainless-core:1.3.8' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index ab074514..ebb1736c 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.7" + implementation "org.pgpainless:pgpainless-sop:1.3.8" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.7 + 1.3.8 ... diff --git a/version.gradle b/version.gradle index 9b1dc318..f89cec34 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.8' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 3000e496bceaf6e4a9bb5960d5e63600ef140600 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 11:48:58 +0100 Subject: [PATCH 0422/1204] PGPainless 1.3.9-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index f89cec34..680b9d45 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.8' - isSnapshot = false + shortVersion = '1.3.9' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From e67c43a6f7376f26cb519d7f1c042b7d2a59cb05 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Nov 2022 15:03:35 +0100 Subject: [PATCH 0423/1204] Bump sop-java to 4.0.2 and improve exception handling --- .../java/org/pgpainless/sop/DecryptImpl.java | 19 ++++++++++++++++--- .../java/org/pgpainless/sop/EncryptImpl.java | 6 ++++-- .../org/pgpainless/sop/ExtractCertImpl.java | 5 +++++ .../org/pgpainless/sop/InlineVerifyImpl.java | 3 +++ version.gradle | 2 +- 5 files changed, 29 insertions(+), 6 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 4957f748..f18ed732 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -61,6 +61,11 @@ public class DecryptImpl implements Decrypt { consumerOptions.addVerificationCerts(certs); + } catch (IOException e) { + if (e.getMessage() != null && e.getMessage().startsWith("unknown object in stream:")) { + throw new SOPGPException.BadData(e); + } + throw e; } catch (PGPException e) { throw new SOPGPException.BadData(e); } @@ -96,15 +101,23 @@ public class DecryptImpl implements Decrypt { } @Override - public DecryptImpl withKey(InputStream keyIn) throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo { + public DecryptImpl withKey(InputStream keyIn) throws SOPGPException.BadData, IOException, SOPGPException.UnsupportedAsymmetricAlgo { try { PGPSecretKeyRingCollection secretKeyCollection = PGPainless.readKeyRing() .secretKeyRingCollection(keyIn); + if (secretKeyCollection.size() == 0) { + throw new SOPGPException.BadData("No key data found."); + } for (PGPSecretKeyRing key : secretKeyCollection) { protector.addSecretKey(key); consumerOptions.addDecryptionKey(key, protector); } - } catch (IOException | PGPException e) { + } catch (IOException e) { + if (e.getMessage() != null && e.getMessage().startsWith("unknown object in stream:")) { + throw new SOPGPException.BadData(e); + } + throw e; + } catch (PGPException e) { throw new SOPGPException.BadData(e); } return this; @@ -132,7 +145,7 @@ public class DecryptImpl implements Decrypt { .onInputStream(ciphertext) .withOptions(consumerOptions); } catch (MissingDecryptionMethodException e) { - throw new SOPGPException.CannotDecrypt(); + throw new SOPGPException.CannotDecrypt("No usable decryption key or password provided.", e); } catch (WrongPassphraseException e) { throw new SOPGPException.KeyIsProtected(); } catch (PGPException | IOException e) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 1b95d87c..9658bd17 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -100,8 +100,10 @@ public class EncryptImpl implements Encrypt { public Encrypt withCert(InputStream cert) throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { try { PGPPublicKeyRingCollection certificates = PGPainless.readKeyRing() - .keyRingCollection(cert, false) - .getPgpPublicKeyRingCollection(); + .publicKeyRingCollection(cert); + if (certificates.size() == 0) { + throw new SOPGPException.BadData("No certificate data found."); + } encryptionOptions.addRecipients(certificates); } catch (KeyException.UnacceptableEncryptionKeyException e) { throw new SOPGPException.CertCannotEncrypt(e.getMessage(), e); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java index 5f694208..16848383 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java @@ -35,6 +35,11 @@ public class ExtractCertImpl implements ExtractCert { PGPSecretKeyRingCollection keys; try { keys = PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream); + } catch (IOException e) { + if (e.getMessage() != null && e.getMessage().startsWith("unknown object in stream:")) { + throw new SOPGPException.BadData(e); + } + throw e; } catch (PGPException e) { throw new IOException("Cannot read keys.", e); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 1e8c4fee..e9994f38 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -19,6 +19,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.exception.MissingDecryptionMethodException; import sop.ReadyWithResult; import sop.Verification; import sop.exception.SOPGPException; @@ -84,6 +85,8 @@ public class InlineVerifyImpl implements InlineVerify { } return verificationList; + } catch (MissingDecryptionMethodException e) { + throw new SOPGPException.BadData("Cannot verify encrypted message.", e); } catch (PGPException e) { throw new SOPGPException.BadData(e); } diff --git a/version.gradle b/version.gradle index 680b9d45..36822069 100644 --- a/version.gradle +++ b/version.gradle @@ -16,6 +16,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '4.0.1' + sopJavaVersion = '4.0.2' } } From db9745d7a29a7e738d39fae209202bfc0b94110d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Nov 2022 15:06:50 +0100 Subject: [PATCH 0424/1204] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89adc4a7..66ba62a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.9-SNAPSHOT +- Bump `sop-java` to `4.0.2` +- SOP: Improve exception handling + ## 1.3.8 - Bump `bcprov` to `1.72` - Bump `bcpg` to `1.72.1` From 095e58eddc04af5a15adb7d8423209058291475a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Nov 2022 15:08:07 +0100 Subject: [PATCH 0425/1204] Update man page documentation --- .../packaging/man/pgpainless-cli-armor.1 | 101 +--------------- .../packaging/man/pgpainless-cli-dearmor.1 | 102 +--------------- .../packaging/man/pgpainless-cli-decrypt.1 | 109 ++--------------- .../packaging/man/pgpainless-cli-encrypt.1 | 111 ++---------------- .../man/pgpainless-cli-extract-cert.1 | 101 +--------------- .../man/pgpainless-cli-generate-completion.1 | 13 +- .../man/pgpainless-cli-generate-key.1 | 110 ++--------------- .../packaging/man/pgpainless-cli-help.1 | 13 +- .../man/pgpainless-cli-inline-detach.1 | 101 +--------------- .../man/pgpainless-cli-inline-sign.1 | 108 ++--------------- .../man/pgpainless-cli-inline-verify.1 | 111 ++---------------- .../packaging/man/pgpainless-cli-sign.1 | 111 ++---------------- .../packaging/man/pgpainless-cli-verify.1 | 109 ++--------------- .../packaging/man/pgpainless-cli-version.1 | 101 +--------------- pgpainless-cli/packaging/man/pgpainless-cli.1 | 16 ++- 15 files changed, 124 insertions(+), 1193 deletions(-) diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 index bc0efe20..d85b6bf9 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli-armor .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-ARMOR" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-ARMOR" "1" "2022-11-06" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -31,7 +31,7 @@ pgpainless\-cli\-armor \- Add ASCII Armor to standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli armor\fP [\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP] +\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP] [\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP] .SH "DESCRIPTION" .SH "OPTIONS" @@ -40,99 +40,8 @@ pgpainless\-cli\-armor \- Add ASCII Armor to standard input .RS 4 Label to be used in the header and tail of the armoring .RE -.SH "EXIT CODES:" .sp -\fB0\fP +\fB\-\-stacktrace\fP .RS 4 -Successful program execution -.RE -.sp -\fB1\fP -.RS 4 -Generic program error -.RE -.sp -\fB3\fP -.RS 4 -Verification requested but no verifiable signature found -.RE -.sp -\fB13\fP -.RS 4 -Unsupported asymmetric algorithm -.RE -.sp -\fB17\fP -.RS 4 -Certificate is not encryption capable -.RE -.sp -\fB19\fP -.RS 4 -Usage error: Missing argument -.RE -.sp -\fB23\fP -.RS 4 -Incomplete verification instructions -.RE -.sp -\fB29\fP -.RS 4 -Unable to decrypt -.RE -.sp -\fB31\fP -.RS 4 -Password is not human\-readable -.RE -.sp -\fB37\fP -.RS 4 -Unsupported Option -.RE -.sp -\fB41\fP -.RS 4 -Invalid data or data of wrong type encountered -.RE -.sp -\fB53\fP -.RS 4 -Non\-text input received where text was expected -.RE -.sp -\fB59\fP -.RS 4 -Output file already exists -.RE -.sp -\fB61\fP -.RS 4 -Input file does not exist -.RE -.sp -\fB67\fP -.RS 4 -Cannot unlock password protected secret key -.RE -.sp -\fB69\fP -.RS 4 -Unsupported subcommand -.RE -.sp -\fB71\fP -.RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter -.RE -.sp -\fB73\fP -.RS 4 -Ambiguous input (a filename matching the designator already exists) -.RE -.sp -\fB79\fP -.RS 4 -Key is not signing capable +Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 b/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 index 0dacf89c..98fae5ea 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli-dearmor .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-DEARMOR" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-DEARMOR" "1" "2022-11-06" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -31,102 +31,12 @@ pgpainless\-cli\-dearmor \- Remove ASCII Armor from standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli dearmor\fP +\fBpgpainless\-cli dearmor\fP [\fB\-\-stacktrace\fP] .SH "DESCRIPTION" -.SH "EXIT CODES:" +.SH "OPTIONS" .sp -\fB0\fP +\fB\-\-stacktrace\fP .RS 4 -Successful program execution -.RE -.sp -\fB1\fP -.RS 4 -Generic program error -.RE -.sp -\fB3\fP -.RS 4 -Verification requested but no verifiable signature found -.RE -.sp -\fB13\fP -.RS 4 -Unsupported asymmetric algorithm -.RE -.sp -\fB17\fP -.RS 4 -Certificate is not encryption capable -.RE -.sp -\fB19\fP -.RS 4 -Usage error: Missing argument -.RE -.sp -\fB23\fP -.RS 4 -Incomplete verification instructions -.RE -.sp -\fB29\fP -.RS 4 -Unable to decrypt -.RE -.sp -\fB31\fP -.RS 4 -Password is not human\-readable -.RE -.sp -\fB37\fP -.RS 4 -Unsupported Option -.RE -.sp -\fB41\fP -.RS 4 -Invalid data or data of wrong type encountered -.RE -.sp -\fB53\fP -.RS 4 -Non\-text input received where text was expected -.RE -.sp -\fB59\fP -.RS 4 -Output file already exists -.RE -.sp -\fB61\fP -.RS 4 -Input file does not exist -.RE -.sp -\fB67\fP -.RS 4 -Cannot unlock password protected secret key -.RE -.sp -\fB69\fP -.RS 4 -Unsupported subcommand -.RE -.sp -\fB71\fP -.RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter -.RE -.sp -\fB73\fP -.RS 4 -Ambiguous input (a filename matching the designator already exists) -.RE -.sp -\fB79\fP -.RS 4 -Key is not signing capable +Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 index c97eb7e9..400adcff 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli-decrypt .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-DECRYPT" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-DECRYPT" "1" "2022-11-06" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -31,7 +31,7 @@ pgpainless\-cli\-decrypt \- Decrypt a message from standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli decrypt\fP [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] +\fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP] [\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP] [\fB\-\-verify\-with\fP=\fICERT\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP]... @@ -65,6 +65,11 @@ Defaults to beginning of time (\(aq\-\(aq). Can be used to learn the session key on successful decryption .RE .sp +\fB\-\-stacktrace\fP +.RS 4 +Print Stacktrace +.RE +.sp \fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP .RS 4 Emits signature verification status to the designated output @@ -87,6 +92,8 @@ Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). Symmetric passphrase to decrypt the message with. .sp Enables decryption based on any "SKESK" packets in the "CIPHERTEXT". +.sp +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) .RE .sp \fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP @@ -102,100 +109,4 @@ Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) [\fIKEY\fP...] .RS 4 Secret keys to attempt decryption with -.RE -.SH "EXIT CODES:" -.sp -\fB0\fP -.RS 4 -Successful program execution -.RE -.sp -\fB1\fP -.RS 4 -Generic program error -.RE -.sp -\fB3\fP -.RS 4 -Verification requested but no verifiable signature found -.RE -.sp -\fB13\fP -.RS 4 -Unsupported asymmetric algorithm -.RE -.sp -\fB17\fP -.RS 4 -Certificate is not encryption capable -.RE -.sp -\fB19\fP -.RS 4 -Usage error: Missing argument -.RE -.sp -\fB23\fP -.RS 4 -Incomplete verification instructions -.RE -.sp -\fB29\fP -.RS 4 -Unable to decrypt -.RE -.sp -\fB31\fP -.RS 4 -Password is not human\-readable -.RE -.sp -\fB37\fP -.RS 4 -Unsupported Option -.RE -.sp -\fB41\fP -.RS 4 -Invalid data or data of wrong type encountered -.RE -.sp -\fB53\fP -.RS 4 -Non\-text input received where text was expected -.RE -.sp -\fB59\fP -.RS 4 -Output file already exists -.RE -.sp -\fB61\fP -.RS 4 -Input file does not exist -.RE -.sp -\fB67\fP -.RS 4 -Cannot unlock password protected secret key -.RE -.sp -\fB69\fP -.RS 4 -Unsupported subcommand -.RE -.sp -\fB71\fP -.RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter -.RE -.sp -\fB73\fP -.RS 4 -Ambiguous input (a filename matching the designator already exists) -.RE -.sp -\fB79\fP -.RS 4 -Key is not signing capable .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 index 78c8e434..5cb9495b 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli-encrypt .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-ENCRYPT" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-ENCRYPT" "1" "2022-11-06" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -31,9 +31,9 @@ pgpainless\-cli\-encrypt \- Encrypt a message from standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli encrypt\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-as\fP=\fI{binary|text}\fP] [\fB\-\-sign\-with\fP=\fIKEY\fP]... -[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]... -[\fICERTS\fP...] +\fBpgpainless\-cli encrypt\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP] +[\fB\-\-sign\-with\fP=\fIKEY\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... +[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fICERTS\fP...] .SH "DESCRIPTION" .SH "OPTIONS" @@ -53,6 +53,11 @@ ASCII armor the output Sign the output with a private key .RE .sp +\fB\-\-stacktrace\fP +.RS 4 +Print Stacktrace +.RE +.sp \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP .RS 4 Passphrase to unlock the secret key(s). @@ -71,100 +76,4 @@ Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) [\fICERTS\fP...] .RS 4 Certificates the message gets encrypted to -.RE -.SH "EXIT CODES:" -.sp -\fB0\fP -.RS 4 -Successful program execution -.RE -.sp -\fB1\fP -.RS 4 -Generic program error -.RE -.sp -\fB3\fP -.RS 4 -Verification requested but no verifiable signature found -.RE -.sp -\fB13\fP -.RS 4 -Unsupported asymmetric algorithm -.RE -.sp -\fB17\fP -.RS 4 -Certificate is not encryption capable -.RE -.sp -\fB19\fP -.RS 4 -Usage error: Missing argument -.RE -.sp -\fB23\fP -.RS 4 -Incomplete verification instructions -.RE -.sp -\fB29\fP -.RS 4 -Unable to decrypt -.RE -.sp -\fB31\fP -.RS 4 -Password is not human\-readable -.RE -.sp -\fB37\fP -.RS 4 -Unsupported Option -.RE -.sp -\fB41\fP -.RS 4 -Invalid data or data of wrong type encountered -.RE -.sp -\fB53\fP -.RS 4 -Non\-text input received where text was expected -.RE -.sp -\fB59\fP -.RS 4 -Output file already exists -.RE -.sp -\fB61\fP -.RS 4 -Input file does not exist -.RE -.sp -\fB67\fP -.RS 4 -Cannot unlock password protected secret key -.RE -.sp -\fB69\fP -.RS 4 -Unsupported subcommand -.RE -.sp -\fB71\fP -.RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter -.RE -.sp -\fB73\fP -.RS 4 -Ambiguous input (a filename matching the designator already exists) -.RE -.sp -\fB79\fP -.RS 4 -Key is not signing capable .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 b/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 index 7beaad14..0482c183 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli-extract-cert .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-EXTRACT\-CERT" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-EXTRACT\-CERT" "1" "2022-11-06" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -31,7 +31,7 @@ pgpainless\-cli\-extract\-cert \- Extract a public key certificate from a secret key from standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli extract\-cert\fP [\fB\-\-[no\-]armor\fP] +\fBpgpainless\-cli extract\-cert\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] .SH "DESCRIPTION" .SH "OPTIONS" @@ -40,99 +40,8 @@ pgpainless\-cli\-extract\-cert \- Extract a public key certificate from a secret .RS 4 ASCII armor the output .RE -.SH "EXIT CODES:" .sp -\fB0\fP +\fB\-\-stacktrace\fP .RS 4 -Successful program execution -.RE -.sp -\fB1\fP -.RS 4 -Generic program error -.RE -.sp -\fB3\fP -.RS 4 -Verification requested but no verifiable signature found -.RE -.sp -\fB13\fP -.RS 4 -Unsupported asymmetric algorithm -.RE -.sp -\fB17\fP -.RS 4 -Certificate is not encryption capable -.RE -.sp -\fB19\fP -.RS 4 -Usage error: Missing argument -.RE -.sp -\fB23\fP -.RS 4 -Incomplete verification instructions -.RE -.sp -\fB29\fP -.RS 4 -Unable to decrypt -.RE -.sp -\fB31\fP -.RS 4 -Password is not human\-readable -.RE -.sp -\fB37\fP -.RS 4 -Unsupported Option -.RE -.sp -\fB41\fP -.RS 4 -Invalid data or data of wrong type encountered -.RE -.sp -\fB53\fP -.RS 4 -Non\-text input received where text was expected -.RE -.sp -\fB59\fP -.RS 4 -Output file already exists -.RE -.sp -\fB61\fP -.RS 4 -Input file does not exist -.RE -.sp -\fB67\fP -.RS 4 -Cannot unlock password protected secret key -.RE -.sp -\fB69\fP -.RS 4 -Unsupported subcommand -.RE -.sp -\fB71\fP -.RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter -.RE -.sp -\fB73\fP -.RS 4 -Ambiguous input (a filename matching the designator already exists) -.RE -.sp -\fB79\fP -.RS 4 -Key is not signing capable +Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 index dfc5448a..637b1231 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli-generate-completion .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: generate-completion 4.6.3 .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-GENERATE\-COMPLETION" "1" "2022-08-07" "generate\-completion 4.6.3" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-GENERATE\-COMPLETION" "1" "2022-11-06" "generate\-completion 4.6.3" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -28,10 +28,10 @@ . LINKSTYLE blue R < > .\} .SH "NAME" -pgpainless\-cli\-generate\-completion \- Generate bash/zsh completion script for pgpainless\-cli. +pgpainless\-cli\-generate\-completion \- Stateless OpenPGP Protocol .SH "SYNOPSIS" .sp -\fBpgpainless\-cli generate\-completion\fP [\fB\-hV\fP] +\fBpgpainless\-cli generate\-completion\fP [\fB\-hV\fP] [\fB\-\-stacktrace\fP] .SH "DESCRIPTION" .sp Generate bash/zsh completion script for pgpainless\-cli. @@ -49,6 +49,11 @@ source <(pgpainless\-cli generate\-completion) Show this help message and exit. .RE .sp +\fB\-\-stacktrace\fP +.RS 4 +Print Stacktrace +.RE +.sp \fB\-V\fP, \fB\-\-version\fP .RS 4 Print version information and exit. diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 index 66d6d1b9..ef950df9 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli-generate-key .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-GENERATE\-KEY" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-GENERATE\-KEY" "1" "2022-11-06" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -31,7 +31,8 @@ pgpainless\-cli\-generate\-key \- Generate a secret key .SH "SYNOPSIS" .sp -\fBpgpainless\-cli generate\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP] [\fI\fP...] +\fBpgpainless\-cli generate\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP] +[\fIUSERID\fP...] .SH "DESCRIPTION" .SH "OPTIONS" @@ -41,6 +42,11 @@ pgpainless\-cli\-generate\-key \- Generate a secret key ASCII armor the output .RE .sp +\fB\-\-stacktrace\fP +.RS 4 +Print Stacktrace +.RE +.sp \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP .RS 4 Password to protect the private key with @@ -49,104 +55,8 @@ Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). .RE .SH "ARGUMENTS" .sp -[\fI\fP...] +[\fIUSERID\fP...] .RS 4 User\-ID, e.g. "Alice <\c .MTO "alice\(atexample.com" "" ">"" -.RE -.SH "EXIT CODES:" -.sp -\fB0\fP -.RS 4 -Successful program execution -.RE -.sp -\fB1\fP -.RS 4 -Generic program error -.RE -.sp -\fB3\fP -.RS 4 -Verification requested but no verifiable signature found -.RE -.sp -\fB13\fP -.RS 4 -Unsupported asymmetric algorithm -.RE -.sp -\fB17\fP -.RS 4 -Certificate is not encryption capable -.RE -.sp -\fB19\fP -.RS 4 -Usage error: Missing argument -.RE -.sp -\fB23\fP -.RS 4 -Incomplete verification instructions -.RE -.sp -\fB29\fP -.RS 4 -Unable to decrypt -.RE -.sp -\fB31\fP -.RS 4 -Password is not human\-readable -.RE -.sp -\fB37\fP -.RS 4 -Unsupported Option -.RE -.sp -\fB41\fP -.RS 4 -Invalid data or data of wrong type encountered -.RE -.sp -\fB53\fP -.RS 4 -Non\-text input received where text was expected -.RE -.sp -\fB59\fP -.RS 4 -Output file already exists -.RE -.sp -\fB61\fP -.RS 4 -Input file does not exist -.RE -.sp -\fB67\fP -.RS 4 -Cannot unlock password protected secret key -.RE -.sp -\fB69\fP -.RS 4 -Unsupported subcommand -.RE -.sp -\fB71\fP -.RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter -.RE -.sp -\fB73\fP -.RS 4 -Ambiguous input (a filename matching the designator already exists) -.RE -.sp -\fB79\fP -.RS 4 -Key is not signing capable .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-help.1 b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 index 8688615a..67ef1ce6 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-help.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli-help .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-HELP" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-HELP" "1" "2022-11-06" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -28,10 +28,10 @@ . LINKSTYLE blue R < > .\} .SH "NAME" -pgpainless\-cli\-help \- Display usage information for the specified subcommand +pgpainless\-cli\-help \- Stateless OpenPGP Protocol .SH "SYNOPSIS" .sp -\fBpgpainless\-cli help\fP [\fB\-h\fP] [\fICOMMAND\fP] +\fBpgpainless\-cli help\fP [\fB\-h\fP] [\fB\-\-stacktrace\fP] [\fICOMMAND\fP] .SH "DESCRIPTION" .sp When no COMMAND is given, the usage help for the main command is displayed. @@ -42,6 +42,11 @@ If a COMMAND is specified, the help for that command is shown. .RS 4 Show usage help for the help command and exit. .RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print Stacktrace +.RE .SH "ARGUMENTS" .sp [\fICOMMAND\fP] diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 index ccb4901a..3d356293 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli-inline-detach .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-INLINE\-DETACH" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-INLINE\-DETACH" "1" "2022-11-06" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -31,7 +31,7 @@ pgpainless\-cli\-inline\-detach \- Split signatures from a clearsigned message .SH "SYNOPSIS" .sp -\fBpgpainless\-cli inline\-detach\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-signatures\-out\fP=\fISIGNATURES\fP] +\fBpgpainless\-cli inline\-detach\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-signatures\-out\fP=\fISIGNATURES\fP] .SH "DESCRIPTION" .SH "OPTIONS" @@ -45,99 +45,8 @@ ASCII armor the output .RS 4 Destination to which a detached signatures block will be written .RE -.SH "EXIT CODES:" .sp -\fB0\fP +\fB\-\-stacktrace\fP .RS 4 -Successful program execution -.RE -.sp -\fB1\fP -.RS 4 -Generic program error -.RE -.sp -\fB3\fP -.RS 4 -Verification requested but no verifiable signature found -.RE -.sp -\fB13\fP -.RS 4 -Unsupported asymmetric algorithm -.RE -.sp -\fB17\fP -.RS 4 -Certificate is not encryption capable -.RE -.sp -\fB19\fP -.RS 4 -Usage error: Missing argument -.RE -.sp -\fB23\fP -.RS 4 -Incomplete verification instructions -.RE -.sp -\fB29\fP -.RS 4 -Unable to decrypt -.RE -.sp -\fB31\fP -.RS 4 -Password is not human\-readable -.RE -.sp -\fB37\fP -.RS 4 -Unsupported Option -.RE -.sp -\fB41\fP -.RS 4 -Invalid data or data of wrong type encountered -.RE -.sp -\fB53\fP -.RS 4 -Non\-text input received where text was expected -.RE -.sp -\fB59\fP -.RS 4 -Output file already exists -.RE -.sp -\fB61\fP -.RS 4 -Input file does not exist -.RE -.sp -\fB67\fP -.RS 4 -Cannot unlock password protected secret key -.RE -.sp -\fB69\fP -.RS 4 -Unsupported subcommand -.RE -.sp -\fB71\fP -.RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter -.RE -.sp -\fB73\fP -.RS 4 -Ambiguous input (a filename matching the designator already exists) -.RE -.sp -\fB79\fP -.RS 4 -Key is not signing capable +Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 index 90d8908f..3f653b40 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli-inline-sign .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-INLINE\-SIGN" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-INLINE\-SIGN" "1" "2022-11-06" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -31,7 +31,8 @@ pgpainless\-cli\-inline\-sign \- Create an inline\-signed message from data on standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli inline\-sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-as\fP=\fI{binary|text|cleartextsigned}\fP] +\fBpgpainless\-cli inline\-sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP= +\fI{binary|text|cleartextsigned}\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fIKEYS\fP...] .SH "DESCRIPTION" @@ -55,6 +56,11 @@ If \(aq\-\-as=text\(aq and the input data is not valid UTF\-8, inline\-sign fail ASCII armor the output .RE .sp +\fB\-\-stacktrace\fP +.RS 4 +Print Stacktrace +.RE +.sp \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP .RS 4 Passphrase to unlock the secret key(s). @@ -66,100 +72,4 @@ Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). [\fIKEYS\fP...] .RS 4 Secret keys used for signing -.RE -.SH "EXIT CODES:" -.sp -\fB0\fP -.RS 4 -Successful program execution -.RE -.sp -\fB1\fP -.RS 4 -Generic program error -.RE -.sp -\fB3\fP -.RS 4 -Verification requested but no verifiable signature found -.RE -.sp -\fB13\fP -.RS 4 -Unsupported asymmetric algorithm -.RE -.sp -\fB17\fP -.RS 4 -Certificate is not encryption capable -.RE -.sp -\fB19\fP -.RS 4 -Usage error: Missing argument -.RE -.sp -\fB23\fP -.RS 4 -Incomplete verification instructions -.RE -.sp -\fB29\fP -.RS 4 -Unable to decrypt -.RE -.sp -\fB31\fP -.RS 4 -Password is not human\-readable -.RE -.sp -\fB37\fP -.RS 4 -Unsupported Option -.RE -.sp -\fB41\fP -.RS 4 -Invalid data or data of wrong type encountered -.RE -.sp -\fB53\fP -.RS 4 -Non\-text input received where text was expected -.RE -.sp -\fB59\fP -.RS 4 -Output file already exists -.RE -.sp -\fB61\fP -.RS 4 -Input file does not exist -.RE -.sp -\fB67\fP -.RS 4 -Cannot unlock password protected secret key -.RE -.sp -\fB69\fP -.RS 4 -Unsupported subcommand -.RE -.sp -\fB71\fP -.RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter -.RE -.sp -\fB73\fP -.RS 4 -Ambiguous input (a filename matching the designator already exists) -.RE -.sp -\fB79\fP -.RS 4 -Key is not signing capable .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 index 2f989285..73f8316d 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli-inline-verify .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-INLINE\-VERIFY" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-INLINE\-VERIFY" "1" "2022-11-06" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -31,8 +31,8 @@ pgpainless\-cli\-inline\-verify \- Verify inline\-signed data from standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli inline\-verify\fP [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] -[\fB\-\-verifications\-out\fP=\fI\fP] \fICERT\fP... +\fBpgpainless\-cli inline\-verify\fP [\fB\-\-stacktrace\fP] [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] +[\fB\-\-verifications\-out\fP=\fI\fP] [\fICERT\fP...] .SH "DESCRIPTION" .SH "OPTIONS" @@ -57,109 +57,18 @@ Reject signatures with a creation date not in range. Defaults to beginning of time ("\-"). .RE .sp +\fB\-\-stacktrace\fP +.RS 4 +Print Stacktrace +.RE +.sp \fB\-\-verifications\-out\fP=\fI\fP .RS 4 File to write details over successful verifications to .RE .SH "ARGUMENTS" .sp -\fICERT\fP... +[\fICERT\fP...] .RS 4 Public key certificates for signature verification -.RE -.SH "EXIT CODES:" -.sp -\fB0\fP -.RS 4 -Successful program execution -.RE -.sp -\fB1\fP -.RS 4 -Generic program error -.RE -.sp -\fB3\fP -.RS 4 -Verification requested but no verifiable signature found -.RE -.sp -\fB13\fP -.RS 4 -Unsupported asymmetric algorithm -.RE -.sp -\fB17\fP -.RS 4 -Certificate is not encryption capable -.RE -.sp -\fB19\fP -.RS 4 -Usage error: Missing argument -.RE -.sp -\fB23\fP -.RS 4 -Incomplete verification instructions -.RE -.sp -\fB29\fP -.RS 4 -Unable to decrypt -.RE -.sp -\fB31\fP -.RS 4 -Password is not human\-readable -.RE -.sp -\fB37\fP -.RS 4 -Unsupported Option -.RE -.sp -\fB41\fP -.RS 4 -Invalid data or data of wrong type encountered -.RE -.sp -\fB53\fP -.RS 4 -Non\-text input received where text was expected -.RE -.sp -\fB59\fP -.RS 4 -Output file already exists -.RE -.sp -\fB61\fP -.RS 4 -Input file does not exist -.RE -.sp -\fB67\fP -.RS 4 -Cannot unlock password protected secret key -.RE -.sp -\fB69\fP -.RS 4 -Unsupported subcommand -.RE -.sp -\fB71\fP -.RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter -.RE -.sp -\fB73\fP -.RS 4 -Ambiguous input (a filename matching the designator already exists) -.RE -.sp -\fB79\fP -.RS 4 -Key is not signing capable .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 index cc830e7f..3c63dccf 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli-sign .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-SIGN" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-SIGN" "1" "2022-11-06" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -31,8 +31,8 @@ pgpainless\-cli\-sign \- Create a detached signature on the data from standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-as\fP=\fI{binary|text}\fP] [\fB\-\-micalg\-out\fP=\fIMICALG\fP] -[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fIKEYS\fP...] +\fBpgpainless\-cli sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP] +[\fB\-\-micalg\-out\fP=\fIMICALG\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fIKEYS\fP...] .SH "DESCRIPTION" .SH "OPTIONS" @@ -42,6 +42,8 @@ pgpainless\-cli\-sign \- Create a detached signature on the data from standard i Specify the output format of the signed message .sp Defaults to \(aqbinary\(aq. +.sp +If \(aq\-\-as=text\(aq and the input data is not valid UTF\-8, sign fails with return code 53. .RE .sp \fB\-\-micalg\-out\fP=\fIMICALG\fP @@ -54,6 +56,11 @@ Emits the digest algorithm used to the specified file in a way that can be used ASCII armor the output .RE .sp +\fB\-\-stacktrace\fP +.RS 4 +Print Stacktrace +.RE +.sp \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP .RS 4 Passphrase to unlock the secret key(s). @@ -65,100 +72,4 @@ Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). [\fIKEYS\fP...] .RS 4 Secret keys used for signing -.RE -.SH "EXIT CODES:" -.sp -\fB0\fP -.RS 4 -Successful program execution -.RE -.sp -\fB1\fP -.RS 4 -Generic program error -.RE -.sp -\fB3\fP -.RS 4 -Verification requested but no verifiable signature found -.RE -.sp -\fB13\fP -.RS 4 -Unsupported asymmetric algorithm -.RE -.sp -\fB17\fP -.RS 4 -Certificate is not encryption capable -.RE -.sp -\fB19\fP -.RS 4 -Usage error: Missing argument -.RE -.sp -\fB23\fP -.RS 4 -Incomplete verification instructions -.RE -.sp -\fB29\fP -.RS 4 -Unable to decrypt -.RE -.sp -\fB31\fP -.RS 4 -Password is not human\-readable -.RE -.sp -\fB37\fP -.RS 4 -Unsupported Option -.RE -.sp -\fB41\fP -.RS 4 -Invalid data or data of wrong type encountered -.RE -.sp -\fB53\fP -.RS 4 -Non\-text input received where text was expected -.RE -.sp -\fB59\fP -.RS 4 -Output file already exists -.RE -.sp -\fB61\fP -.RS 4 -Input file does not exist -.RE -.sp -\fB67\fP -.RS 4 -Cannot unlock password protected secret key -.RE -.sp -\fB69\fP -.RS 4 -Unsupported subcommand -.RE -.sp -\fB71\fP -.RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter -.RE -.sp -\fB73\fP -.RS 4 -Ambiguous input (a filename matching the designator already exists) -.RE -.sp -\fB79\fP -.RS 4 -Key is not signing capable .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 index 297f8374..a07ef2d7 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli-verify .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-VERIFY" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-VERIFY" "1" "2022-11-06" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -31,7 +31,8 @@ pgpainless\-cli\-verify \- Verify a detached signature over the data from standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli verify\fP [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] \fISIGNATURE\fP \fICERT\fP... +\fBpgpainless\-cli verify\fP [\fB\-\-stacktrace\fP] [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] \fISIGNATURE\fP +\fICERT\fP... .SH "DESCRIPTION" .SH "OPTIONS" @@ -44,6 +45,11 @@ Reject signatures with a creation date not in range. .sp Defaults to beginning of time ("\-"). .RE +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print Stacktrace +.RE .SH "ARGUMENTS" .sp \fISIGNATURE\fP @@ -53,101 +59,4 @@ Detached signature .sp \fICERT\fP... .RS 4 -Public key certificates for signature verification -.RE -.SH "EXIT CODES:" -.sp -\fB0\fP -.RS 4 -Successful program execution -.RE -.sp -\fB1\fP -.RS 4 -Generic program error -.RE -.sp -\fB3\fP -.RS 4 -Verification requested but no verifiable signature found -.RE -.sp -\fB13\fP -.RS 4 -Unsupported asymmetric algorithm -.RE -.sp -\fB17\fP -.RS 4 -Certificate is not encryption capable -.RE -.sp -\fB19\fP -.RS 4 -Usage error: Missing argument -.RE -.sp -\fB23\fP -.RS 4 -Incomplete verification instructions -.RE -.sp -\fB29\fP -.RS 4 -Unable to decrypt -.RE -.sp -\fB31\fP -.RS 4 -Password is not human\-readable -.RE -.sp -\fB37\fP -.RS 4 -Unsupported Option -.RE -.sp -\fB41\fP -.RS 4 -Invalid data or data of wrong type encountered -.RE -.sp -\fB53\fP -.RS 4 -Non\-text input received where text was expected -.RE -.sp -\fB59\fP -.RS 4 -Output file already exists -.RE -.sp -\fB61\fP -.RS 4 -Input file does not exist -.RE -.sp -\fB67\fP -.RS 4 -Cannot unlock password protected secret key -.RE -.sp -\fB69\fP -.RS 4 -Unsupported subcommand -.RE -.sp -\fB71\fP -.RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter -.RE -.sp -\fB73\fP -.RS 4 -Ambiguous input (a filename matching the designator already exists) -.RE -.sp -\fB79\fP -.RS 4 -Key is not signing capable .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 index 0a235d7a..9e80f4b3 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli-version .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-VERSION" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-VERSION" "1" "2022-11-06" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -31,7 +31,7 @@ pgpainless\-cli\-version \- Display version information about the tool .SH "SYNOPSIS" .sp -\fBpgpainless\-cli version\fP [\fB\-\-extended\fP | \fB\-\-backend\fP] +\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP] .SH "DESCRIPTION" .SH "OPTIONS" @@ -45,99 +45,8 @@ Print information about the cryptographic backend .RS 4 Print an extended version string .RE -.SH "EXIT CODES:" .sp -\fB0\fP +\fB\-\-stacktrace\fP .RS 4 -Successful program execution -.RE -.sp -\fB1\fP -.RS 4 -Generic program error -.RE -.sp -\fB3\fP -.RS 4 -Verification requested but no verifiable signature found -.RE -.sp -\fB13\fP -.RS 4 -Unsupported asymmetric algorithm -.RE -.sp -\fB17\fP -.RS 4 -Certificate is not encryption capable -.RE -.sp -\fB19\fP -.RS 4 -Usage error: Missing argument -.RE -.sp -\fB23\fP -.RS 4 -Incomplete verification instructions -.RE -.sp -\fB29\fP -.RS 4 -Unable to decrypt -.RE -.sp -\fB31\fP -.RS 4 -Password is not human\-readable -.RE -.sp -\fB37\fP -.RS 4 -Unsupported Option -.RE -.sp -\fB41\fP -.RS 4 -Invalid data or data of wrong type encountered -.RE -.sp -\fB53\fP -.RS 4 -Non\-text input received where text was expected -.RE -.sp -\fB59\fP -.RS 4 -Output file already exists -.RE -.sp -\fB61\fP -.RS 4 -Input file does not exist -.RE -.sp -\fB67\fP -.RS 4 -Cannot unlock password protected secret key -.RE -.sp -\fB69\fP -.RS 4 -Unsupported subcommand -.RE -.sp -\fB71\fP -.RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter -.RE -.sp -\fB73\fP -.RS 4 -Ambiguous input (a filename matching the designator already exists) -.RE -.sp -\fB79\fP -.RS 4 -Key is not signing capable +Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli.1 b/pgpainless-cli/packaging/man/pgpainless-cli.1 index 171c394e..14bd9fb9 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli.1 @@ -2,12 +2,12 @@ .\" Title: pgpainless-cli .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-08-07 +.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI" "1" "2022-08-07" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI" "1" "2022-11-06" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -31,14 +31,20 @@ pgpainless\-cli \- Stateless OpenPGP Protocol .SH "SYNOPSIS" .sp -\fBpgpainless\-cli\fP [COMMAND] +\fBpgpainless\-cli\fP [\fB\-\-stacktrace\fP] [COMMAND] .SH "DESCRIPTION" +.SH "OPTIONS" +.sp +\fB\-\-stacktrace\fP +.RS 4 +Print Stacktrace +.RE .SH "COMMANDS" .sp \fBhelp\fP .RS 4 -Display usage information for the specified subcommand +Stateless OpenPGP Protocol .RE .sp \fBarmor\fP @@ -103,7 +109,7 @@ Display version information about the tool .sp \fBgenerate\-completion\fP .RS 4 -Generate bash/zsh completion script for pgpainless\-cli. +Stateless OpenPGP Protocol .RE .SH "EXIT CODES:" .sp From 78de682a14843b27806a1c0b0f6e01900bcbad8b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Nov 2022 15:16:28 +0100 Subject: [PATCH 0426/1204] PGPainless 1.3.9 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ba62a0..b5ea7c57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.3.9-SNAPSHOT +## 1.3.9 - Bump `sop-java` to `4.0.2` - SOP: Improve exception handling diff --git a/README.md b/README.md index 305e5cea..6c1f8368 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.8' + implementation 'org.pgpainless:pgpainless-core:1.3.9' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index ebb1736c..2dd87b60 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.8" + implementation "org.pgpainless:pgpainless-sop:1.3.9" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.8 + 1.3.9 ... diff --git a/version.gradle b/version.gradle index 36822069..5613a109 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.9' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From b4420772a0371c94e479efe0899e581c97042097 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Nov 2022 15:18:26 +0100 Subject: [PATCH 0427/1204] PGPainless 1.3.10-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 5613a109..ec61ab42 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.9' - isSnapshot = false + shortVersion = '1.3.10' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From b02ae86ff62cfbc5b172ff732a6540a8dfa54e40 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Nov 2022 22:59:38 +0100 Subject: [PATCH 0428/1204] Annotate SignatureSubpacketsUtil methods with @Nullable and @Nonnull --- .../subpackets/SignatureSubpacketsUtil.java | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index 9ebc03e8..cbcbeafc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -68,7 +68,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return issuer fingerprint or null */ - public static IssuerFingerprint getIssuerFingerprint(PGPSignature signature) { + public static @Nullable IssuerFingerprint getIssuerFingerprint(PGPSignature signature) { return hashedOrUnhashed(signature, SignatureSubpacket.issuerFingerprint); } @@ -79,7 +79,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return v4 fingerprint of the issuer, or null */ - public static OpenPgpFingerprint getIssuerFingerprintAsOpenPgpFingerprint(PGPSignature signature) { + public static @Nullable OpenPgpFingerprint getIssuerFingerprintAsOpenPgpFingerprint(PGPSignature signature) { IssuerFingerprint subpacket = getIssuerFingerprint(signature); if (subpacket == null) { return null; @@ -101,7 +101,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return issuer key-id or null */ - public static IssuerKeyID getIssuerKeyId(PGPSignature signature) { + public static @Nullable IssuerKeyID getIssuerKeyId(PGPSignature signature) { return hashedOrUnhashed(signature, SignatureSubpacket.issuerKeyId); } @@ -112,7 +112,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return issuer key-id as {@link Long} */ - public static Long getIssuerKeyIdAsLong(PGPSignature signature) { + public static @Nullable Long getIssuerKeyIdAsLong(PGPSignature signature) { IssuerKeyID keyID = getIssuerKeyId(signature); if (keyID == null) { return null; @@ -128,7 +128,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return revocation reason */ - public static RevocationReason getRevocationReason(PGPSignature signature) { + public static @Nullable RevocationReason getRevocationReason(PGPSignature signature) { return hashed(signature, SignatureSubpacket.revocationReason); } @@ -140,7 +140,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return signature creation time subpacket */ - public static SignatureCreationTime getSignatureCreationTime(PGPSignature signature) { + public static @Nullable SignatureCreationTime getSignatureCreationTime(PGPSignature signature) { return hashed(signature, SignatureSubpacket.signatureCreationTime); } @@ -151,7 +151,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return signature expiration time */ - public static SignatureExpirationTime getSignatureExpirationTime(PGPSignature signature) { + public static @Nullable SignatureExpirationTime getSignatureExpirationTime(PGPSignature signature) { return hashed(signature, SignatureSubpacket.signatureExpirationTime); } @@ -163,7 +163,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return expiration time as date */ - public static Date getSignatureExpirationTimeAsDate(PGPSignature signature) { + public static @Nullable Date getSignatureExpirationTimeAsDate(PGPSignature signature) { SignatureExpirationTime subpacket = getSignatureExpirationTime(signature); if (subpacket == null) { return null; @@ -178,7 +178,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return key expiration time */ - public static KeyExpirationTime getKeyExpirationTime(PGPSignature signature) { + public static @Nullable KeyExpirationTime getKeyExpirationTime(PGPSignature signature) { return hashed(signature, SignatureSubpacket.keyExpirationTime); } @@ -192,7 +192,7 @@ public final class SignatureSubpacketsUtil { * @param signingKey signature creation key * @return key expiration time as date */ - public static Date getKeyExpirationTimeAsDate(PGPSignature signature, PGPPublicKey signingKey) { + public static @Nullable Date getKeyExpirationTimeAsDate(PGPSignature signature, PGPPublicKey signingKey) { if (signature.getKeyID() != signingKey.getKeyID()) { throw new IllegalArgumentException("Provided key (" + Long.toHexString(signingKey.getKeyID()) + ") did not create the signature (" + Long.toHexString(signature.getKeyID()) + ")"); } @@ -230,7 +230,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return revocable subpacket */ - public static Revocable getRevocable(PGPSignature signature) { + public static @Nullable Revocable getRevocable(PGPSignature signature) { return hashed(signature, SignatureSubpacket.revocable); } @@ -240,7 +240,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return symm. algo. prefs */ - public static PreferredAlgorithms getPreferredSymmetricAlgorithms(PGPSignature signature) { + public static @Nullable PreferredAlgorithms getPreferredSymmetricAlgorithms(PGPSignature signature) { return hashed(signature, SignatureSubpacket.preferredSymmetricAlgorithms); } @@ -252,7 +252,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return ordered set of symmetric key algorithm preferences */ - public static Set parsePreferredSymmetricKeyAlgorithms(PGPSignature signature) { + public static @Nonnull Set parsePreferredSymmetricKeyAlgorithms(PGPSignature signature) { Set algorithms = new LinkedHashSet<>(); PreferredAlgorithms preferences = getPreferredSymmetricAlgorithms(signature); if (preferences != null) { @@ -272,7 +272,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return hash algo prefs */ - public static PreferredAlgorithms getPreferredHashAlgorithms(PGPSignature signature) { + public static @Nullable PreferredAlgorithms getPreferredHashAlgorithms(PGPSignature signature) { return hashed(signature, SignatureSubpacket.preferredHashAlgorithms); } @@ -284,7 +284,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return ordered set of hash algorithm preferences */ - public static Set parsePreferredHashAlgorithms(PGPSignature signature) { + public static @Nonnull Set parsePreferredHashAlgorithms(PGPSignature signature) { Set algorithms = new LinkedHashSet<>(); PreferredAlgorithms preferences = getPreferredHashAlgorithms(signature); if (preferences != null) { @@ -304,7 +304,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return compression algo prefs */ - public static PreferredAlgorithms getPreferredCompressionAlgorithms(PGPSignature signature) { + public static @Nullable PreferredAlgorithms getPreferredCompressionAlgorithms(PGPSignature signature) { return hashed(signature, SignatureSubpacket.preferredCompressionAlgorithms); } @@ -316,7 +316,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return ordered set of compression algorithm preferences */ - public static Set parsePreferredCompressionAlgorithms(PGPSignature signature) { + public static @Nonnull Set parsePreferredCompressionAlgorithms(PGPSignature signature) { Set algorithms = new LinkedHashSet<>(); PreferredAlgorithms preferences = getPreferredCompressionAlgorithms(signature); if (preferences != null) { @@ -336,7 +336,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return primary user id */ - public static PrimaryUserID getPrimaryUserId(PGPSignature signature) { + public static @Nullable PrimaryUserID getPrimaryUserId(PGPSignature signature) { return hashed(signature, SignatureSubpacket.primaryUserId); } @@ -346,7 +346,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return key flags */ - public static KeyFlags getKeyFlags(PGPSignature signature) { + public static @Nullable KeyFlags getKeyFlags(PGPSignature signature) { return hashed(signature, SignatureSubpacket.keyFlags); } @@ -357,7 +357,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return list of key flags */ - public static List parseKeyFlags(@Nullable PGPSignature signature) { + public static @Nullable List parseKeyFlags(@Nullable PGPSignature signature) { if (signature == null) { return null; } @@ -374,7 +374,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return features subpacket */ - public static Features getFeatures(PGPSignature signature) { + public static @Nullable Features getFeatures(PGPSignature signature) { return hashed(signature, SignatureSubpacket.features); } @@ -401,7 +401,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return signature target */ - public static SignatureTarget getSignatureTarget(PGPSignature signature) { + public static @Nullable SignatureTarget getSignatureTarget(PGPSignature signature) { return hashedOrUnhashed(signature, SignatureSubpacket.signatureTarget); } @@ -411,7 +411,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return hashed notations */ - public static List getHashedNotationData(PGPSignature signature) { + public static @Nonnull List getHashedNotationData(PGPSignature signature) { NotationData[] notations = signature.getHashedSubPackets().getNotationDataOccurrences(); return Arrays.asList(notations); } @@ -424,7 +424,7 @@ public final class SignatureSubpacketsUtil { * @param notationName notation name * @return list of matching notation data objects */ - public static List getHashedNotationData(PGPSignature signature, String notationName) { + public static @Nonnull List getHashedNotationData(PGPSignature signature, String notationName) { List allNotations = getHashedNotationData(signature); List withName = new ArrayList<>(); for (NotationData data : allNotations) { @@ -441,7 +441,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return unhashed notations */ - public static List getUnhashedNotationData(PGPSignature signature) { + public static @Nonnull List getUnhashedNotationData(PGPSignature signature) { NotationData[] notations = signature.getUnhashedSubPackets().getNotationDataOccurrences(); return Arrays.asList(notations); } @@ -454,7 +454,7 @@ public final class SignatureSubpacketsUtil { * @param notationName notation name * @return list of matching notation data objects */ - public static List getUnhashedNotationData(PGPSignature signature, String notationName) { + public static @Nonnull List getUnhashedNotationData(PGPSignature signature, String notationName) { List allNotations = getUnhashedNotationData(signature); List withName = new ArrayList<>(); for (NotationData data : allNotations) { @@ -471,7 +471,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return revocation key */ - public static RevocationKey getRevocationKey(PGPSignature signature) { + public static @Nullable RevocationKey getRevocationKey(PGPSignature signature) { return hashed(signature, SignatureSubpacket.revocationKey); } @@ -482,7 +482,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return signers user-id */ - public static SignerUserID getSignerUserID(PGPSignature signature) { + public static @Nullable SignerUserID getSignerUserID(PGPSignature signature) { return hashed(signature, SignatureSubpacket.signerUserId); } @@ -492,7 +492,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return intended recipient fingerprint subpackets */ - public static List getIntendedRecipientFingerprints(PGPSignature signature) { + public static @Nonnull List getIntendedRecipientFingerprints(PGPSignature signature) { org.bouncycastle.bcpg.SignatureSubpacket[] subpackets = signature.getHashedSubPackets().getSubpackets(SignatureSubpacket.intendedRecipientFingerprint.getCode()); List intendedRecipients = new ArrayList<>(subpackets.length); for (org.bouncycastle.bcpg.SignatureSubpacket subpacket : subpackets) { @@ -509,7 +509,7 @@ public final class SignatureSubpacketsUtil { * * @throws PGPException in case the embedded signatures cannot be parsed */ - public static PGPSignatureList getEmbeddedSignature(PGPSignature signature) throws PGPException { + public static @Nullable PGPSignatureList getEmbeddedSignature(PGPSignature signature) throws PGPException { PGPSignatureList hashed = signature.getHashedSubPackets().getEmbeddedSignatures(); if (!hashed.isEmpty()) { return hashed; @@ -523,7 +523,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return exportable certification subpacket */ - public static Exportable getExportableCertification(PGPSignature signature) { + public static @Nullable Exportable getExportableCertification(PGPSignature signature) { return hashed(signature, SignatureSubpacket.exportableCertification); } @@ -533,7 +533,7 @@ public final class SignatureSubpacketsUtil { * @param signature signature * @return trust signature subpacket */ - public static TrustSignature getTrustSignature(PGPSignature signature) { + public static @Nullable TrustSignature getTrustSignature(PGPSignature signature) { return hashed(signature, SignatureSubpacket.trustSignature); } @@ -546,7 +546,7 @@ public final class SignatureSubpacketsUtil { * @param

generic subpacket type * @return list of subpackets from the hashed area */ - private static

P hashed(PGPSignature signature, SignatureSubpacket type) { + private static @Nullable

P hashed(PGPSignature signature, SignatureSubpacket type) { return getSignatureSubpacket(signature.getHashedSubPackets(), type); } @@ -559,7 +559,7 @@ public final class SignatureSubpacketsUtil { * @param

generic subpacket type * @return list of subpackets from the unhashed area */ - private static

P unhashed(PGPSignature signature, SignatureSubpacket type) { + private static @Nullable

P unhashed(PGPSignature signature, SignatureSubpacket type) { return getSignatureSubpacket(signature.getUnhashedSubPackets(), type); } @@ -572,7 +572,7 @@ public final class SignatureSubpacketsUtil { * @param

generic subpacket type * @return list of subpackets from the hashed/unhashed area */ - private static

P hashedOrUnhashed(PGPSignature signature, SignatureSubpacket type) { + private static @Nullable

P hashedOrUnhashed(PGPSignature signature, SignatureSubpacket type) { P hashedSubpacket = hashed(signature, type); return hashedSubpacket != null ? hashedSubpacket : unhashed(signature, type); } @@ -585,7 +585,7 @@ public final class SignatureSubpacketsUtil { * @param

generic return type of the subpacket * @return last occurrence of the subpacket in the vector */ - public static

P getSignatureSubpacket(PGPSignatureSubpacketVector vector, SignatureSubpacket type) { + public static @Nullable

P getSignatureSubpacket(PGPSignatureSubpacketVector vector, SignatureSubpacket type) { if (vector == null) { // Almost never happens, but may be caused by broken signatures. return null; From 50d18a45815d2d15b3af01e3d7aa7abbdac120bd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Nov 2022 23:00:28 +0100 Subject: [PATCH 0429/1204] Fix NPE when validating signature made by key without keyflags on direct key sigature (Presumably) fixes #332 --- .../pgpainless/signature/consumer/CertificateValidator.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java index 4c4c6689..c629de2d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java @@ -171,7 +171,9 @@ public final class CertificateValidator { if (signingSubkey == primaryKey) { if (!directKeySignatures.isEmpty()) { - if (KeyFlag.hasKeyFlag(SignatureSubpacketsUtil.getKeyFlags(directKeySignatures.get(0)).getFlags(), KeyFlag.SIGN_DATA)) { + PGPSignature directKeySignature = directKeySignatures.get(0); + KeyFlags keyFlags = SignatureSubpacketsUtil.getKeyFlags(directKeySignature); + if (keyFlags != null && KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.SIGN_DATA)) { return true; } } @@ -225,7 +227,7 @@ public final class CertificateValidator { } PGPSignature directKeySig = directKeySignatures.get(0); KeyFlags directKeyFlags = SignatureSubpacketsUtil.getKeyFlags(directKeySig); - if (!KeyFlag.hasKeyFlag(directKeyFlags.getFlags(), KeyFlag.SIGN_DATA)) { + if (directKeyFlags == null || !KeyFlag.hasKeyFlag(directKeyFlags.getFlags(), KeyFlag.SIGN_DATA)) { throw new SignatureValidationException("Signature was made by key which is not capable of signing (no keyflags on binding sig, no SIGN flag on direct-key sig)."); } } else if (!KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.SIGN_DATA)) { From 5ebeeed3e8b86a5940da583c1edfb7e268f6c4df Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Nov 2022 16:49:06 +0100 Subject: [PATCH 0430/1204] Bump sop-java to 4.0.3 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index ec61ab42..0f15467b 100644 --- a/version.gradle +++ b/version.gradle @@ -16,6 +16,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '4.0.2' + sopJavaVersion = '4.0.3' } } From 3cd621b436de3cb2cffd01fd7fb52f9a5f1cbbd2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Nov 2022 16:51:19 +0100 Subject: [PATCH 0431/1204] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5ea7c57..5b1ec6da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.10-SNAPSHOT +- Bump `sop-java` to `4.0.3` +- Fix: Fix NPE when verifying signature made by key without key flags on direct-key signature + ## 1.3.9 - Bump `sop-java` to `4.0.2` - SOP: Improve exception handling From 4143ad3165b397561f2ee410f6d724f95c531b2f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Nov 2022 16:55:00 +0100 Subject: [PATCH 0432/1204] PGPainless 1.3.10 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b1ec6da..efff7de9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.3.10-SNAPSHOT +## 1.3.10 - Bump `sop-java` to `4.0.3` - Fix: Fix NPE when verifying signature made by key without key flags on direct-key signature diff --git a/README.md b/README.md index 6c1f8368..c24728e9 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.9' + implementation 'org.pgpainless:pgpainless-core:1.3.10' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 2dd87b60..6c82fdcd 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.9" + implementation "org.pgpainless:pgpainless-sop:1.3.10" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.9 + 1.3.10 ... diff --git a/version.gradle b/version.gradle index 0f15467b..8a7dd76c 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.10' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From faae5c64b1685ae4a17729f22b11cad18dcf915f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 7 Nov 2022 16:57:22 +0100 Subject: [PATCH 0433/1204] PGPainless 1.3.11-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 8a7dd76c..a8b3c6b7 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.10' - isSnapshot = false + shortVersion = '1.3.11' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From c253732ad96159a99710e8165c6a8e701013c1ab Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Nov 2022 15:30:29 +0100 Subject: [PATCH 0434/1204] Do not reject bnacksig signatures when they predate subkey binding date Fixes #334 --- .../org/pgpainless/signature/consumer/SignatureValidator.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java index 56614f4f..af245235 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java @@ -375,7 +375,9 @@ public abstract class SignatureValidator { public void verify(PGPSignature signature) throws SignatureValidationException { signatureHasHashedCreationTime().verify(signature); signatureDoesNotPredateSigningKey(creator).verify(signature); - signatureDoesNotPredateSigningKeyBindingDate(creator).verify(signature); + if (signature.getSignatureType() != SignatureType.PRIMARYKEY_BINDING.getCode()) { + signatureDoesNotPredateSigningKeyBindingDate(creator).verify(signature); + } } }; } From c77c96f8498e962ea70497cffb40982f76d85ced Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Nov 2022 15:30:57 +0100 Subject: [PATCH 0435/1204] SOP verify: force data to be non-openpgp data --- .../src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index e6e2768e..1f9aa32b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -62,6 +62,8 @@ public class DetachedVerifyImpl implements DetachedVerify { @Override public List data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { + options.forceNonOpenPgpData(); + DecryptionStream decryptionStream; try { decryptionStream = PGPainless.decryptAndOrVerify() From 1c127933bd0c0b0c63264eefd1b34273fd703726 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Nov 2022 15:50:01 +0100 Subject: [PATCH 0436/1204] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index efff7de9..58c4b052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.11-SNAPSHOT +- Fix: When verifying subkey binding signatures with embedded recycled primary + key binding signatures, do not reject signature if primary key binding + predates subkey binding +- SOP `verify`: Forcefully expect `data()` to be non-OpenPGP data + ## 1.3.10 - Bump `sop-java` to `4.0.3` - Fix: Fix NPE when verifying signature made by key without key flags on direct-key signature From e15dd70b85b3292ecf83514a6fd58b229e354b2a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Nov 2022 22:01:20 +0100 Subject: [PATCH 0437/1204] SOP: Unify key/certificate reading code --- .../java/org/pgpainless/sop/DecryptImpl.java | 39 +++--------- .../pgpainless/sop/DetachedVerifyImpl.java | 9 +-- .../java/org/pgpainless/sop/EncryptImpl.java | 31 ++++------ .../org/pgpainless/sop/ExtractCertImpl.java | 17 +----- .../org/pgpainless/sop/InlineSignImpl.java | 19 +++--- .../org/pgpainless/sop/InlineVerifyImpl.java | 9 +-- .../java/org/pgpainless/sop/KeyReader.java | 61 +++++++++++++++++++ 7 files changed, 93 insertions(+), 92 deletions(-) create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index f18ed732..11b20f82 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -52,22 +52,9 @@ public class DecryptImpl implements Decrypt { @Override public DecryptImpl verifyWithCert(InputStream certIn) throws SOPGPException.BadData, IOException { - try { - PGPPublicKeyRingCollection certs = PGPainless.readKeyRing().keyRingCollection(certIn, false) - .getPgpPublicKeyRingCollection(); - if (certs.size() == 0) { - throw new SOPGPException.BadData(new PGPException("No certificates provided.")); - } - + PGPPublicKeyRingCollection certs = KeyReader.readPublicKeys(certIn, true); + if (certs != null) { consumerOptions.addVerificationCerts(certs); - - } catch (IOException e) { - if (e.getMessage() != null && e.getMessage().startsWith("unknown object in stream:")) { - throw new SOPGPException.BadData(e); - } - throw e; - } catch (PGPException e) { - throw new SOPGPException.BadData(e); } return this; } @@ -102,23 +89,11 @@ public class DecryptImpl implements Decrypt { @Override public DecryptImpl withKey(InputStream keyIn) throws SOPGPException.BadData, IOException, SOPGPException.UnsupportedAsymmetricAlgo { - try { - PGPSecretKeyRingCollection secretKeyCollection = PGPainless.readKeyRing() - .secretKeyRingCollection(keyIn); - if (secretKeyCollection.size() == 0) { - throw new SOPGPException.BadData("No key data found."); - } - for (PGPSecretKeyRing key : secretKeyCollection) { - protector.addSecretKey(key); - consumerOptions.addDecryptionKey(key, protector); - } - } catch (IOException e) { - if (e.getMessage() != null && e.getMessage().startsWith("unknown object in stream:")) { - throw new SOPGPException.BadData(e); - } - throw e; - } catch (PGPException e) { - throw new SOPGPException.BadData(e); + PGPSecretKeyRingCollection secretKeyCollection = KeyReader.readSecretKeys(keyIn, true); + + for (PGPSecretKeyRing key : secretKeyCollection) { + protector.addSecretKey(key); + consumerOptions.addDecryptionKey(key, protector); } return this; } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index 1f9aa32b..3065addf 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -39,13 +39,8 @@ public class DetachedVerifyImpl implements DetachedVerify { } @Override - public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData { - PGPPublicKeyRingCollection certificates; - try { - certificates = PGPainless.readKeyRing().publicKeyRingCollection(cert); - } catch (IOException | PGPException e) { - throw new SOPGPException.BadData(e); - } + public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData, IOException { + PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true); options.addVerificationCerts(certificates); return this; } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 9658bd17..61a46731 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -58,28 +58,23 @@ public class EncryptImpl implements Encrypt { @Override public Encrypt signWith(InputStream keyIn) - throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { + throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { if (signingOptions == null) { signingOptions = SigningOptions.get(); } - - try { - PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); - if (keys.size() != 1) { - throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size())); - } - PGPSecretKeyRing signingKey = keys.iterator().next(); - - KeyRingInfo info = PGPainless.inspectKeyRing(signingKey); - if (info.getSigningSubkeys().isEmpty()) { - throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(signingKey) + " cannot sign."); - } - - protector.addSecretKey(signingKey); - signingKeys.add(signingKey); - } catch (IOException | PGPException e) { - throw new SOPGPException.BadData(e); + PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true); + if (keys.size() != 1) { + throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size())); } + PGPSecretKeyRing signingKey = keys.iterator().next(); + + KeyRingInfo info = PGPainless.inspectKeyRing(signingKey); + if (info.getSigningSubkeys().isEmpty()) { + throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(signingKey) + " cannot sign."); + } + + protector.addSecretKey(signingKey); + signingKeys.add(signingKey); return this; } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java index 16848383..5be3ad31 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java @@ -10,7 +10,6 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.List; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; @@ -32,21 +31,7 @@ public class ExtractCertImpl implements ExtractCert { @Override public Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData { - PGPSecretKeyRingCollection keys; - try { - keys = PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream); - } catch (IOException e) { - if (e.getMessage() != null && e.getMessage().startsWith("unknown object in stream:")) { - throw new SOPGPException.BadData(e); - } - throw e; - } catch (PGPException e) { - throw new IOException("Cannot read keys.", e); - } - - if (keys == null || keys.size() == 0) { - throw new SOPGPException.BadData(new PGPException("No key data found.")); - } + PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyInputStream, true); List certs = new ArrayList<>(); for (PGPSecretKeyRing key : keys) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index 3bdc8fc3..c0bd29f4 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -51,19 +51,14 @@ public class InlineSignImpl implements InlineSign { @Override public InlineSign key(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { - try { - PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); - - for (PGPSecretKeyRing key : keys) { - KeyRingInfo info = PGPainless.inspectKeyRing(key); - if (!info.isUsableForSigning()) { - throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys."); - } - protector.addSecretKey(key); - signingKeys.add(key); + PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true); + for (PGPSecretKeyRing key : keys) { + KeyRingInfo info = PGPainless.inspectKeyRing(key); + if (!info.isUsableForSigning()) { + throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys."); } - } catch (PGPException | KeyException e) { - throw new SOPGPException.BadData(e); + protector.addSecretKey(key); + signingKeys.add(key); } return this; } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index e9994f38..4af53685 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -42,13 +42,8 @@ public class InlineVerifyImpl implements InlineVerify { } @Override - public InlineVerify cert(InputStream cert) throws SOPGPException.BadData { - PGPPublicKeyRingCollection certificates; - try { - certificates = PGPainless.readKeyRing().publicKeyRingCollection(cert); - } catch (IOException | PGPException e) { - throw new SOPGPException.BadData(e); - } + public InlineVerify cert(InputStream cert) throws SOPGPException.BadData, IOException { + PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true); options.addVerificationCerts(certificates); return this; } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java new file mode 100644 index 00000000..064a5e39 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.pgpainless.PGPainless; +import sop.exception.SOPGPException; + +import java.io.IOException; +import java.io.InputStream; + +class KeyReader { + + static PGPSecretKeyRingCollection readSecretKeys(InputStream keyInputStream, boolean requireContent) + throws IOException, SOPGPException.BadData { + PGPSecretKeyRingCollection keys; + try { + keys = PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream); + } catch (IOException e) { + String message = e.getMessage(); + if (message == null) { + throw e; + } + if (message.startsWith("unknown object in stream:") || + message.startsWith("invalid header encountered")) { + throw new SOPGPException.BadData(e); + } + throw e; + } catch (PGPException e) { + throw new IOException("Cannot read keys.", e); + } + + if (requireContent && (keys == null || keys.size() == 0)) { + throw new SOPGPException.BadData(new PGPException("No key data found.")); + } + + return keys; + } + + static PGPPublicKeyRingCollection readPublicKeys(InputStream certIn, boolean requireContent) throws IOException { + PGPPublicKeyRingCollection certs; + try { + certs = PGPainless.readKeyRing().publicKeyRingCollection(certIn); + } catch (IOException e) { + if (e.getMessage() != null && e.getMessage().startsWith("unknown object in stream:")) { + throw new SOPGPException.BadData(e); + } + throw e; + } catch (PGPException e) { + throw new SOPGPException.BadData(e); + } + if (requireContent && (certs == null || certs.size() == 0)) { + throw new SOPGPException.BadData(new PGPException("No cert data found.")); + } + return certs; + } +} From fd55ce3657057f43dc9c682a445e232efce23437 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Nov 2022 22:01:52 +0100 Subject: [PATCH 0438/1204] Fix key/password matching in SOPs detached sign command --- .../org/pgpainless/sop/DetachedSignImpl.java | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java index 0ce326a9..f6ab8172 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java @@ -24,6 +24,7 @@ import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.KeyException; +import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.ArmoredOutputStreamFactory; @@ -41,6 +42,7 @@ public class DetachedSignImpl implements DetachedSign { private SignAs mode = SignAs.Binary; private final SigningOptions signingOptions = SigningOptions.get(); private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + private final List signingKeys = new ArrayList<>(); @Override public DetachedSign noArmor() { @@ -56,19 +58,14 @@ public class DetachedSignImpl implements DetachedSign { @Override public DetachedSign key(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { - try { - PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn); - - for (PGPSecretKeyRing key : keys) { - KeyRingInfo info = PGPainless.inspectKeyRing(key); - if (!info.isUsableForSigning()) { - throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys."); - } - protector.addSecretKey(key); - signingOptions.addDetachedSignature(protector, key, modeToSigType(mode)); + PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true); + for (PGPSecretKeyRing key : keys) { + KeyRingInfo info = PGPainless.inspectKeyRing(key); + if (!info.isUsableForSigning()) { + throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys."); } - } catch (PGPException | KeyException e) { - throw new SOPGPException.BadData(e); + protector.addSecretKey(key); + signingKeys.add(key); } return this; } @@ -82,6 +79,16 @@ public class DetachedSignImpl implements DetachedSign { @Override public ReadyWithResult data(InputStream data) throws IOException { + for (PGPSecretKeyRing key : signingKeys) { + try { + signingOptions.addDetachedSignature(protector, key, modeToSigType(mode)); + } catch (KeyException.UnacceptableSigningKeyException | KeyException.MissingSecretKeyException e) { + throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(key) + " cannot sign.", e); + } catch (PGPException e) { + throw new SOPGPException.KeyIsProtected("Key " + OpenPgpFingerprint.of(key) + " cannot be unlocked.", e); + } + } + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); try { EncryptionStream signingStream = PGPainless.encryptAndOrSign() From 858f8e00f3f38c32be10171bfbe2e04d0d49c364 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Nov 2022 22:02:16 +0100 Subject: [PATCH 0439/1204] Rework CLI tests --- .../pgpainless/cli/commands/ArmorCmdTest.java | 99 +-- .../org/pgpainless/cli/commands/CLITest.java | 166 +++++ .../cli/commands/DearmorCmdTest.java | 89 ++- .../cli/commands/ExtractCertCmdTest.java | 76 ++- .../cli/commands/GenerateCertCmdTest.java | 106 --- .../cli/commands/GenerateKeyCmdTest.java | 96 +++ .../cli/commands/InlineDetachCmdTest.java | 143 ++--- .../RoundTripEncryptDecryptCmdTest.java | 606 +++++++++++++++--- ...oundTripInlineSignInlineVerifyCmdTest.java | 236 +++++++ .../commands/RoundTripSignVerifyCmdTest.java | 360 ++++++++--- .../cli/commands/VersionCmdTest.java | 44 ++ 11 files changed, 1548 insertions(+), 473 deletions(-) create mode 100644 pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java delete mode 100644 pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertCmdTest.java create mode 100644 pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java create mode 100644 pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java create mode 100644 pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java index 711796e6..c1fb810f 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java @@ -5,87 +5,96 @@ package org.pgpainless.cli.commands; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import com.ginsberg.junit.exit.FailOnSystemExit; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; -import org.pgpainless.cli.PGPainlessCLI; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; -public class ArmorCmdTest { +public class ArmorCmdTest extends CLITest { - private static PrintStream originalSout; - - @BeforeEach - public void saveSout() { - originalSout = System.out; + public ArmorCmdTest() { + super(LoggerFactory.getLogger(ArmorCmdTest.class)); } - @AfterEach - public void restoreSout() { - System.setOut(originalSout); - } + private static final String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 62E9 DDA4 F20F 8341 D2BC 4B4C 8B07 5177 01F9 534C\n" + + "Comment: alice@pgpainless.org\n" + + "\n" + + "lFgEY2vOkhYJKwYBBAHaRw8BAQdAqGOtLd1tKnuwaYYcdr2/7C0cPiCCggRMKG+W\n" + + "t32QQdEAAP9VaBzjk/AaAqyykZnQHmS1HByEvRLv5/4yJMSr22451BFjtBRhbGlj\n" + + "ZUBwZ3BhaW5sZXNzLm9yZ4iOBBMWCgBBBQJja86SCRCLB1F3AflTTBYhBGLp3aTy\n" + + "D4NB0rxLTIsHUXcB+VNMAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAACZhAP4s\n" + + "8hn/RBDvyLvGROOd15EYATnWlgyi+b5WXP6cELalJwD1FZy3RROhfNtZWcJPS43f\n" + + "G03pYNyb0NXoitIMAaXEB5xdBGNrzpISCisGAQQBl1UBBQEBB0CqCcYethOynfni\n" + + "8uRO+r/cZWp9hCLy8pRIExKqzcyEFAMBCAcAAP9sRRLoZkLpDaTNNrtIBovXu2AN\n" + + "hL8keUMWtVcuEHnkQA6iiHUEGBYKAB0FAmNrzpICngECmwwFFgIDAQAECwkIBwUV\n" + + "CgkICwAKCRCLB1F3AflTTBVpAP491etrjqCMWx2bBaw3K1vP0Mix6U0vF3J4kP9U\n" + + "eZm6owEA4kX9VAGESvLgIc7CEiswmxdWjxnLQyCRtWXfjgFmYQucWARja86SFgkr\n" + + "BgEEAdpHDwEBB0DBslhDpWC6CV3xJUSo071NSO5Cf4fgOwOj+QHs8mpFbwABAPkQ\n" + + "ioSydYiMi04LyfPohyrhhcdJDHallQg+jYHHUb2pEJCI1QQYFgoAfQUCY2vOkgKe\n" + + "AQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNrzpIACgkQiHlkvEXh+f1e\n" + + "ywEA9A2GLU9LxCJxZf2X4qcZY//YJDChIZHPnY0Vaek1DsMBAN1YILrH2rxQeCXj\n" + + "m4bUKfJIRrGt6ZJscwORgNI1dFQFAAoJEIsHUXcB+VNMK3gA/3vvPm57JsHA860w\n" + + "lB4D1II71oFNL8TFnJqTAvpSKe1AAP49S4mKB4PE0ElcDo7n+nEYt6ba8IMRDlMo\n" + + "rsH85mUgCw==\n" + + "=EMKf\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; @Test - @FailOnSystemExit - public void armorSecretKey() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org"); - byte[] bytes = secretKey.getEncoded(); + public void armorSecretKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(key); + byte[] binary = secretKeys.getEncoded(); - System.setIn(new ByteArrayInputStream(bytes)); - ByteArrayOutputStream armorOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(armorOut)); - PGPainlessCLI.execute("armor"); + pipeBytesToStdin(binary); + ByteArrayOutputStream armorOut = pipeStdoutToStream(); + assertSuccess(executeCommand("armor")); PGPSecretKeyRing armored = PGPainless.readKeyRing().secretKeyRing(armorOut.toString()); - assertArrayEquals(secretKey.getEncoded(), armored.getEncoded()); + assertArrayEquals(secretKeys.getEncoded(), armored.getEncoded()); } @Test - @FailOnSystemExit - public void armorPublicKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org"); + public void armorPublicKey() throws IOException { + PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(key); PGPPublicKeyRing publicKey = PGPainless.extractCertificate(secretKey); byte[] bytes = publicKey.getEncoded(); - System.setIn(new ByteArrayInputStream(bytes)); - ByteArrayOutputStream armorOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(armorOut)); - PGPainlessCLI.execute("armor"); + pipeBytesToStdin(bytes); + ByteArrayOutputStream armorOut = pipeStdoutToStream(); + assertSuccess(executeCommand("armor")); PGPPublicKeyRing armored = PGPainless.readKeyRing().publicKeyRing(armorOut.toString()); assertArrayEquals(publicKey.getEncoded(), armored.getEncoded()); } @Test - @FailOnSystemExit - public void armorMessage() { + public void armorMessage() throws IOException { String message = "Hello, World!\n"; - System.setIn(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))); - ByteArrayOutputStream armorOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(armorOut)); - PGPainlessCLI.execute("armor"); + pipeStringToStdin(message); + ByteArrayOutputStream armorOut = pipeStdoutToStream(); + assertSuccess(executeCommand("armor")); String armored = armorOut.toString(); - assertTrue(armored.startsWith("-----BEGIN PGP MESSAGE-----\n")); assertTrue(armored.contains("SGVsbG8sIFdvcmxkIQo=")); } + @Test + public void labelNotYetSupported() throws IOException { + pipeStringToStdin("Hello, World!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("armor", "--label", "Message"); + assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java new file mode 100644 index 00000000..d5d076cc --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import javax.annotation.Nonnull; + +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.opentest4j.TestAbortedException; +import org.pgpainless.cli.TestUtils; +import org.pgpainless.sop.SOPImpl; +import org.slf4j.Logger; +import sop.cli.picocli.SopCLI; + +public abstract class CLITest { + + protected File testDirectory; + protected InputStream stdin; + protected PrintStream stdout; + + protected final Logger LOGGER; + + + public CLITest(@Nonnull Logger logger) { + LOGGER = logger; + SopCLI.setSopInstance(new SOPImpl()); + } + + @BeforeEach + public void setup() throws IOException { + testDirectory = TestUtils.createTempDirectory(); + testDirectory.deleteOnExit(); + LOGGER.debug(testDirectory.getAbsolutePath()); + stdin = System.in; + stdout = System.out; + } + + @AfterEach + public void cleanup() throws IOException { + resetStreams(); + } + + public File nonExistentFile(String name) { + File file = new File(testDirectory, name); + if (file.exists()) { + throw new TestAbortedException("File " + file.getAbsolutePath() + " already exists."); + } + return file; + } + + public File pipeStdoutToFile(String name) throws IOException { + File file = new File(testDirectory, name); + file.deleteOnExit(); + if (!file.createNewFile()) { + throw new TestAbortedException("Cannot create new file " + file.getAbsolutePath()); + } + System.setOut(new PrintStream(Files.newOutputStream(file.toPath()))); + return file; + } + + public ByteArrayOutputStream pipeStdoutToStream() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + pipeStdoutToStream(out); + return out; + } + + public void pipeStdoutToStream(OutputStream stream) { + System.setOut(new PrintStream(stream)); + } + + public void pipeFileToStdin(File file) throws IOException { + System.setIn(Files.newInputStream(file.toPath())); + } + + public void pipeBytesToStdin(byte[] bytes) { + System.setIn(new ByteArrayInputStream(bytes)); + } + + public void pipeStringToStdin(String string) { + System.setIn(new ByteArrayInputStream(string.getBytes(StandardCharsets.UTF_8))); + } + + public void resetStdout() { + if (System.out != stdout) { + System.out.flush(); + System.out.close(); + } + System.setOut(stdout); + } + + public void resetStdin() throws IOException { + if (System.in != stdin) { + System.in.close(); + } + System.setIn(stdin); + } + + public void resetStreams() throws IOException { + resetStdout(); + resetStdin(); + } + + public File writeFile(String name, String data) throws IOException { + return writeFile(name, data.getBytes(StandardCharsets.UTF_8)); + } + + public File writeFile(String name, byte[] bytes) throws IOException { + return writeFile(name, new ByteArrayInputStream(bytes)); + } + + public File writeFile(String name, InputStream data) throws IOException { + File file = new File(testDirectory, name); + if (!file.createNewFile()) { + throw new TestAbortedException("Cannot create new file " + file.getAbsolutePath()); + } + file.deleteOnExit(); + try (FileOutputStream fileOut = new FileOutputStream(file)) { + Streams.pipeAll(data, fileOut); + fileOut.flush(); + } + return file; + } + + public byte[] readBytesFromFile(File file) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try (FileInputStream fileIn = new FileInputStream(file)) { + Streams.pipeAll(fileIn, buffer); + } catch (FileNotFoundException e) { + throw new TestAbortedException("File " + file.getAbsolutePath() + " does not exist!", e); + } catch (IOException e) { + throw new TestAbortedException("Cannot read from file " + file.getAbsolutePath(), e); + } + return buffer.toByteArray(); + } + + public String readStringFromFile(File file) { + return new String(readBytesFromFile(file), StandardCharsets.UTF_8); + } + + public int executeCommand(String... command) throws IOException { + int exitCode = SopCLI.execute(command); + resetStreams(); + return exitCode; + } + + public void assertSuccess(int exitCode) { + assertEquals(0, exitCode, "Expected successful program execution"); + } +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java index a49f4db1..4ebaf21d 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java @@ -7,73 +7,73 @@ package org.pgpainless.cli.commands; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import com.ginsberg.junit.exit.FailOnSystemExit; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; -import org.pgpainless.cli.PGPainlessCLI; +import org.slf4j.LoggerFactory; -public class DearmorCmdTest { +public class DearmorCmdTest extends CLITest { - private PrintStream originalSout; - - @BeforeEach - public void saveSout() { - this.originalSout = System.out; + public DearmorCmdTest() { + super(LoggerFactory.getLogger(DearmorCmdTest.class)); } - @AfterEach - public void restoreSout() { - System.setOut(originalSout); - } + private static final String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 62E9 DDA4 F20F 8341 D2BC 4B4C 8B07 5177 01F9 534C\n" + + "Comment: alice@pgpainless.org\n" + + "\n" + + "lFgEY2vOkhYJKwYBBAHaRw8BAQdAqGOtLd1tKnuwaYYcdr2/7C0cPiCCggRMKG+W\n" + + "t32QQdEAAP9VaBzjk/AaAqyykZnQHmS1HByEvRLv5/4yJMSr22451BFjtBRhbGlj\n" + + "ZUBwZ3BhaW5sZXNzLm9yZ4iOBBMWCgBBBQJja86SCRCLB1F3AflTTBYhBGLp3aTy\n" + + "D4NB0rxLTIsHUXcB+VNMAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAACZhAP4s\n" + + "8hn/RBDvyLvGROOd15EYATnWlgyi+b5WXP6cELalJwD1FZy3RROhfNtZWcJPS43f\n" + + "G03pYNyb0NXoitIMAaXEB5xdBGNrzpISCisGAQQBl1UBBQEBB0CqCcYethOynfni\n" + + "8uRO+r/cZWp9hCLy8pRIExKqzcyEFAMBCAcAAP9sRRLoZkLpDaTNNrtIBovXu2AN\n" + + "hL8keUMWtVcuEHnkQA6iiHUEGBYKAB0FAmNrzpICngECmwwFFgIDAQAECwkIBwUV\n" + + "CgkICwAKCRCLB1F3AflTTBVpAP491etrjqCMWx2bBaw3K1vP0Mix6U0vF3J4kP9U\n" + + "eZm6owEA4kX9VAGESvLgIc7CEiswmxdWjxnLQyCRtWXfjgFmYQucWARja86SFgkr\n" + + "BgEEAdpHDwEBB0DBslhDpWC6CV3xJUSo071NSO5Cf4fgOwOj+QHs8mpFbwABAPkQ\n" + + "ioSydYiMi04LyfPohyrhhcdJDHallQg+jYHHUb2pEJCI1QQYFgoAfQUCY2vOkgKe\n" + + "AQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNrzpIACgkQiHlkvEXh+f1e\n" + + "ywEA9A2GLU9LxCJxZf2X4qcZY//YJDChIZHPnY0Vaek1DsMBAN1YILrH2rxQeCXj\n" + + "m4bUKfJIRrGt6ZJscwORgNI1dFQFAAoJEIsHUXcB+VNMK3gA/3vvPm57JsHA860w\n" + + "lB4D1II71oFNL8TFnJqTAvpSKe1AAP49S4mKB4PE0ElcDo7n+nEYt6ba8IMRDlMo\n" + + "rsH85mUgCw==\n" + + "=EMKf\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; @Test - @FailOnSystemExit - public void dearmorSecretKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org"); - String armored = PGPainless.asciiArmor(secretKey); + public void dearmorSecretKey() throws IOException { + PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(key); - System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8))); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("dearmor"); + pipeStringToStdin(key); + ByteArrayOutputStream dearmored = pipeStdoutToStream(); + assertSuccess(executeCommand("dearmor")); - assertArrayEquals(secretKey.getEncoded(), out.toByteArray()); + assertArrayEquals(secretKey.getEncoded(), dearmored.toByteArray()); } @Test - @FailOnSystemExit - public void dearmorCertificate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() - .modernKeyRing("alice@pgpainless.org"); + public void dearmorCertificate() throws IOException { + PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(key); PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); - String armored = PGPainless.asciiArmor(certificate); + String armoredCert = PGPainless.asciiArmor(certificate); - System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8))); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("dearmor"); + pipeStringToStdin(armoredCert); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("dearmor")); assertArrayEquals(certificate.getEncoded(), out.toByteArray()); } @Test - @FailOnSystemExit - public void dearmorMessage() { + public void dearmorMessage() throws IOException { String armored = "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.69\n" + "\n" + @@ -81,10 +81,9 @@ public class DearmorCmdTest { "=fkLo\n" + "-----END PGP MESSAGE-----"; - System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8))); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("dearmor"); + pipeStringToStdin(armored); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("dearmor")); assertEquals("Hello, World\n", out.toString()); } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java index 1d20fb0e..3eebcb47 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java @@ -4,42 +4,96 @@ package org.pgpainless.cli.commands; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; -import java.io.PrintStream; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import com.ginsberg.junit.exit.FailOnSystemExit; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; -import org.pgpainless.cli.PGPainlessCLI; import org.pgpainless.key.info.KeyRingInfo; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; -public class ExtractCertCmdTest { +public class ExtractCertCmdTest extends CLITest { + + public ExtractCertCmdTest() { + super(LoggerFactory.getLogger(ExtractCertCmdTest.class)); + } @Test - @FailOnSystemExit public void testExtractCert() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .simpleEcKeyRing("Juliet Capulet "); - ByteArrayInputStream inputStream = new ByteArrayInputStream(secretKeys.getEncoded()); - System.setIn(inputStream); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); + pipeBytesToStdin(secretKeys.getEncoded()); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("extract-cert", "--armor")); + + assertTrue(out.toString().startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n")); - PGPainlessCLI.execute("extract-cert"); PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(out.toByteArray()); KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys); assertFalse(info.isSecretKey()); assertTrue(info.isUserIdValid("Juliet Capulet ")); } + + @Test + public void testExtractCertFromCertFails() throws IOException { + // Generate key + File keyFile = pipeStdoutToFile("key.asc"); + assertSuccess(executeCommand("generate-key", "Alice ")); + + // extract cert from key (success) + pipeFileToStdin(keyFile); + File certFile = pipeStdoutToFile("cert.asc"); + assertSuccess(executeCommand("extract-cert")); + + // extract cert from cert (fail) + pipeFileToStdin(certFile); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("extract-cert"); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void extractCertFromGarbageFails() throws IOException { + pipeStringToStdin("This is a bunch of garbage!"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("extract-cert"); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testExtractCertUnarmored() throws IOException { + // Generate key + File keyFile = pipeStdoutToFile("key.asc"); + assertSuccess(executeCommand("generate-key", "Alice ")); + + // extract cert from key (success) + pipeFileToStdin(keyFile); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("extract-cert", "--no-armor")); + + assertFalse(out.toString().startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n")); + + pipeBytesToStdin(out.toByteArray()); + ByteArrayOutputStream armored = pipeStdoutToStream(); + assertSuccess(executeCommand("armor")); + + assertTrue(armored.toString().startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n")); + } + } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertCmdTest.java deleted file mode 100644 index 63afc39f..00000000 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateCertCmdTest.java +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.cli.commands; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.pgpainless.cli.TestUtils.ARMOR_PRIVATE_KEY_HEADER_BYTES; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; - -import com.ginsberg.junit.exit.FailOnSystemExit; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.cli.PGPainlessCLI; -import org.pgpainless.cli.TestUtils; -import org.pgpainless.key.info.KeyInfo; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.util.Passphrase; - -public class GenerateCertCmdTest { - - @Test - @FailOnSystemExit - public void testKeyGeneration() throws IOException, PGPException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("generate-key", "--armor", "Juliet Capulet "); - - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(out.toByteArray()); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - assertTrue(info.isFullyDecrypted()); - assertTrue(info.isUserIdValid("Juliet Capulet ")); - - for (PGPSecretKey key : secretKeys) { - assertTrue(testPassphrase(key, null)); - } - - byte[] outBegin = new byte[37]; - System.arraycopy(out.toByteArray(), 0, outBegin, 0, 37); - assertArrayEquals(outBegin, ARMOR_PRIVATE_KEY_HEADER_BYTES); - } - - @Test - @FailOnSystemExit - public void testGenerateKeyWithPassword() throws IOException, PGPException { - PrintStream orig = System.out; - try { - // Write password to file - File tempDir = TestUtils.createTempDirectory(); - File passwordFile = TestUtils.writeTempFile(tempDir, "sw0rdf1sh".getBytes(StandardCharsets.UTF_8)); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("generate-key", "Juliet Capulet ", - "--with-key-password", passwordFile.getAbsolutePath()); - - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(out.toByteArray()); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - assertFalse(info.isFullyDecrypted()); - assertTrue(info.isFullyEncrypted()); - - for (PGPSecretKey key : secretKeys) { - assertTrue(testPassphrase(key, "sw0rdf1sh")); - } - } finally { - System.setOut(orig); - } - } - - private boolean testPassphrase(PGPSecretKey key, String passphrase) throws PGPException { - if (KeyInfo.isEncrypted(key)) { - UnlockSecretKey.unlockSecretKey(key, Passphrase.fromPassword(passphrase)); - } else { - if (passphrase != null) { - return false; - } - UnlockSecretKey.unlockSecretKey(key, (PBESecretKeyDecryptor) null); - } - return true; - } - - @Test - @FailOnSystemExit - public void testNoArmor() { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("generate-key", "--no-armor", "Test "); - - byte[] outBegin = new byte[37]; - System.arraycopy(out.toByteArray(), 0, outBegin, 0, 37); - assertFalse(Arrays.equals(outBegin, ARMOR_PRIVATE_KEY_HEADER_BYTES)); - } -} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java new file mode 100644 index 00000000..2e4e6e7f --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; + +public class GenerateKeyCmdTest extends CLITest { + + public GenerateKeyCmdTest() { + super(LoggerFactory.getLogger(GenerateKeyCmdTest.class)); + } + + @Test + public void testGenerateKey() throws IOException { + File keyFile = pipeStdoutToFile("key.asc"); + assertSuccess(executeCommand("generate-key", "Alice ")); + + String key = readStringFromFile(keyFile); + assertTrue(key.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\n")); + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(key); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertTrue(info.isFullyDecrypted()); + assertEquals(Collections.singletonList("Alice "), info.getUserIds()); + } + + @Test + public void testGenerateBinaryKey() throws IOException { + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("generate-key", "--no-armor", + "Alice ")); + + byte[] key = out.toByteArray(); + String firstHexOctet = Hex.toHexString(key, 0, 1); + assertTrue(firstHexOctet.equals("c5") || firstHexOctet.equals("94")); + } + + @Test + public void testGenerateKeyWithMultipleUserIds() throws IOException { + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("generate-key", + "Alice ", "Alice ")); + + String key = out.toString(); + assertTrue(key.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\n")); + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(key); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertTrue(info.isFullyDecrypted()); + assertEquals(Arrays.asList("Alice ", "Alice "), info.getUserIds()); + } + + @Test + public void testPasswordProtectedKey() throws IOException, PGPException { + File passwordFile = writeFile("password", "sw0rdf1sh"); + passwordFile.deleteOnExit(); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("generate-key", + "--with-key-password", passwordFile.getAbsolutePath(), "Alice ")); + + String key = out.toString(); + assertTrue(key.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\n")); + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(key); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertTrue(info.isFullyEncrypted()); + + assertNotNull(UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), Passphrase.fromPassword("sw0rdf1sh"))); + } + + @Test + public void testGeneratePasswordProtectedKey_missingPasswordFile() throws IOException { + int exit = executeCommand("generate-key", + "--with-key-password", "nonexistent", "Alice "); + + assertEquals(SOPGPException.MissingInput.EXIT_CODE, exit, "Expected MISSING_INPUT (" + SOPGPException.MissingInput.EXIT_CODE + ")"); + } +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java index aed4c581..eabdd6ad 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java @@ -6,33 +6,18 @@ package org.pgpainless.cli.commands; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.PrintStream; import java.nio.charset.StandardCharsets; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.pgpainless.cli.PGPainlessCLI; import org.pgpainless.cli.TestUtils; +import org.slf4j.LoggerFactory; import sop.exception.SOPGPException; -public class InlineDetachCmdTest { - - private PrintStream originalSout; - private static File tempDir; - private static File certFile; +public class InlineDetachCmdTest extends CLITest { private static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "Version: BCPG v1.64\n" + @@ -49,28 +34,6 @@ public class InlineDetachCmdTest { "=a1W7\n" + "-----END PGP PUBLIC KEY BLOCK-----"; - @BeforeAll - public static void createTempDir() throws IOException { - tempDir = TestUtils.createTempDirectory(); - - certFile = new File(tempDir, "cert.asc"); - assertTrue(certFile.createNewFile()); - try (FileOutputStream out = new FileOutputStream(certFile)) { - ByteArrayInputStream in = new ByteArrayInputStream(CERT.getBytes(StandardCharsets.UTF_8)); - Streams.pipeAll(in, out); - } - } - - @BeforeEach - public void saveSout() { - this.originalSout = System.out; - } - - @AfterEach - public void restoreSout() { - System.setOut(originalSout); - } - private static final String CLEAR_SIGNED_MESSAGE = "-----BEGIN PGP SIGNED MESSAGE-----\n" + "Hash: SHA512\n" + "\n" + @@ -103,91 +66,67 @@ public class InlineDetachCmdTest { "Unfold the imagined happiness that both\n" + "Receive in either by this dear encounter."; + public InlineDetachCmdTest() { + super(LoggerFactory.getLogger(InlineDetachCmdTest.class)); + } + @Test public void detachInbandSignatureAndMessage() throws IOException { - // Clearsigned In - ByteArrayInputStream clearSignedIn = new ByteArrayInputStream(CLEAR_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8)); - System.setIn(clearSignedIn); + pipeStringToStdin(CLEAR_SIGNED_MESSAGE); + ByteArrayOutputStream msgOut = pipeStdoutToStream(); + File sigFile = nonExistentFile("sig.out"); - // Plaintext Out - ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(msgOut)); - - // Detach - File tempSigFile = new File(tempDir, "sig.out"); - PGPainlessCLI.main(new String[] {"inline-detach", "--signatures-out=" + tempSigFile.getAbsolutePath()}); + assertSuccess(executeCommand("inline-detach", "--signatures-out", sigFile.getAbsolutePath())); + assertTrue(sigFile.exists(), "Signature file must have been written."); // Test equality with expected values assertEquals(CLEAR_SIGNED_BODY, msgOut.toString()); - try (FileInputStream sigIn = new FileInputStream(tempSigFile)) { - ByteArrayOutputStream sigBytes = new ByteArrayOutputStream(); - Streams.pipeAll(sigIn, sigBytes); - String sig = sigBytes.toString(); - TestUtils.assertSignatureIsArmored(sigBytes.toByteArray()); - TestUtils.assertSignatureEquals(CLEAR_SIGNED_SIGNATURE, sig); - } catch (FileNotFoundException e) { - fail("Signature File must have been written.", e); - } + String sig = readStringFromFile(sigFile); + TestUtils.assertSignatureIsArmored(sig.getBytes()); + TestUtils.assertSignatureEquals(CLEAR_SIGNED_SIGNATURE, sig); // Check if produced signature still checks out - System.setIn(new ByteArrayInputStream(msgOut.toByteArray())); - ByteArrayOutputStream verifyOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(verifyOut)); - PGPainlessCLI.main(new String[] {"verify", tempSigFile.getAbsolutePath(), certFile.getAbsolutePath()}); - - assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C\n", verifyOut.toString()); + File certFile = writeFile("cert.asc", CERT); + pipeStringToStdin(msgOut.toString()); + ByteArrayOutputStream verifyOut = pipeStdoutToStream(); + assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), certFile.getAbsolutePath())); + assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C\n", + verifyOut.toString()); } @Test public void detachInbandSignatureAndMessageNoArmor() throws IOException { - // Clearsigned In - ByteArrayInputStream clearSignedIn = new ByteArrayInputStream(CLEAR_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8)); - System.setIn(clearSignedIn); + pipeStringToStdin(CLEAR_SIGNED_MESSAGE); + ByteArrayOutputStream msgOut = pipeStdoutToStream(); + File sigFile = nonExistentFile("sig.out"); - // Plaintext Out - ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(msgOut)); - - // Detach - File tempSigFile = new File(tempDir, "sig.asc"); - PGPainlessCLI.main(new String[] {"inline-detach", "--signatures-out=" + tempSigFile.getAbsolutePath(), "--no-armor"}); + assertSuccess(executeCommand("inline-detach", "--signatures-out", sigFile.getAbsolutePath(), "--no-armor")); // Test equality with expected values assertEquals(CLEAR_SIGNED_BODY, msgOut.toString()); - try (FileInputStream sigIn = new FileInputStream(tempSigFile)) { - ByteArrayOutputStream sigBytes = new ByteArrayOutputStream(); - Streams.pipeAll(sigIn, sigBytes); - byte[] sig = sigBytes.toByteArray(); - TestUtils.assertSignatureIsNotArmored(sig); - TestUtils.assertSignatureEquals(CLEAR_SIGNED_SIGNATURE.getBytes(StandardCharsets.UTF_8), sig); - } catch (FileNotFoundException e) { - fail("Signature File must have been written.", e); - } + assertTrue(sigFile.exists(), "Signature file must have been written."); + byte[] sig = readBytesFromFile(sigFile); + + TestUtils.assertSignatureIsNotArmored(sig); + TestUtils.assertSignatureEquals(CLEAR_SIGNED_SIGNATURE.getBytes(StandardCharsets.UTF_8), sig); // Check if produced signature still checks out - System.setIn(new ByteArrayInputStream(msgOut.toByteArray())); - ByteArrayOutputStream verifyOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(verifyOut)); - PGPainlessCLI.main(new String[] {"verify", tempSigFile.getAbsolutePath(), certFile.getAbsolutePath()}); - - assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C\n", verifyOut.toString()); + pipeBytesToStdin(msgOut.toByteArray()); + ByteArrayOutputStream verifyOut = pipeStdoutToStream(); + File certFile = writeFile("cert.asc", CERT); + assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), certFile.getAbsolutePath())); + assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C\n", + verifyOut.toString()); } @Test - @ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE) public void existingSignatureOutCausesException() throws IOException { - // Clearsigned In - ByteArrayInputStream clearSignedIn = new ByteArrayInputStream(CLEAR_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8)); - System.setIn(clearSignedIn); - - // Plaintext Out - ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(msgOut)); - - // Detach - File existingSigFile = new File(tempDir, "sig.existing"); - assertTrue(existingSigFile.createNewFile()); - PGPainlessCLI.main(new String[] {"inline-detach", "--signatures-out=" + existingSigFile.getAbsolutePath()}); + pipeStringToStdin(CLEAR_SIGNED_MESSAGE); + ByteArrayOutputStream msgOut = pipeStdoutToStream(); + File existingSigFile = writeFile("sig.asc", CLEAR_SIGNED_SIGNATURE); + int exit = executeCommand("inline-detach", "--signatures-out", existingSigFile.getAbsolutePath()); + assertEquals(SOPGPException.OutputExists.EXIT_CODE, exit); + assertEquals(0, msgOut.size()); } } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java index dba9f47b..5d183ea6 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java @@ -5,111 +5,557 @@ package org.pgpainless.cli.commands; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; import com.ginsberg.junit.exit.FailOnSystemExit; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; -import org.pgpainless.cli.PGPainlessCLI; -import org.pgpainless.cli.TestUtils; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; -public class RoundTripEncryptDecryptCmdTest { +public class RoundTripEncryptDecryptCmdTest extends CLITest { - private static File tempDir; - private static PrintStream originalSout; - - @BeforeAll - public static void prepare() throws IOException { - tempDir = TestUtils.createTempDirectory(); + public RoundTripEncryptDecryptCmdTest() { + super(LoggerFactory.getLogger(RoundTripEncryptDecryptCmdTest.class)); } + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: A2EC 077F C977 E15D D799 EFF9 2C0D 3C12 3CF5 1C08\n" + + "Comment: Alice \n" + + "\n" + + "lFgEY2veRhYJKwYBBAHaRw8BAQdAeJYBoCcnGPQ3nchyyBrWQ83q3hqJnfZn2mqh\n" + + "d1M7WwsAAP0R1ELnfdJhXcfjaYPLHzwy1i34FxP5g3tvdgg9Q7VmchActBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmNr3kYJECwNPBI89RwI\n" + + "FiEEouwHf8l34V3Xme/5LA08Ejz1HAgCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAAe6YA/2sO483Vi2Fgs4ejv8FykyO96IVrMoYhw3Od4LyWEyDfAQDi15LxJJm6\n" + + "T2sXdENVigdwDJiELxjOtbmivuJutxkWCJxdBGNr3kYSCisGAQQBl1UBBQEBB0CS\n" + + "zXjySHqlicxG3QlrVeTIqwKitL1sWsx0MCDmT1C8dAMBCAcAAP9VNkfMQvYAlYSP\n" + + "aYEkwEOc8/XpiloVKtPzxwVCPlXFeBDCiHUEGBYKAB0FAmNr3kYCngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRAsDTwSPPUcCOT4AQDZcN5a/e8Qr+LNBIyXXLgJWGsL\n" + + "59nsKHBbDURnxbEnMQEAybS8u+Rsb82yW4CfaA4CLRTC3eDc5Y4QwYWzLogWNwic\n" + + "WARja95GFgkrBgEEAdpHDwEBB0DcdwQufWLq6ASku4JWBBd9JplRVhK0cXWuTE73\n" + + "uWltuwABAI0bVQXvgDnxTs6kUO7JIWtokM5lI/1bfG4L1YOfnXIgD7CI1QQYFgoA\n" + + "fQUCY2veRgKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNr3kYACgkQ\n" + + "7NC/hj9lyaWVAwEA3ze1LCi1reGfB5tS3Au6A8aalyk4UV0iVOXxwV5r+E4BAJGz\n" + + "ZMFF/iQ/rOcSAsHPp4ggezZALDIkT2Hrn6iLDdsLAAoJECwNPBI89RwIuBIBAMxG\n" + + "u/s4maOFozcO4JoCZTsLHGy70SG6UuVQjK0EyJJ1APoDEfK+qTlC7/FoijMA6Ew9\n" + + "aesZ2IHgpwA7jlyHSgwLDw==\n" + + "=H3HU\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + private static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: A2EC 077F C977 E15D D799 EFF9 2C0D 3C12 3CF5 1C08\n" + + "Comment: Alice \n" + + "\n" + + "mDMEY2veRhYJKwYBBAHaRw8BAQdAeJYBoCcnGPQ3nchyyBrWQ83q3hqJnfZn2mqh\n" + + "d1M7Wwu0HEFsaWNlIDxhbGljZUBwZ3BhaW5sZXNzLm9yZz6IjwQTFgoAQQUCY2ve\n" + + "RgkQLA08Ejz1HAgWIQSi7Ad/yXfhXdeZ7/ksDTwSPPUcCAKeAQKbAQUWAgMBAAQL\n" + + "CQgHBRUKCQgLApkBAAB7pgD/aw7jzdWLYWCzh6O/wXKTI73ohWsyhiHDc53gvJYT\n" + + "IN8BAOLXkvEkmbpPaxd0Q1WKB3AMmIQvGM61uaK+4m63GRYIuDgEY2veRhIKKwYB\n" + + "BAGXVQEFAQEHQJLNePJIeqWJzEbdCWtV5MirAqK0vWxazHQwIOZPULx0AwEIB4h1\n" + + "BBgWCgAdBQJja95GAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQLA08Ejz1HAjk\n" + + "+AEA2XDeWv3vEK/izQSMl1y4CVhrC+fZ7ChwWw1EZ8WxJzEBAMm0vLvkbG/NsluA\n" + + "n2gOAi0Uwt3g3OWOEMGFsy6IFjcIuDMEY2veRhYJKwYBBAHaRw8BAQdA3HcELn1i\n" + + "6ugEpLuCVgQXfSaZUVYStHF1rkxO97lpbbuI1QQYFgoAfQUCY2veRgKeAQKbAgUW\n" + + "AgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNr3kYACgkQ7NC/hj9lyaWVAwEA3ze1\n" + + "LCi1reGfB5tS3Au6A8aalyk4UV0iVOXxwV5r+E4BAJGzZMFF/iQ/rOcSAsHPp4gg\n" + + "ezZALDIkT2Hrn6iLDdsLAAoJECwNPBI89RwIuBIBAMxGu/s4maOFozcO4JoCZTsL\n" + + "HGy70SG6UuVQjK0EyJJ1APoDEfK+qTlC7/FoijMA6Ew9aesZ2IHgpwA7jlyHSgwL\n" + + "Dw==\n" + + "=c1PZ\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + @Test @FailOnSystemExit public void encryptAndDecryptAMessage() throws IOException { - originalSout = System.out; - File julietKeyFile = new File(tempDir, "juliet.key"); - assertTrue(julietKeyFile.createNewFile()); + // Juliets key and cert + File julietKeyFile = pipeStdoutToFile("juliet.key"); + assertSuccess(executeCommand("generate-key", "Juliet ")); - File julietCertFile = new File(tempDir, "juliet.asc"); - assertTrue(julietCertFile.createNewFile()); + pipeFileToStdin(julietKeyFile); + File julietCertFile = pipeStdoutToFile("juliet.cert"); + assertSuccess(executeCommand("extract-cert")); - File romeoKeyFile = new File(tempDir, "romeo.key"); - assertTrue(romeoKeyFile.createNewFile()); + // Romeos key and cert + File romeoKeyFile = pipeStdoutToFile("romeo.key"); + assertSuccess(executeCommand("generate-key", "Romeo ")); - File romeoCertFile = new File(tempDir, "romeo.asc"); - assertTrue(romeoCertFile.createNewFile()); - - File msgAscFile = new File(tempDir, "msg.asc"); - assertTrue(msgAscFile.createNewFile()); - - OutputStream julietKeyOut = new FileOutputStream(julietKeyFile); - System.setOut(new PrintStream(julietKeyOut)); - PGPainlessCLI.execute("generate-key", "Juliet Capulet "); - julietKeyOut.close(); - - FileInputStream julietKeyIn = new FileInputStream(julietKeyFile); - System.setIn(julietKeyIn); - OutputStream julietCertOut = new FileOutputStream(julietCertFile); - System.setOut(new PrintStream(julietCertOut)); - PGPainlessCLI.execute("extract-cert"); - julietKeyIn.close(); - julietCertOut.close(); - - OutputStream romeoKeyOut = new FileOutputStream(romeoKeyFile); - System.setOut(new PrintStream(romeoKeyOut)); - PGPainlessCLI.execute("generate-key", "Romeo Montague "); - romeoKeyOut.close(); - - FileInputStream romeoKeyIn = new FileInputStream(romeoKeyFile); - System.setIn(romeoKeyIn); - OutputStream romeoCertOut = new FileOutputStream(romeoCertFile); - System.setOut(new PrintStream(romeoCertOut)); - PGPainlessCLI.execute("extract-cert"); - romeoKeyIn.close(); - romeoCertOut.close(); + File romeoCertFile = pipeStdoutToFile("romeo.cert"); + pipeFileToStdin(romeoKeyFile); + assertSuccess(executeCommand("extract-cert")); + // Romeo encrypts signs and encrypts for Juliet and himself String msg = "Hello World!\n"; - ByteArrayInputStream msgIn = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); - System.setIn(msgIn); - OutputStream msgAscOut = new FileOutputStream(msgAscFile); - System.setOut(new PrintStream(msgAscOut)); - PGPainlessCLI.execute("encrypt", - "--sign-with", romeoKeyFile.getAbsolutePath(), - julietCertFile.getAbsolutePath()); - msgAscOut.close(); + File encryptedMessageFile = pipeStdoutToFile("msg.asc"); + pipeStringToStdin(msg); + assertSuccess(executeCommand("encrypt", "--sign-with", romeoKeyFile.getAbsolutePath(), + julietCertFile.getAbsolutePath(), romeoCertFile.getAbsolutePath())); - File verifyFile = new File(tempDir, "verify.txt"); - - FileInputStream msgAscIn = new FileInputStream(msgAscFile); - System.setIn(msgAscIn); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - PrintStream pOut = new PrintStream(out); - System.setOut(pOut); - PGPainlessCLI.execute("decrypt", - "--verify-out", verifyFile.getAbsolutePath(), + // Juliet can decrypt and verify with Romeos cert + pipeFileToStdin(encryptedMessageFile); + File verificationsFile = nonExistentFile("verifications"); + ByteArrayOutputStream decrypted = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with", romeoCertFile.getAbsolutePath(), - julietKeyFile.getAbsolutePath()); - msgAscIn.close(); + julietKeyFile.getAbsolutePath())); + assertEquals(msg, decrypted.toString()); + + // Romeo can decrypt and verify too + pipeFileToStdin(encryptedMessageFile); + File anotherVerificationsFile = nonExistentFile("anotherVerifications"); + decrypted = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--verifications-out", anotherVerificationsFile.getAbsolutePath(), + "--verify-with", romeoCertFile.getAbsolutePath(), + romeoKeyFile.getAbsolutePath())); + assertEquals(msg, decrypted.toString()); + + String julietsVerif = readStringFromFile(verificationsFile); + String romeosVerif = readStringFromFile(anotherVerificationsFile); + assertEquals(julietsVerif, romeosVerif); + assertFalse(julietsVerif.isEmpty()); + assertEquals(103, julietsVerif.length()); // 103 is number of symbols in [DATE, FINGER, FINGER] for V4 + } + + @Test + public void testMissingArgumentsIfNoArgsSupplied() throws IOException { + int exit = executeCommand("encrypt"); + assertEquals(SOPGPException.MissingArg.EXIT_CODE, exit); + } + + @Test + public void testEncryptingForKeyFails() throws IOException { + File notACert = writeFile("key.asc", KEY); + + pipeStringToStdin("Hello, World!"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("encrypt", notACert.getAbsolutePath()); + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testEncrypt_SignWithCertFails() throws IOException { + File cert = writeFile("cert.asc", CERT); + // noinspection UnnecessaryLocalVariable + File notAKey = cert; + + pipeStringToStdin("Hello, World!"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("encrypt", "--sign-with", notAKey.getAbsolutePath(), cert.getAbsolutePath()); + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testDecryptVerifyOut_withoutVerifyWithFails() throws IOException { + File key = writeFile("key.asc", KEY); + + File verifications = nonExistentFile("verifications"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("decrypt", "--verifications-out", verifications.getAbsolutePath(), key.getAbsolutePath()); + + assertEquals(SOPGPException.IncompleteVerification.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testVerificationsOutAlreadyExistFails() throws IOException { + File key = writeFile("key.asc", KEY); + File cert = writeFile("cert.asc", CERT); + + File verifications = writeFile("verifications", "this file is not empty"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("decrypt", "--verify-with", cert.getAbsolutePath(), + "--verifications-out", verifications.getAbsolutePath(), + key.getAbsolutePath()); + + assertEquals(SOPGPException.OutputExists.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testSessionKeyOutWritesSessionKeyOut() throws IOException { + File key = writeFile("key.asc", KEY); + File sessionKeyFile = nonExistentFile("session.key"); + + String plaintext = "Hello, World!\n"; + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + String sessionKey = "9:B6FAD96B7ED2DA27D8A36EAEA75DAB7AC587180B14D8A24BD7263524F3DDECC3\n"; + + pipeStringToStdin(ciphertext); + ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(), key.getAbsolutePath())); + + assertEquals(plaintext, plaintextOut.toString()); + String resultSessionKey = readStringFromFile(sessionKeyFile); + assertEquals(sessionKey, resultSessionKey); + } + + @Test + public void decryptMessageWithSessionKey() throws IOException { + String plaintext = "Hello, World!\n"; + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + String sessionKey = "9:B6FAD96B7ED2DA27D8A36EAEA75DAB7AC587180B14D8A24BD7263524F3DDECC3\n"; + + File sessionKeyFile = writeFile("session.key", sessionKey); + + pipeStringToStdin(ciphertext); + ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--with-session-key", sessionKeyFile.getAbsolutePath())); + + assertEquals(plaintext, plaintextOut.toString()); + } + + @Test + public void testDecryptWithSessionKeyVerifyWithYieldsExpectedVerifications() throws IOException { + String plaintext = "Hello, World!\n"; + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + String sessionKey = "9:B6FAD96B7ED2DA27D8A36EAEA75DAB7AC587180B14D8A24BD7263524F3DDECC3\n"; + + File cert = writeFile("cert.asc", CERT); + File sessionKeyFile = writeFile("session.key", sessionKey); + File verifications = nonExistentFile("verifications"); + + pipeStringToStdin(ciphertext); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--with-session-key", sessionKeyFile.getAbsolutePath(), + "--verify-with", cert.getAbsolutePath(), "--verifications-out", verifications.getAbsolutePath())); + + assertEquals(plaintext, out.toString()); + String verificationString = readStringFromFile(verifications); + assertEquals("2022-11-09T17:22:48Z C0DCEC44B1A173664B05DABCECD0BF863F65C9A5 A2EC077FC977E15DD799EFF92C0D3C123CF51C08\n", + verificationString); + } + + @Test + public void encryptAndDecryptMessageWithPassphrase() throws IOException { + File passwordFile = writeFile("password", "c1tizâ‚Ŧn4"); + String message = "I cannot think of meaningful messages for test vectors rn"; + + pipeStringToStdin(message); + ByteArrayOutputStream ciphertext = pipeStdoutToStream(); + assertSuccess(executeCommand("encrypt", "--with-password", passwordFile.getAbsolutePath())); + + String ciphertextString = ciphertext.toString(); + assertTrue(ciphertextString.startsWith("-----BEGIN PGP MESSAGE-----\n")); + + pipeBytesToStdin(ciphertext.toByteArray()); + ByteArrayOutputStream plaintext = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--with-password", passwordFile.getAbsolutePath())); + + assertEquals(message, plaintext.toString()); + } + + @Test + public void testEncryptWithIncapableCert() throws PGPException, + InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .addUserId("No Crypt ") + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .build(); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); + File certFile = writeFile("cert.pgp", cert.getEncoded()); + + pipeStringToStdin("Hello, World!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("encrypt", certFile.getAbsolutePath()); + + assertEquals(SOPGPException.CertCannotEncrypt.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testSignWithIncapableKey() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .addUserId("Cannot Sign ") + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .build(); + File keyFile = writeFile("key.pgp", secretKeys.getEncoded()); + File certFile = writeFile("cert.pgp", PGPainless.extractCertificate(secretKeys).getEncoded()); + + pipeStringToStdin("Hello, World!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("encrypt", "--sign-with", keyFile.getAbsolutePath(), + certFile.getAbsolutePath()); + + assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testEncryptDecryptRoundTripWithPasswordProtectedKey() throws IOException { + // generate password protected key + File passwordFile = writeFile("password", "fooBarBaz420"); + File keyFile = pipeStdoutToFile("key.asc"); + assertSuccess(executeCommand("generate-key", + "--with-key-password", passwordFile.getAbsolutePath(), + "Pascal Password ")); + + // extract cert + File certFile = pipeStdoutToFile("cert.asc"); + pipeFileToStdin(keyFile); + assertSuccess(executeCommand("extract-cert")); + + // encrypt and sign message + String msg = "Hello, World!\n"; + pipeStringToStdin(msg); + File encryptedFile = pipeStdoutToFile("msg.asc"); + assertSuccess(executeCommand("encrypt", + "--sign-with", keyFile.getAbsolutePath(), + "--with-key-password", passwordFile.getAbsolutePath(), + "--no-armor", + "--as", "binary", + certFile.getAbsolutePath())); + + // Decrypt + File verificationsFile = nonExistentFile("verifications"); + pipeFileToStdin(encryptedFile); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", + "--verify-with", certFile.getAbsolutePath(), + "--verifications-out", verificationsFile.getAbsolutePath(), + "--with-key-password", passwordFile.getAbsolutePath(), + keyFile.getAbsolutePath())); assertEquals(msg, out.toString()); } - @AfterAll - public static void after() { - System.setOut(originalSout); - // CHECKSTYLE:OFF - System.out.println(tempDir.getAbsolutePath()); - // CHECKSTYLE:ON + @Test + public void decryptGarbageFails() throws IOException { + File keyFile = writeFile("key.asc", KEY); + pipeStringToStdin("Some Garbage!"); + int exitCode = executeCommand("decrypt", keyFile.getAbsolutePath()); + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + } + + @Test + public void decryptMessageWithWrongKeyFails() throws IOException { + File keyFile = pipeStdoutToFile("key.asc"); + assertSuccess(executeCommand("generate-key", "Bob ")); + // message was *not* created with key above + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + + pipeStringToStdin(ciphertext); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("decrypt", keyFile.getAbsolutePath()); + assertEquals(SOPGPException.CannotDecrypt.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void encryptWithPasswordADecryptWithPasswordBFails() throws IOException { + File password1 = writeFile("password1", "swordfish"); + File password2 = writeFile("password2", "orange"); + + pipeStringToStdin("Bonjour, le monde!\n"); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("encrypt", "--with-password", password1.getAbsolutePath())); + + pipeBytesToStdin(ciphertextOut.toByteArray()); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("decrypt", "--with-password", password2.getAbsolutePath()); + assertEquals(SOPGPException.CannotDecrypt.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void encryptWithGarbageCertFails() throws IOException { + File garbageCert = writeFile("cert.asc", "This is garbage!"); + + pipeStringToStdin("Hallo, Welt!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("encrypt", garbageCert.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void encrypt_signWithGarbageKeyFails() throws IOException { + File cert = writeFile("cert.asc", CERT); + File garbageKey = writeFile("key.asc", "This is not a key!"); + + pipeStringToStdin("Salut!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("encrypt", "--sign-with", garbageKey.getAbsolutePath(), + cert.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void decrypt_withGarbageKeyFails() throws IOException { + File key = writeFile("key.asc", "this is garbage"); + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + + pipeStringToStdin(ciphertext); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("decrypt", key.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void decrypt_verifyWithGarbageCertFails() throws IOException { + File key = writeFile("key.asc", KEY); + File cert = writeFile("cert.asc", "now this is garbage"); + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + File verificationsFile = nonExistentFile("verifications"); + + pipeStringToStdin(ciphertext); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("decrypt", key.getAbsolutePath(), + "--verify-with", cert.getAbsolutePath(), + "--verifications-out", verificationsFile.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void encryptWithProtectedKey_wrongPassphraseFails() throws IOException { + File password = writeFile("passphrase1", "orange"); + File wrongPassword = writeFile("passphrase2", "blue"); + + File keyFile = pipeStdoutToFile("key.asc"); + assertSuccess(executeCommand("generate-key", "Pedro ", + "--with-key-password", password.getAbsolutePath())); + + File certFile = pipeStdoutToFile("cert.asc"); + pipeFileToStdin(keyFile); + assertSuccess(executeCommand("extract-cert")); + + // Use no passphrase to unlock the key + String msg = "Guten Tag, Welt!\n"; + pipeStringToStdin(msg); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("encrypt", "--sign-with", keyFile.getAbsolutePath(), + certFile.getAbsolutePath()); + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + + // use wrong passphrase to unlock key when signing message + pipeStringToStdin("Guten Tag, Welt!\n"); + out = pipeStdoutToStream(); + exitCode = executeCommand("encrypt", "--sign-with", keyFile.getAbsolutePath(), + "--with-key-password", wrongPassword.getAbsolutePath(), + certFile.getAbsolutePath()); + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + + // use correct passphrase and encrypt+sign message + pipeStringToStdin("Guten Tag, Welt!\n"); + out = pipeStdoutToStream(); + assertSuccess(executeCommand("encrypt", "--sign-with", keyFile.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath(), + certFile.getAbsolutePath())); + String ciphertext = out.toString(); + + // Use no passphrase to decrypt key when decrypting + pipeStringToStdin(ciphertext); + out = pipeStdoutToStream(); + exitCode = executeCommand("decrypt", keyFile.getAbsolutePath()); + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + + // Use wrong passphrase to decrypt key when decrypting + pipeStringToStdin(ciphertext); + out = pipeStdoutToStream(); + exitCode = executeCommand("decrypt", "--with-key-password", wrongPassword.getAbsolutePath(), + keyFile.getAbsolutePath()); + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + + // User correct passphrase to decrypt key when decrypting + pipeStringToStdin(ciphertext); + out = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--with-key-password", password.getAbsolutePath(), + keyFile.getAbsolutePath())); + assertEquals(msg, out.toString()); } } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java new file mode 100644 index 00000000..c80019d1 --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -0,0 +1,236 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { + + public RoundTripInlineSignInlineVerifyCmdTest() { + super(LoggerFactory.getLogger(RoundTripInlineSignInlineVerifyCmdTest.class)); + } + + private static final String KEY_1_PASSWORD = "takeDemHobbits2Isengard"; + private static final String KEY_1 = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 59F4 EC7D 4A87 3E69 7029 8FDE 9FF0 8738 DFC0 0224\n" + + "Comment: Legolas \n" + + "\n" + + "lIYEY2wKdxYJKwYBBAHaRw8BAQdALfUbOSOsPDg4IgX7Mrub3EtkX0rp02orL/0j\n" + + "2VpV1rf+CQMCVICwUO0SkvdgcPdvXO1cW4KIp6HCVVV6VgU5cvBlmrk9PNUQVBkb\n" + + "6S7oXQu0CgGwJ+QdbooBQqOjMy2MDy+UXaURTaVyWcmetsZJZzD2wrQhTGVnb2xh\n" + + "cyA8bGVnb2xhc0BmZWxsb3dzaGlwLnJpbmc+iI8EExYKAEEFAmNsCncJEJ/whzjf\n" + + "wAIkFiEEWfTsfUqHPmlwKY/en/CHON/AAiQCngECmwEFFgIDAQAECwkIBwUVCgkI\n" + + "CwKZAQAAE10BAN9tN4Le1p4giS6P/yFuKFlDBOeiq1S4EqwYG7qdcqemAP45O3w4\n" + + "3sXliOJBGDR/l/lOMHdPcTOb7VRwWbpIqx8LBJyLBGNsCncSCisGAQQBl1UBBQEB\n" + + "B0AMc+7s6uBqAQcDvfKkD5zYbmB9ZfwIjRWQq/XF+g8KQwMBCAf+CQMCVICwUO0S\n" + + "kvdgHLmKhKW1xxCNZAqQcIHa9F/cqb6Sq/oVFHj2bEYzmGVvFCVUpP7KJWGTeFT+\n" + + "BYK779quIqjxHOfzC3Jmo3BHkUPWYOa0rIh1BBgWCgAdBQJjbAp3Ap4BApsMBRYC\n" + + "AwEABAsJCAcFFQoJCAsACgkQn/CHON/AAiRUewD9HtKrCUf3S1yR28emzITWPgJS\n" + + "UA5mkzEMnYspV7zU4jgA/R6jj/5QqPszElCQNZGtvsDUwYo10iRlQkxPshcPNakJ\n" + + "nIYEY2wKdxYJKwYBBAHaRw8BAQdAYxpRGib/f/tu65gbsV22nmysVVmVgiQuDxyH\n" + + "rz7VCi/+CQMCVICwUO0SkvdgOYYbWltjQRDM3SW/Zw/DiZN9MYZYa0MTgs0SHoaM\n" + + "5LU7jMxNmPR1UtSqEO36QqW91q4fpEkGrdWE4gwjm1bth8pyYKiSFojVBBgWCgB9\n" + + "BQJjbAp3Ap4BApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUCY2wKdwAKCRCW\n" + + "K491s9xIMHwKAQDpSWQqiFxFvls9eRGtJ1eQT+L3Z2rDel5zNV44IdTf/wEA0vnJ\n" + + "ouSKKuiH6Ck2OEkXbElH6gdQvOCYA7Z9gVeeHQoACgkQn/CHON/AAiSD6QD+LTZx\n" + + "NU+t4wQlWOkSsjOLsH/Sk5DZq+4HyQnStlxUJpUBALZFkZps65IP03VkPnQWigfs\n" + + "YgztJA1z/rmm3fmFgMMG\n" + + "=daDH\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + private static final String CERT_1 = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 59F4 EC7D 4A87 3E69 7029 8FDE 9FF0 8738 DFC0 0224\n" + + "Comment: Legolas \n" + + "\n" + + "mDMEY2wKdxYJKwYBBAHaRw8BAQdALfUbOSOsPDg4IgX7Mrub3EtkX0rp02orL/0j\n" + + "2VpV1re0IUxlZ29sYXMgPGxlZ29sYXNAZmVsbG93c2hpcC5yaW5nPoiPBBMWCgBB\n" + + "BQJjbAp3CRCf8Ic438ACJBYhBFn07H1Khz5pcCmP3p/whzjfwAIkAp4BApsBBRYC\n" + + "AwEABAsJCAcFFQoJCAsCmQEAABNdAQDfbTeC3taeIIkuj/8hbihZQwTnoqtUuBKs\n" + + "GBu6nXKnpgD+OTt8ON7F5YjiQRg0f5f5TjB3T3Ezm+1UcFm6SKsfCwS4OARjbAp3\n" + + "EgorBgEEAZdVAQUBAQdADHPu7OrgagEHA73ypA+c2G5gfWX8CI0VkKv1xfoPCkMD\n" + + "AQgHiHUEGBYKAB0FAmNsCncCngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRCf8Ic4\n" + + "38ACJFR7AP0e0qsJR/dLXJHbx6bMhNY+AlJQDmaTMQydiylXvNTiOAD9HqOP/lCo\n" + + "+zMSUJA1ka2+wNTBijXSJGVCTE+yFw81qQm4MwRjbAp3FgkrBgEEAdpHDwEBB0Bj\n" + + "GlEaJv9/+27rmBuxXbaebKxVWZWCJC4PHIevPtUKL4jVBBgWCgB9BQJjbAp3Ap4B\n" + + "ApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUCY2wKdwAKCRCWK491s9xIMHwK\n" + + "AQDpSWQqiFxFvls9eRGtJ1eQT+L3Z2rDel5zNV44IdTf/wEA0vnJouSKKuiH6Ck2\n" + + "OEkXbElH6gdQvOCYA7Z9gVeeHQoACgkQn/CHON/AAiSD6QD+LTZxNU+t4wQlWOkS\n" + + "sjOLsH/Sk5DZq+4HyQnStlxUJpUBALZFkZps65IP03VkPnQWigfsYgztJA1z/rmm\n" + + "3fmFgMMG\n" + + "=/lYl\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + private static final String CERT_1_SIGNING_KEY = + "D8906FEB9842569834FEDA9E962B8F75B3DC4830 59F4EC7D4A873E6970298FDE9FF08738DFC00224"; + + private static final String KEY_2 = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: AEA0 FD2C 899D 3FC0 7781 5F00 2656 0D2A E53D B86F\n" + + "Comment: Gollum \n" + + "\n" + + "lFgEY2wKphYJKwYBBAHaRw8BAQdA9MXACulaJvjIuMKbsc+/fLJ523lODbHmuTpc\n" + + "jpPdjaEAAP9Edg7yeIGEeNP0GrndUpNeZyFAXAlCHJObDbS80G6BBw9ktBlHb2xs\n" + + "dW0gPGdvbGx1bUBkZWVwLmNhdmU+iI8EExYKAEEFAmNsCqYJECZWDSrlPbhvFiEE\n" + + "rqD9LImdP8B3gV8AJlYNKuU9uG8CngECmwEFFgIDAQAECwkIBwUVCgkICwKZAQAA\n" + + "KSkBAOMq6ymNH83E5CBA/mn3DYLhnujzC9cVf/iX2zrsdXMvAQCWdfFy/PlGhP3K\n" + + "M+ej6WIRsx24Yy/NhNPcRJUzcv6dC5xdBGNsCqYSCisGAQQBl1UBBQEBB0DiN/5n\n" + + "AFQafWjnSkKhctFCNkfVRrnAea/2T/D8fYWeYwMBCAcAAP9HbxOhwxqz8I+pwk3e\n" + + "kZXNolWqagrYZkpNvqlBb/JJWBGViHUEGBYKAB0FAmNsCqYCngECmwwFFgIDAQAE\n" + + "CwkIBwUVCgkICwAKCRAmVg0q5T24bw2EAP4pUHVA2pkVspzEttIaQxdoHcnbwjae\n" + + "q12TmWqWDFFvwgD+O2EqHn0iXW49EOQrlP8g+bdWUlT0ZIW3C3Fv7nNA3AScWARj\n" + + "bAqmFgkrBgEEAdpHDwEBB0BHsmdF1Q0aU3YRVDeXGb904Nb7H/cxcasDhcbu2FTo\n" + + "HAAA/j1+WzozN/3lefo76eyENKkXl4f1rQlUreqytuaTsb0WEq6I1QQYFgoAfQUC\n" + + "Y2wKpgKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNsCqYACgkQj73T\n" + + "bQGDFnN9OwD/QDDi1qq7DrGlENQf2mPDh36YgM7bREY1vHEbbUNoqy4A/RJzMuwt\n" + + "L1M49UzQS7OIGP12/9cT66XPGjpCL+6zLPwCAAoJECZWDSrlPbhvw3ABAOE7/Iit\n" + + "ntMexrSK5jCd9JdCCNb2rjR6XA18rXFGOrVBAPwLKAogNFQlP2kUsObTnIaTCro2\n" + + "cjK8WE1pfIwQ0ArPCQ==\n" + + "=SzrG\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + private static final String CERT_2 = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: AEA0 FD2C 899D 3FC0 7781 5F00 2656 0D2A E53D B86F\n" + + "Comment: Gollum \n" + + "\n" + + "mDMEY2wKphYJKwYBBAHaRw8BAQdA9MXACulaJvjIuMKbsc+/fLJ523lODbHmuTpc\n" + + "jpPdjaG0GUdvbGx1bSA8Z29sbHVtQGRlZXAuY2F2ZT6IjwQTFgoAQQUCY2wKpgkQ\n" + + "JlYNKuU9uG8WIQSuoP0siZ0/wHeBXwAmVg0q5T24bwKeAQKbAQUWAgMBAAQLCQgH\n" + + "BRUKCQgLApkBAAApKQEA4yrrKY0fzcTkIED+afcNguGe6PML1xV/+JfbOux1cy8B\n" + + "AJZ18XL8+UaE/coz56PpYhGzHbhjL82E09xElTNy/p0LuDgEY2wKphIKKwYBBAGX\n" + + "VQEFAQEHQOI3/mcAVBp9aOdKQqFy0UI2R9VGucB5r/ZP8Px9hZ5jAwEIB4h1BBgW\n" + + "CgAdBQJjbAqmAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQJlYNKuU9uG8NhAD+\n" + + "KVB1QNqZFbKcxLbSGkMXaB3J28I2nqtdk5lqlgxRb8IA/jthKh59Il1uPRDkK5T/\n" + + "IPm3VlJU9GSFtwtxb+5zQNwEuDMEY2wKphYJKwYBBAHaRw8BAQdAR7JnRdUNGlN2\n" + + "EVQ3lxm/dODW+x/3MXGrA4XG7thU6ByI1QQYFgoAfQUCY2wKpgKeAQKbAgUWAgMB\n" + + "AAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNsCqYACgkQj73TbQGDFnN9OwD/QDDi1qq7\n" + + "DrGlENQf2mPDh36YgM7bREY1vHEbbUNoqy4A/RJzMuwtL1M49UzQS7OIGP12/9cT\n" + + "66XPGjpCL+6zLPwCAAoJECZWDSrlPbhvw3ABAOE7/IitntMexrSK5jCd9JdCCNb2\n" + + "rjR6XA18rXFGOrVBAPwLKAogNFQlP2kUsObTnIaTCro2cjK8WE1pfIwQ0ArPCQ==\n" + + "=j1LR\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + private static final String CERT_2_SIGNING_KEY = + "7A073EDF273C902796D259528FBDD36D01831673 AEA0FD2C899D3FC077815F0026560D2AE53DB86F"; + + private static final String MESSAGE = "One does not simply use OpenPGP!\n" + + "\n" + + "There is only one Lord of the Keys, only one who can bend them to his will. And he does not share power."; + + @Test + public void createCleartextSignedMessage() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password", KEY_1_PASSWORD); + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + "--as", "cleartextsigned", + key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath())); + + String cleartextSigned = ciphertextOut.toString(); + assertTrue(cleartextSigned.startsWith("-----BEGIN PGP SIGNED MESSAGE-----\n" + + "Hash: ")); + assertTrue(cleartextSigned.contains(MESSAGE)); + assertTrue(cleartextSigned.contains("\n-----BEGIN PGP SIGNATURE-----\n")); + assertTrue(cleartextSigned.endsWith("-----END PGP SIGNATURE-----\n")); + } + + @Test + public void createAndVerifyCleartextSignedMessage() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password", KEY_1_PASSWORD); + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + "--as", "cleartextsigned", + key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath())); + + File cert = writeFile("cert.asc", CERT_1); + File verifications = nonExistentFile("verifications"); + pipeStringToStdin(ciphertextOut.toString()); + ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-verify", + "--verifications-out", verifications.getAbsolutePath(), + cert.getAbsolutePath())); + + assertEquals(MESSAGE, plaintextOut.toString()); + String verificationString = readStringFromFile(verifications); + assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); + } + + @Test + public void createAndVerifyMultiKeyBinarySignedMessage() throws IOException { + File key1Pass = writeFile("password", KEY_1_PASSWORD); + File key1 = writeFile("key1.asc", KEY_1); + File key2 = writeFile("key2.asc", KEY_2); + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + "--as", "binary", + "--no-armor", + key2.getAbsolutePath(), + "--with-key-password", key1Pass.getAbsolutePath(), + key1.getAbsolutePath())); + + assertFalse(ciphertextOut.toString().startsWith("-----BEGIN PGP SIGNED MESSAGE-----\n")); + byte[] unarmoredMessage = ciphertextOut.toByteArray(); + + File cert1 = writeFile("cert1.asc", CERT_1); + File cert2 = writeFile("cert2.asc", CERT_2); + File verificationFile = nonExistentFile("verifications"); + pipeBytesToStdin(unarmoredMessage); + ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-verify", + "--verifications-out", verificationFile.getAbsolutePath(), + cert1.getAbsolutePath(), cert2.getAbsolutePath())); + + assertEquals(MESSAGE, plaintextOut.toString()); + String verification = readStringFromFile(verificationFile); + assertTrue(verification.contains(CERT_1_SIGNING_KEY)); + assertTrue(verification.contains(CERT_2_SIGNING_KEY)); + } + + @Test + public void createTextSignedMessageInlineDetachAndDetachedVerify() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password", KEY_1_PASSWORD); + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + "--as", "cleartextsigned", + key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath())); + + File sigFile = nonExistentFile("sig.asc"); + pipeStringToStdin(ciphertextOut.toString()); + ByteArrayOutputStream msgOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-detach", + "--signatures-out", sigFile.getAbsolutePath())); + assertEquals(MESSAGE, msgOut.toString()); + + File cert = writeFile("cert.asc", CERT_1); + pipeStringToStdin(msgOut.toString()); + ByteArrayOutputStream verificationsOut = pipeStdoutToStream(); + assertSuccess(executeCommand("verify", + sigFile.getAbsolutePath(), + cert.getAbsolutePath())); + + String verificationString = verificationsOut.toString(); + assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); + } +} diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java index 0fce1273..7f420054 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java @@ -5,129 +5,321 @@ package org.pgpainless.cli.commands; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileReader; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Date; -import com.ginsberg.junit.exit.FailOnSystemExit; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; -import org.pgpainless.cli.PGPainlessCLI; -import org.pgpainless.cli.TestUtils; +import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.util.KeyRingUtils; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; +import sop.util.UTCUtil; -public class RoundTripSignVerifyCmdTest { +public class RoundTripSignVerifyCmdTest extends CLITest { - private static File tempDir; - private static PrintStream originalSout; + public RoundTripSignVerifyCmdTest() { + super(LoggerFactory.getLogger(RoundTripSignVerifyCmdTest.class)); + } - @BeforeAll - public static void prepare() throws IOException { - tempDir = TestUtils.createTempDirectory(); + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9DA0 9423 C9F9 4BA4 CCA3 0951 099B 11BF 296A 373E\n" + + "Comment: Sigmund \n" + + "\n" + + "lFgEY2vzkhYJKwYBBAHaRw8BAQdA+Z2OAFQf0k64Au7hIZfXh/ijclabddvwh7Nh\n" + + "kedJ3ZUAAQCZy5p1cvQvRIWUopHwhnrD/oVAa1dNT/nA3cihQ5gkZBHPtCBTaWdt\n" + + "dW5kIDxzaWdtdW5kQHBncGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJja/OSCRAJmxG/\n" + + "KWo3PhYhBJ2glCPJ+UukzKMJUQmbEb8pajc+Ap4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAACM9AP9APloI2waD5gXsJqzenRVU4n/VmZUvcdUyhlbpab/0HQEAlaTw\n" + + "ZvxVyaf8EMFSJOY+LcgacHaZDHRPA1nS3bIfKwycXQRja/OSEgorBgEEAZdVAQUB\n" + + "AQdA1WL4QKgRxbvzW91ICM6PoICSNh2QHK6j0pIdN/cqXz0DAQgHAAD/bOk3WqbF\n" + + "QAE8xxm0w/KDZzL1N0yPcBQ5z4XKmu77FCgQ04h1BBgWCgAdBQJja/OSAp4BApsM\n" + + "BRYCAwEABAsJCAcFFQoJCAsACgkQCZsRvylqNz6rgQEAzoG6HnPCYi2i2c6/ufuy\n" + + "pBkLby2u1JjD0CWSbrM4dZ0A/j/pI4a9b8LcrZcuY2QwHqsXPAJp8QtOOQN6gTvN\n" + + "WcQNnFgEY2vzkhYJKwYBBAHaRw8BAQdAsxcDCvst/GbWxQvvOpChSvmbqWeuBgm3\n" + + "1vRoujFVFcYAAP9Ww46yfWipb8OivTSX+PvgdUhEeVgxENpsyOQLLhQP/RFziNUE\n" + + "GBYKAH0FAmNr85ICngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJja/OS\n" + + "AAoJENqfQTmGIR3GtsMBAL+b1Zo5giQKJGEyx5aGwAz3AwtGiT6QDS9FH6HyM855\n" + + "AP4uAXDiaNxYTugqnG471jYX/hhJqIROeDGrEIkkAp+qDwAKCRAJmxG/KWo3PhOX\n" + + "AP45LPV6I4+D3h8etdiEA2DVvNcpRA8l4WkNcq4q8H1SjwD/c/rX3FCUIWLlAHoR\n" + + "WxCFj+gDgqDNLzwoA4iNo1VMtQc=\n" + + "=/Np6\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9DA0 9423 C9F9 4BA4 CCA3 0951 099B 11BF 296A 373E\n" + + "Comment: Sigmund \n" + + "\n" + + "mDMEY2vzkhYJKwYBBAHaRw8BAQdA+Z2OAFQf0k64Au7hIZfXh/ijclabddvwh7Nh\n" + + "kedJ3ZW0IFNpZ211bmQgPHNpZ211bmRAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEF\n" + + "AmNr85IJEAmbEb8pajc+FiEEnaCUI8n5S6TMowlRCZsRvylqNz4CngECmwEFFgID\n" + + "AQAECwkIBwUVCgkICwKZAQAAIz0A/0A+WgjbBoPmBewmrN6dFVTif9WZlS9x1TKG\n" + + "Vulpv/QdAQCVpPBm/FXJp/wQwVIk5j4tyBpwdpkMdE8DWdLdsh8rDLg4BGNr85IS\n" + + "CisGAQQBl1UBBQEBB0DVYvhAqBHFu/Nb3UgIzo+ggJI2HZAcrqPSkh039ypfPQMB\n" + + "CAeIdQQYFgoAHQUCY2vzkgKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEAmbEb8p\n" + + "ajc+q4EBAM6Buh5zwmItotnOv7n7sqQZC28trtSYw9Alkm6zOHWdAP4/6SOGvW/C\n" + + "3K2XLmNkMB6rFzwCafELTjkDeoE7zVnEDbgzBGNr85IWCSsGAQQB2kcPAQEHQLMX\n" + + "Awr7Lfxm1sUL7zqQoUr5m6lnrgYJt9b0aLoxVRXGiNUEGBYKAH0FAmNr85ICngEC\n" + + "mwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJja/OSAAoJENqfQTmGIR3GtsMB\n" + + "AL+b1Zo5giQKJGEyx5aGwAz3AwtGiT6QDS9FH6HyM855AP4uAXDiaNxYTugqnG47\n" + + "1jYX/hhJqIROeDGrEIkkAp+qDwAKCRAJmxG/KWo3PhOXAP45LPV6I4+D3h8etdiE\n" + + "A2DVvNcpRA8l4WkNcq4q8H1SjwD/c/rX3FCUIWLlAHoRWxCFj+gDgqDNLzwoA4iN\n" + + "o1VMtQc=\n" + + "=KuJ4\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + private static final String PLAINTEXT = "Hello, World!\n"; + private static final String BINARY_SIG = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: PGPainless\n" + + "\n" + + "iHUEABYKACcFAmNr9BgJENqfQTmGIR3GFiEEREwQqwEe+EJMg/Cp2p9BOYYhHcYA\n" + + "AKocAP48P2C3TU33T3Zy73clw0eBa1oW9pwxTGuFxhgOBzmoSwEArj0781GlpTB0\n" + + "Vnr2PjPYEqzB+ZuOzOnGhsVGob4c3Ao=\n" + + "=VWAZ\n" + + "-----END PGP SIGNATURE-----"; + private static final String BINARY_SIG_VERIFICATION = + "2022-11-09T18:40:24Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E\n"; + private static final String TEXT_SIG = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: PGPainless\n" + + "\n" + + "iHUEARYKACcFAmNr9E4JENqfQTmGIR3GFiEEREwQqwEe+EJMg/Cp2p9BOYYhHcYA\n" + + "AG+CAQD1B3GAAlyxahSiGhvJv7YAI1m6qGcI7dIXcV7FkAFPSgEAlZ0UpCC8oGR+\n" + + "hi/mQlex4z0hDWSA4abAjclPTJ+qkAI=\n" + + "=s5xn\n" + + "-----END PGP SIGNATURE-----"; + private static final String TEXT_SIG_VERIFICATION = + "2022-11-09T18:41:18Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E\n"; + private static final Date TEXT_SIG_CREATION = UTCUtil.parseUTCDate("2022-11-09T18:41:18Z"); + + @Test + public void createArmoredSignature() throws IOException { + File keyFile = writeFile("key.asc", KEY); + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("sign", "--as", "text", keyFile.getAbsolutePath())); + assertTrue(out.toString().startsWith("-----BEGIN PGP SIGNATURE-----\n")); } @Test - @FailOnSystemExit - public void testSignatureCreationAndVerification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - originalSout = System.out; - InputStream originalIn = System.in; + public void createUnarmoredSignature() throws IOException { + File keyFile = writeFile("key.asc", KEY); + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("sign", "--no-armor", keyFile.getAbsolutePath())); + assertFalse(out.toString().startsWith("-----BEGIN PGP SIGNATURE-----\n")); + } - // Write alice key to disc - File aliceKeyFile = new File(tempDir, "alice.key"); - assertTrue(aliceKeyFile.createNewFile()); - PGPSecretKeyRing aliceKeys = PGPainless.generateKeyRing() - .modernKeyRing("alice"); - OutputStream aliceKeyOut = new FileOutputStream(aliceKeyFile); - Streams.pipeAll(new ByteArrayInputStream(aliceKeys.getEncoded()), aliceKeyOut); - aliceKeyOut.close(); + @Test + public void unarmorArmoredSigAndVerify() throws IOException { + File certFile = writeFile("cert.asc", CERT); - // Write alice pub key to disc - File aliceCertFile = new File(tempDir, "alice.pub"); - assertTrue(aliceCertFile.createNewFile()); - PGPPublicKeyRing alicePub = KeyRingUtils.publicKeyRingFrom(aliceKeys); - OutputStream aliceCertOut = new FileOutputStream(aliceCertFile); - Streams.pipeAll(new ByteArrayInputStream(alicePub.getEncoded()), aliceCertOut); - aliceCertOut.close(); + pipeStringToStdin(BINARY_SIG); + File unarmoredSigFile = pipeStdoutToFile("sig.pgp"); + assertSuccess(executeCommand("dearmor")); - // Write test data to disc - String data = "If privacy is outlawed, only outlaws will have privacy.\n"; - File dataFile = new File(tempDir, "data"); - assertTrue(dataFile.createNewFile()); - FileOutputStream dataOut = new FileOutputStream(dataFile); - Streams.pipeAll(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), dataOut); - dataOut.close(); + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("verify", unarmoredSigFile.getAbsolutePath(), certFile.getAbsolutePath())); - // Define micalg output file - File micalgOut = new File(tempDir, "micalg"); + assertEquals(BINARY_SIG_VERIFICATION, out.toString()); + } - // Sign test data - FileInputStream dataIn = new FileInputStream(dataFile); - System.setIn(dataIn); - File sigFile = new File(tempDir, "sig.asc"); - assertTrue(sigFile.createNewFile()); - FileOutputStream sigOut = new FileOutputStream(sigFile); - System.setOut(new PrintStream(sigOut)); - PGPainlessCLI.execute("sign", "--armor", "--micalg-out", micalgOut.getAbsolutePath(), aliceKeyFile.getAbsolutePath()); - sigOut.close(); + @Test + public void testNotBefore() throws IOException { + File cert = writeFile("cert.asc", CERT); + File sigFile = writeFile("sig.asc", TEXT_SIG); + Date plus1Minute = new Date(TEXT_SIG_CREATION.getTime() + 1000 * 60); + + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(), + "--not-before", UTCUtil.formatUTCDate(plus1Minute)); + + assertEquals(SOPGPException.NoSignature.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + + Date minus1Minute = new Date(TEXT_SIG_CREATION.getTime() - 1000 * 60); + pipeStringToStdin(PLAINTEXT); + out = pipeStdoutToStream(); + exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(), + "--not-before", UTCUtil.formatUTCDate(minus1Minute)); + + assertSuccess(exitCode); + assertEquals(TEXT_SIG_VERIFICATION, out.toString()); + } + + @Test + public void testNotAfter() throws IOException { + File cert = writeFile("cert.asc", CERT); + File sigFile = writeFile("sig.asc", TEXT_SIG); + + Date minus1Minute = new Date(TEXT_SIG_CREATION.getTime() - 1000 * 60); + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(), + "--not-after", UTCUtil.formatUTCDate(minus1Minute)); + + assertEquals(SOPGPException.NoSignature.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + + Date plus1Minute = new Date(TEXT_SIG_CREATION.getTime() + 1000 * 60); + pipeStringToStdin(PLAINTEXT); + out = pipeStdoutToStream(); + exitCode = executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath(), + "--not-after", UTCUtil.formatUTCDate(plus1Minute)); + + assertSuccess(exitCode); + assertEquals(TEXT_SIG_VERIFICATION, out.toString()); + } + + @Test + public void testSignWithIncapableKey() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + .addUserId("Cannot Sign ") + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .build(); + File keyFile = writeFile("key.pgp", secretKeys.getEncoded()); + + pipeStringToStdin("Hello, World!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("sign", keyFile.getAbsolutePath()); + + assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void testSignatureCreationAndVerification() + throws IOException { + // Create key and cert + File aliceKeyFile = pipeStdoutToFile("alice.key"); + assertSuccess(executeCommand("generate-key", "Alice ")); + File aliceCertFile = pipeStdoutToFile("alice.cert"); + pipeFileToStdin(aliceKeyFile); + assertSuccess(executeCommand("extract-cert")); + + File micalgOut = nonExistentFile("micalg"); + String msg = "If privacy is outlawed, only outlaws will have privacy.\n"; + File dataFile = writeFile("data", msg); + + // sign data + File sigFile = pipeStdoutToFile("sig.asc"); + pipeFileToStdin(dataFile); + assertSuccess(executeCommand("sign", + "--armor", + "--as", "binary", + "--micalg-out", micalgOut.getAbsolutePath(), + aliceKeyFile.getAbsolutePath())); // verify test data signature - ByteArrayOutputStream verifyOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(verifyOut)); - dataIn = new FileInputStream(dataFile); - System.setIn(dataIn); - PGPainlessCLI.execute("verify", sigFile.getAbsolutePath(), aliceCertFile.getAbsolutePath()); - dataIn.close(); + pipeFileToStdin(dataFile); + ByteArrayOutputStream verificationsOut = pipeStdoutToStream(); + assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), aliceCertFile.getAbsolutePath())); // Test verification output + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(readBytesFromFile(aliceCertFile)); + KeyRingInfo info = PGPainless.inspectKeyRing(cert); + // [date] [signing-key-fp] [primary-key-fp] signed by [key.pub] - String verification = verifyOut.toString(); + String verification = verificationsOut.toString(); String[] split = verification.split(" "); - OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(aliceKeys); - OpenPgpV4Fingerprint signingKeyFingerprint = new OpenPgpV4Fingerprint(new KeyRingInfo(alicePub, new Date()).getSigningSubkeys().get(0)); + OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(cert); + OpenPgpV4Fingerprint signingKeyFingerprint = new OpenPgpV4Fingerprint(info.getSigningSubkeys().get(0)); assertEquals(signingKeyFingerprint.toString(), split[1].trim(), verification); assertEquals(primaryKeyFingerprint.toString(), split[2].trim()); // Test micalg output - assertTrue(micalgOut.exists()); - FileReader fileReader = new FileReader(micalgOut); - BufferedReader bufferedReader = new BufferedReader(fileReader); - String line = bufferedReader.readLine(); - assertNull(bufferedReader.readLine()); - bufferedReader.close(); - assertEquals("pgp-sha512", line); - - System.setIn(originalIn); + String content = readStringFromFile(micalgOut); + assertEquals("pgp-sha512", content); } - @AfterAll - public static void after() { - System.setOut(originalSout); - // CHECKSTYLE:OFF - System.out.println(tempDir.getAbsolutePath()); - // CHECKSTYLE:ON + private static final String PROTECTED_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 738E EAB2 503D 322D 613A C42A B18E 8BF8 884F C050\n" + + "Comment: Axel \n" + + "\n" + + "lIYEY2v6aRYJKwYBBAHaRw8BAQdA3PXtH19zYpVQ9zTU3zlY+iXUptelAO3z4vK/\n" + + "M2FkmrP+CQMCYgVa6K+InVJguITSDIA+HQ6vhOZ5Dbanqx7GFbJbJLD2fWrxhTSr\n" + + "BUWGaUWTqN647auD/kUI8phH1cedVL6CzVR+YWvaWj9zZHr/CYXLobQaQXhlbCA8\n" + + "YXhlbEBwZ3BhaW5sZXNzLm9yZz6IjwQTFgoAQQUCY2v6aQkQsY6L+IhPwFAWIQRz\n" + + "juqyUD0yLWE6xCqxjov4iE/AUAKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAACq\n" + + "zgEAkxB+dUI7Jjcg5zRvT1EfE9DKCI1qTsxOAU/ZXLcSXLkBAJtWRsyptetZvjzB\n" + + "Ze2A7ArOl4q+IvKvun/d783YyRMInIsEY2v6aRIKKwYBBAGXVQEFAQEHQPFmlZ+o\n" + + "jCGEo2X0474vJfRG7blctuZXmCbC0sLO7MgzAwEIB/4JAwJiBVror4idUmDFhBq4\n" + + "lEhJxjCVc6aSD6+EWRT3YdplqCmNdynnrPombUFst6LfJFzns3H3d0rCeXHfQr93\n" + + "GrHTLkHfW8G3x0PJJPiqFkBviHUEGBYKAB0FAmNr+mkCngECmwwFFgIDAQAECwkI\n" + + "BwUVCgkICwAKCRCxjov4iE/AUNC2AP9WDx4lHt9oYFLSrM8vMLRFI31U8TkYrtCe\n" + + "pYICE76cIAEA5+wEbtE5vQrLxOqIRueVVdzwK9kTeMvSIQfc9PNoyQKchgRja/pp\n" + + "FgkrBgEEAdpHDwEBB0CyAEVlCUbFr3dBBG3MQ84hjCPfYqSx9kYsTN8j5Og6uP4J\n" + + "AwJiBVror4idUmCIFuAYXia0YpEhEpB/Lrn/D6/WAUPEgZjNLMvJzL//EmhkWfEa\n" + + "OfQz/fslj1erWNjLKNiW5C/TvGapDfjbn596AkNlcd1JiNUEGBYKAH0FAmNr+mkC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJja/ppAAoJELRgil1uCuQj\n" + + "VUYBAJecbedwwqWQITVqucEBIraTRoc6ZGkN8jytDp8z9CsBAQDrb/W/J/kze6ln\n" + + "nRyJSriWF3SjcKOGIRkUslmdJEPPCQAKCRCxjov4iE/AUAvbAQDBBgQFG8acTT5L\n" + + "cyIi1Ix9/XBG7G23SSs6l7Beap8M+wEAmK13NYuq7Mv/mct8iIKZbBFH9aAiY+nX\n" + + "3Uct4Q5f0w0=\n" + + "=K65R\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + private static final String PASSPHRASE = "orange"; + private static final String SIGNING_KEY = "9846F3606EE875FB77EC8808B4608A5D6E0AE423 738EEAB2503D322D613AC42AB18E8BF8884FC050"; + + @Test + public void signWithProtectedKey_missingPassphraseFails() throws IOException { + File key = writeFile("key.asc", PROTECTED_KEY); + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("sign", key.getAbsolutePath()); + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); } + + @Test + public void signWithProtectedKey_wrongPassphraseFails() throws IOException { + File password = writeFile("password", "blue"); + File key = writeFile("key.asc", PROTECTED_KEY); + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("sign", key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath()); + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void signWithProtectedKey() throws IOException { + File password = writeFile("password", PASSPHRASE); + File key = writeFile("key.asc", PROTECTED_KEY); + pipeStringToStdin(PROTECTED_KEY); + File cert = pipeStdoutToFile("cert.asc"); + assertSuccess(executeCommand("extract-cert")); + + pipeStringToStdin(PLAINTEXT); + File sigFile = pipeStdoutToFile("sig.asc"); + assertSuccess(executeCommand("sign", key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath())); + + pipeStringToStdin(PLAINTEXT); + ByteArrayOutputStream verificationOut = pipeStdoutToStream(); + assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), cert.getAbsolutePath())); + assertTrue(verificationOut.toString().contains(SIGNING_KEY)); + } + } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java new file mode 100644 index 00000000..2e4aa7e4 --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +public class VersionCmdTest extends CLITest { + + public VersionCmdTest() { + super(LoggerFactory.getLogger(VersionCmdTest.class)); + } + + @Test + public void testVersion() throws IOException { + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("version")); + assertTrue(out.toString().startsWith("PGPainless-SOP ")); + } + + @Test + public void testGetBackendVersion() throws IOException { + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("version", "--backend")); + assertTrue(out.toString().startsWith("Bouncy Castle ")); + } + + @Test + public void testExtendedVersion() throws IOException { + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("version", "--extended")); + String info = out.toString(); + assertTrue(info.startsWith("PGPainless-SOP ")); + assertTrue(info.contains("Bouncy Castle")); + assertTrue(info.contains("Stateless OpenPGP Protocol")); + } +} From 52c0ec120809883c9a560df95c32762bfc120cb2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Nov 2022 22:08:38 +0100 Subject: [PATCH 0440/1204] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58c4b052..158b5150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ SPDX-License-Identifier: CC0-1.0 key binding signatures, do not reject signature if primary key binding predates subkey binding - SOP `verify`: Forcefully expect `data()` to be non-OpenPGP data +- SOP `sign`: Fix matching of keys and passphrases +- CLI: Added tons of tests \o/ ## 1.3.10 - Bump `sop-java` to `4.0.3` From c35deaed165c5ca18527a22d1e09aac180f08774 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Nov 2022 22:12:23 +0100 Subject: [PATCH 0441/1204] PGPainless 1.3.11 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 6 +++--- version.gradle | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 158b5150..db710e47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.3.11-SNAPSHOT +## 1.3.11 - Fix: When verifying subkey binding signatures with embedded recycled primary key binding signatures, do not reject signature if primary key binding predates subkey binding diff --git a/README.md b/README.md index c24728e9..09ae92a3 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.10' + implementation 'org.pgpainless:pgpainless-core:1.3.11' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 6c82fdcd..27864e38 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless-SOP -[![Spec Revision: 3](https://img.shields.io/badge/Spec%20Revision-3-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) +[![Spec Revision: 4](https://img.shields.io/badge/Spec%20Revision-4-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) [![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-sop)](https://search.maven.org/artifact/org.pgpainless/pgpainless-sop) [![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-sop/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-sop) @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.10" + implementation "org.pgpainless:pgpainless-sop:1.3.11" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.10 + 1.3.11 ... diff --git a/version.gradle b/version.gradle index a8b3c6b7..3afdf817 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.11' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 2d6f9738ecc390ae037a70f68a79bb7b3bbce99f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Nov 2022 22:14:36 +0100 Subject: [PATCH 0442/1204] PGPainless 1.3.12-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 3afdf817..a6e0a54f 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.11' - isSnapshot = false + shortVersion = '1.3.12' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 48005da7f3c6ecff2c3a3ad27e7414c9ab023811 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 13:18:22 +0100 Subject: [PATCH 0443/1204] SOP : Do not armor already-armored data. --- .../main/java/org/pgpainless/sop/ArmorImpl.java | 16 +++++++++++++++- .../test/java/org/pgpainless/sop/ArmorTest.java | 4 +++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java index 71bc3e6b..d65c2925 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java @@ -11,6 +11,7 @@ import java.io.OutputStream; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.util.io.Streams; +import org.pgpainless.decryption_verification.OpenPgpInputStream; import org.pgpainless.util.ArmoredOutputStreamFactory; import sop.Ready; import sop.enums.ArmorLabel; @@ -31,8 +32,21 @@ public class ArmorImpl implements Armor { public void writeTo(OutputStream outputStream) throws IOException { // By buffering the output stream, we can improve performance drastically BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); + + // Determine nature of the given data + OpenPgpInputStream openPgpIn = new OpenPgpInputStream(data); + openPgpIn.reset(); + + if (openPgpIn.isAsciiArmored()) { + // armoring already-armored data is an idempotent operation + Streams.pipeAll(openPgpIn, bufferedOutputStream); + bufferedOutputStream.flush(); + openPgpIn.close(); + return; + } + ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(bufferedOutputStream); - Streams.pipeAll(data, armor); + Streams.pipeAll(openPgpIn, armor); bufferedOutputStream.flush(); armor.close(); } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java index 95129dfa..3830912a 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java @@ -29,7 +29,9 @@ public class ArmorTest { @Test public void armor() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice").getEncoded(); - byte[] knownGoodArmor = ArmorUtils.toAsciiArmoredString(data).getBytes(StandardCharsets.UTF_8); + byte[] knownGoodArmor = ArmorUtils.toAsciiArmoredString(data) + .replace("Version: PGPainless\n", "") // armor command does not add version anymore + .getBytes(StandardCharsets.UTF_8); byte[] armored = new SOPImpl() .armor() .data(data) From 86b06ee5e3f7040505b5e26f610938820b89eef7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 13:45:25 +0100 Subject: [PATCH 0444/1204] SOP: Hide armor version header by default --- .../util/ArmoredOutputStreamFactory.java | 18 ++++++++++++++---- .../main/java/org/pgpainless/sop/SOPImpl.java | 5 +++++ .../org/pgpainless/sop/ExtractCertTest.java | 2 -- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java index 403115ba..67cc1c20 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java @@ -31,7 +31,11 @@ public final class ArmoredOutputStreamFactory { */ public static ArmoredOutputStream get(OutputStream outputStream) { ArmoredOutputStream armoredOutputStream = new ArmoredOutputStream(outputStream); - armoredOutputStream.setHeader(ArmorUtils.HEADER_VERSION, version); + armoredOutputStream.clearHeaders(); + if (version != null && !version.isEmpty()) { + armoredOutputStream.setHeader(ArmorUtils.HEADER_VERSION, version); + } + for (String comment : comment) { ArmorUtils.addCommentHeader(armoredOutputStream, comment); } @@ -55,10 +59,16 @@ public final class ArmoredOutputStreamFactory { * @param versionString version string */ public static void setVersionInfo(String versionString) { - if (versionString == null || versionString.trim().isEmpty()) { - throw new IllegalArgumentException("Version Info MUST NOT be null NOR empty."); + if (versionString == null) { + version = null; + return; + } + String trimmed = versionString.trim(); + if (trimmed.isEmpty()) { + version = null; + } else { + version = trimmed; } - version = versionString; } /** diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java index 35ff8994..28772f10 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -4,6 +4,7 @@ package org.pgpainless.sop; +import org.pgpainless.util.ArmoredOutputStreamFactory; import sop.SOP; import sop.operation.Armor; import sop.operation.Dearmor; @@ -20,6 +21,10 @@ import sop.operation.Version; public class SOPImpl implements SOP { + static { + ArmoredOutputStreamFactory.setVersionInfo(null); + } + @Override public Version version() { return new VersionImpl(); diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ExtractCertTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ExtractCertTest.java index 84a1f471..b3910482 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ExtractCertTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ExtractCertTest.java @@ -18,7 +18,6 @@ import sop.exception.SOPGPException; public class ExtractCertTest { public static final String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: PGPainless\n" + "Comment: A8D9 9FF4 C8DD BBA6 C610 A6B7 9ACB 2195 A9BC DF5B\n" + "Comment: Alice \n" + "\n" + @@ -42,7 +41,6 @@ public class ExtractCertTest { "-----END PGP PRIVATE KEY BLOCK-----\n"; public static final String cert = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "Version: PGPainless\n" + "Comment: A8D9 9FF4 C8DD BBA6 C610 A6B7 9ACB 2195 A9BC DF5B\n" + "Comment: Alice \n" + "\n" + From 243d64fcb486310d92cfed7bbc88b0e016395b26 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 13:46:05 +0100 Subject: [PATCH 0445/1204] Bump sop-java to 4.0.5 and adopt changes (--as=clearsigned) --- .../commands/RoundTripInlineSignInlineVerifyCmdTest.java | 6 +++--- .../src/main/java/org/pgpainless/sop/InlineSignImpl.java | 8 ++++---- .../test/java/org/pgpainless/sop/InlineDetachTest.java | 2 +- .../org/pgpainless/sop/InlineSignVerifyRoundtripTest.java | 2 +- version.gradle | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index c80019d1..3d7980b0 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -134,7 +134,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { pipeStringToStdin(MESSAGE); ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); assertSuccess(executeCommand("inline-sign", - "--as", "cleartextsigned", + "--as", "clearsigned", key.getAbsolutePath(), "--with-key-password", password.getAbsolutePath())); @@ -154,7 +154,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { pipeStringToStdin(MESSAGE); ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); assertSuccess(executeCommand("inline-sign", - "--as", "cleartextsigned", + "--as", "clearsigned", key.getAbsolutePath(), "--with-key-password", password.getAbsolutePath())); @@ -212,7 +212,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { pipeStringToStdin(MESSAGE); ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); assertSuccess(executeCommand("inline-sign", - "--as", "cleartextsigned", + "--as", "clearsigned", key.getAbsolutePath(), "--with-key-password", password.getAbsolutePath())); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index c0bd29f4..d0cfbfcd 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -32,7 +32,7 @@ import sop.operation.InlineSign; public class InlineSignImpl implements InlineSign { private boolean armor = true; - private InlineSignAs mode = InlineSignAs.Binary; + private InlineSignAs mode = InlineSignAs.binary; private final SigningOptions signingOptions = new SigningOptions(); private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); private final List signingKeys = new ArrayList<>(); @@ -74,7 +74,7 @@ public class InlineSignImpl implements InlineSign { public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, IOException, SOPGPException.ExpectedText { for (PGPSecretKeyRing key : signingKeys) { try { - if (mode == InlineSignAs.CleartextSigned) { + if (mode == InlineSignAs.clearsigned) { signingOptions.addDetachedSignature(protector, key); } else { signingOptions.addInlineSignature(protector, key, modeToSigType(mode)); @@ -87,7 +87,7 @@ public class InlineSignImpl implements InlineSign { } ProducerOptions producerOptions = ProducerOptions.sign(signingOptions); - if (mode == InlineSignAs.CleartextSigned) { + if (mode == InlineSignAs.clearsigned) { producerOptions.setCleartextSigned(); producerOptions.setAsciiArmor(true); } else { @@ -119,7 +119,7 @@ public class InlineSignImpl implements InlineSign { } private static DocumentSignatureType modeToSigType(InlineSignAs mode) { - return mode == InlineSignAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT + return mode == InlineSignAs.binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT; } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java index b4bfe815..1054babb 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java @@ -63,7 +63,7 @@ public class InlineDetachTest { byte[] cleartextSigned = sop.inlineSign() .key(key) .withKeyPassword("sw0rdf1sh") - .mode(InlineSignAs.CleartextSigned) + .mode(InlineSignAs.clearsigned) .data(data).getBytes(); // actually detach the message diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java index ee892501..b24b729c 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java @@ -36,7 +36,7 @@ public class InlineSignVerifyRoundtripTest { byte[] inlineSigned = sop.inlineSign() .key(key) .withKeyPassword("sw0rdf1sh") - .mode(InlineSignAs.CleartextSigned) + .mode(InlineSignAs.clearsigned) .data(message).getBytes(); ByteArrayAndResult> result = sop.inlineVerify() diff --git a/version.gradle b/version.gradle index a6e0a54f..ee8e64fb 100644 --- a/version.gradle +++ b/version.gradle @@ -16,6 +16,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '4.0.3' + sopJavaVersion = '4.0.5' } } From ae88fdf4ab0ba32b819b1dd40d76fb76d815ca44 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 13:49:28 +0100 Subject: [PATCH 0446/1204] Document ArmoredOutputStreamFactory.setVersionInfo(null) --- .../java/org/pgpainless/util/ArmoredOutputStreamFactory.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java index 67cc1c20..fb2cd4f5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java @@ -55,6 +55,7 @@ public final class ArmoredOutputStreamFactory { /** * Overwrite the version header of ASCII armors with a custom value. * Newlines in the version info string result in multiple version header entries. + * If this is set to

null
, then the version header is omitted altogether. * * @param versionString version string */ From 3fc19f56afe48566f0af49fcb034c994390b6198 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 13:52:53 +0100 Subject: [PATCH 0447/1204] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db710e47..b3ff90f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.12-SNAPSHOT +- Bump `sop-java` to `4.0.5` +- Fix: `sop inline-sign`: Adopt `--as=clearsigned` instead of `--as=cleartextsigned` +- SOP: Hide `Version: PGPainless` armor header in all armored outputs +- Fix: `sop armor`: Do not re-armor already armored data + ## 1.3.11 - Fix: When verifying subkey binding signatures with embedded recycled primary key binding signatures, do not reject signature if primary key binding From 678dac902f4b9bfee8cda0a292e7d11f416b1db7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 13:57:28 +0100 Subject: [PATCH 0448/1204] PGPainless 1.3.12 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ff90f0..f49dcf60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.3.12-SNAPSHOT +## 1.3.12 - Bump `sop-java` to `4.0.5` - Fix: `sop inline-sign`: Adopt `--as=clearsigned` instead of `--as=cleartextsigned` - SOP: Hide `Version: PGPainless` armor header in all armored outputs diff --git a/README.md b/README.md index 09ae92a3..94ea915b 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.11' + implementation 'org.pgpainless:pgpainless-core:1.3.12' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 27864e38..ee27b0b5 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.11" + implementation "org.pgpainless:pgpainless-sop:1.3.12" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.11 + 1.3.12 ... diff --git a/version.gradle b/version.gradle index ee8e64fb..b3d2b4f6 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.12' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 95ba4e46ca2bcfaefbf3c9cb33f32af31accb59e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 14:00:36 +0100 Subject: [PATCH 0449/1204] PGPainless 1.3.13-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index b3d2b4f6..9bf15d8c 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.12' - isSnapshot = false + shortVersion = '1.3.13' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 40715c3e0416d1b36166b25c6cfda4a667efa544 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Nov 2022 20:26:26 +0100 Subject: [PATCH 0450/1204] Bump sop-java to 4.0.7 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 9bf15d8c..6d0daa22 100644 --- a/version.gradle +++ b/version.gradle @@ -16,6 +16,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '4.0.5' + sopJavaVersion = '4.0.7' } } From d7c567649db34d103b597522521e3cd8f98f4e3d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Nov 2022 20:26:43 +0100 Subject: [PATCH 0451/1204] Improve documentation of bouncyPgVersion --- version.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 6d0daa22..9d05c960 100644 --- a/version.gradle +++ b/version.gradle @@ -9,8 +9,10 @@ allprojects { pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' - // unfortunately we rely on 1.72.1 for a patch for https://github.com/bcgit/bc-java/issues/1257 + // When using bouncyCastleVersion 1.72: + // unfortunately we rely on 1.72.1 or 1.72.3 for a patch for https://github.com/bcgit/bc-java/issues/1257 // which is a bug we introduced with a PR against BC :/ oops + // When bouncyCastleVersion is 1.71, bouncyPgVersion can simply be set to 1.71 as well. bouncyPgVersion = '1.72.1' junitVersion = '5.8.2' logbackVersion = '1.2.11' From 29009cf2246268d76987c58e448f257a7a543a1e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Nov 2022 20:27:45 +0100 Subject: [PATCH 0452/1204] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f49dcf60..9b727a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.3.13-SNAPSHOT +- Bump `sop-java` to `4.0.7` + ## 1.3.12 - Bump `sop-java` to `4.0.5` - Fix: `sop inline-sign`: Adopt `--as=clearsigned` instead of `--as=cleartextsigned` From aacfc0f995490eb2a9e467c099aad1e85aa05f12 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Nov 2022 20:33:04 +0100 Subject: [PATCH 0453/1204] PGPainless 1.3.13 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b727a6b..551728aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.3.13-SNAPSHOT +## 1.3.13 - Bump `sop-java` to `4.0.7` ## 1.3.12 diff --git a/README.md b/README.md index 94ea915b..90f6be81 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.12' + implementation 'org.pgpainless:pgpainless-core:1.3.13' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index ee27b0b5..4a18136d 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.12" + implementation "org.pgpainless:pgpainless-sop:1.3.13" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.12 + 1.3.13 ... diff --git a/version.gradle b/version.gradle index 9d05c960..9cb598cf 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.3.13' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From ec7237390a5712a2144c1081ffdb571719a80e20 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Nov 2022 20:36:54 +0100 Subject: [PATCH 0454/1204] PGPainless 1.3.14-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 9cb598cf..dc50f086 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.13' - isSnapshot = false + shortVersion = '1.3.14' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From bc73d26118921f5f07804b9f0248bb28c9d05e94 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 8 Sep 2022 18:23:20 +0200 Subject: [PATCH 0455/1204] Add Pushdown Automaton for checking OpenPGP message syntax The automaton implements what is described in https://github.com/pgpainless/pgpainless/blob/main/misc/OpenPGPMessageFormat.md However, some differences exist to adopt it to BouncyCastle Part of #237 --- .../PushdownAutomaton.java | 391 ++++++++++++++++++ .../MalformedOpenPgpMessageException.java | 31 ++ .../PushDownAutomatonTest.java | 205 +++++++++ 3 files changed, 627 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java new file mode 100644 index 00000000..6930de4b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java @@ -0,0 +1,391 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import java.util.Stack; + +import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.msg; +import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.ops; +import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.terminus; + +/** + * Pushdown Automaton to verify the correct syntax of OpenPGP messages during decryption. + *

+ * OpenPGP messages MUST follow certain rules in order to be well-formed. + * Section §11.3. of RFC4880 specifies a formal grammar for OpenPGP messages. + *

+ * This grammar was transformed into a pushdown automaton, which is implemented below. + * The automaton only ends up in a valid state ({@link #isValid()} iff the OpenPGP message conformed to the + * grammar. + *

+ * There are some specialties with this implementation though: + * Bouncy Castle combines ESKs and Encrypted Data Packets into a single object, so we do not have to + * handle those manually. + *

+ * Bouncy Castle further combines OnePassSignatures and Signatures into lists, so instead of pushing multiple + * 'o's onto the stack repeatedly, a sequence of OnePassSignatures causes a single 'o' to be pushed to the stack. + * The same is true for Signatures. + *

+ * Therefore, a message is valid, even if the number of OnePassSignatures and Signatures does not match. + * If a message contains at least one OnePassSignature, it is sufficient if there is at least one Signature to + * not cause a {@link MalformedOpenPgpMessageException}. + * + * @see RFC4880 §11.3. OpenPGP Messages + */ +public class PushdownAutomaton { + + public enum InputAlphabet { + /** + * A {@link PGPLiteralData} packet. + */ + LiteralData, + /** + * A {@link PGPSignatureList} object. + */ + Signatures, + /** + * A {@link PGPOnePassSignatureList} object. + */ + OnePassSignatures, + /** + * A {@link PGPCompressedData} packet. + * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify + * its nested packet sequence. + */ + CompressedData, + /** + * A {@link PGPEncryptedDataList} object. + * This object combines multiple ESKs and the corresponding Symmetrically Encrypted + * (possibly Integrity Protected) Data packet. + */ + EncryptedData, + /** + * Marks the end of a (sub-) sequence. + * This input is given if the end of an OpenPGP message is reached. + * This might be the case for the end of the whole ciphertext, or the end of a packet with nested contents + * (e.g. the end of a Compressed Data packet). + */ + EndOfSequence + } + + public enum StackAlphabet { + /** + * OpenPGP Message. + */ + msg, + /** + * OnePassSignature (in case of BC this represents a OnePassSignatureList). + */ + ops, + /** + * ESK. Not used, as BC combines encrypted data with their encrypted session keys. + */ + esk, + /** + * Special symbol representing the end of the message. + */ + terminus + } + + /** + * Set of states of the automaton. + * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, PushdownAutomaton)} + * method. + */ + public enum State { + + OpenPgpMessage { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + if (stackItem != msg) { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + switch (input) { + + case LiteralData: + return LiteralMessage; + + case Signatures: + automaton.pushStack(msg); + return OpenPgpMessage; + + case OnePassSignatures: + automaton.pushStack(ops); + automaton.pushStack(msg); + return OpenPgpMessage; + + case CompressedData: + return CompressedMessage; + + case EncryptedData: + return EncryptedMessage; + + case EndOfSequence: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + LiteralMessage { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + CompressedMessage { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + EncryptedMessage { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + CorrespondingSignature { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + Valid { + @Override + State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + throw new MalformedOpenPgpMessageException(this, input, null); + } + }, + ; + + /** + * Pop the automatons stack and transition to another state. + * If no valid transition from the current state is available given the popped stack item and input symbol, + * a {@link MalformedOpenPgpMessageException} is thrown. + * Otherwise, the stack is manipulated according to the valid transition and the new state is returned. + * + * @param input input symbol + * @param automaton automaton + * @return new state of the automaton + * @throws MalformedOpenPgpMessageException in case of an illegal input symbol + */ + abstract State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException; + } + + private final Stack stack = new Stack<>(); + private State state; + // Some OpenPGP packets have nested contents (e.g. compressed / encrypted data). + PushdownAutomaton nestedSequence = null; + + public PushdownAutomaton() { + state = State.OpenPgpMessage; + stack.push(terminus); + stack.push(msg); + } + + /** + * Process the next input packet. + * + * @param input input + * @throws MalformedOpenPgpMessageException in case the input packet is illegal here + */ + public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { + _next(input); + } + + /** + * Process the next input packet. + * This method returns true, iff the given input triggered a successful closing of this PDAs nested PDA. + *

+ * This is for example the case, if the current packet is a Compressed Data packet which contains a + * valid nested OpenPGP message and the last input was {@link InputAlphabet#EndOfSequence} indicating the + * end of the Compressed Data packet. + *

+ * If the input triggered this PDAs nested PDA to close its nested PDA, this method returns false + * in order to prevent this PDA from closing its nested PDA prematurely. + * + * @param input input + * @return true if this just closed its nested sequence, false otherwise + * @throws MalformedOpenPgpMessageException if the input is illegal + */ + private boolean _next(InputAlphabet input) throws MalformedOpenPgpMessageException { + if (nestedSequence != null) { + boolean sequenceInNestedSequenceWasClosed = nestedSequence._next(input); + if (sequenceInNestedSequenceWasClosed) return false; // No need to close out nested sequence too. + } else { + // make a state transition in this automaton + state = state.transition(input, this); + + // If the processed packet contains nested sequence, open nested automaton for it + if (input == InputAlphabet.CompressedData || input == InputAlphabet.EncryptedData) { + nestedSequence = new PushdownAutomaton(); + } + } + + if (input != InputAlphabet.EndOfSequence) { + return false; + } + + // Close nested sequence if needed + boolean nestedIsInnerMost = nestedSequence != null && nestedSequence.isInnerMost(); + if (nestedIsInnerMost) { + if (nestedSequence.isValid()) { + // Close nested sequence + nestedSequence = null; + return true; + } else { + throw new MalformedOpenPgpMessageException("Climbing up nested message validation failed." + + " Automaton for current nesting level is not in valid state: " + nestedSequence.getState() + " " + nestedSequence.stack.peek() + " (Input was " + input + ")"); + } + } + return false; + } + + /** + * Return the current state of the PDA. + * + * @return state + */ + private State getState() { + return state; + } + + /** + * Return true, if the PDA is in a valid state (the OpenPGP message is valid). + * + * @return true if valid, false otherwise + */ + public boolean isValid() { + return getState() == State.Valid && stack.isEmpty(); + } + + /** + * Pop an item from the stack. + * + * @return stack item + */ + private StackAlphabet popStack() { + return stack.pop(); + } + + /** + * Push an item onto the stack. + * + * @param item item + */ + private void pushStack(StackAlphabet item) { + stack.push(item); + } + + /** + * Return true, if this packet sequence has no nested sequence. + * A nested sequence is for example the content of a Compressed Data packet. + * + * @return true if PDA is innermost, false if it has a nested sequence + */ + private boolean isInnerMost() { + return nestedSequence == null; + } + + @Override + public String toString() { + StringBuilder out = new StringBuilder("State: ").append(state) + .append(", Stack (asc.): ").append(stack) + .append('\n'); + if (nestedSequence != null) { + // recursively call toString() on nested PDAs and indent their representation + String nestedToString = nestedSequence.toString(); + String[] lines = nestedToString.split("\n"); + for (int i = 0; i < lines.length; i++) { + String nestedLine = lines[i]; + out.append(i == 0 ? "⤡ " : " ") // indent nested PDA + .append(nestedLine) + .append('\n'); + } + } + return out.toString(); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java new file mode 100644 index 00000000..07fed365 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception; + +import org.bouncycastle.openpgp.PGPException; +import org.pgpainless.decryption_verification.PushdownAutomaton; + +/** + * Exception that gets thrown if the OpenPGP message is malformed. + * Malformed messages are messages which do not follow the grammar specified in the RFC. + * + * @see RFC4880 §11.3. OpenPGP Messages + */ +public class MalformedOpenPgpMessageException extends PGPException { + + public MalformedOpenPgpMessageException(String message) { + super(message); + } + + public MalformedOpenPgpMessageException(String message, MalformedOpenPgpMessageException cause) { + super(message, cause); + } + + public MalformedOpenPgpMessageException(PushdownAutomaton.State state, + PushdownAutomaton.InputAlphabet input, + PushdownAutomaton.StackAlphabet stackItem) { + this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java new file mode 100644 index 00000000..1bd07308 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java @@ -0,0 +1,205 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.junit.jupiter.api.Test; +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PushDownAutomatonTest { + + /** + * MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS MSG SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * SIG MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS COMP(MSG) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS ENC(COMP(COMP(MSG))) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOpsSignedEncryptedCompressedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * MSG SIG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testLiteralPlusSigsFails() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.Signatures)); + } + + /** + * MSG MSG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG MSG) SIG is invalid (two literal packets are illegal). + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedMessageWithTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG) MSG SIG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedMessageWithTwoLiteralDataPacketsFails2() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG SIG) is invalid (MSG SIG does not form valid nested message). + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testCorrespondingSignaturesOfOpsSignedMessageAreLayerFurtherDownFails() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.Signatures)); + } + + /** + * Empty COMP is invalid. + */ + @Test + public void testEmptyCompressedDataIsInvalid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence)); + } + + @Test + public void testOPSSignedEncryptedCompressedOPSSignedMessageIsValid() throws MalformedOpenPgpMessageException { + PushdownAutomaton automaton = new PushdownAutomaton(); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + + automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } +} From bf8949d7f4f4651289baed158de7d61e2d938c59 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 12 Sep 2022 15:28:54 +0200 Subject: [PATCH 0456/1204] WIP: Implement custom PGPDecryptionStream --- .../pgpainless/algorithm/OpenPgpPacket.java | 71 +++++ .../PGPDecryptionStream.java | 238 ++++++++++++++ .../PushdownAutomaton.java | 6 + .../MalformedOpenPgpMessageException.java | 11 + .../algorithm/OpenPgpPacketTest.java | 38 +++ .../PGPDecryptionStreamTest.java | 290 ++++++++++++++++++ 6 files changed, 654 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java new file mode 100644 index 00000000..63d14a31 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import org.bouncycastle.bcpg.PacketTags; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +public enum OpenPgpPacket { + PKESK(PacketTags.PUBLIC_KEY_ENC_SESSION), + SIG(PacketTags.SIGNATURE), + SKESK(PacketTags.SYMMETRIC_KEY_ENC_SESSION), + OPS(PacketTags.ONE_PASS_SIGNATURE), + SK(PacketTags.SECRET_KEY), + PK(PacketTags.PUBLIC_KEY), + SSK(PacketTags.SECRET_SUBKEY), + COMP(PacketTags.COMPRESSED_DATA), + SED(PacketTags.SYMMETRIC_KEY_ENC), + MARKER(PacketTags.MARKER), + LIT(PacketTags.LITERAL_DATA), + TRUST(PacketTags.TRUST), + UID(PacketTags.USER_ID), + PSK(PacketTags.PUBLIC_SUBKEY), + UATTR(PacketTags.USER_ATTRIBUTE), + SEIPD(PacketTags.SYM_ENC_INTEGRITY_PRO), + MOD(PacketTags.MOD_DETECTION_CODE), + + EXP_1(PacketTags.EXPERIMENTAL_1), + EXP_2(PacketTags.EXPERIMENTAL_2), + EXP_3(PacketTags.EXPERIMENTAL_3), + EXP_4(PacketTags.EXPERIMENTAL_4), + ; + + static final Map MAP = new HashMap<>(); + + static { + for (OpenPgpPacket p : OpenPgpPacket.values()) { + MAP.put(p.getTag(), p); + } + } + + final int tag; + + @Nullable + public static OpenPgpPacket fromTag(int tag) { + return MAP.get(tag); + } + + @Nonnull + public static OpenPgpPacket requireFromTag(int tag) { + OpenPgpPacket p = fromTag(tag); + if (p == null) { + throw new NoSuchElementException("No OpenPGP packet known for tag " + tag); + } + return p; + } + + OpenPgpPacket(int tag) { + this.tag = tag; + } + + int getTag() { + return tag; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java new file mode 100644 index 00000000..c3053555 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java @@ -0,0 +1,238 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.ModDetectionCodePacket; +import org.bouncycastle.bcpg.OnePassSignaturePacket; +import org.bouncycastle.bcpg.Packet; +import org.bouncycastle.bcpg.PacketTags; +import org.bouncycastle.bcpg.PublicKeyEncSessionPacket; +import org.bouncycastle.bcpg.SignaturePacket; +import org.bouncycastle.bcpg.SymmetricEncDataPacket; +import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket; +import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket; +import org.bouncycastle.bcpg.TrustPacket; +import org.bouncycastle.bcpg.UserAttributePacket; +import org.bouncycastle.bcpg.UserIDPacket; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.implementation.ImplementationFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.NoSuchElementException; +import java.util.Stack; + +public class PGPDecryptionStream extends InputStream { + + PushdownAutomaton automaton = new PushdownAutomaton(); + // nested streams, outermost at the bottom of the stack + Stack packetLayers = new Stack<>(); + + public PGPDecryptionStream(InputStream inputStream) throws IOException, PGPException { + try { + packetLayers.push(Layer.initial(inputStream)); + walkLayer(); + } catch (MalformedOpenPgpMessageException e) { + throw e.toRuntimeException(); + } + } + + private void walkLayer() throws PGPException, IOException { + if (packetLayers.isEmpty()) { + return; + } + + Layer layer = packetLayers.peek(); + BCPGInputStream inputStream = (BCPGInputStream) layer.inputStream; + + loop: while (true) { + if (inputStream.nextPacketTag() == -1) { + popLayer(); + break loop; + } + OpenPgpPacket tag = nextTagOrThrow(inputStream); + switch (tag) { + + case PKESK: + PublicKeyEncSessionPacket pkeskPacket = (PublicKeyEncSessionPacket) inputStream.readPacket(); + PGPEncryptedDataList encList = null; + break; + case SIG: + automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); + while (inputStream.nextPacketTag() == PacketTags.SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { + Packet packet = inputStream.readPacket(); + if (packet instanceof SignaturePacket) { + SignaturePacket sig = (SignaturePacket) packet; + sig.encode(bcpgOut); + } + } + PGPSignatureList signatures = (PGPSignatureList) ImplementationFactory.getInstance() + .getPGPObjectFactory(buf.toByteArray()).nextObject(); + break; + case SKESK: + SymmetricKeyEncSessionPacket skeskPacket = (SymmetricKeyEncSessionPacket) inputStream.readPacket(); + + break; + case OPS: + automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); + buf = new ByteArrayOutputStream(); + bcpgOut = new BCPGOutputStream(buf); + while (inputStream.nextPacketTag() == PacketTags.ONE_PASS_SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { + Packet packet = inputStream.readPacket(); + if (packet instanceof OnePassSignaturePacket) { + OnePassSignaturePacket sig = (OnePassSignaturePacket) packet; + sig.encode(bcpgOut); + } + } + PGPOnePassSignatureList onePassSignatures = (PGPOnePassSignatureList) ImplementationFactory.getInstance() + .getPGPObjectFactory(buf.toByteArray()).nextObject(); + break; + case SK: + break; + case PK: + break; + case SSK: + break; + case COMP: + automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); + PGPCompressedData compressedData = new PGPCompressedData(inputStream); + inputStream = new BCPGInputStream(compressedData.getDataStream()); + packetLayers.push(Layer.CompressedData(inputStream)); + break; + case SED: + automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); + SymmetricEncDataPacket symmetricEncDataPacket = (SymmetricEncDataPacket) inputStream.readPacket(); + break; + case MARKER: + inputStream.readPacket(); // discard + break; + case LIT: + automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); + PGPLiteralData literalData = new PGPLiteralData(inputStream); + packetLayers.push(Layer.LiteralMessage(literalData.getDataStream())); + break loop; + case TRUST: + TrustPacket trustPacket = (TrustPacket) inputStream.readPacket(); + break; + case UID: + UserIDPacket userIDPacket = (UserIDPacket) inputStream.readPacket(); + break; + case PSK: + break; + case UATTR: + UserAttributePacket userAttributePacket = (UserAttributePacket) inputStream.readPacket(); + break; + case SEIPD: + automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); + SymmetricEncIntegrityPacket symmetricEncIntegrityPacket = (SymmetricEncIntegrityPacket) inputStream.readPacket(); + break; + case MOD: + ModDetectionCodePacket modDetectionCodePacket = (ModDetectionCodePacket) inputStream.readPacket(); + break; + case EXP_1: + break; + case EXP_2: + break; + case EXP_3: + break; + case EXP_4: + break; + } + } + } + + private OpenPgpPacket nextTagOrThrow(BCPGInputStream inputStream) + throws IOException, InvalidOpenPgpPacketException { + try { + return OpenPgpPacket.requireFromTag(inputStream.nextPacketTag()); + } catch (NoSuchElementException e) { + throw new InvalidOpenPgpPacketException(e.getMessage()); + } + } + + private void popLayer() throws MalformedOpenPgpMessageException { + if (packetLayers.pop().isNested) + automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + } + + @Override + public int read() throws IOException { + if (packetLayers.isEmpty()) { + try { + automaton.assertValid(); + } catch (MalformedOpenPgpMessageException e) { + throw e.toRuntimeException(); + } + return -1; + } + + int r = -1; + try { + r = packetLayers.peek().inputStream.read(); + } catch (IOException e) { + } + if (r == -1) { + try { + popLayer(); + walkLayer(); + } catch (MalformedOpenPgpMessageException e) { + throw e.toRuntimeException(); + } + catch (PGPException e) { + throw new RuntimeException(e); + } + return read(); + } + return r; + } + + public static class InvalidOpenPgpPacketException extends PGPException { + + public InvalidOpenPgpPacketException(String message) { + super(message); + } + } + + private static class Layer { + InputStream inputStream; + boolean isNested; + + private Layer(InputStream inputStream, boolean isNested) { + this.inputStream = inputStream; + this.isNested = isNested; + } + + static Layer initial(InputStream inputStream) { + BCPGInputStream bcpgIn; + if (inputStream instanceof BCPGInputStream) { + bcpgIn = (BCPGInputStream) inputStream; + } else { + bcpgIn = new BCPGInputStream(inputStream); + } + return new Layer(bcpgIn, true); + } + + static Layer LiteralMessage(InputStream inputStream) { + return new Layer(inputStream, false); + } + + static Layer CompressedData(InputStream inputStream) { + return new Layer(inputStream, true); + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java index 6930de4b..86075280 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java @@ -342,6 +342,12 @@ public class PushdownAutomaton { return getState() == State.Valid && stack.isEmpty(); } + public void assertValid() throws MalformedOpenPgpMessageException { + if (!isValid()) { + throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString()); + } + } + /** * Pop an item from the stack. * diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index 07fed365..9ce2284d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -28,4 +28,15 @@ public class MalformedOpenPgpMessageException extends PGPException { PushdownAutomaton.StackAlphabet stackItem) { this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } + + public RTE toRuntimeException() { + return new RTE(this); + } + + public static class RTE extends RuntimeException { + + public RTE(MalformedOpenPgpMessageException e) { + super(e); + } + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java new file mode 100644 index 00000000..88146605 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import org.bouncycastle.bcpg.PacketTags; +import org.junit.jupiter.api.Test; + +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class OpenPgpPacketTest { + + @Test + public void testFromInvalidTag() { + int tag = PacketTags.RESERVED; + assertNull(OpenPgpPacket.fromTag(tag)); + assertThrows(NoSuchElementException.class, + () -> OpenPgpPacket.requireFromTag(tag)); + } + + @Test + public void testFromExistingTags() { + for (OpenPgpPacket p : OpenPgpPacket.values()) { + assertEquals(p, OpenPgpPacket.fromTag(p.getTag())); + assertEquals(p, OpenPgpPacket.requireFromTag(p.getTag())); + } + } + + @Test + public void testPKESKTagMatches() { + assertEquals(PacketTags.PUBLIC_KEY_ENC_SESSION, OpenPgpPacket.PKESK.getTag()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java new file mode 100644 index 00000000..bb2742b5 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java @@ -0,0 +1,290 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.ArmoredInputStreamFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class PGPDecryptionStreamTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + + "Comment: Alice \n" + + "\n" + + "lFgEYxzSCBYJKwYBBAHaRw8BAQdAeJU8m4GOJb1eQgv/ryilFHRfNLTYFMNqL6zj\n" + + "r0vF7dsAAP42rAtngpJ6dZxoZlJX0Je65zk1VMPeTrXaWfPS2HSKBRGptBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMc0ggJEHpwH8aQTT9M\n" + + "FiEE2gWEjzfUaOb5gsiJenAfxpBNP0wCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAApZEBALUXHtvswPZG28YO+16Men6/fpk+scvqpNMnD4ty3IkAAPwK6TuXjNnZ\n" + + "0XuWdnilvLMV23Ai1d5g6em+lwLK5M2SApxdBGMc0ggSCisGAQQBl1UBBQEBB0D8\n" + + "mNUVX8y2MXFaSeFYqOTPFnGT7dgNVdn6yc0UtkkHOgMBCAcAAP9y9OtP4SX9voPb\n" + + "ID2u9PkJKgo4hTB8NK5LouGppdRtEBGriHUEGBYKAB0FAmMc0ggCngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRB6cB/GkE0/TAywAQDpZRJS/joFH4+xcwheqWfI7ay/\n" + + "WfojUoGQMYGnUjsgYwEAkceRUsgkqI0SVgYvuglfaQpZ9k2ns1mZGVLkXvu/yQyc\n" + + "WARjHNIIFgkrBgEEAdpHDwEBB0BGN9BybSOrj8B6gim1SjbB/IiqAshlzMDunVkQ\n" + + "X23npQABAJqvjOOY7qhBuTusC5/Q5+25iLrhMn4TI+LXlJHMVNOaE0OI1QQYFgoA\n" + + "fQUCYxzSCAKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMc0ggACgkQ\n" + + "KALh4BJQXl6yTQD/dh0N5228Uwtu7XHy6dmpMRX62cac5tXQ9WaDzpy8STgBAMdn\n" + + "Mq948UOYEhdk/ZY2/hwux/4t+FHvqrXW8ziBe4cLAAoJEHpwH8aQTT9M1hQA/3Ms\n" + + "P3kzoed3VsWu1ZMr7dKEngbc6SoJ2XPayzN0QYJaAQCIY5NcT9mZF97HWV3Vgeum\n" + + "00sWMHXfkW3+nl5OpUZaDA==\n" + + "=THgv\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final String PLAINTEXT = "Hello, World!\n"; + + private static final String LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCg==\n" + + "=WGju\n" + + "-----END PGP MESSAGE-----"; + + private static final String LIT_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCssUYgAAAAAASGVsbG8sIFdvcmxkIQo=\n" + + "=A91Q\n" + + "-----END PGP MESSAGE-----"; + + private static final String COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owE7LZLEAAIeqTk5+ToK4flFOSmKXAA=\n" + + "=ZYDg\n" + + "-----END PGP MESSAGE-----"; + + private static final String COMP = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEDAA==\n" + + "=MDzg\n" + + "-----END PGP MESSAGE-----"; + + private static final String COMP_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEBRwC4/6MDQlpoOTFBWSZTWVuW2KAAAAr3hGAQBABgBABAAIAWBJAAAAggADFM\n" + + "ABNBqBo00N6puqWR+TqInoXQ58XckU4UJBbltigA\n" + + "=K9Zl\n" + + "-----END PGP MESSAGE-----"; + + private static final String SIG_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "iHUEABYKACcFAmMc1i0JECgC4eASUF5eFiEEjN3RiJxCf/TyYOQjKALh4BJQXl4A\n" + + "AHkrAP98uPpqrgIix7epgL7MM1cjXXGSxqbDfXHwgptk1YGQlgD/fw89VGcXwFaI\n" + + "2k7kpXQYy/1BqnovM/jZ3X3mXhhTaAOjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" + + "=WKPn\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void genLIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + armorOut.close(); + } + + @Test + public void processLIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + assertEquals(PLAINTEXT, out.toString()); + armorIn.close(); + } + + @Test + public void getLIT_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + armorOut.close(); + } + + @Test + public void processLIT_LIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + assertThrows(MalformedOpenPgpMessageException.RTE.class, () -> Streams.pipeAll(decIn, out)); + } + + @Test + public void genCOMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut.close(); + armorOut.close(); + } + + @Test + public void processCOMP_LIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + decIn.close(); + armorIn.close(); + + assertEquals(PLAINTEXT, out.toString()); + } + + @Test + public void genCOMP() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + compOut.close(); + armorOut.close(); + } + + @Test + public void processCOMP() throws IOException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + assertThrows(MalformedOpenPgpMessageException.RTE.class, () -> { + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + Streams.drain(decIn); + }); + } + + @Test + public void genCOMP_COMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + + PGPCompressedDataGenerator compGen1 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut1 = compGen1.open(armorOut); + + PGPCompressedDataGenerator compGen2 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.BZIP2); + OutputStream compOut2 = compGen2.open(compOut1); + + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut2, PGPLiteralDataGenerator.BINARY, "", PGPLiteralDataGenerator.NOW, new byte[1 << 9]); + + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut2.close(); + compOut1.close(); + armorOut.close(); + } + + @Test + public void processCOMP_COMP_LIT() throws PGPException, IOException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_COMP_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + decIn.close(); + + assertEquals(PLAINTEXT, out.toString()); + } + + @Test + public void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + System.out.println(PGPainless.asciiArmor( + PGPainless.generateKeyRing().modernKeyRing("Alice ") + )); + } + + @Test + public void genSIG_LIT() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); + EncryptionStream signer = PGPainless.encryptAndOrSign() + .onOutputStream(msgOut) + .withOptions( + ProducerOptions.sign( + SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys) + ).setAsciiArmor(false) + ); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), signer); + signer.close(); + EncryptionResult result = signer.getResult(); + PGPSignature detachedSignature = result.getDetachedSignatures().get(result.getDetachedSignatures().keySet().iterator().next()).iterator().next(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(out); + armorOut.flush(); + detachedSignature.encode(armorOut); + armorOut.write(msgOut.toByteArray()); + armorOut.close(); + + String armored = out.toString(); + System.out.println(armored + .replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n") + .replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----")); + } + + @Test + public void processSIG_LIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(SIG_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + decIn.close(); + + System.out.println(out); + } +} From e86062c427dde86b3d75c5af2b3aa1f75c6b11a1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Sep 2022 19:23:59 +0200 Subject: [PATCH 0457/1204] WIP: Replace nesting with independent instancing --- ...ream.java => MessageDecryptionStream.java} | 210 ++++++----- .../OpenPgpMessageInputStream.java | 331 ++++++++++++++++++ .../automaton/InputAlphabet.java | 41 +++ .../NestingPDA.java} | 90 +---- .../automaton/PDA.java | 237 +++++++++++++ .../automaton/StackAlphabet.java | 20 ++ .../MalformedOpenPgpMessageException.java | 25 +- .../org/pgpainless/key/info/KeyRingInfo.java | 40 ++- .../OpenPgpMessageInputStreamTest.java | 86 +++++ .../PGPDecryptionStreamTest.java | 73 +++- .../PushDownAutomatonTest.java | 205 ----------- .../automaton/NestingPDATest.java | 205 +++++++++++ .../automaton/PDATest.java | 75 ++++ 13 files changed, 1227 insertions(+), 411 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{PGPDecryptionStream.java => MessageDecryptionStream.java} (59%) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{PushdownAutomaton.java => automaton/NestingPDA.java} (78%) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java similarity index 59% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java index c3053555..335e9d57 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PGPDecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java @@ -12,41 +12,50 @@ import org.bouncycastle.bcpg.Packet; import org.bouncycastle.bcpg.PacketTags; import org.bouncycastle.bcpg.PublicKeyEncSessionPacket; import org.bouncycastle.bcpg.SignaturePacket; -import org.bouncycastle.bcpg.SymmetricEncDataPacket; -import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket; import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket; -import org.bouncycastle.bcpg.TrustPacket; -import org.bouncycastle.bcpg.UserAttributePacket; -import org.bouncycastle.bcpg.UserIDPacket; import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPPBEEncryptedData; import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.decryption_verification.automaton.InputAlphabet; +import org.pgpainless.decryption_verification.automaton.NestingPDA; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.exception.MessageNotIntegrityProtectedException; +import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.util.Passphrase; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.ArrayList; +import java.util.List; import java.util.NoSuchElementException; import java.util.Stack; -public class PGPDecryptionStream extends InputStream { +public class MessageDecryptionStream extends InputStream { - PushdownAutomaton automaton = new PushdownAutomaton(); + private final ConsumerOptions options; + + NestingPDA automaton = new NestingPDA(); // nested streams, outermost at the bottom of the stack Stack packetLayers = new Stack<>(); + List pkeskList = new ArrayList<>(); + List skeskList = new ArrayList<>(); - public PGPDecryptionStream(InputStream inputStream) throws IOException, PGPException { - try { - packetLayers.push(Layer.initial(inputStream)); - walkLayer(); - } catch (MalformedOpenPgpMessageException e) { - throw e.toRuntimeException(); - } + public MessageDecryptionStream(InputStream inputStream, ConsumerOptions options) + throws IOException, PGPException { + this.options = options; + packetLayers.push(Layer.initial(inputStream)); + walkLayer(); } private void walkLayer() throws PGPException, IOException { @@ -54,6 +63,7 @@ public class PGPDecryptionStream extends InputStream { return; } + // We are currently in the deepest layer Layer layer = packetLayers.peek(); BCPGInputStream inputStream = (BCPGInputStream) layer.inputStream; @@ -65,33 +75,23 @@ public class PGPDecryptionStream extends InputStream { OpenPgpPacket tag = nextTagOrThrow(inputStream); switch (tag) { - case PKESK: - PublicKeyEncSessionPacket pkeskPacket = (PublicKeyEncSessionPacket) inputStream.readPacket(); - PGPEncryptedDataList encList = null; - break; - case SIG: - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); + case LIT: + automaton.next(InputAlphabet.LiteralData); + PGPLiteralData literalData = new PGPLiteralData(inputStream); + packetLayers.push(Layer.literalMessage(literalData.getDataStream())); + break loop; + case COMP: + automaton.next(InputAlphabet.CompressedData); + PGPCompressedData compressedData = new PGPCompressedData(inputStream); + inputStream = new BCPGInputStream(compressedData.getDataStream()); + packetLayers.push(Layer.compressedData(inputStream)); + break; + + case OPS: + automaton.next(InputAlphabet.OnePassSignatures); ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - while (inputStream.nextPacketTag() == PacketTags.SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { - Packet packet = inputStream.readPacket(); - if (packet instanceof SignaturePacket) { - SignaturePacket sig = (SignaturePacket) packet; - sig.encode(bcpgOut); - } - } - PGPSignatureList signatures = (PGPSignatureList) ImplementationFactory.getInstance() - .getPGPObjectFactory(buf.toByteArray()).nextObject(); - break; - case SKESK: - SymmetricKeyEncSessionPacket skeskPacket = (SymmetricKeyEncSessionPacket) inputStream.readPacket(); - - break; - case OPS: - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - buf = new ByteArrayOutputStream(); - bcpgOut = new BCPGOutputStream(buf); while (inputStream.nextPacketTag() == PacketTags.ONE_PASS_SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { Packet packet = inputStream.readPacket(); if (packet instanceof OnePassSignaturePacket) { @@ -102,60 +102,103 @@ public class PGPDecryptionStream extends InputStream { PGPOnePassSignatureList onePassSignatures = (PGPOnePassSignatureList) ImplementationFactory.getInstance() .getPGPObjectFactory(buf.toByteArray()).nextObject(); break; - case SK: + + case SIG: + automaton.next(InputAlphabet.Signatures); + + buf = new ByteArrayOutputStream(); + bcpgOut = new BCPGOutputStream(buf); + while (inputStream.nextPacketTag() == PacketTags.SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { + Packet packet = inputStream.readPacket(); + if (packet instanceof SignaturePacket) { + SignaturePacket sig = (SignaturePacket) packet; + sig.encode(bcpgOut); + } + } + PGPSignatureList signatures = (PGPSignatureList) ImplementationFactory.getInstance() + .getPGPObjectFactory(buf.toByteArray()).nextObject(); break; - case PK: + + case PKESK: + PublicKeyEncSessionPacket pkeskPacket = (PublicKeyEncSessionPacket) inputStream.readPacket(); + pkeskList.add(pkeskPacket); break; - case SSK: - break; - case COMP: - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - PGPCompressedData compressedData = new PGPCompressedData(inputStream); - inputStream = new BCPGInputStream(compressedData.getDataStream()); - packetLayers.push(Layer.CompressedData(inputStream)); + + case SKESK: + SymmetricKeyEncSessionPacket skeskPacket = (SymmetricKeyEncSessionPacket) inputStream.readPacket(); + skeskList.add(skeskPacket); break; + case SED: - automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); - SymmetricEncDataPacket symmetricEncDataPacket = (SymmetricEncDataPacket) inputStream.readPacket(); - break; + if (!options.isIgnoreMDCErrors()) { + throw new MessageNotIntegrityProtectedException(); + } + // No break; we continue below! + case SEIPD: + automaton.next(InputAlphabet.EncryptedData); + PGPEncryptedDataList encryptedDataList = assembleEncryptedDataList(inputStream); + + for (PGPEncryptedData encData : encryptedDataList) { + if (encData instanceof PGPPBEEncryptedData) { + PGPPBEEncryptedData skenc = (PGPPBEEncryptedData) encData; + for (Passphrase passphrase : options.getDecryptionPassphrases()) { + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); + InputStream decryptedIn = skenc.getDataStream(decryptorFactory); + packetLayers.push(Layer.encryptedData(new BCPGInputStream(decryptedIn))); + walkLayer(); + break loop; + } + } + } + throw new MissingDecryptionMethodException("Cannot decrypt message."); + case MARKER: inputStream.readPacket(); // discard break; - case LIT: - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - PGPLiteralData literalData = new PGPLiteralData(inputStream); - packetLayers.push(Layer.LiteralMessage(literalData.getDataStream())); - break loop; - case TRUST: - TrustPacket trustPacket = (TrustPacket) inputStream.readPacket(); - break; - case UID: - UserIDPacket userIDPacket = (UserIDPacket) inputStream.readPacket(); - break; + + case SK: + case PK: + case SSK: case PSK: - break; + case TRUST: + case UID: case UATTR: - UserAttributePacket userAttributePacket = (UserAttributePacket) inputStream.readPacket(); - break; - case SEIPD: - automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); - SymmetricEncIntegrityPacket symmetricEncIntegrityPacket = (SymmetricEncIntegrityPacket) inputStream.readPacket(); - break; + throw new MalformedOpenPgpMessageException("OpenPGP packet " + tag + " MUST NOT be part of OpenPGP messages."); case MOD: ModDetectionCodePacket modDetectionCodePacket = (ModDetectionCodePacket) inputStream.readPacket(); break; case EXP_1: - break; case EXP_2: - break; case EXP_3: - break; case EXP_4: - break; + throw new MalformedOpenPgpMessageException("Experimental packet " + tag + " found inside the message."); } } } + private PGPEncryptedDataList assembleEncryptedDataList(BCPGInputStream inputStream) + throws IOException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); + + for (SymmetricKeyEncSessionPacket skesk : skeskList) { + bcpgOut.write(skesk.getEncoded()); + } + skeskList.clear(); + for (PublicKeyEncSessionPacket pkesk : pkeskList) { + bcpgOut.write(pkesk.getEncoded()); + } + pkeskList.clear(); + + SequenceInputStream sqin = new SequenceInputStream( + new ByteArrayInputStream(buf.toByteArray()), inputStream); + + PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) ImplementationFactory.getInstance() + .getPGPObjectFactory(sqin).nextObject(); + return encryptedDataList; + } + private OpenPgpPacket nextTagOrThrow(BCPGInputStream inputStream) throws IOException, InvalidOpenPgpPacketException { try { @@ -167,17 +210,13 @@ public class PGPDecryptionStream extends InputStream { private void popLayer() throws MalformedOpenPgpMessageException { if (packetLayers.pop().isNested) - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.EndOfSequence); } @Override public int read() throws IOException { if (packetLayers.isEmpty()) { - try { - automaton.assertValid(); - } catch (MalformedOpenPgpMessageException e) { - throw e.toRuntimeException(); - } + automaton.assertValid(); return -1; } @@ -187,13 +226,10 @@ public class PGPDecryptionStream extends InputStream { } catch (IOException e) { } if (r == -1) { + popLayer(); try { - popLayer(); walkLayer(); - } catch (MalformedOpenPgpMessageException e) { - throw e.toRuntimeException(); - } - catch (PGPException e) { + } catch (PGPException e) { throw new RuntimeException(e); } return read(); @@ -227,11 +263,15 @@ public class PGPDecryptionStream extends InputStream { return new Layer(bcpgIn, true); } - static Layer LiteralMessage(InputStream inputStream) { + static Layer literalMessage(InputStream inputStream) { return new Layer(inputStream, false); } - static Layer CompressedData(InputStream inputStream) { + static Layer compressedData(InputStream inputStream) { + return new Layer(inputStream, true); + } + + static Layer encryptedData(InputStream inputStream) { return new Layer(inputStream, true); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java new file mode 100644 index 00000000..549fe46b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -0,0 +1,331 @@ +package org.pgpainless.decryption_verification; + +import com.sun.tools.javac.code.Attribute; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.OnePassSignaturePacket; +import org.bouncycastle.bcpg.Packet; +import org.bouncycastle.bcpg.PacketTags; +import org.bouncycastle.bcpg.SignaturePacket; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPPBEEncryptedData; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.decryption_verification.automaton.InputAlphabet; +import org.pgpainless.decryption_verification.automaton.PDA; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.exception.MessageNotIntegrityProtectedException; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; +import org.pgpainless.util.Tuple; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class OpenPgpMessageInputStream extends InputStream { + + protected final PDA automaton = new PDA(); + protected final ConsumerOptions options; + protected final BCPGInputStream bcpgIn; + protected InputStream in; + + private List signatures = new ArrayList<>(); + private List onePassSignatures = new ArrayList<>(); + + public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) + throws IOException, PGPException { + this.options = options; + // TODO: Use BCPGInputStream.wrap(inputStream); + if (inputStream instanceof BCPGInputStream) { + this.bcpgIn = (BCPGInputStream) inputStream; + } else { + this.bcpgIn = new BCPGInputStream(inputStream); + } + + walk(); + } + + private void walk() throws IOException, PGPException { + loop: while (true) { + + int tag = bcpgIn.nextPacketTag(); + if (tag == -1) { + break loop; + } + + OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); + switch (nextPacket) { + case LIT: + automaton.next(InputAlphabet.LiteralData); + PGPLiteralData literalData = new PGPLiteralData(bcpgIn); + in = literalData.getDataStream(); + break loop; + + case COMP: + automaton.next(InputAlphabet.CompressedData); + PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); + in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options); + break loop; + + case OPS: + automaton.next(InputAlphabet.OnePassSignatures); + readOnePassSignatures(); + break; + + case SIG: + automaton.next(InputAlphabet.Signatures); + readSignatures(); + break; + + case PKESK: + case SKESK: + case SED: + case SEIPD: + automaton.next(InputAlphabet.EncryptedData); + PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); + + // TODO: Replace with !encDataList.isIntegrityProtected() + if (!encDataList.get(0).isIntegrityProtected()) { + throw new MessageNotIntegrityProtectedException(); + } + + SortedESKs esks = new SortedESKs(encDataList); + + // TODO: try session keys + + // Try passwords + for (PGPPBEEncryptedData skesk : esks.skesks) { + for (Passphrase passphrase : options.getDecryptionPassphrases()) { + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + break loop; + } catch (PGPException e) { + // password mismatch? Try next password + } + + } + } + + // Try (known) secret keys + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + long keyId = pkesk.getKeyID(); + PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); + if (decryptionKeys == null) { + continue; + } + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); + PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); + + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + break loop; + } catch (PGPException e) { + // hm :/ + } + } + + // try anonymous secret keys + for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { + for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + break loop; + } catch (PGPException e) { + // hm :/ + } + } + } + + // TODO: try interactive password callbacks + + break loop; + + case MARKER: + bcpgIn.readPacket(); // skip marker packet + break; + + case SK: + case PK: + case SSK: + case PSK: + case TRUST: + case UID: + case UATTR: + + case MOD: + break; + + case EXP_1: + case EXP_2: + case EXP_3: + case EXP_4: + break; + } + } + } + + private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { + int algorithm = pkesk.getAlgorithm(); + List> decryptionKeyCandidates = new ArrayList<>(); + + for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + for (PGPPublicKey publicKey : info.getEncryptionSubkeys(EncryptionPurpose.ANY)) { + if (publicKey.getAlgorithm() == algorithm && info.isSecretKeyAvailable(publicKey.getKeyID())) { + PGPSecretKey candidate = secretKeys.getSecretKey(publicKey.getKeyID()); + decryptionKeyCandidates.add(new Tuple<>(secretKeys, candidate)); + } + } + } + return decryptionKeyCandidates; + } + + private PGPSecretKeyRing getDecryptionKey(long keyID) { + for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { + PGPSecretKey decryptionKey = secretKeys.getSecretKey(keyID); + if (decryptionKey == null) { + continue; + } + return secretKeys; + } + return null; + } + + private void readOnePassSignatures() throws IOException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); + int tag = bcpgIn.nextPacketTag(); + while (tag == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { + Packet packet = bcpgIn.readPacket(); + if (tag == PacketTags.ONE_PASS_SIGNATURE) { + OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; + sigPacket.encode(bcpgOut); + } + } + bcpgOut.close(); + + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); + PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); + for (PGPOnePassSignature ops : signatureList) { + onePassSignatures.add(ops); + } + } + + private void readSignatures() throws IOException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); + int tag = bcpgIn.nextPacketTag(); + while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { + Packet packet = bcpgIn.readPacket(); + if (tag == PacketTags.SIGNATURE) { + SignaturePacket sigPacket = (SignaturePacket) packet; + sigPacket.encode(bcpgOut); + } + } + bcpgOut.close(); + + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); + PGPSignatureList signatureList = (PGPSignatureList) objectFactory.nextObject(); + for (PGPSignature signature : signatureList) { + signatures.add(signature); + } + } + + @Override + public int read() throws IOException { + int r = -1; + try { + r = in.read(); + } catch (IOException e) { + // + } + if (r == -1) { + if (in instanceof OpenPgpMessageInputStream) { + in.close(); + } else { + try { + walk(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + } + return r; + } + + @Override + public void close() throws IOException { + try { + in.close(); + // Nested streams (except LiteralData) need to be closed. + if (automaton.getState() != PDA.State.LiteralMessage) { + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); + } + } catch (IOException e) { + // + } + + super.close(); + } + + private static class SortedESKs { + + private List skesks = new ArrayList<>(); + private List pkesks = new ArrayList<>(); + private List anonPkesks = new ArrayList<>(); + + SortedESKs(PGPEncryptedDataList esks) { + for (PGPEncryptedData esk : esks) { + if (esk instanceof PGPPBEEncryptedData) { + skesks.add((PGPPBEEncryptedData) esk); + } else if (esk instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; + if (pkesk.getKeyID() != 0) { + pkesks.add(pkesk); + } else { + anonPkesks.add(pkesk); + } + } else { + throw new IllegalArgumentException("Unknown ESK class type."); + } + } + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java new file mode 100644 index 00000000..d015a4b3 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java @@ -0,0 +1,41 @@ +package org.pgpainless.decryption_verification.automaton; + +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPSignatureList; + +public enum InputAlphabet { + /** + * A {@link PGPLiteralData} packet. + */ + LiteralData, + /** + * A {@link PGPSignatureList} object. + */ + Signatures, + /** + * A {@link PGPOnePassSignatureList} object. + */ + OnePassSignatures, + /** + * A {@link PGPCompressedData} packet. + * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify + * its nested packet sequence. + */ + CompressedData, + /** + * A {@link PGPEncryptedDataList} object. + * This object combines multiple ESKs and the corresponding Symmetrically Encrypted + * (possibly Integrity Protected) Data packet. + */ + EncryptedData, + /** + * Marks the end of a (sub-) sequence. + * This input is given if the end of an OpenPGP message is reached. + * This might be the case for the end of the whole ciphertext, or the end of a packet with nested contents + * (e.g. the end of a Compressed Data packet). + */ + EndOfSequence +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java similarity index 78% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java index 86075280..cf5ee674 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/PushdownAutomaton.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java @@ -1,17 +1,12 @@ -package org.pgpainless.decryption_verification; +package org.pgpainless.decryption_verification.automaton; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPSignatureList; import org.pgpainless.exception.MalformedOpenPgpMessageException; import java.util.Stack; -import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.ops; -import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlphabet.terminus; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.msg; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; /** * Pushdown Automaton to verify the correct syntax of OpenPGP messages during decryption. @@ -37,71 +32,18 @@ import static org.pgpainless.decryption_verification.PushdownAutomaton.StackAlph * * @see RFC4880 §11.3. OpenPGP Messages */ -public class PushdownAutomaton { - - public enum InputAlphabet { - /** - * A {@link PGPLiteralData} packet. - */ - LiteralData, - /** - * A {@link PGPSignatureList} object. - */ - Signatures, - /** - * A {@link PGPOnePassSignatureList} object. - */ - OnePassSignatures, - /** - * A {@link PGPCompressedData} packet. - * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify - * its nested packet sequence. - */ - CompressedData, - /** - * A {@link PGPEncryptedDataList} object. - * This object combines multiple ESKs and the corresponding Symmetrically Encrypted - * (possibly Integrity Protected) Data packet. - */ - EncryptedData, - /** - * Marks the end of a (sub-) sequence. - * This input is given if the end of an OpenPGP message is reached. - * This might be the case for the end of the whole ciphertext, or the end of a packet with nested contents - * (e.g. the end of a Compressed Data packet). - */ - EndOfSequence - } - - public enum StackAlphabet { - /** - * OpenPGP Message. - */ - msg, - /** - * OnePassSignature (in case of BC this represents a OnePassSignatureList). - */ - ops, - /** - * ESK. Not used, as BC combines encrypted data with their encrypted session keys. - */ - esk, - /** - * Special symbol representing the end of the message. - */ - terminus - } +public class NestingPDA { /** * Set of states of the automaton. - * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, PushdownAutomaton)} + * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, NestingPDA)} * method. */ public enum State { OpenPgpMessage { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); if (stackItem != msg) { throw new MalformedOpenPgpMessageException(this, input, stackItem); @@ -135,7 +77,7 @@ public class PushdownAutomaton { LiteralMessage { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { @@ -165,7 +107,7 @@ public class PushdownAutomaton { CompressedMessage { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { case Signatures: @@ -194,7 +136,7 @@ public class PushdownAutomaton { EncryptedMessage { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { case Signatures: @@ -223,7 +165,7 @@ public class PushdownAutomaton { CorrespondingSignature { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { return Valid; @@ -235,7 +177,7 @@ public class PushdownAutomaton { Valid { @Override - State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException { + State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { throw new MalformedOpenPgpMessageException(this, input, null); } }, @@ -252,15 +194,15 @@ public class PushdownAutomaton { * @return new state of the automaton * @throws MalformedOpenPgpMessageException in case of an illegal input symbol */ - abstract State transition(InputAlphabet input, PushdownAutomaton automaton) throws MalformedOpenPgpMessageException; + abstract State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException; } private final Stack stack = new Stack<>(); private State state; // Some OpenPGP packets have nested contents (e.g. compressed / encrypted data). - PushdownAutomaton nestedSequence = null; + NestingPDA nestedSequence = null; - public PushdownAutomaton() { + public NestingPDA() { state = State.OpenPgpMessage; stack.push(terminus); stack.push(msg); @@ -301,7 +243,7 @@ public class PushdownAutomaton { // If the processed packet contains nested sequence, open nested automaton for it if (input == InputAlphabet.CompressedData || input == InputAlphabet.EncryptedData) { - nestedSequence = new PushdownAutomaton(); + nestedSequence = new NestingPDA(); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java new file mode 100644 index 00000000..6b989720 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -0,0 +1,237 @@ +package org.pgpainless.decryption_verification.automaton; + +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import java.util.Stack; + +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.msg; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops; +import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; + +public class PDA { + /** + * Set of states of the automaton. + * Each state defines its valid transitions in their {@link NestingPDA.State#transition(InputAlphabet, NestingPDA)} + * method. + */ + public enum State { + + OpenPgpMessage { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + if (stackItem != msg) { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + switch (input) { + + case LiteralData: + return LiteralMessage; + + case Signatures: + automaton.pushStack(msg); + return OpenPgpMessage; + + case OnePassSignatures: + automaton.pushStack(ops); + automaton.pushStack(msg); + return OpenPgpMessage; + + case CompressedData: + return CompressedMessage; + + case EncryptedData: + return EncryptedMessage; + + case EndOfSequence: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + LiteralMessage { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + CompressedMessage { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + EncryptedMessage { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + switch (input) { + case Signatures: + if (stackItem == ops) { + return CorrespondingSignature; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case EndOfSequence: + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + + case LiteralData: + case OnePassSignatures: + case CompressedData: + case EncryptedData: + default: + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + CorrespondingSignature { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + StackAlphabet stackItem = automaton.popStack(); + if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { + return Valid; + } else { + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } + }, + + Valid { + @Override + State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { + throw new MalformedOpenPgpMessageException(this, input, null); + } + }, + ; + + /** + * Pop the automatons stack and transition to another state. + * If no valid transition from the current state is available given the popped stack item and input symbol, + * a {@link MalformedOpenPgpMessageException} is thrown. + * Otherwise, the stack is manipulated according to the valid transition and the new state is returned. + * + * @param input input symbol + * @param automaton automaton + * @return new state of the automaton + * @throws MalformedOpenPgpMessageException in case of an illegal input symbol + */ + abstract State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException; + } + + private final Stack stack = new Stack<>(); + private State state; + + public PDA() { + state = State.OpenPgpMessage; + stack.push(terminus); + stack.push(msg); + } + + public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { + State old = state; + StackAlphabet stackItem = stack.isEmpty() ? null : stack.peek(); + state = state.transition(input, this); + System.out.println("Transition from " + old + " to " + state + " via " + input + " with stack " + stackItem); + } + + /** + * Return the current state of the PDA. + * + * @return state + */ + public State getState() { + return state; + } + + /** + * Return true, if the PDA is in a valid state (the OpenPGP message is valid). + * + * @return true if valid, false otherwise + */ + public boolean isValid() { + return getState() == State.Valid && stack.isEmpty(); + } + + public void assertValid() throws MalformedOpenPgpMessageException { + if (!isValid()) { + throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString()); + } + } + + /** + * Pop an item from the stack. + * + * @return stack item + */ + private StackAlphabet popStack() { + return stack.pop(); + } + + /** + * Push an item onto the stack. + * + * @param item item + */ + private void pushStack(StackAlphabet item) { + stack.push(item); + } + + @Override + public String toString() { + return "State: " + state + " Stack: " + stack; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java new file mode 100644 index 00000000..97dad3d8 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java @@ -0,0 +1,20 @@ +package org.pgpainless.decryption_verification.automaton; + +public enum StackAlphabet { + /** + * OpenPGP Message. + */ + msg, + /** + * OnePassSignature (in case of BC this represents a OnePassSignatureList). + */ + ops, + /** + * ESK. Not used, as BC combines encrypted data with their encrypted session keys. + */ + esk, + /** + * Special symbol representing the end of the message. + */ + terminus +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index 9ce2284d..21b6e807 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -4,8 +4,10 @@ package org.pgpainless.exception; -import org.bouncycastle.openpgp.PGPException; -import org.pgpainless.decryption_verification.PushdownAutomaton; +import org.pgpainless.decryption_verification.automaton.InputAlphabet; +import org.pgpainless.decryption_verification.automaton.NestingPDA; +import org.pgpainless.decryption_verification.automaton.PDA; +import org.pgpainless.decryption_verification.automaton.StackAlphabet; /** * Exception that gets thrown if the OpenPGP message is malformed. @@ -13,7 +15,7 @@ import org.pgpainless.decryption_verification.PushdownAutomaton; * * @see RFC4880 §11.3. OpenPGP Messages */ -public class MalformedOpenPgpMessageException extends PGPException { +public class MalformedOpenPgpMessageException extends RuntimeException { public MalformedOpenPgpMessageException(String message) { super(message); @@ -23,20 +25,17 @@ public class MalformedOpenPgpMessageException extends PGPException { super(message, cause); } - public MalformedOpenPgpMessageException(PushdownAutomaton.State state, - PushdownAutomaton.InputAlphabet input, - PushdownAutomaton.StackAlphabet stackItem) { + public MalformedOpenPgpMessageException(NestingPDA.State state, + InputAlphabet input, + StackAlphabet stackItem) { this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } - public RTE toRuntimeException() { - return new RTE(this); + public MalformedOpenPgpMessageException(PDA.State state, InputAlphabet input, StackAlphabet stackItem) { + this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } - public static class RTE extends RuntimeException { - - public RTE(MalformedOpenPgpMessageException e) { - super(e); - } + public MalformedOpenPgpMessageException(String message, PDA automaton) { + super(message + automaton.toString()); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index b818290b..fa4168dd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -1100,29 +1100,33 @@ public class KeyRingInfo { List signingKeys = getSigningSubkeys(); for (PGPPublicKey pk : signingKeys) { - PGPSecretKey sk = getSecretKey(pk.getKeyID()); - if (sk == null) { - // Missing secret key - continue; - } - S2K s2K = sk.getS2K(); - // Unencrypted key - if (s2K == null) { - return true; - } - - // Secret key on smart-card - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - continue; - } - // protected secret key - return true; + return isSecretKeyAvailable(pk.getKeyID()); } // No usable secret key found return false; } + public boolean isSecretKeyAvailable(long keyId) { + PGPSecretKey sk = getSecretKey(keyId); + if (sk == null) { + // Missing secret key + return false; + } + S2K s2K = sk.getS2K(); + // Unencrypted key + if (s2K == null) { + return true; + } + + // Secret key on smart-card + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + return false; + } + // protected secret key + return true; + } + private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) { if (getPublicKey(keyID) == null) { throw new NoSuchElementException("No subkey with key id " + Long.toHexString(keyID) + " found on this key."); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java new file mode 100644 index 00000000..ae6390ce --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -0,0 +1,86 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.util.ArmoredInputStreamFactory; +import org.pgpainless.util.Passphrase; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_COMP_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PASSPHRASE; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PLAINTEXT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SENC_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SIG_LIT; + +public class OpenPgpMessageInputStreamTest { + + @Test + public void testProcessLIT() throws IOException, PGPException { + String plain = process(LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + } + + @Test + public void testProcessLIT_LIT_fails() { + assertThrows(MalformedOpenPgpMessageException.class, + () -> process(LIT_LIT, ConsumerOptions.get())); + } + + @Test + public void testProcessCOMP_LIT() throws PGPException, IOException { + String plain = process(COMP_LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + } + + @Test + public void testProcessCOMP_fails() { + assertThrows(MalformedOpenPgpMessageException.class, + () -> process(COMP, ConsumerOptions.get())); + } + + @Test + public void testProcessCOMP_COMP_LIT() throws PGPException, IOException { + String plain = process(COMP_COMP_LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + } + + @Test + public void testProcessSIG_LIT() throws PGPException, IOException { + String plain = process(SIG_LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + } + + @Test + public void testProcessSENC_LIT() throws PGPException, IOException { + String plain = process(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + assertEquals(PLAINTEXT, plain); + } + + private String process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + OpenPgpMessageInputStream in = get(armoredMessage, options); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(in, out); + in.close(); + return out.toString(); + } + + private OpenPgpMessageInputStream get(String armored, ConsumerOptions options) throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + OpenPgpMessageInputStream pgpIn = new OpenPgpMessageInputStream(armorIn, options); + return pgpIn; + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java index bb2742b5..a84da9d5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java @@ -12,6 +12,8 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; @@ -19,6 +21,7 @@ import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmoredInputStreamFactory; +import org.pgpainless.util.Passphrase; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -33,7 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; public class PGPDecryptionStreamTest { - private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + "Comment: Alice \n" + @@ -58,9 +61,10 @@ public class PGPDecryptionStreamTest { "=THgv\n" + "-----END PGP PRIVATE KEY BLOCK-----"; - private static final String PLAINTEXT = "Hello, World!\n"; + public static final String PLAINTEXT = "Hello, World!\n"; + public static final String PASSPHRASE = "sw0rdf1sh"; - private static final String LIT = "" + + public static final String LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + @@ -68,7 +72,7 @@ public class PGPDecryptionStreamTest { "=WGju\n" + "-----END PGP MESSAGE-----"; - private static final String LIT_LIT = "" + + public static final String LIT_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -76,7 +80,7 @@ public class PGPDecryptionStreamTest { "=A91Q\n" + "-----END PGP MESSAGE-----"; - private static final String COMP_LIT = "" + + public static final String COMP_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -84,7 +88,7 @@ public class PGPDecryptionStreamTest { "=ZYDg\n" + "-----END PGP MESSAGE-----"; - private static final String COMP = "" + + public static final String COMP = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -92,7 +96,7 @@ public class PGPDecryptionStreamTest { "=MDzg\n" + "-----END PGP MESSAGE-----"; - private static final String COMP_COMP_LIT = "" + + public static final String COMP_COMP_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -101,7 +105,7 @@ public class PGPDecryptionStreamTest { "=K9Zl\n" + "-----END PGP MESSAGE-----"; - private static final String SIG_LIT = "" + + public static final String SIG_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -111,6 +115,15 @@ public class PGPDecryptionStreamTest { "=WKPn\n" + "-----END PGP MESSAGE-----"; + public static final String SENC_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0ECQMCuZ0qHNXWnGhg0j8Bdm1cxV65sYb7jDgb4rRMtdNpQ1dC4UpSYuk9YWS2\n" + + "DpNEijbX8b/P1UOK2kJczNDADMRegZuLEI+dNsBnJjk=\n" + + "=i4Y0\n" + + "-----END PGP MESSAGE-----"; + @Test public void genLIT() throws IOException { ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); @@ -125,7 +138,7 @@ public class PGPDecryptionStreamTest { public void processLIT() throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decIn, out); @@ -152,10 +165,10 @@ public class PGPDecryptionStreamTest { public void processLIT_LIT() throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT_LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); ByteArrayOutputStream out = new ByteArrayOutputStream(); - assertThrows(MalformedOpenPgpMessageException.RTE.class, () -> Streams.pipeAll(decIn, out)); + assertThrows(MalformedOpenPgpMessageException.class, () -> Streams.pipeAll(decIn, out)); } @Test @@ -175,7 +188,7 @@ public class PGPDecryptionStreamTest { public void processCOMP_LIT() throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decIn, out); @@ -198,8 +211,8 @@ public class PGPDecryptionStreamTest { public void processCOMP() throws IOException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - assertThrows(MalformedOpenPgpMessageException.RTE.class, () -> { - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + assertThrows(MalformedOpenPgpMessageException.class, () -> { + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); Streams.drain(decIn); }); } @@ -228,7 +241,7 @@ public class PGPDecryptionStreamTest { public void processCOMP_COMP_LIT() throws PGPException, IOException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_COMP_LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decIn, out); @@ -279,7 +292,35 @@ public class PGPDecryptionStreamTest { public void processSIG_LIT() throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(SIG_LIT.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - PGPDecryptionStream decIn = new PGPDecryptionStream(armorIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decIn, out); + decIn.close(); + + System.out.println(out); + } + + @Test + public void genSENC_LIT() throws PGPException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + enc.close(); + + System.out.println(out); + } + + @Test + public void processSENC_LIT() throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(SENC_LIT.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get() + .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decIn, out); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java deleted file mode 100644 index 1bd07308..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PushDownAutomatonTest.java +++ /dev/null @@ -1,205 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import org.junit.jupiter.api.Test; -import org.pgpainless.exception.MalformedOpenPgpMessageException; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class PushDownAutomatonTest { - - /** - * MSG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS MSG SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * SIG MSG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS COMP(MSG) SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS ENC(COMP(COMP(MSG))) SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOpsSignedEncryptedCompressedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * MSG SIG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testLiteralPlusSigsFails() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.Signatures)); - } - - /** - * MSG MSG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG MSG) SIG is invalid (two literal packets are illegal). - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedMessageWithTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG) MSG SIG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedMessageWithTwoLiteralDataPacketsFails2() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG SIG) is invalid (MSG SIG does not form valid nested message). - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testCorrespondingSignaturesOfOpsSignedMessageAreLayerFurtherDownFails() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.Signatures)); - } - - /** - * Empty COMP is invalid. - */ - @Test - public void testEmptyCompressedDataIsInvalid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence)); - } - - @Test - public void testOPSSignedEncryptedCompressedOPSSignedMessageIsValid() throws MalformedOpenPgpMessageException { - PushdownAutomaton automaton = new PushdownAutomaton(); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - - automaton.next(PushdownAutomaton.InputAlphabet.EncryptedData); - automaton.next(PushdownAutomaton.InputAlphabet.OnePassSignatures); - - automaton.next(PushdownAutomaton.InputAlphabet.CompressedData); - automaton.next(PushdownAutomaton.InputAlphabet.LiteralData); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - automaton.next(PushdownAutomaton.InputAlphabet.Signatures); - automaton.next(PushdownAutomaton.InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java new file mode 100644 index 00000000..8c1c4921 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java @@ -0,0 +1,205 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.automaton; + +import org.junit.jupiter.api.Test; +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NestingPDATest { + + /** + * MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS MSG SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * SIG MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS COMP(MSG) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS ENC(COMP(COMP(MSG))) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOpsSignedEncryptedCompressedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.EncryptedData); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.CompressedData); + + automaton.next(InputAlphabet.LiteralData); + + automaton.next(InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.EndOfSequence); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * MSG SIG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testLiteralPlusSigsFails() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.Signatures)); + } + + /** + * MSG MSG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG MSG) SIG is invalid (two literal packets are illegal). + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedMessageWithTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG) MSG SIG is invalid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedMessageWithTwoLiteralDataPacketsFails2() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.LiteralData)); + } + + /** + * OPS COMP(MSG SIG) is invalid (MSG SIG does not form valid nested message). + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testCorrespondingSignaturesOfOpsSignedMessageAreLayerFurtherDownFails() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.Signatures)); + } + + /** + * Empty COMP is invalid. + */ + @Test + public void testEmptyCompressedDataIsInvalid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> automaton.next(InputAlphabet.EndOfSequence)); + } + + @Test + public void testOPSSignedEncryptedCompressedOPSSignedMessageIsValid() throws MalformedOpenPgpMessageException { + NestingPDA automaton = new NestingPDA(); + automaton.next(InputAlphabet.OnePassSignatures); + + automaton.next(InputAlphabet.EncryptedData); + automaton.next(InputAlphabet.OnePassSignatures); + + automaton.next(InputAlphabet.CompressedData); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java new file mode 100644 index 00000000..6e8a38d6 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java @@ -0,0 +1,75 @@ +package org.pgpainless.decryption_verification.automaton; + +import org.junit.jupiter.api.Test; +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PDATest { + + + /** + * MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { + PDA automaton = new PDA(); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + /** + * OPS MSG SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { + PDA automaton = new PDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + + /** + * SIG MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { + PDA automaton = new PDA(); + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.LiteralData); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + + + /** + * OPS COMP(MSG) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + PDA automaton = new PDA(); + automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.CompressedData); + // Here would be a nested PDA for the LiteralData packet + automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.EndOfSequence); + + assertTrue(automaton.isValid()); + } + +} From d81c0d44006d1037b8d577869dd3a49cbf494872 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Sep 2022 20:22:31 +0200 Subject: [PATCH 0458/1204] Fix tests --- .../OpenPgpMessageInputStream.java | 57 ++++++++++++++++--- .../automaton/PDA.java | 9 ++- .../OpenPgpMessageInputStreamTest.java | 12 ++++ .../PGPDecryptionStreamTest.java | 30 ++++++++++ 4 files changed, 98 insertions(+), 10 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 549fe46b..788be4ab 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -1,6 +1,5 @@ package org.pgpainless.decryption_verification; -import com.sun.tools.javac.code.Attribute; import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.bcpg.BCPGOutputStream; import org.bouncycastle.bcpg.OnePassSignaturePacket; @@ -25,13 +24,14 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; -import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; +import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -114,7 +114,27 @@ public class OpenPgpMessageInputStream extends InputStream { SortedESKs esks = new SortedESKs(encDataList); - // TODO: try session keys + if (options.getSessionKey() != null) { + SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getSessionKeyDataDecryptorFactory(options.getSessionKey()); + // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) + PGPEncryptedData esk = esks.all().get(0); + try { + if (esk instanceof PGPPBEEncryptedData) { + PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; + in = skesk.getDataStream(decryptorFactory); + break loop; + } else if (esk instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; + in = pkesk.getDataStream(decryptorFactory); + break loop; + } else { + throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); + } + } catch (PGPException e) { + // Session key mismatch? + } + } // Try passwords for (PGPPBEEncryptedData skesk : esks.skesks) { @@ -174,7 +194,7 @@ public class OpenPgpMessageInputStream extends InputStream { // TODO: try interactive password callbacks - break loop; + throw new MissingDecryptionMethodException("No working decryption method found."); case MARKER: bcpgIn.readPacket(); // skip marker packet @@ -256,6 +276,7 @@ public class OpenPgpMessageInputStream extends InputStream { if (tag == PacketTags.SIGNATURE) { SignaturePacket sigPacket = (SignaturePacket) packet; sigPacket.encode(bcpgOut); + tag = bcpgIn.nextPacketTag(); } } bcpgOut.close(); @@ -270,16 +291,21 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public int read() throws IOException { int r = -1; - try { - r = in.read(); - } catch (IOException e) { - // + if (in != null) { + try { + r = in.read(); + } catch (IOException e) { + // + } } if (r == -1) { if (in instanceof OpenPgpMessageInputStream) { + System.out.println("Read -1: close " + automaton); in.close(); + in = null; } else { try { + System.out.println("Walk " + automaton); walk(); } catch (PGPException e) { throw new RuntimeException(e); @@ -291,8 +317,15 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public void close() throws IOException { + if (in == null) { + System.out.println("Close " + automaton); + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); + return; + } try { in.close(); + in = null; // Nested streams (except LiteralData) need to be closed. if (automaton.getState() != PDA.State.LiteralMessage) { automaton.next(InputAlphabet.EndOfSequence); @@ -327,5 +360,13 @@ public class OpenPgpMessageInputStream extends InputStream { } } } + + public List all() { + List esks = new ArrayList<>(); + esks.addAll(skesks); + esks.addAll(pkesks); + esks.addAll(anonPkesks); + return esks; + } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index 6b989720..a3384070 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -9,6 +9,9 @@ import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; public class PDA { + + private static int ID = 0; + /** * Set of states of the automaton. * Each state defines its valid transitions in their {@link NestingPDA.State#transition(InputAlphabet, NestingPDA)} @@ -174,18 +177,20 @@ public class PDA { private final Stack stack = new Stack<>(); private State state; + private int id; public PDA() { state = State.OpenPgpMessage; stack.push(terminus); stack.push(msg); + this.id = ID++; } public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { State old = state; StackAlphabet stackItem = stack.isEmpty() ? null : stack.peek(); state = state.transition(input, this); - System.out.println("Transition from " + old + " to " + state + " via " + input + " with stack " + stackItem); + System.out.println(id + ": Transition from " + old + " to " + state + " via " + input + " with stack " + stackItem); } /** @@ -232,6 +237,6 @@ public class PDA { @Override public String toString() { - return "State: " + state + " Stack: " + stack; + return "PDA " + id + ": State: " + state + " Stack: " + stack; } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index ae6390ce..5b42101c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -2,8 +2,10 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; @@ -18,9 +20,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_COMP_LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_LIT; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.KEY; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT_LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PASSPHRASE; +import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PENC_COMP_LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PLAINTEXT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SENC_LIT; import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SIG_LIT; @@ -69,6 +73,14 @@ public class OpenPgpMessageInputStreamTest { assertEquals(PLAINTEXT, plain); } + @Test + public void testProcessPENC_COMP_LIT() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + String plain = process(PENC_COMP_LIT, ConsumerOptions.get() + .addDecryptionKey(secretKeys)); + assertEquals(PLAINTEXT, plain); + } + private String process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java index a84da9d5..8da44ec8 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java @@ -7,6 +7,7 @@ import org.bouncycastle.openpgp.PGPCompressedDataGenerator; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; @@ -124,6 +125,17 @@ public class PGPDecryptionStreamTest { "=i4Y0\n" + "-----END PGP MESSAGE-----"; + public static final String PENC_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4Dyqa/GWUy6WsSAQdAQ62BwmUt8Iby0+jvrLhMgST79KR/as+dyl0nf1uki2sw\n" + + "Thg1Ojtf0hOyJgcpQ4nP2Q0wYFR0F1sCydaIlTGreYZHlGtybP7/Ml6KNZILTRWP\n" + + "0kYBkGBgK7oQWRIVyoF2POvEP6EX1X8nvQk7O3NysVdRVbnia7gE3AzRYuha4kxs\n" + + "pI6xJkntLMS3K6him1Y9FHINIASFSB+C\n" + + "=5p00\n" + + "-----END PGP MESSAGE-----"; + @Test public void genLIT() throws IOException { ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); @@ -328,4 +340,22 @@ public class PGPDecryptionStreamTest { System.out.println(out); } + + @Test + public void genPENC_COMP_LIT() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addRecipient(cert)) + .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); + enc.close(); + + System.out.println(out); + } } From 0753f4d38acbc0bf1f2257496871ffa26734b0e9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 14 Sep 2022 19:29:47 +0200 Subject: [PATCH 0459/1204] Work on getting signature verification to function again --- .../OpenPgpMessageInputStream.java | 145 +++++++++++++----- .../automaton/PDA.java | 7 + 2 files changed, 116 insertions(+), 36 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 788be4ab..e95c9ff7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -30,19 +30,24 @@ import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; +import org.pgpainless.decryption_verification.automaton.StackAlphabet; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.signature.consumer.DetachedSignatureCheck; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; public class OpenPgpMessageInputStream extends InputStream { @@ -52,12 +57,12 @@ public class OpenPgpMessageInputStream extends InputStream { protected final BCPGInputStream bcpgIn; protected InputStream in; - private List signatures = new ArrayList<>(); - private List onePassSignatures = new ArrayList<>(); + private boolean closed = false; + + private Signatures signatures = new Signatures(); public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { - this.options = options; // TODO: Use BCPGInputStream.wrap(inputStream); if (inputStream instanceof BCPGInputStream) { this.bcpgIn = (BCPGInputStream) inputStream; @@ -65,41 +70,61 @@ public class OpenPgpMessageInputStream extends InputStream { this.bcpgIn = new BCPGInputStream(inputStream); } - walk(); + this.options = options; + this.signatures.addDetachedSignatures(options.getDetachedSignatures()); + + consumePackets(); } - private void walk() throws IOException, PGPException { - loop: while (true) { - - int tag = bcpgIn.nextPacketTag(); - if (tag == -1) { - break loop; - } - + /** + * This method consumes OpenPGP packets from the current {@link BCPGInputStream}. + * Once it reaches a "nested" OpenPGP packet (Literal Data, Compressed Data, Encrypted Data), it sets

in
+ * to the nested stream and breaks the loop. + * The nested stream is either a simple {@link InputStream} (in case of Literal Data), or another + * {@link OpenPgpMessageInputStream} in case of Compressed and Encrypted Data. + * + * @throws IOException + * @throws PGPException + */ + private void consumePackets() + throws IOException, PGPException { + int tag; + loop: while ((tag = bcpgIn.nextPacketTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); switch (nextPacket) { + + // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); PGPLiteralData literalData = new PGPLiteralData(bcpgIn); in = literalData.getDataStream(); break loop; + // Compressed Data - the content contains another OpenPGP message case COMP: automaton.next(InputAlphabet.CompressedData); PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options); break loop; + // One Pass Signatures case OPS: automaton.next(InputAlphabet.OnePassSignatures); - readOnePassSignatures(); + signatures.addOnePassSignatures(readOnePassSignatures()); break; + // Signatures - either prepended to the message, or corresponding to the One Pass Signatures case SIG: automaton.next(InputAlphabet.Signatures); - readSignatures(); + PGPSignatureList signatureList = readSignatures(); + if (automaton.peekStack() == StackAlphabet.ops) { + signatures.addOnePassCorrespondingSignatures(signatureList); + } else { + signatures.addPrependedSignatures(signatureList); + } break; + // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) case PKESK: case SKESK: case SED: @@ -200,6 +225,7 @@ public class OpenPgpMessageInputStream extends InputStream { bcpgIn.readPacket(); // skip marker packet break; + // Key Packets are illegal in this context case SK: case PK: case SSK: @@ -209,13 +235,14 @@ public class OpenPgpMessageInputStream extends InputStream { case UATTR: case MOD: - break; + throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); + // Experimental Packets are not supported case EXP_1: case EXP_2: case EXP_3: case EXP_4: - break; + throw new MalformedOpenPgpMessageException("Unsupported Packet in Stream: " + nextPacket); } } } @@ -247,7 +274,7 @@ public class OpenPgpMessageInputStream extends InputStream { return null; } - private void readOnePassSignatures() throws IOException { + private PGPOnePassSignatureList readOnePassSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag = bcpgIn.nextPacketTag(); @@ -262,12 +289,10 @@ public class OpenPgpMessageInputStream extends InputStream { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); - for (PGPOnePassSignature ops : signatureList) { - onePassSignatures.add(ops); - } + return signatureList; } - private void readSignatures() throws IOException { + private PGPSignatureList readSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag = bcpgIn.nextPacketTag(); @@ -283,9 +308,7 @@ public class OpenPgpMessageInputStream extends InputStream { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); PGPSignatureList signatureList = (PGPSignatureList) objectFactory.nextObject(); - for (PGPSignature signature : signatureList) { - signatures.add(signature); - } + return signatureList; } @Override @@ -298,15 +321,17 @@ public class OpenPgpMessageInputStream extends InputStream { // } } - if (r == -1) { + if (r != -1) { + byte b = (byte) r; + signatures.update(b); + } else { if (in instanceof OpenPgpMessageInputStream) { - System.out.println("Read -1: close " + automaton); in.close(); in = null; } else { try { System.out.println("Walk " + automaton); - walk(); + consumePackets(); } catch (PGPException e) { throw new RuntimeException(e); } @@ -317,25 +342,24 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public void close() throws IOException { - if (in == null) { - System.out.println("Close " + automaton); - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); + if (closed) { return; } - try { + + if (in != null) { in.close(); - in = null; - // Nested streams (except LiteralData) need to be closed. + in = null; // TODO: Collect result of in before nulling if (automaton.getState() != PDA.State.LiteralMessage) { automaton.next(InputAlphabet.EndOfSequence); automaton.assertValid(); } - } catch (IOException e) { - // + } else { + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); } super.close(); + closed = true; } private static class SortedESKs { @@ -369,4 +393,53 @@ public class OpenPgpMessageInputStream extends InputStream { return esks; } } + + private static class Signatures { + List detachedSignatures = new ArrayList<>(); + List prependedSignatures = new ArrayList<>(); + List onePassSignatures = new ArrayList<>(); + List correspondingSignatures = new ArrayList<>(); + + void addDetachedSignatures(Collection signatures) { + this.detachedSignatures.addAll(signatures); + } + + void addPrependedSignatures(PGPSignatureList signatures) { + for (PGPSignature signature : signatures) { + this.prependedSignatures.add(signature); + } + } + + void addOnePassSignatures(PGPOnePassSignatureList signatures) { + for (PGPOnePassSignature ops : signatures) { + this.onePassSignatures.add(ops); + } + } + + void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { + for (PGPSignature signature : signatures) { + correspondingSignatures.add(signature); + } + } + + public void update(byte b) { + /** + for (PGPSignature prepended : prependedSignatures) { + prepended.update(b); + } + for (PGPOnePassSignature ops : onePassSignatures) { + ops.update(b); + } + for (PGPSignature detached : detachedSignatures) { + detached.update(b); + } + */ + } + + public void finish() { + for (PGPSignature detached : detachedSignatures) { + + } + } + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index a3384070..793a3451 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -202,6 +202,13 @@ public class PDA { return state; } + public StackAlphabet peekStack() { + if (stack.isEmpty()) { + return null; + } + return stack.peek(); + } + /** * Return true, if the PDA is in a valid state (the OpenPGP message is valid). * From 7b9db97212eb541ffa05ad026bdb49214103de17 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 14 Sep 2022 19:41:22 +0200 Subject: [PATCH 0460/1204] Clean close() method --- .../OpenPgpMessageInputStream.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index e95c9ff7..d95e3e3c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -348,17 +348,11 @@ public class OpenPgpMessageInputStream extends InputStream { if (in != null) { in.close(); - in = null; // TODO: Collect result of in before nulling - if (automaton.getState() != PDA.State.LiteralMessage) { - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); - } - } else { - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); + in = null; } - super.close(); + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); closed = true; } From 9366700895de58a8d4903718e515f4866888fe60 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 14 Sep 2022 20:10:42 +0200 Subject: [PATCH 0461/1204] Add read(b,off,len) --- .../OpenPgpMessageInputStream.java | 71 +++++++++++++------ 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index d95e3e3c..4c6a85c8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -38,14 +38,12 @@ import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.signature.consumer.DetachedSignatureCheck; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -88,6 +86,7 @@ public class OpenPgpMessageInputStream extends InputStream { */ private void consumePackets() throws IOException, PGPException { + System.out.println("Walk " + automaton); int tag; loop: while ((tag = bcpgIn.nextPacketTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); @@ -237,7 +236,7 @@ public class OpenPgpMessageInputStream extends InputStream { case MOD: throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); - // Experimental Packets are not supported + // Experimental Packets are not supported case EXP_1: case EXP_2: case EXP_3: @@ -313,15 +312,19 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public int read() throws IOException { - int r = -1; - if (in != null) { - try { - r = in.read(); - } catch (IOException e) { - // - } + if (in == null) { + automaton.assertValid(); + return -1; } - if (r != -1) { + + int r; + try { + r = in.read(); + } catch (IOException e) { + r = -1; + } + boolean eos = r == -1; + if (!eos) { byte b = (byte) r; signatures.update(b); } else { @@ -330,7 +333,32 @@ public class OpenPgpMessageInputStream extends InputStream { in = null; } else { try { - System.out.println("Walk " + automaton); + System.out.println("Read consume"); + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + } + return r; + } + + @Override + public int read(byte[] b, int off, int len) + throws IOException { + + if (in == null) { + automaton.assertValid(); + return -1; + } + + int r = in.read(b, off, len); + if (r == -1) { + if (in instanceof OpenPgpMessageInputStream) { + in.close(); + in = null; + } else { + try { consumePackets(); } catch (PGPException e) { throw new RuntimeException(e); @@ -343,6 +371,7 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public void close() throws IOException { if (closed) { + automaton.assertValid(); return; } @@ -418,15 +447,15 @@ public class OpenPgpMessageInputStream extends InputStream { public void update(byte b) { /** - for (PGPSignature prepended : prependedSignatures) { - prepended.update(b); - } - for (PGPOnePassSignature ops : onePassSignatures) { - ops.update(b); - } - for (PGPSignature detached : detachedSignatures) { - detached.update(b); - } + for (PGPSignature prepended : prependedSignatures) { + prepended.update(b); + } + for (PGPOnePassSignature ops : onePassSignatures) { + ops.update(b); + } + for (PGPSignature detached : detachedSignatures) { + detached.update(b); + } */ } From 54d7d0c7aeaac5a31dbfe15d7459074c590413dd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 16 Sep 2022 00:51:49 +0200 Subject: [PATCH 0462/1204] Implement experimental signature verification (correctness only) --- .../MessageDecryptionStream.java | 278 ------------- .../OpenPgpMessageInputStream.java | 167 +++++++- .../automaton/NestingPDA.java | 339 ---------------- .../MalformedOpenPgpMessageException.java | 15 - .../OpenPgpMessageInputStreamTest.java | 368 +++++++++++++++++- .../PGPDecryptionStreamTest.java | 361 ----------------- .../automaton/NestingPDATest.java | 205 ---------- 7 files changed, 497 insertions(+), 1236 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java deleted file mode 100644 index 335e9d57..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageDecryptionStream.java +++ /dev/null @@ -1,278 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import org.bouncycastle.bcpg.BCPGInputStream; -import org.bouncycastle.bcpg.BCPGOutputStream; -import org.bouncycastle.bcpg.ModDetectionCodePacket; -import org.bouncycastle.bcpg.OnePassSignaturePacket; -import org.bouncycastle.bcpg.Packet; -import org.bouncycastle.bcpg.PacketTags; -import org.bouncycastle.bcpg.PublicKeyEncSessionPacket; -import org.bouncycastle.bcpg.SignaturePacket; -import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPPBEEncryptedData; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.pgpainless.algorithm.OpenPgpPacket; -import org.pgpainless.decryption_verification.automaton.InputAlphabet; -import org.pgpainless.decryption_verification.automaton.NestingPDA; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.pgpainless.exception.MessageNotIntegrityProtectedException; -import org.pgpainless.exception.MissingDecryptionMethodException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.util.Passphrase; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.SequenceInputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Stack; - -public class MessageDecryptionStream extends InputStream { - - private final ConsumerOptions options; - - NestingPDA automaton = new NestingPDA(); - // nested streams, outermost at the bottom of the stack - Stack packetLayers = new Stack<>(); - List pkeskList = new ArrayList<>(); - List skeskList = new ArrayList<>(); - - public MessageDecryptionStream(InputStream inputStream, ConsumerOptions options) - throws IOException, PGPException { - this.options = options; - packetLayers.push(Layer.initial(inputStream)); - walkLayer(); - } - - private void walkLayer() throws PGPException, IOException { - if (packetLayers.isEmpty()) { - return; - } - - // We are currently in the deepest layer - Layer layer = packetLayers.peek(); - BCPGInputStream inputStream = (BCPGInputStream) layer.inputStream; - - loop: while (true) { - if (inputStream.nextPacketTag() == -1) { - popLayer(); - break loop; - } - OpenPgpPacket tag = nextTagOrThrow(inputStream); - switch (tag) { - - case LIT: - automaton.next(InputAlphabet.LiteralData); - PGPLiteralData literalData = new PGPLiteralData(inputStream); - packetLayers.push(Layer.literalMessage(literalData.getDataStream())); - break loop; - - case COMP: - automaton.next(InputAlphabet.CompressedData); - PGPCompressedData compressedData = new PGPCompressedData(inputStream); - inputStream = new BCPGInputStream(compressedData.getDataStream()); - packetLayers.push(Layer.compressedData(inputStream)); - break; - - case OPS: - automaton.next(InputAlphabet.OnePassSignatures); - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - while (inputStream.nextPacketTag() == PacketTags.ONE_PASS_SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { - Packet packet = inputStream.readPacket(); - if (packet instanceof OnePassSignaturePacket) { - OnePassSignaturePacket sig = (OnePassSignaturePacket) packet; - sig.encode(bcpgOut); - } - } - PGPOnePassSignatureList onePassSignatures = (PGPOnePassSignatureList) ImplementationFactory.getInstance() - .getPGPObjectFactory(buf.toByteArray()).nextObject(); - break; - - case SIG: - automaton.next(InputAlphabet.Signatures); - - buf = new ByteArrayOutputStream(); - bcpgOut = new BCPGOutputStream(buf); - while (inputStream.nextPacketTag() == PacketTags.SIGNATURE || inputStream.nextPacketTag() == PacketTags.MARKER) { - Packet packet = inputStream.readPacket(); - if (packet instanceof SignaturePacket) { - SignaturePacket sig = (SignaturePacket) packet; - sig.encode(bcpgOut); - } - } - PGPSignatureList signatures = (PGPSignatureList) ImplementationFactory.getInstance() - .getPGPObjectFactory(buf.toByteArray()).nextObject(); - break; - - case PKESK: - PublicKeyEncSessionPacket pkeskPacket = (PublicKeyEncSessionPacket) inputStream.readPacket(); - pkeskList.add(pkeskPacket); - break; - - case SKESK: - SymmetricKeyEncSessionPacket skeskPacket = (SymmetricKeyEncSessionPacket) inputStream.readPacket(); - skeskList.add(skeskPacket); - break; - - case SED: - if (!options.isIgnoreMDCErrors()) { - throw new MessageNotIntegrityProtectedException(); - } - // No break; we continue below! - case SEIPD: - automaton.next(InputAlphabet.EncryptedData); - PGPEncryptedDataList encryptedDataList = assembleEncryptedDataList(inputStream); - - for (PGPEncryptedData encData : encryptedDataList) { - if (encData instanceof PGPPBEEncryptedData) { - PGPPBEEncryptedData skenc = (PGPPBEEncryptedData) encData; - for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - InputStream decryptedIn = skenc.getDataStream(decryptorFactory); - packetLayers.push(Layer.encryptedData(new BCPGInputStream(decryptedIn))); - walkLayer(); - break loop; - } - } - } - throw new MissingDecryptionMethodException("Cannot decrypt message."); - - case MARKER: - inputStream.readPacket(); // discard - break; - - case SK: - case PK: - case SSK: - case PSK: - case TRUST: - case UID: - case UATTR: - throw new MalformedOpenPgpMessageException("OpenPGP packet " + tag + " MUST NOT be part of OpenPGP messages."); - case MOD: - ModDetectionCodePacket modDetectionCodePacket = (ModDetectionCodePacket) inputStream.readPacket(); - break; - case EXP_1: - case EXP_2: - case EXP_3: - case EXP_4: - throw new MalformedOpenPgpMessageException("Experimental packet " + tag + " found inside the message."); - } - } - } - - private PGPEncryptedDataList assembleEncryptedDataList(BCPGInputStream inputStream) - throws IOException { - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - - for (SymmetricKeyEncSessionPacket skesk : skeskList) { - bcpgOut.write(skesk.getEncoded()); - } - skeskList.clear(); - for (PublicKeyEncSessionPacket pkesk : pkeskList) { - bcpgOut.write(pkesk.getEncoded()); - } - pkeskList.clear(); - - SequenceInputStream sqin = new SequenceInputStream( - new ByteArrayInputStream(buf.toByteArray()), inputStream); - - PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) ImplementationFactory.getInstance() - .getPGPObjectFactory(sqin).nextObject(); - return encryptedDataList; - } - - private OpenPgpPacket nextTagOrThrow(BCPGInputStream inputStream) - throws IOException, InvalidOpenPgpPacketException { - try { - return OpenPgpPacket.requireFromTag(inputStream.nextPacketTag()); - } catch (NoSuchElementException e) { - throw new InvalidOpenPgpPacketException(e.getMessage()); - } - } - - private void popLayer() throws MalformedOpenPgpMessageException { - if (packetLayers.pop().isNested) - automaton.next(InputAlphabet.EndOfSequence); - } - - @Override - public int read() throws IOException { - if (packetLayers.isEmpty()) { - automaton.assertValid(); - return -1; - } - - int r = -1; - try { - r = packetLayers.peek().inputStream.read(); - } catch (IOException e) { - } - if (r == -1) { - popLayer(); - try { - walkLayer(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - return read(); - } - return r; - } - - public static class InvalidOpenPgpPacketException extends PGPException { - - public InvalidOpenPgpPacketException(String message) { - super(message); - } - } - - private static class Layer { - InputStream inputStream; - boolean isNested; - - private Layer(InputStream inputStream, boolean isNested) { - this.inputStream = inputStream; - this.isNested = isNested; - } - - static Layer initial(InputStream inputStream) { - BCPGInputStream bcpgIn; - if (inputStream instanceof BCPGInputStream) { - bcpgIn = (BCPGInputStream) inputStream; - } else { - bcpgIn = new BCPGInputStream(inputStream); - } - return new Layer(bcpgIn, true); - } - - static Layer literalMessage(InputStream inputStream) { - return new Layer(inputStream, false); - } - - static Layer compressedData(InputStream inputStream) { - return new Layer(inputStream, true); - } - - static Layer encryptedData(InputStream inputStream) { - return new Layer(inputStream, true); - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 4c6a85c8..8dff7189 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -18,11 +18,13 @@ import org.bouncycastle.openpgp.PGPPBEEncryptedData; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; import org.pgpainless.PGPainless; @@ -38,6 +40,7 @@ import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; @@ -57,7 +60,7 @@ public class OpenPgpMessageInputStream extends InputStream { private boolean closed = false; - private Signatures signatures = new Signatures(); + private Signatures signatures; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { @@ -69,6 +72,7 @@ public class OpenPgpMessageInputStream extends InputStream { } this.options = options; + this.signatures = new Signatures(options); this.signatures.addDetachedSignatures(options.getDetachedSignatures()); consumePackets(); @@ -88,8 +92,9 @@ public class OpenPgpMessageInputStream extends InputStream { throws IOException, PGPException { System.out.println("Walk " + automaton); int tag; - loop: while ((tag = bcpgIn.nextPacketTag()) != -1) { + loop: while ((tag = getTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); + System.out.println(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream @@ -114,9 +119,10 @@ public class OpenPgpMessageInputStream extends InputStream { // Signatures - either prepended to the message, or corresponding to the One Pass Signatures case SIG: + boolean isCorrespondingToOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); PGPSignatureList signatureList = readSignatures(); - if (automaton.peekStack() == StackAlphabet.ops) { + if (isCorrespondingToOPS) { signatures.addOnePassCorrespondingSignatures(signatureList); } else { signatures.addPrependedSignatures(signatureList); @@ -246,6 +252,19 @@ public class OpenPgpMessageInputStream extends InputStream { } } + private int getTag() throws IOException { + try { + return bcpgIn.nextPacketTag(); + } catch (IOException e) { + if ("Stream closed".equals(e.getMessage())) { + // ZipInflater Streams sometimes close under our feet -.- + // Therefore we catch resulting IOEs and return -1 instead. + return -1; + } + throw e; + } + } + private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { int algorithm = pkesk.getAlgorithm(); List> decryptionKeyCandidates = new ArrayList<>(); @@ -276,8 +295,8 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPOnePassSignatureList readOnePassSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag = bcpgIn.nextPacketTag(); - while (tag == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { + int tag; + while ((tag = getTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; @@ -294,13 +313,13 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPSignatureList readSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag = bcpgIn.nextPacketTag(); + int tag = getTag(); while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.SIGNATURE) { SignaturePacket sigPacket = (SignaturePacket) packet; sigPacket.encode(bcpgOut); - tag = bcpgIn.nextPacketTag(); + tag = getTag(); } } bcpgOut.close(); @@ -328,6 +347,16 @@ public class OpenPgpMessageInputStream extends InputStream { byte b = (byte) r; signatures.update(b); } else { + in.close(); + in = null; + + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + signatures.finish(); + /* if (in instanceof OpenPgpMessageInputStream) { in.close(); in = null; @@ -335,10 +364,12 @@ public class OpenPgpMessageInputStream extends InputStream { try { System.out.println("Read consume"); consumePackets(); + signatures.finish(); } catch (PGPException e) { throw new RuntimeException(e); } } + */ } return r; } @@ -354,6 +385,16 @@ public class OpenPgpMessageInputStream extends InputStream { int r = in.read(b, off, len); if (r == -1) { + in.close(); + in = null; + + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + signatures.finish(); + /* if (in instanceof OpenPgpMessageInputStream) { in.close(); in = null; @@ -364,6 +405,7 @@ public class OpenPgpMessageInputStream extends InputStream { throw new RuntimeException(e); } } + */ } return r; } @@ -380,6 +422,12 @@ public class OpenPgpMessageInputStream extends InputStream { in = null; } + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + automaton.next(InputAlphabet.EndOfSequence); automaton.assertValid(); closed = true; @@ -418,50 +466,133 @@ public class OpenPgpMessageInputStream extends InputStream { } private static class Signatures { + final ConsumerOptions options; List detachedSignatures = new ArrayList<>(); List prependedSignatures = new ArrayList<>(); List onePassSignatures = new ArrayList<>(); List correspondingSignatures = new ArrayList<>(); + + private Signatures(ConsumerOptions options) { + this.options = options; + } + void addDetachedSignatures(Collection signatures) { + for (PGPSignature signature : signatures) { + long keyId = SignatureUtils.determineIssuerKeyId(signature); + PGPPublicKeyRing certificate = findCertificate(keyId); + initialize(signature, certificate, keyId); + } this.detachedSignatures.addAll(signatures); } void addPrependedSignatures(PGPSignatureList signatures) { + System.out.println("Adding " + signatures.size() + " prepended Signatures"); for (PGPSignature signature : signatures) { + long keyId = SignatureUtils.determineIssuerKeyId(signature); + PGPPublicKeyRing certificate = findCertificate(keyId); + initialize(signature, certificate, keyId); this.prependedSignatures.add(signature); } } void addOnePassSignatures(PGPOnePassSignatureList signatures) { + System.out.println("Adding " + signatures.size() + " OPSs"); for (PGPOnePassSignature ops : signatures) { + PGPPublicKeyRing certificate = findCertificate(ops.getKeyID()); + initialize(ops, certificate); this.onePassSignatures.add(ops); } } void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { + System.out.println("Adding " + signatures.size() + " Corresponding Signatures"); for (PGPSignature signature : signatures) { correspondingSignatures.add(signature); } } + private void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { + if (certificate == null) { + // SHIT + return; + } + PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() + .getPGPContentVerifierBuilderProvider(); + try { + signature.init(verifierProvider, certificate.getPublicKey(keyId)); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + + private void initialize(PGPOnePassSignature ops, PGPPublicKeyRing certificate) { + if (certificate == null) { + return; + } + PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() + .getPGPContentVerifierBuilderProvider(); + try { + ops.init(verifierProvider, certificate.getPublicKey(ops.getKeyID())); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + + private PGPPublicKeyRing findCertificate(long keyId) { + for (PGPPublicKeyRing cert : options.getCertificates()) { + PGPPublicKey verificationKey = cert.getPublicKey(keyId); + if (verificationKey != null) { + return cert; + } + } + return null; // TODO: Missing cert for sig + } + public void update(byte b) { - /** - for (PGPSignature prepended : prependedSignatures) { - prepended.update(b); - } - for (PGPOnePassSignature ops : onePassSignatures) { - ops.update(b); - } - for (PGPSignature detached : detachedSignatures) { - detached.update(b); - } - */ + for (PGPSignature prepended : prependedSignatures) { + prepended.update(b); + } + for (PGPOnePassSignature ops : onePassSignatures) { + ops.update(b); + } + for (PGPSignature detached : detachedSignatures) { + detached.update(b); + } } public void finish() { for (PGPSignature detached : detachedSignatures) { + boolean verified = false; + try { + verified = detached.verify(); + } catch (PGPException e) { + System.out.println(e.getMessage()); + } + System.out.println("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + } + for (PGPSignature prepended : prependedSignatures) { + boolean verified = false; + try { + verified = prepended.verify(); + } catch (PGPException e) { + System.out.println(e.getMessage()); + } + System.out.println("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + } + + + for (int i = 0; i < onePassSignatures.size(); i++) { + PGPOnePassSignature ops = onePassSignatures.get(i); + PGPSignature signature = correspondingSignatures.get(correspondingSignatures.size() - i - 1); + boolean verified = false; + try { + verified = ops.verify(signature); + } catch (PGPException e) { + System.out.println(e.getMessage()); + } + System.out.println("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java deleted file mode 100644 index cf5ee674..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/NestingPDA.java +++ /dev/null @@ -1,339 +0,0 @@ -package org.pgpainless.decryption_verification.automaton; - -import org.pgpainless.exception.MalformedOpenPgpMessageException; - -import java.util.Stack; - -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; - -/** - * Pushdown Automaton to verify the correct syntax of OpenPGP messages during decryption. - *

- * OpenPGP messages MUST follow certain rules in order to be well-formed. - * Section §11.3. of RFC4880 specifies a formal grammar for OpenPGP messages. - *

- * This grammar was transformed into a pushdown automaton, which is implemented below. - * The automaton only ends up in a valid state ({@link #isValid()} iff the OpenPGP message conformed to the - * grammar. - *

- * There are some specialties with this implementation though: - * Bouncy Castle combines ESKs and Encrypted Data Packets into a single object, so we do not have to - * handle those manually. - *

- * Bouncy Castle further combines OnePassSignatures and Signatures into lists, so instead of pushing multiple - * 'o's onto the stack repeatedly, a sequence of OnePassSignatures causes a single 'o' to be pushed to the stack. - * The same is true for Signatures. - *

- * Therefore, a message is valid, even if the number of OnePassSignatures and Signatures does not match. - * If a message contains at least one OnePassSignature, it is sufficient if there is at least one Signature to - * not cause a {@link MalformedOpenPgpMessageException}. - * - * @see RFC4880 §11.3. OpenPGP Messages - */ -public class NestingPDA { - - /** - * Set of states of the automaton. - * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, NestingPDA)} - * method. - */ - public enum State { - - OpenPgpMessage { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - if (stackItem != msg) { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - switch (input) { - - case LiteralData: - return LiteralMessage; - - case Signatures: - automaton.pushStack(msg); - return OpenPgpMessage; - - case OnePassSignatures: - automaton.pushStack(ops); - automaton.pushStack(msg); - return OpenPgpMessage; - - case CompressedData: - return CompressedMessage; - - case EncryptedData: - return EncryptedMessage; - - case EndOfSequence: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - LiteralMessage { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - - case Signatures: - if (stackItem == ops) { - return CorrespondingSignature; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignatures: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - CompressedMessage { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - case Signatures: - if (stackItem == ops) { - return CorrespondingSignature; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignatures: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - EncryptedMessage { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - case Signatures: - if (stackItem == ops) { - return CorrespondingSignature; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignatures: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - CorrespondingSignature { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - Valid { - @Override - State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException { - throw new MalformedOpenPgpMessageException(this, input, null); - } - }, - ; - - /** - * Pop the automatons stack and transition to another state. - * If no valid transition from the current state is available given the popped stack item and input symbol, - * a {@link MalformedOpenPgpMessageException} is thrown. - * Otherwise, the stack is manipulated according to the valid transition and the new state is returned. - * - * @param input input symbol - * @param automaton automaton - * @return new state of the automaton - * @throws MalformedOpenPgpMessageException in case of an illegal input symbol - */ - abstract State transition(InputAlphabet input, NestingPDA automaton) throws MalformedOpenPgpMessageException; - } - - private final Stack stack = new Stack<>(); - private State state; - // Some OpenPGP packets have nested contents (e.g. compressed / encrypted data). - NestingPDA nestedSequence = null; - - public NestingPDA() { - state = State.OpenPgpMessage; - stack.push(terminus); - stack.push(msg); - } - - /** - * Process the next input packet. - * - * @param input input - * @throws MalformedOpenPgpMessageException in case the input packet is illegal here - */ - public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { - _next(input); - } - - /** - * Process the next input packet. - * This method returns true, iff the given input triggered a successful closing of this PDAs nested PDA. - *

- * This is for example the case, if the current packet is a Compressed Data packet which contains a - * valid nested OpenPGP message and the last input was {@link InputAlphabet#EndOfSequence} indicating the - * end of the Compressed Data packet. - *

- * If the input triggered this PDAs nested PDA to close its nested PDA, this method returns false - * in order to prevent this PDA from closing its nested PDA prematurely. - * - * @param input input - * @return true if this just closed its nested sequence, false otherwise - * @throws MalformedOpenPgpMessageException if the input is illegal - */ - private boolean _next(InputAlphabet input) throws MalformedOpenPgpMessageException { - if (nestedSequence != null) { - boolean sequenceInNestedSequenceWasClosed = nestedSequence._next(input); - if (sequenceInNestedSequenceWasClosed) return false; // No need to close out nested sequence too. - } else { - // make a state transition in this automaton - state = state.transition(input, this); - - // If the processed packet contains nested sequence, open nested automaton for it - if (input == InputAlphabet.CompressedData || input == InputAlphabet.EncryptedData) { - nestedSequence = new NestingPDA(); - } - } - - if (input != InputAlphabet.EndOfSequence) { - return false; - } - - // Close nested sequence if needed - boolean nestedIsInnerMost = nestedSequence != null && nestedSequence.isInnerMost(); - if (nestedIsInnerMost) { - if (nestedSequence.isValid()) { - // Close nested sequence - nestedSequence = null; - return true; - } else { - throw new MalformedOpenPgpMessageException("Climbing up nested message validation failed." + - " Automaton for current nesting level is not in valid state: " + nestedSequence.getState() + " " + nestedSequence.stack.peek() + " (Input was " + input + ")"); - } - } - return false; - } - - /** - * Return the current state of the PDA. - * - * @return state - */ - private State getState() { - return state; - } - - /** - * Return true, if the PDA is in a valid state (the OpenPGP message is valid). - * - * @return true if valid, false otherwise - */ - public boolean isValid() { - return getState() == State.Valid && stack.isEmpty(); - } - - public void assertValid() throws MalformedOpenPgpMessageException { - if (!isValid()) { - throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString()); - } - } - - /** - * Pop an item from the stack. - * - * @return stack item - */ - private StackAlphabet popStack() { - return stack.pop(); - } - - /** - * Push an item onto the stack. - * - * @param item item - */ - private void pushStack(StackAlphabet item) { - stack.push(item); - } - - /** - * Return true, if this packet sequence has no nested sequence. - * A nested sequence is for example the content of a Compressed Data packet. - * - * @return true if PDA is innermost, false if it has a nested sequence - */ - private boolean isInnerMost() { - return nestedSequence == null; - } - - @Override - public String toString() { - StringBuilder out = new StringBuilder("State: ").append(state) - .append(", Stack (asc.): ").append(stack) - .append('\n'); - if (nestedSequence != null) { - // recursively call toString() on nested PDAs and indent their representation - String nestedToString = nestedSequence.toString(); - String[] lines = nestedToString.split("\n"); - for (int i = 0; i < lines.length; i++) { - String nestedLine = lines[i]; - out.append(i == 0 ? "⤡ " : " ") // indent nested PDA - .append(nestedLine) - .append('\n'); - } - } - return out.toString(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index 21b6e807..0069209c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -5,7 +5,6 @@ package org.pgpainless.exception; import org.pgpainless.decryption_verification.automaton.InputAlphabet; -import org.pgpainless.decryption_verification.automaton.NestingPDA; import org.pgpainless.decryption_verification.automaton.PDA; import org.pgpainless.decryption_verification.automaton.StackAlphabet; @@ -21,21 +20,7 @@ public class MalformedOpenPgpMessageException extends RuntimeException { super(message); } - public MalformedOpenPgpMessageException(String message, MalformedOpenPgpMessageException cause) { - super(message, cause); - } - - public MalformedOpenPgpMessageException(NestingPDA.State state, - InputAlphabet input, - StackAlphabet stackItem) { - this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); - } - public MalformedOpenPgpMessageException(PDA.State state, InputAlphabet input, StackAlphabet stackItem) { this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } - - public MalformedOpenPgpMessageException(String message, PDA automaton) { - super(message + automaton.toString()); - } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index 5b42101c..af1fac44 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -1,87 +1,402 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_COMP_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.COMP_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.KEY; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.LIT_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PASSPHRASE; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PENC_COMP_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.PLAINTEXT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SENC_LIT; -import static org.pgpainless.decryption_verification.PGPDecryptionStreamTest.SIG_LIT; public class OpenPgpMessageInputStreamTest { + + public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + + "Comment: Alice \n" + + "\n" + + "lFgEYxzSCBYJKwYBBAHaRw8BAQdAeJU8m4GOJb1eQgv/ryilFHRfNLTYFMNqL6zj\n" + + "r0vF7dsAAP42rAtngpJ6dZxoZlJX0Je65zk1VMPeTrXaWfPS2HSKBRGptBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMc0ggJEHpwH8aQTT9M\n" + + "FiEE2gWEjzfUaOb5gsiJenAfxpBNP0wCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAApZEBALUXHtvswPZG28YO+16Men6/fpk+scvqpNMnD4ty3IkAAPwK6TuXjNnZ\n" + + "0XuWdnilvLMV23Ai1d5g6em+lwLK5M2SApxdBGMc0ggSCisGAQQBl1UBBQEBB0D8\n" + + "mNUVX8y2MXFaSeFYqOTPFnGT7dgNVdn6yc0UtkkHOgMBCAcAAP9y9OtP4SX9voPb\n" + + "ID2u9PkJKgo4hTB8NK5LouGppdRtEBGriHUEGBYKAB0FAmMc0ggCngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRB6cB/GkE0/TAywAQDpZRJS/joFH4+xcwheqWfI7ay/\n" + + "WfojUoGQMYGnUjsgYwEAkceRUsgkqI0SVgYvuglfaQpZ9k2ns1mZGVLkXvu/yQyc\n" + + "WARjHNIIFgkrBgEEAdpHDwEBB0BGN9BybSOrj8B6gim1SjbB/IiqAshlzMDunVkQ\n" + + "X23npQABAJqvjOOY7qhBuTusC5/Q5+25iLrhMn4TI+LXlJHMVNOaE0OI1QQYFgoA\n" + + "fQUCYxzSCAKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMc0ggACgkQ\n" + + "KALh4BJQXl6yTQD/dh0N5228Uwtu7XHy6dmpMRX62cac5tXQ9WaDzpy8STgBAMdn\n" + + "Mq948UOYEhdk/ZY2/hwux/4t+FHvqrXW8ziBe4cLAAoJEHpwH8aQTT9M1hQA/3Ms\n" + + "P3kzoed3VsWu1ZMr7dKEngbc6SoJ2XPayzN0QYJaAQCIY5NcT9mZF97HWV3Vgeum\n" + + "00sWMHXfkW3+nl5OpUZaDA==\n" + + "=THgv\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String PLAINTEXT = "Hello, World!\n"; + public static final String PASSPHRASE = "sw0rdf1sh"; + + public static final String LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCg==\n" + + "=WGju\n" + + "-----END PGP MESSAGE-----"; + + public static final String LIT_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCssUYgAAAAAASGVsbG8sIFdvcmxkIQo=\n" + + "=A91Q\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owE7LZLEAAIeqTk5+ToK4flFOSmKXAA=\n" + + "=ZYDg\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEDAA==\n" + + "=MDzg\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEBRwC4/6MDQlpoOTFBWSZTWVuW2KAAAAr3hGAQBABgBABAAIAWBJAAAAggADFM\n" + + "ABNBqBo00N6puqWR+TqInoXQ58XckU4UJBbltigA\n" + + "=K9Zl\n" + + "-----END PGP MESSAGE-----"; + + public static final String SIG_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "iHUEABYKACcFAmMc1i0JECgC4eASUF5eFiEEjN3RiJxCf/TyYOQjKALh4BJQXl4A\n" + + "AHkrAP98uPpqrgIix7epgL7MM1cjXXGSxqbDfXHwgptk1YGQlgD/fw89VGcXwFaI\n" + + "2k7kpXQYy/1BqnovM/jZ3X3mXhhTaAOjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" + + "=WKPn\n" + + "-----END PGP MESSAGE-----"; + + public static final String SENC_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0ECQMCuZ0qHNXWnGhg0j8Bdm1cxV65sYb7jDgb4rRMtdNpQ1dC4UpSYuk9YWS2\n" + + "DpNEijbX8b/P1UOK2kJczNDADMRegZuLEI+dNsBnJjk=\n" + + "=i4Y0\n" + + "-----END PGP MESSAGE-----"; + + public static final String PENC_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4Dyqa/GWUy6WsSAQdAQ62BwmUt8Iby0+jvrLhMgST79KR/as+dyl0nf1uki2sw\n" + + "Thg1Ojtf0hOyJgcpQ4nP2Q0wYFR0F1sCydaIlTGreYZHlGtybP7/Ml6KNZILTRWP\n" + + "0kYBkGBgK7oQWRIVyoF2POvEP6EX1X8nvQk7O3NysVdRVbnia7gE3AzRYuha4kxs\n" + + "pI6xJkntLMS3K6him1Y9FHINIASFSB+C\n" + + "=5p00\n" + + "-----END PGP MESSAGE-----"; + + public static final String OPS_LIT_SIG = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "kA0DAAoWKALh4BJQXl4ByxRiAAAAAABIZWxsbywgV29ybGQhCoh1BAAWCgAnBQJj\n" + + "I3fSCRAoAuHgElBeXhYhBIzd0YicQn/08mDkIygC4eASUF5eAADLOgEA766VyMMv\n" + + "sxfQwQHly3T6ySHSNhYEpoyvdxVqhjBBR+EA/3i6C8lKFPPTh/PvTGbVFOl+eUSV\n" + + "I0w3c+BRY/pO0m4H\n" + + "=tkTV\n" + + "-----END PGP MESSAGE-----"; + + public static void main(String[] args) throws Exception { + // genLIT(); + // genLIT_LIT(); + // genCOMP_LIT(); + // genCOMP(); + // genCOMP_COMP_LIT(); + // genKey(); + // genSIG_LIT(); + // genSENC_LIT(); + // genPENC_COMP_LIT(); + genOPS_LIT_SIG(); + } + + public static void genLIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + armorOut.close(); + } + + public static void genLIT_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + armorOut.close(); + } + + public static void genCOMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut.close(); + armorOut.close(); + } + + public static void genCOMP() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + compOut.close(); + armorOut.close(); + } + + public static void genCOMP_COMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + + PGPCompressedDataGenerator compGen1 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut1 = compGen1.open(armorOut); + + PGPCompressedDataGenerator compGen2 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.BZIP2); + OutputStream compOut2 = compGen2.open(compOut1); + + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut2, PGPLiteralDataGenerator.BINARY, "", PGPLiteralDataGenerator.NOW, new byte[1 << 9]); + + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut2.close(); + compOut1.close(); + armorOut.close(); + } + + public static void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + System.out.println(PGPainless.asciiArmor( + PGPainless.generateKeyRing().modernKeyRing("Alice ") + )); + } + + public static void genSIG_LIT() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); + EncryptionStream signer = PGPainless.encryptAndOrSign() + .onOutputStream(msgOut) + .withOptions( + ProducerOptions.sign( + SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys) + ).setAsciiArmor(false) + ); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), signer); + signer.close(); + EncryptionResult result = signer.getResult(); + PGPSignature detachedSignature = result.getDetachedSignatures().get(result.getDetachedSignatures().keySet().iterator().next()).iterator().next(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(out); + armorOut.flush(); + detachedSignature.encode(armorOut); + armorOut.write(msgOut.toByteArray()); + armorOut.close(); + + String armored = out.toString(); + System.out.println(armored + .replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n") + .replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----")); + } + + public static void genSENC_LIT() throws PGPException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + enc.close(); + + System.out.println(out); + } + + public static void genPENC_COMP_LIT() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addRecipient(cert)) + .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); + enc.close(); + + System.out.println(out); + } + + public static void genOPS_LIT_SIG() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.sign(SigningOptions.get() + .addSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys)) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); + enc.close(); + + System.out.println(out); + } + @Test public void testProcessLIT() throws IOException, PGPException { - String plain = process(LIT, ConsumerOptions.get()); + String plain = processReadBuffered(LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(LIT, ConsumerOptions.get()); assertEquals(PLAINTEXT, plain); } @Test public void testProcessLIT_LIT_fails() { assertThrows(MalformedOpenPgpMessageException.class, - () -> process(LIT_LIT, ConsumerOptions.get())); + () -> processReadBuffered(LIT_LIT, ConsumerOptions.get())); + + assertThrows(MalformedOpenPgpMessageException.class, + () -> processReadSequential(LIT_LIT, ConsumerOptions.get())); } @Test public void testProcessCOMP_LIT() throws PGPException, IOException { - String plain = process(COMP_LIT, ConsumerOptions.get()); + String plain = processReadBuffered(COMP_LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(COMP_LIT, ConsumerOptions.get()); assertEquals(PLAINTEXT, plain); } @Test public void testProcessCOMP_fails() { assertThrows(MalformedOpenPgpMessageException.class, - () -> process(COMP, ConsumerOptions.get())); + () -> processReadBuffered(COMP, ConsumerOptions.get())); + + assertThrows(MalformedOpenPgpMessageException.class, + () -> processReadSequential(COMP, ConsumerOptions.get())); } @Test public void testProcessCOMP_COMP_LIT() throws PGPException, IOException { - String plain = process(COMP_COMP_LIT, ConsumerOptions.get()); + String plain = processReadBuffered(COMP_COMP_LIT, ConsumerOptions.get()); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(COMP_COMP_LIT, ConsumerOptions.get()); assertEquals(PLAINTEXT, plain); } @Test public void testProcessSIG_LIT() throws PGPException, IOException { - String plain = process(SIG_LIT, ConsumerOptions.get()); + PGPPublicKeyRing cert = PGPainless.extractCertificate( + PGPainless.readKeyRing().secretKeyRing(KEY)); + + String plain = processReadBuffered(SIG_LIT, ConsumerOptions.get() + .addVerificationCert(cert)); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(SIG_LIT, ConsumerOptions.get() + .addVerificationCert(cert)); assertEquals(PLAINTEXT, plain); } @Test public void testProcessSENC_LIT() throws PGPException, IOException { - String plain = process(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + String plain = processReadBuffered(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); assertEquals(PLAINTEXT, plain); } @Test public void testProcessPENC_COMP_LIT() throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - String plain = process(PENC_COMP_LIT, ConsumerOptions.get() + String plain = processReadBuffered(PENC_COMP_LIT, ConsumerOptions.get() + .addDecryptionKey(secretKeys)); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(PENC_COMP_LIT, ConsumerOptions.get() .addDecryptionKey(secretKeys)); assertEquals(PLAINTEXT, plain); } - private String process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + @Test + public void testProcessOPS_LIT_SIG() throws IOException, PGPException { + PGPPublicKeyRing cert = PGPainless.extractCertificate(PGPainless.readKeyRing().secretKeyRing(KEY)); + String plain = processReadBuffered(OPS_LIT_SIG, ConsumerOptions.get() + .addVerificationCert(cert)); + assertEquals(PLAINTEXT, plain); + + plain = processReadSequential(OPS_LIT_SIG, ConsumerOptions.get() + .addVerificationCert(cert)); + assertEquals(PLAINTEXT, plain); + } + + private String processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(in, out); @@ -89,6 +404,19 @@ public class OpenPgpMessageInputStreamTest { return out.toString(); } + private String processReadSequential(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + OpenPgpMessageInputStream in = get(armoredMessage, options); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + int r; + while ((r = in.read()) != -1) { + out.write(r); + } + + in.close(); + return out.toString(); + } + private OpenPgpMessageInputStream get(String armored, ConsumerOptions options) throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java deleted file mode 100644 index 8da44ec8..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PGPDecryptionStreamTest.java +++ /dev/null @@ -1,361 +0,0 @@ -package org.pgpainless.decryption_verification; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.bcpg.CompressionAlgorithmTags; -import org.bouncycastle.openpgp.PGPCompressedDataGenerator; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPLiteralDataGenerator; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.encryption_signing.EncryptionOptions; -import org.pgpainless.encryption_signing.EncryptionResult; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.util.ArmoredInputStreamFactory; -import org.pgpainless.util.Passphrase; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class PGPDecryptionStreamTest { - - public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: PGPainless\n" + - "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + - "Comment: Alice \n" + - "\n" + - "lFgEYxzSCBYJKwYBBAHaRw8BAQdAeJU8m4GOJb1eQgv/ryilFHRfNLTYFMNqL6zj\n" + - "r0vF7dsAAP42rAtngpJ6dZxoZlJX0Je65zk1VMPeTrXaWfPS2HSKBRGptBxBbGlj\n" + - "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMc0ggJEHpwH8aQTT9M\n" + - "FiEE2gWEjzfUaOb5gsiJenAfxpBNP0wCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + - "AQAApZEBALUXHtvswPZG28YO+16Men6/fpk+scvqpNMnD4ty3IkAAPwK6TuXjNnZ\n" + - "0XuWdnilvLMV23Ai1d5g6em+lwLK5M2SApxdBGMc0ggSCisGAQQBl1UBBQEBB0D8\n" + - "mNUVX8y2MXFaSeFYqOTPFnGT7dgNVdn6yc0UtkkHOgMBCAcAAP9y9OtP4SX9voPb\n" + - "ID2u9PkJKgo4hTB8NK5LouGppdRtEBGriHUEGBYKAB0FAmMc0ggCngECmwwFFgID\n" + - "AQAECwkIBwUVCgkICwAKCRB6cB/GkE0/TAywAQDpZRJS/joFH4+xcwheqWfI7ay/\n" + - "WfojUoGQMYGnUjsgYwEAkceRUsgkqI0SVgYvuglfaQpZ9k2ns1mZGVLkXvu/yQyc\n" + - "WARjHNIIFgkrBgEEAdpHDwEBB0BGN9BybSOrj8B6gim1SjbB/IiqAshlzMDunVkQ\n" + - "X23npQABAJqvjOOY7qhBuTusC5/Q5+25iLrhMn4TI+LXlJHMVNOaE0OI1QQYFgoA\n" + - "fQUCYxzSCAKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMc0ggACgkQ\n" + - "KALh4BJQXl6yTQD/dh0N5228Uwtu7XHy6dmpMRX62cac5tXQ9WaDzpy8STgBAMdn\n" + - "Mq948UOYEhdk/ZY2/hwux/4t+FHvqrXW8ziBe4cLAAoJEHpwH8aQTT9M1hQA/3Ms\n" + - "P3kzoed3VsWu1ZMr7dKEngbc6SoJ2XPayzN0QYJaAQCIY5NcT9mZF97HWV3Vgeum\n" + - "00sWMHXfkW3+nl5OpUZaDA==\n" + - "=THgv\n" + - "-----END PGP PRIVATE KEY BLOCK-----"; - - public static final String PLAINTEXT = "Hello, World!\n"; - public static final String PASSPHRASE = "sw0rdf1sh"; - - public static final String LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "yxRiAAAAAABIZWxsbywgV29ybGQhCg==\n" + - "=WGju\n" + - "-----END PGP MESSAGE-----"; - - public static final String LIT_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "yxRiAAAAAABIZWxsbywgV29ybGQhCssUYgAAAAAASGVsbG8sIFdvcmxkIQo=\n" + - "=A91Q\n" + - "-----END PGP MESSAGE-----"; - - public static final String COMP_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "owE7LZLEAAIeqTk5+ToK4flFOSmKXAA=\n" + - "=ZYDg\n" + - "-----END PGP MESSAGE-----"; - - public static final String COMP = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "owEDAA==\n" + - "=MDzg\n" + - "-----END PGP MESSAGE-----"; - - public static final String COMP_COMP_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "owEBRwC4/6MDQlpoOTFBWSZTWVuW2KAAAAr3hGAQBABgBABAAIAWBJAAAAggADFM\n" + - "ABNBqBo00N6puqWR+TqInoXQ58XckU4UJBbltigA\n" + - "=K9Zl\n" + - "-----END PGP MESSAGE-----"; - - public static final String SIG_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "iHUEABYKACcFAmMc1i0JECgC4eASUF5eFiEEjN3RiJxCf/TyYOQjKALh4BJQXl4A\n" + - "AHkrAP98uPpqrgIix7epgL7MM1cjXXGSxqbDfXHwgptk1YGQlgD/fw89VGcXwFaI\n" + - "2k7kpXQYy/1BqnovM/jZ3X3mXhhTaAOjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" + - "=WKPn\n" + - "-----END PGP MESSAGE-----"; - - public static final String SENC_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "jA0ECQMCuZ0qHNXWnGhg0j8Bdm1cxV65sYb7jDgb4rRMtdNpQ1dC4UpSYuk9YWS2\n" + - "DpNEijbX8b/P1UOK2kJczNDADMRegZuLEI+dNsBnJjk=\n" + - "=i4Y0\n" + - "-----END PGP MESSAGE-----"; - - public static final String PENC_COMP_LIT = "" + - "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "hF4Dyqa/GWUy6WsSAQdAQ62BwmUt8Iby0+jvrLhMgST79KR/as+dyl0nf1uki2sw\n" + - "Thg1Ojtf0hOyJgcpQ4nP2Q0wYFR0F1sCydaIlTGreYZHlGtybP7/Ml6KNZILTRWP\n" + - "0kYBkGBgK7oQWRIVyoF2POvEP6EX1X8nvQk7O3NysVdRVbnia7gE3AzRYuha4kxs\n" + - "pI6xJkntLMS3K6him1Y9FHINIASFSB+C\n" + - "=5p00\n" + - "-----END PGP MESSAGE-----"; - - @Test - public void genLIT() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); - OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - armorOut.close(); - } - - @Test - public void processLIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - assertEquals(PLAINTEXT, out.toString()); - armorIn.close(); - } - - @Test - public void getLIT_LIT() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); - OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - - litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - - armorOut.close(); - } - - @Test - public void processLIT_LIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(LIT_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - assertThrows(MalformedOpenPgpMessageException.class, () -> Streams.pipeAll(decIn, out)); - } - - @Test - public void genCOMP_LIT() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); - OutputStream compOut = compGen.open(armorOut); - PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); - OutputStream litOut = litGen.open(compOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - compOut.close(); - armorOut.close(); - } - - @Test - public void processCOMP_LIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - decIn.close(); - armorIn.close(); - - assertEquals(PLAINTEXT, out.toString()); - } - - @Test - public void genCOMP() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); - OutputStream compOut = compGen.open(armorOut); - compOut.close(); - armorOut.close(); - } - - @Test - public void processCOMP() throws IOException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - assertThrows(MalformedOpenPgpMessageException.class, () -> { - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - Streams.drain(decIn); - }); - } - - @Test - public void genCOMP_COMP_LIT() throws IOException { - ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); - - PGPCompressedDataGenerator compGen1 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); - OutputStream compOut1 = compGen1.open(armorOut); - - PGPCompressedDataGenerator compGen2 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.BZIP2); - OutputStream compOut2 = compGen2.open(compOut1); - - PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); - OutputStream litOut = litGen.open(compOut2, PGPLiteralDataGenerator.BINARY, "", PGPLiteralDataGenerator.NOW, new byte[1 << 9]); - - litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - litOut.close(); - compOut2.close(); - compOut1.close(); - armorOut.close(); - } - - @Test - public void processCOMP_COMP_LIT() throws PGPException, IOException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(COMP_COMP_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - decIn.close(); - - assertEquals(PLAINTEXT, out.toString()); - } - - @Test - public void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - System.out.println(PGPainless.asciiArmor( - PGPainless.generateKeyRing().modernKeyRing("Alice ") - )); - } - - @Test - public void genSIG_LIT() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); - EncryptionStream signer = PGPainless.encryptAndOrSign() - .onOutputStream(msgOut) - .withOptions( - ProducerOptions.sign( - SigningOptions.get() - .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys) - ).setAsciiArmor(false) - ); - - Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), signer); - signer.close(); - EncryptionResult result = signer.getResult(); - PGPSignature detachedSignature = result.getDetachedSignatures().get(result.getDetachedSignatures().keySet().iterator().next()).iterator().next(); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ArmoredOutputStream armorOut = new ArmoredOutputStream(out); - armorOut.flush(); - detachedSignature.encode(armorOut); - armorOut.write(msgOut.toByteArray()); - armorOut.close(); - - String armored = out.toString(); - System.out.println(armored - .replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n") - .replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----")); - } - - @Test - public void processSIG_LIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(SIG_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get()); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - decIn.close(); - - System.out.println(out); - } - - @Test - public void genSENC_LIT() throws PGPException, IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) - .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() - .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) - .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); - enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); - enc.close(); - - System.out.println(out); - } - - @Test - public void processSENC_LIT() throws IOException, PGPException { - ByteArrayInputStream bytesIn = new ByteArrayInputStream(SENC_LIT.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - MessageDecryptionStream decIn = new MessageDecryptionStream(armorIn, ConsumerOptions.get() - .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decIn, out); - decIn.close(); - - System.out.println(out); - } - - @Test - public void genPENC_COMP_LIT() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) - .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() - .addRecipient(cert)) - .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); - - Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); - enc.close(); - - System.out.println(out); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java deleted file mode 100644 index 8c1c4921..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/NestingPDATest.java +++ /dev/null @@ -1,205 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.automaton; - -import org.junit.jupiter.api.Test; -import org.pgpainless.exception.MalformedOpenPgpMessageException; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NestingPDATest { - - /** - * MSG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS MSG SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * SIG MSG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS COMP(MSG) SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * OPS ENC(COMP(COMP(MSG))) SIG is valid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOpsSignedEncryptedCompressedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.EncryptedData); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.CompressedData); - - automaton.next(InputAlphabet.LiteralData); - - automaton.next(InputAlphabet.EndOfSequence); - automaton.next(InputAlphabet.EndOfSequence); - automaton.next(InputAlphabet.EndOfSequence); - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } - - /** - * MSG SIG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testLiteralPlusSigsFails() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.Signatures)); - } - - /** - * MSG MSG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG MSG) SIG is invalid (two literal packets are illegal). - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedMessageWithTwoLiteralDataPacketsFails() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG) MSG SIG is invalid. - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testOPSSignedMessageWithTwoLiteralDataPacketsFails2() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.LiteralData)); - } - - /** - * OPS COMP(MSG SIG) is invalid (MSG SIG does not form valid nested message). - * - * @throws MalformedOpenPgpMessageException fail - */ - @Test - public void testCorrespondingSignaturesOfOpsSignedMessageAreLayerFurtherDownFails() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.Signatures)); - } - - /** - * Empty COMP is invalid. - */ - @Test - public void testEmptyCompressedDataIsInvalid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.CompressedData); - assertThrows(MalformedOpenPgpMessageException.class, - () -> automaton.next(InputAlphabet.EndOfSequence)); - } - - @Test - public void testOPSSignedEncryptedCompressedOPSSignedMessageIsValid() throws MalformedOpenPgpMessageException { - NestingPDA automaton = new NestingPDA(); - automaton.next(InputAlphabet.OnePassSignatures); - - automaton.next(InputAlphabet.EncryptedData); - automaton.next(InputAlphabet.OnePassSignatures); - - automaton.next(InputAlphabet.CompressedData); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); - - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - automaton.next(InputAlphabet.Signatures); - automaton.next(InputAlphabet.EndOfSequence); - - assertTrue(automaton.isValid()); - } -} From 7537c9520c07fd3499f683a37b6f651f5f6b8549 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 19 Sep 2022 13:07:33 +0200 Subject: [PATCH 0463/1204] WIP: Add LayerMetadata class --- .../pgpainless/algorithm/OpenPgpPacket.java | 2 +- .../OpenPgpMessageInputStream.java | 271 ++++++++++-------- 2 files changed, 149 insertions(+), 124 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java index 63d14a31..41e3fb08 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java @@ -29,7 +29,7 @@ public enum OpenPgpPacket { PSK(PacketTags.PUBLIC_SUBKEY), UATTR(PacketTags.USER_ATTRIBUTE), SEIPD(PacketTags.SYM_ENC_INTEGRITY_PRO), - MOD(PacketTags.MOD_DETECTION_CODE), + MDC(PacketTags.MOD_DETECTION_CODE), EXP_1(PacketTags.EXPERIMENTAL_1), EXP_2(PacketTags.EXPERIMENTAL_2), diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 8dff7189..1a38fced 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification; import org.bouncycastle.bcpg.BCPGInputStream; @@ -27,9 +31,12 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; +import org.bouncycastle.pqc.crypto.rainbow.Layer; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; import org.pgpainless.decryption_verification.automaton.StackAlphabet; @@ -55,15 +62,22 @@ public class OpenPgpMessageInputStream extends InputStream { protected final PDA automaton = new PDA(); protected final ConsumerOptions options; + protected final OpenPgpMetadata.Builder resultBuilder; protected final BCPGInputStream bcpgIn; protected InputStream in; private boolean closed = false; private Signatures signatures; + private LayerMetadata layerMetadata; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { + this(inputStream, options, null); + } + + OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, LayerMetadata layerMetadata) + throws PGPException, IOException { // TODO: Use BCPGInputStream.wrap(inputStream); if (inputStream instanceof BCPGInputStream) { this.bcpgIn = (BCPGInputStream) inputStream; @@ -72,12 +86,35 @@ public class OpenPgpMessageInputStream extends InputStream { } this.options = options; + this.resultBuilder = OpenPgpMetadata.getBuilder(); this.signatures = new Signatures(options); this.signatures.addDetachedSignatures(options.getDetachedSignatures()); consumePackets(); } + static class LayerMetadata { + + private CompressionAlgorithm compressionAlgorithm; + private SymmetricKeyAlgorithm symmetricKeyAlgorithm; + private LayerMetadata child; + + public LayerMetadata setCompressionAlgorithm(CompressionAlgorithm algorithm) { + this.compressionAlgorithm = algorithm; + return this; + } + + public LayerMetadata setSymmetricEncryptionAlgorithm(SymmetricKeyAlgorithm algorithm) { + this.symmetricKeyAlgorithm = algorithm; + return this; + } + + public LayerMetadata setChild(LayerMetadata child) { + this.child = child; + return this; + } + } + /** * This method consumes OpenPGP packets from the current {@link BCPGInputStream}. * Once it reaches a "nested" OpenPGP packet (Literal Data, Compressed Data, Encrypted Data), it sets

in
@@ -92,7 +129,7 @@ public class OpenPgpMessageInputStream extends InputStream { throws IOException, PGPException { System.out.println("Walk " + automaton); int tag; - loop: while ((tag = getTag()) != -1) { + loop: while ((tag = nextTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); System.out.println(nextPacket); switch (nextPacket) { @@ -108,7 +145,9 @@ public class OpenPgpMessageInputStream extends InputStream { case COMP: automaton.next(InputAlphabet.CompressedData); PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); - in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options); + LayerMetadata compressionLayer = new LayerMetadata(); + compressionLayer.setCompressionAlgorithm(CompressionAlgorithm.fromId(compressedData.getAlgorithm())); + in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); break loop; // One Pass Signatures @@ -119,10 +158,10 @@ public class OpenPgpMessageInputStream extends InputStream { // Signatures - either prepended to the message, or corresponding to the One Pass Signatures case SIG: - boolean isCorrespondingToOPS = automaton.peekStack() == StackAlphabet.ops; + boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); PGPSignatureList signatureList = readSignatures(); - if (isCorrespondingToOPS) { + if (isSigForOPS) { signatures.addOnePassCorrespondingSignatures(signatureList); } else { signatures.addPrependedSignatures(signatureList); @@ -135,99 +174,15 @@ public class OpenPgpMessageInputStream extends InputStream { case SED: case SEIPD: automaton.next(InputAlphabet.EncryptedData); - PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); - - // TODO: Replace with !encDataList.isIntegrityProtected() - if (!encDataList.get(0).isIntegrityProtected()) { - throw new MessageNotIntegrityProtectedException(); + if (processEncryptedData()) { + break loop; } - SortedESKs esks = new SortedESKs(encDataList); - - if (options.getSessionKey() != null) { - SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getSessionKeyDataDecryptorFactory(options.getSessionKey()); - // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) - PGPEncryptedData esk = esks.all().get(0); - try { - if (esk instanceof PGPPBEEncryptedData) { - PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; - in = skesk.getDataStream(decryptorFactory); - break loop; - } else if (esk instanceof PGPPublicKeyEncryptedData) { - PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - in = pkesk.getDataStream(decryptorFactory); - break loop; - } else { - throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); - } - } catch (PGPException e) { - // Session key mismatch? - } - } - - // Try passwords - for (PGPPBEEncryptedData skesk : esks.skesks) { - for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decrypted = skesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); - break loop; - } catch (PGPException e) { - // password mismatch? Try next password - } - - } - } - - // Try (known) secret keys - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - long keyId = pkesk.getKeyID(); - PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); - if (decryptionKeys == null) { - continue; - } - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); - PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); - - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); - break loop; - } catch (PGPException e) { - // hm :/ - } - } - - // try anonymous secret keys - for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { - for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); - break loop; - } catch (PGPException e) { - // hm :/ - } - } - } - - // TODO: try interactive password callbacks - throw new MissingDecryptionMethodException("No working decryption method found."); + // Marker Packets need to be skipped and ignored case MARKER: - bcpgIn.readPacket(); // skip marker packet + bcpgIn.readPacket(); // skip break; // Key Packets are illegal in this context @@ -238,8 +193,11 @@ public class OpenPgpMessageInputStream extends InputStream { case TRUST: case UID: case UATTR: + throw new MalformedOpenPgpMessageException("Illegal Packet in Stream: " + nextPacket); - case MOD: + // MDC packet is usually processed by PGPEncryptedDataList, so it is very likely we encounter this + // packet out of order + case MDC: throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); // Experimental Packets are not supported @@ -252,7 +210,100 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private int getTag() throws IOException { + private boolean processEncryptedData() throws IOException, PGPException { + PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); + + // TODO: Replace with !encDataList.isIntegrityProtected() + if (!encDataList.get(0).isIntegrityProtected()) { + throw new MessageNotIntegrityProtectedException(); + } + + SortedESKs esks = new SortedESKs(encDataList); + + // Try session key + if (options.getSessionKey() != null) { + SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getSessionKeyDataDecryptorFactory(options.getSessionKey()); + // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) + PGPEncryptedData esk = esks.all().get(0); + try { + if (esk instanceof PGPPBEEncryptedData) { + PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; + in = skesk.getDataStream(decryptorFactory); + return true; + } else if (esk instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; + in = pkesk.getDataStream(decryptorFactory); + return true; + } else { + throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); + } + } catch (PGPException e) { + // Session key mismatch? + } + } + + // Try passwords + for (PGPPBEEncryptedData skesk : esks.skesks) { + for (Passphrase passphrase : options.getDecryptionPassphrases()) { + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + return true; + } catch (PGPException e) { + // password mismatch? Try next password + } + + } + } + + // Try (known) secret keys + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + long keyId = pkesk.getKeyID(); + PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); + if (decryptionKeys == null) { + continue; + } + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); + PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); + + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + return true; + } catch (PGPException e) { + // hm :/ + } + } + + // try anonymous secret keys + for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { + for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(decrypted, options); + return true; + } catch (PGPException e) { + // hm :/ + } + } + } + + // we did not yet succeed in decrypting any session key :/ + return false; + } + + private int nextTag() throws IOException { try { return bcpgIn.nextPacketTag(); } catch (IOException e) { @@ -296,7 +347,7 @@ public class OpenPgpMessageInputStream extends InputStream { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag; - while ((tag = getTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { + while ((tag = nextTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; @@ -313,13 +364,13 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPSignatureList readSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag = getTag(); + int tag = nextTag(); while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.SIGNATURE) { SignaturePacket sigPacket = (SignaturePacket) packet; sigPacket.encode(bcpgOut); - tag = getTag(); + tag = nextTag(); } } bcpgOut.close(); @@ -356,20 +407,6 @@ public class OpenPgpMessageInputStream extends InputStream { throw new RuntimeException(e); } signatures.finish(); - /* - if (in instanceof OpenPgpMessageInputStream) { - in.close(); - in = null; - } else { - try { - System.out.println("Read consume"); - consumePackets(); - signatures.finish(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - */ } return r; } @@ -394,18 +431,6 @@ public class OpenPgpMessageInputStream extends InputStream { throw new RuntimeException(e); } signatures.finish(); - /* - if (in instanceof OpenPgpMessageInputStream) { - in.close(); - in = null; - } else { - try { - consumePackets(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - */ } return r; } From efdf2bca0d5b0fcc08448e074d1c9558ec55ff6e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 23 Sep 2022 14:04:44 +0200 Subject: [PATCH 0464/1204] WIP: Play around with TeeInputStreams --- .../OpenPgpMessageInputStream.java | 33 +++++++++-- .../TeeBCPGInputStream.java | 35 +++++++++++ .../TeeBCPGInputStreamTest.java | 58 +++++++++++++++++++ 3 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 1a38fced..990e628d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -343,7 +343,8 @@ public class OpenPgpMessageInputStream extends InputStream { return null; } - private PGPOnePassSignatureList readOnePassSignatures() throws IOException { + private PGPOnePassSignatureListWrapper readOnePassSignatures() throws IOException { + List encapsulating = new ArrayList<>(); ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag; @@ -351,14 +352,16 @@ public class OpenPgpMessageInputStream extends InputStream { Packet packet = bcpgIn.readPacket(); if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; - sigPacket.encode(bcpgOut); + byte[] bytes = sigPacket.getEncoded(); + encapsulating.add(bytes[bytes.length - 1] == 1); + bcpgOut.write(bytes); } } bcpgOut.close(); PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); - return signatureList; + return new PGPOnePassSignatureListWrapper(signatureList, encapsulating); } private PGPSignatureList readSignatures() throws IOException { @@ -490,6 +493,26 @@ public class OpenPgpMessageInputStream extends InputStream { } } + /** + * Workaround for BC not exposing, whether an OPS is encapsulating or not. + * TODO: Remove once our PR is merged + * + * @see PR against BC + */ + private static class PGPOnePassSignatureListWrapper { + private final PGPOnePassSignatureList list; + private final List encapsulating; + + public PGPOnePassSignatureListWrapper(PGPOnePassSignatureList signatures, List encapsulating) { + this.list = signatures; + this.encapsulating = encapsulating; + } + + public int size() { + return list.size(); + } + } + private static class Signatures { final ConsumerOptions options; List detachedSignatures = new ArrayList<>(); @@ -521,9 +544,9 @@ public class OpenPgpMessageInputStream extends InputStream { } } - void addOnePassSignatures(PGPOnePassSignatureList signatures) { + void addOnePassSignatures(PGPOnePassSignatureListWrapper signatures) { System.out.println("Adding " + signatures.size() + " OPSs"); - for (PGPOnePassSignature ops : signatures) { + for (PGPOnePassSignature ops : signatures.list) { PGPPublicKeyRing certificate = findCertificate(ops.getKeyID()); initialize(ops, certificate); this.onePassSignatures.add(ops); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java new file mode 100644 index 00000000..ab24f22e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -0,0 +1,35 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.BCPGInputStream; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class TeeBCPGInputStream extends BCPGInputStream { + + private final OutputStream out; + + public TeeBCPGInputStream(InputStream in, OutputStream outputStream) { + super(in); + this.out = outputStream; + } + + @Override + public int read() throws IOException { + int r = super.read(); + if (r != -1) { + out.write(r); + } + return r; + } + + @Override + public int read(byte[] buf, int off, int len) throws IOException { + int r = super.read(buf, off, len); + if (r > 0) { + out.write(buf, off, r); + } + return r; + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java new file mode 100644 index 00000000..765221d3 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java @@ -0,0 +1,58 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.Packet; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPException; +import org.junit.jupiter.api.Test; +import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.util.ArmoredInputStreamFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class TeeBCPGInputStreamTest { + + private static final String INBAND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "owGbwMvMyCUWdXSHvVTUtXbG0yJJDCDgkZqTk6+jEJ5flJOiyNVRysIoxsXAxsqU\n" + + "GDiVjUGRUwCmQUyRRWnOn9Z/PIseF3Yz6cCEL05nZDj1OClo75WVTjNmJPemW6qV\n" + + "6ki//1K1++2s0qTP+0N11O4z/BVLDDdxnmQryS+5VXjBX7/0Hxnm/eqeX6Zum35r\n" + + "M8e7ufwA\n" + + "=RDiy\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void test() throws IOException, PGPException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(out); + + ByteArrayInputStream bytesIn = new ByteArrayInputStream(INBAND_SIGNED.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + BCPGInputStream bcpgIn = new BCPGInputStream(armorIn); + TeeBCPGInputStream teeIn = new TeeBCPGInputStream(bcpgIn, armorOut); + + ByteArrayOutputStream nestedOut = new ByteArrayOutputStream(); + ArmoredOutputStream nestedArmorOut = new ArmoredOutputStream(nestedOut); + + PGPCompressedData compressedData = new PGPCompressedData(teeIn); + InputStream nestedStream = compressedData.getDataStream(); + BCPGInputStream nestedBcpgIn = new BCPGInputStream(nestedStream); + TeeBCPGInputStream nestedTeeIn = new TeeBCPGInputStream(nestedBcpgIn, nestedArmorOut); + + int tag; + while ((tag = nestedTeeIn.nextPacketTag()) != -1) { + System.out.println(OpenPgpPacket.requireFromTag(tag)); + Packet packet = nestedTeeIn.readPacket(); + } + + nestedArmorOut.close(); + System.out.println(nestedOut); + } +} From 5c93eb3705c551edc9d05bfa2a1b159084a66dd5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 26 Sep 2022 18:21:06 +0200 Subject: [PATCH 0465/1204] Wip: Introduce MessageMetadata class --- .../MessageMetadata.java | 249 ++++++++++++++++++ .../OpenPgpMessageInputStream.java | 108 ++++---- .../automaton/PDA.java | 3 - .../MessageMetadataTest.java | 84 ++++++ .../OpenPgpMessageInputStreamTest.java | 182 +++++++------ 5 files changed, 498 insertions(+), 128 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java new file mode 100644 index 00000000..7c09fc8d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -0,0 +1,249 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.SessionKey; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +public class MessageMetadata { + + protected Message message; + + public MessageMetadata(@Nonnull Message message) { + this.message = message; + } + + public @Nullable SymmetricKeyAlgorithm getEncryptionAlgorithm() { + Iterator algorithms = getEncryptionAlgorithms(); + if (algorithms.hasNext()) { + return algorithms.next(); + } + return null; + } + + public @Nonnull Iterator getEncryptionAlgorithms() { + return new LayerIterator(message) { + @Override + public boolean matches(Nested layer) { + return layer instanceof EncryptedData; + } + + @Override + public SymmetricKeyAlgorithm getProperty(Layer last) { + return ((EncryptedData) last).algorithm; + } + }; + } + + public @Nullable CompressionAlgorithm getCompressionAlgorithm() { + Iterator algorithms = getCompressionAlgorithms(); + if (algorithms.hasNext()) { + return algorithms.next(); + } + return null; + } + + public @Nonnull Iterator getCompressionAlgorithms() { + return new LayerIterator(message) { + @Override + public boolean matches(Nested layer) { + return layer instanceof CompressedData; + } + + @Override + public CompressionAlgorithm getProperty(Layer last) { + return ((CompressedData) last).algorithm; + } + }; + } + + public String getFilename() { + return findLiteralData().getFileName(); + } + + public Date getModificationDate() { + return findLiteralData().getModificationDate(); + } + + public StreamEncoding getFormat() { + return findLiteralData().getFormat(); + } + + private LiteralData findLiteralData() { + Nested nested = message.child; + while (nested.hasNestedChild()) { + Layer layer = (Layer) nested; + nested = layer.child; + } + return (LiteralData) nested; + } + + public static abstract class Layer { + protected final List verifiedSignatures = new ArrayList<>(); + protected final List failedSignatures = new ArrayList<>(); + protected Nested child; + + public Nested getChild() { + return child; + } + + public void setChild(Nested child) { + this.child = child; + } + + public List getVerifiedSignatures() { + return new ArrayList<>(verifiedSignatures); + } + + public List getFailedSignatures() { + return new ArrayList<>(failedSignatures); + } + } + + public interface Nested { + boolean hasNestedChild(); + } + + public static class Message extends Layer { + + } + + public static class LiteralData implements Nested { + protected String fileName; + protected Date modificationDate; + protected StreamEncoding format; + + public LiteralData() { + this("", new Date(0L), StreamEncoding.BINARY); + } + + public LiteralData(String fileName, Date modificationDate, StreamEncoding format) { + this.fileName = fileName; + this.modificationDate = modificationDate; + this.format = format; + } + + public String getFileName() { + return fileName; + } + + public Date getModificationDate() { + return modificationDate; + } + + public StreamEncoding getFormat() { + return format; + } + + @Override + public boolean hasNestedChild() { + return false; + } + } + + public static class CompressedData extends Layer implements Nested { + protected final CompressionAlgorithm algorithm; + + public CompressedData(CompressionAlgorithm zip) { + this.algorithm = zip; + } + + public CompressionAlgorithm getAlgorithm() { + return algorithm; + } + + @Override + public boolean hasNestedChild() { + return true; + } + } + + public static class EncryptedData extends Layer implements Nested { + protected final SymmetricKeyAlgorithm algorithm; + protected SessionKey sessionKey; + protected List recipients; + + public EncryptedData(SymmetricKeyAlgorithm algorithm) { + this.algorithm = algorithm; + } + + public SymmetricKeyAlgorithm getAlgorithm() { + return algorithm; + } + + public SessionKey getSessionKey() { + return sessionKey; + } + + public List getRecipients() { + return new ArrayList<>(recipients); + } + + @Override + public boolean hasNestedChild() { + return true; + } + } + + + private static abstract class LayerIterator implements Iterator { + private Nested current; + Layer last = null; + + public LayerIterator(Message message) { + super(); + this.current = message.child; + if (matches(current)) { + last = (Layer) current; + } + } + + @Override + public boolean hasNext() { + if (last == null) { + findNext(); + } + return last != null; + } + + @Override + public O next() { + if (last == null) { + findNext(); + } + if (last != null) { + O property = getProperty(last); + last = null; + return property; + } + throw new NoSuchElementException(); + } + + private void findNext() { + while (current instanceof Layer) { + current = ((Layer) current).child; + if (matches(current)) { + last = (Layer) current; + break; + } + } + } + + abstract boolean matches(Nested layer); + + abstract O getProperty(Layer last); + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 990e628d..44ebb27e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -31,11 +31,11 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.bouncycastle.pqc.crypto.rainbow.Layer; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; @@ -69,14 +69,14 @@ public class OpenPgpMessageInputStream extends InputStream { private boolean closed = false; private Signatures signatures; - private LayerMetadata layerMetadata; + private MessageMetadata.Layer metadata; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { - this(inputStream, options, null); + this(inputStream, options, new MessageMetadata.Message()); } - OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, LayerMetadata layerMetadata) + OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, MessageMetadata.Layer metadata) throws PGPException, IOException { // TODO: Use BCPGInputStream.wrap(inputStream); if (inputStream instanceof BCPGInputStream) { @@ -86,33 +86,12 @@ public class OpenPgpMessageInputStream extends InputStream { } this.options = options; + this.metadata = metadata; this.resultBuilder = OpenPgpMetadata.getBuilder(); this.signatures = new Signatures(options); this.signatures.addDetachedSignatures(options.getDetachedSignatures()); - consumePackets(); - } - - static class LayerMetadata { - - private CompressionAlgorithm compressionAlgorithm; - private SymmetricKeyAlgorithm symmetricKeyAlgorithm; - private LayerMetadata child; - - public LayerMetadata setCompressionAlgorithm(CompressionAlgorithm algorithm) { - this.compressionAlgorithm = algorithm; - return this; - } - - public LayerMetadata setSymmetricEncryptionAlgorithm(SymmetricKeyAlgorithm algorithm) { - this.symmetricKeyAlgorithm = algorithm; - return this; - } - - public LayerMetadata setChild(LayerMetadata child) { - this.child = child; - return this; - } + consumePackets(); // nom nom nom } /** @@ -127,27 +106,21 @@ public class OpenPgpMessageInputStream extends InputStream { */ private void consumePackets() throws IOException, PGPException { - System.out.println("Walk " + automaton); int tag; loop: while ((tag = nextTag()) != -1) { OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); - System.out.println(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); - PGPLiteralData literalData = new PGPLiteralData(bcpgIn); - in = literalData.getDataStream(); + processLiteralData(); break loop; // Compressed Data - the content contains another OpenPGP message case COMP: automaton.next(InputAlphabet.CompressedData); - PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); - LayerMetadata compressionLayer = new LayerMetadata(); - compressionLayer.setCompressionAlgorithm(CompressionAlgorithm.fromId(compressedData.getAlgorithm())); - in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); + processCompressedData(); break loop; // One Pass Signatures @@ -160,12 +133,7 @@ public class OpenPgpMessageInputStream extends InputStream { case SIG: boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); - PGPSignatureList signatureList = readSignatures(); - if (isSigForOPS) { - signatures.addOnePassCorrespondingSignatures(signatureList); - } else { - signatures.addPrependedSignatures(signatureList); - } + processSignature(isSigForOPS); break; // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) @@ -210,6 +178,29 @@ public class OpenPgpMessageInputStream extends InputStream { } } + private void processSignature(boolean isSigForOPS) throws IOException { + PGPSignatureList signatureList = readSignatures(); + if (isSigForOPS) { + signatures.addOnePassCorrespondingSignatures(signatureList); + } else { + signatures.addPrependedSignatures(signatureList); + } + } + + private void processCompressedData() throws IOException, PGPException { + PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); + MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( + CompressionAlgorithm.fromId(compressedData.getAlgorithm())); + in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); + } + + private void processLiteralData() throws IOException { + PGPLiteralData literalData = new PGPLiteralData(bcpgIn); + this.metadata.setChild(new MessageMetadata.LiteralData(literalData.getFileName(), literalData.getModificationTime(), + StreamEncoding.requireFromCode(literalData.getFormat()))); + in = literalData.getDataStream(); + } + private boolean processEncryptedData() throws IOException, PGPException { PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); @@ -227,13 +218,14 @@ public class OpenPgpMessageInputStream extends InputStream { // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) PGPEncryptedData esk = esks.all().get(0); try { + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(options.getSessionKey().getAlgorithm()); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; - in = skesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(skesk.getDataStream(decryptorFactory), options, encryptedData); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - in = pkesk.getDataStream(decryptorFactory); + in = new OpenPgpMessageInputStream(pkesk.getDataStream(decryptorFactory), options, encryptedData); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -250,7 +242,9 @@ public class OpenPgpMessageInputStream extends InputStream { .getPBEDataDecryptorFactory(passphrase); try { InputStream decrypted = skesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(skesk.getSymmetricAlgorithm(decryptorFactory))); + in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // password mismatch? Try next password @@ -274,7 +268,9 @@ public class OpenPgpMessageInputStream extends InputStream { .getPublicKeyDataDecryptorFactory(privateKey); try { InputStream decrypted = pkesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -291,7 +287,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { InputStream decrypted = pkesk.getDataStream(decryptorFactory); - in = new OpenPgpMessageInputStream(decrypted, options); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -402,6 +400,7 @@ public class OpenPgpMessageInputStream extends InputStream { signatures.update(b); } else { in.close(); + collectMetadata(); in = null; try { @@ -426,6 +425,7 @@ public class OpenPgpMessageInputStream extends InputStream { int r = in.read(b, off, len); if (r == -1) { in.close(); + collectMetadata(); in = null; try { @@ -447,6 +447,7 @@ public class OpenPgpMessageInputStream extends InputStream { if (in != null) { in.close(); + collectMetadata(); in = null; } @@ -461,6 +462,21 @@ public class OpenPgpMessageInputStream extends InputStream { closed = true; } + private void collectMetadata() { + if (in instanceof OpenPgpMessageInputStream) { + OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) in; + MessageMetadata.Layer childLayer = child.metadata; + this.metadata.setChild((MessageMetadata.Nested) childLayer); + } + } + + public MessageMetadata getMetadata() { + if (!closed) { + throw new IllegalStateException("Stream must be closed before access to metadata can be granted."); + } + return new MessageMetadata((MessageMetadata.Message) metadata); + } + private static class SortedESKs { private List skesks = new ArrayList<>(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index 793a3451..feb759ea 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -187,10 +187,7 @@ public class PDA { } public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { - State old = state; - StackAlphabet stackItem = stack.isEmpty() ? null : stack.peek(); state = state.transition(input, this); - System.out.println(id + ": Transition from " + old + " to " + state + " via " + input + " with stack " + stackItem); } /** diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java new file mode 100644 index 00000000..9f887eb9 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.DateUtil; + +import java.util.Date; +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MessageMetadataTest { + + @Test + public void processTestMessage_COMP_ENC_ENC_LIT() { + // Note: COMP of ENC does not make sense, since ENC is indistinguishable from randomness + // and randomness cannot be encrypted. + // For the sake of testing though, this is okay. + MessageMetadata.Message message = new MessageMetadata.Message(); + + MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128); + MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256); + MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData(); + + message.setChild(compressedData); + compressedData.setChild(encryptedData); + encryptedData.setChild(encryptedData1); + encryptedData1.setChild(literalData); + + MessageMetadata metadata = new MessageMetadata(message); + + // Check encryption algs + assertEquals(SymmetricKeyAlgorithm.AES_128, metadata.getEncryptionAlgorithm(), "getEncryptionAlgorithm() returns alg of outermost EncryptedData"); + Iterator encryptionAlgs = metadata.getEncryptionAlgorithms(); + assertTrue(encryptionAlgs.hasNext(), "There is at least one EncryptedData child"); + assertTrue(encryptionAlgs.hasNext(), "The child is still there"); + assertEquals(SymmetricKeyAlgorithm.AES_128, encryptionAlgs.next(), "The first algo is AES128"); + assertTrue(encryptionAlgs.hasNext(), "There is another EncryptedData"); + assertTrue(encryptionAlgs.hasNext(), "There is *still* another EncryptedData"); + assertEquals(SymmetricKeyAlgorithm.AES_256, encryptionAlgs.next(), "The second algo is AES256"); + assertFalse(encryptionAlgs.hasNext(), "There is no more EncryptedData"); + assertFalse(encryptionAlgs.hasNext(), "There *still* is no more EncryptedData"); + + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm(), "getCompressionAlgorithm() returns alg of outermost CompressedData"); + Iterator compAlgs = metadata.getCompressionAlgorithms(); + assertTrue(compAlgs.hasNext()); + assertTrue(compAlgs.hasNext()); + assertEquals(CompressionAlgorithm.ZIP, compAlgs.next()); + assertFalse(compAlgs.hasNext()); + assertFalse(compAlgs.hasNext()); + + assertEquals("", metadata.getFilename()); + JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + } + + @Test + public void testProcessLiteralDataMessage() { + MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData( + "collateral_murder.zip", + DateUtil.parseUTCDate("2010-04-05 10:12:03 UTC"), + StreamEncoding.BINARY); + MessageMetadata.Message message = new MessageMetadata.Message(); + message.setChild(literalData); + + MessageMetadata metadata = new MessageMetadata(message); + assertNull(metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); + assertEquals("collateral_murder.zip", metadata.getFilename()); + assertEquals(DateUtil.parseUTCDate("2010-04-05 10:12:03 UTC"), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index af1fac44..8219ec68 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -1,5 +1,21 @@ package org.pgpainless.decryption_verification; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Date; +import java.util.Iterator; +import java.util.stream.Stream; + import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.CompressionAlgorithmTags; @@ -11,9 +27,14 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Test; +import org.junit.JUtils; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; @@ -23,17 +44,7 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import org.pgpainless.util.Tuple; public class OpenPgpMessageInputStreamTest { @@ -304,107 +315,119 @@ public class OpenPgpMessageInputStreamTest { System.out.println(out); } - @Test - public void testProcessLIT() throws IOException, PGPException { - String plain = processReadBuffered(LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); + interface Processor { + Tuple process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException; } - @Test - public void testProcessLIT_LIT_fails() { + private static Stream provideMessageProcessors() { + return Stream.of( + Arguments.of(Named.of("read(buf,off,len)", (Processor) OpenPgpMessageInputStreamTest::processReadBuffered)), + Arguments.of(Named.of("read()", (Processor) OpenPgpMessageInputStreamTest::processReadSequential))); + } + + @ParameterizedTest(name = "Process LIT using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessLIT(Processor processor) throws IOException, PGPException { + Tuple result = processor.process(LIT, ConsumerOptions.get()); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + + MessageMetadata metadata = result.getB(); + assertNull(metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); + assertEquals("", metadata.getFilename()); + JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + } + + @ParameterizedTest(name = "Process LIT LIT using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessLIT_LIT_fails(Processor processor) { assertThrows(MalformedOpenPgpMessageException.class, - () -> processReadBuffered(LIT_LIT, ConsumerOptions.get())); + () -> processor.process(LIT_LIT, ConsumerOptions.get())); + } + @ParameterizedTest(name = "Process COMP(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_LIT(Processor processor) throws PGPException, IOException { + Tuple result = processor.process(COMP_LIT, ConsumerOptions.get()); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + } + + @ParameterizedTest(name = "Process COMP using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_fails(Processor processor) { assertThrows(MalformedOpenPgpMessageException.class, - () -> processReadSequential(LIT_LIT, ConsumerOptions.get())); + () -> processor.process(COMP, ConsumerOptions.get())); } - @Test - public void testProcessCOMP_LIT() throws PGPException, IOException { - String plain = processReadBuffered(COMP_LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(COMP_LIT, ConsumerOptions.get()); + @ParameterizedTest(name = "Process COMP(COMP(LIT)) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_COMP_LIT(Processor processor) throws PGPException, IOException { + Tuple result = processor.process(COMP_COMP_LIT, ConsumerOptions.get()); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + Iterator compressionAlgorithms = metadata.getCompressionAlgorithms(); + assertEquals(CompressionAlgorithm.ZIP, compressionAlgorithms.next()); + assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next()); + assertFalse(compressionAlgorithms.hasNext()); } - @Test - public void testProcessCOMP_fails() { - assertThrows(MalformedOpenPgpMessageException.class, - () -> processReadBuffered(COMP, ConsumerOptions.get())); - - assertThrows(MalformedOpenPgpMessageException.class, - () -> processReadSequential(COMP, ConsumerOptions.get())); - } - - @Test - public void testProcessCOMP_COMP_LIT() throws PGPException, IOException { - String plain = processReadBuffered(COMP_COMP_LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(COMP_COMP_LIT, ConsumerOptions.get()); - assertEquals(PLAINTEXT, plain); - } - - @Test - public void testProcessSIG_LIT() throws PGPException, IOException { + @ParameterizedTest(name = "Process SIG LIT using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessSIG_LIT(Processor processor) throws PGPException, IOException { PGPPublicKeyRing cert = PGPainless.extractCertificate( PGPainless.readKeyRing().secretKeyRing(KEY)); - String plain = processReadBuffered(SIG_LIT, ConsumerOptions.get() - .addVerificationCert(cert)); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(SIG_LIT, ConsumerOptions.get() + Tuple result = processor.process(SIG_LIT, ConsumerOptions.get() .addVerificationCert(cert)); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); } - @Test - public void testProcessSENC_LIT() throws PGPException, IOException { - String plain = processReadBuffered(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + @ParameterizedTest(name = "Process SENC(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessSENC_LIT(Processor processor) throws PGPException, IOException { + Tuple result = processor.process(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); } - @Test - public void testProcessPENC_COMP_LIT() throws IOException, PGPException { + @ParameterizedTest(name = "Process PENC(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessPENC_COMP_LIT(Processor processor) throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - String plain = processReadBuffered(PENC_COMP_LIT, ConsumerOptions.get() - .addDecryptionKey(secretKeys)); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(PENC_COMP_LIT, ConsumerOptions.get() + Tuple result = processor.process(PENC_COMP_LIT, ConsumerOptions.get() .addDecryptionKey(secretKeys)); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); } - @Test - public void testProcessOPS_LIT_SIG() throws IOException, PGPException { + @ParameterizedTest(name = "Process OPS LIT SIG using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessOPS_LIT_SIG(Processor processor) throws IOException, PGPException { PGPPublicKeyRing cert = PGPainless.extractCertificate(PGPainless.readKeyRing().secretKeyRing(KEY)); - String plain = processReadBuffered(OPS_LIT_SIG, ConsumerOptions.get() - .addVerificationCert(cert)); - assertEquals(PLAINTEXT, plain); - - plain = processReadSequential(OPS_LIT_SIG, ConsumerOptions.get() + Tuple result = processor.process(OPS_LIT_SIG, ConsumerOptions.get() .addVerificationCert(cert)); + String plain = result.getA(); assertEquals(PLAINTEXT, plain); } - private String processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(in, out); in.close(); - return out.toString(); + MessageMetadata metadata = in.getMetadata(); + return new Tuple<>(out.toString(), metadata); } - private String processReadSequential(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + private static Tuple processReadSequential(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -414,10 +437,11 @@ public class OpenPgpMessageInputStreamTest { } in.close(); - return out.toString(); + MessageMetadata metadata = in.getMetadata(); + return new Tuple<>(out.toString(), metadata); } - private OpenPgpMessageInputStream get(String armored, ConsumerOptions options) throws IOException, PGPException { + private static OpenPgpMessageInputStream get(String armored, ConsumerOptions options) throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); OpenPgpMessageInputStream pgpIn = new OpenPgpMessageInputStream(armorIn, options); From e25f6e17124e8500d28dd84413f956e0477ca887 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 27 Sep 2022 16:11:55 +0200 Subject: [PATCH 0466/1204] Fix checkstyle issues --- .../MessageMetadata.java | 6 +- .../OpenPgpMessageInputStream.java | 23 +++---- .../automaton/InputAlphabet.java | 4 ++ .../automaton/PDA.java | 4 ++ .../automaton/StackAlphabet.java | 4 ++ .../automaton/package-info.java | 8 +++ .../OpenPgpMessageInputStreamTest.java | 60 +++++++++++-------- .../TeeBCPGInputStreamTest.java | 7 ++- 8 files changed, 75 insertions(+), 41 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 7c09fc8d..cbf251a8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -90,7 +90,7 @@ public class MessageMetadata { return (LiteralData) nested; } - public static abstract class Layer { + public abstract static class Layer { protected final List verifiedSignatures = new ArrayList<>(); protected final List failedSignatures = new ArrayList<>(); protected Nested child; @@ -198,11 +198,11 @@ public class MessageMetadata { } - private static abstract class LayerIterator implements Iterator { + private abstract static class LayerIterator implements Iterator { private Nested current; Layer last = null; - public LayerIterator(Message message) { + LayerIterator(Message message) { super(); this.current = message.child; if (matches(current)) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 44ebb27e..58699fcd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -50,6 +50,8 @@ import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -60,6 +62,8 @@ import java.util.List; public class OpenPgpMessageInputStream extends InputStream { + private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); + protected final PDA automaton = new PDA(); protected final ConsumerOptions options; protected final OpenPgpMetadata.Builder resultBuilder; @@ -519,7 +523,7 @@ public class OpenPgpMessageInputStream extends InputStream { private final PGPOnePassSignatureList list; private final List encapsulating; - public PGPOnePassSignatureListWrapper(PGPOnePassSignatureList signatures, List encapsulating) { + PGPOnePassSignatureListWrapper(PGPOnePassSignatureList signatures, List encapsulating) { this.list = signatures; this.encapsulating = encapsulating; } @@ -529,7 +533,7 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private static class Signatures { + private static final class Signatures { final ConsumerOptions options; List detachedSignatures = new ArrayList<>(); List prependedSignatures = new ArrayList<>(); @@ -551,7 +555,6 @@ public class OpenPgpMessageInputStream extends InputStream { } void addPrependedSignatures(PGPSignatureList signatures) { - System.out.println("Adding " + signatures.size() + " prepended Signatures"); for (PGPSignature signature : signatures) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); @@ -561,7 +564,6 @@ public class OpenPgpMessageInputStream extends InputStream { } void addOnePassSignatures(PGPOnePassSignatureListWrapper signatures) { - System.out.println("Adding " + signatures.size() + " OPSs"); for (PGPOnePassSignature ops : signatures.list) { PGPPublicKeyRing certificate = findCertificate(ops.getKeyID()); initialize(ops, certificate); @@ -570,7 +572,6 @@ public class OpenPgpMessageInputStream extends InputStream { } void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { - System.out.println("Adding " + signatures.size() + " Corresponding Signatures"); for (PGPSignature signature : signatures) { correspondingSignatures.add(signature); } @@ -631,9 +632,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = detached.verify(); } catch (PGPException e) { - System.out.println(e.getMessage()); + LOGGER.debug("Cannot verify detached signature.", e); } - System.out.println("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + LOGGER.debug("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } for (PGPSignature prepended : prependedSignatures) { @@ -641,9 +642,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = prepended.verify(); } catch (PGPException e) { - System.out.println(e.getMessage()); + LOGGER.debug("Cannot verify prepended signature.", e); } - System.out.println("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + LOGGER.debug("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } @@ -654,9 +655,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = ops.verify(signature); } catch (PGPException e) { - System.out.println(e.getMessage()); + LOGGER.debug("Cannot verify OPS signature.", e); } - System.out.println("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + LOGGER.debug("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java index d015a4b3..8e795f5b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification.automaton; import org.bouncycastle.openpgp.PGPCompressedData; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index feb759ea..dda2adce 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification.automaton; import org.pgpainless.exception.MalformedOpenPgpMessageException; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java index 97dad3d8..09865f31 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification.automaton; public enum StackAlphabet { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java new file mode 100644 index 00000000..80a79e85 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Pushdown Automaton to verify validity of packet sequences according to the OpenPGP Message format. + */ +package org.pgpainless.decryption_verification.automaton; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index 8219ec68..6962e279 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -35,6 +35,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; @@ -118,7 +119,7 @@ public class OpenPgpMessageInputStreamTest { "=K9Zl\n" + "-----END PGP MESSAGE-----"; - public static final String SIG_LIT = "" + + public static final String SIG_COMP_LIT = "" + "-----BEGIN PGP MESSAGE-----\n" + "Version: BCPG v1.71\n" + "\n" + @@ -235,9 +236,9 @@ public class OpenPgpMessageInputStreamTest { } public static void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - System.out.println(PGPainless.asciiArmor( - PGPainless.generateKeyRing().modernKeyRing("Alice ") - )); + PGPainless.asciiArmor( + PGPainless.generateKeyRing().modernKeyRing("Alice "), + System.out); } public static void genSIG_LIT() throws PGPException, IOException { @@ -265,54 +266,46 @@ public class OpenPgpMessageInputStreamTest { armorOut.close(); String armored = out.toString(); + // CHECKSTYLE:OFF System.out.println(armored .replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n") .replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----")); + // CHECKSTYLE:ON } public static void genSENC_LIT() throws PGPException, IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) + .onOutputStream(System.out) .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); enc.close(); - - System.out.println(out); } public static void genPENC_COMP_LIT() throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) + .onOutputStream(System.out) .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() .addRecipient(cert)) .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); enc.close(); - - System.out.println(out); } public static void genOPS_LIT_SIG() throws PGPException, IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - ByteArrayOutputStream out = new ByteArrayOutputStream(); EncryptionStream enc = PGPainless.encryptAndOrSign() - .onOutputStream(out) + .onOutputStream(System.out) .withOptions(ProducerOptions.sign(SigningOptions.get() .addSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys)) .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); enc.close(); - - System.out.println(out); } interface Processor { @@ -322,7 +315,8 @@ public class OpenPgpMessageInputStreamTest { private static Stream provideMessageProcessors() { return Stream.of( Arguments.of(Named.of("read(buf,off,len)", (Processor) OpenPgpMessageInputStreamTest::processReadBuffered)), - Arguments.of(Named.of("read()", (Processor) OpenPgpMessageInputStreamTest::processReadSequential))); + Arguments.of(Named.of("read()", (Processor) OpenPgpMessageInputStreamTest::processReadSequential)) + ); } @ParameterizedTest(name = "Process LIT using {0}") @@ -349,7 +343,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process COMP(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessCOMP_LIT(Processor processor) throws PGPException, IOException { + public void testProcessCOMP_LIT(Processor processor) + throws PGPException, IOException { Tuple result = processor.process(COMP_LIT, ConsumerOptions.get()); String plain = result.getA(); assertEquals(PLAINTEXT, plain); @@ -366,7 +361,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process COMP(COMP(LIT)) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessCOMP_COMP_LIT(Processor processor) throws PGPException, IOException { + public void testProcessCOMP_COMP_LIT(Processor processor) + throws PGPException, IOException { Tuple result = processor.process(COMP_COMP_LIT, ConsumerOptions.get()); String plain = result.getA(); assertEquals(PLAINTEXT, plain); @@ -376,29 +372,37 @@ public class OpenPgpMessageInputStreamTest { assertEquals(CompressionAlgorithm.ZIP, compressionAlgorithms.next()); assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next()); assertFalse(compressionAlgorithms.hasNext()); + assertNull(metadata.getEncryptionAlgorithm()); } - @ParameterizedTest(name = "Process SIG LIT using {0}") + @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessSIG_LIT(Processor processor) throws PGPException, IOException { + public void testProcessSIG_COMP_LIT(Processor processor) throws PGPException, IOException { PGPPublicKeyRing cert = PGPainless.extractCertificate( PGPainless.readKeyRing().secretKeyRing(KEY)); - Tuple result = processor.process(SIG_LIT, ConsumerOptions.get() + Tuple result = processor.process(SIG_COMP_LIT, ConsumerOptions.get() .addVerificationCert(cert)); String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); } @ParameterizedTest(name = "Process SENC(LIT) using {0}") @MethodSource("provideMessageProcessors") public void testProcessSENC_LIT(Processor processor) throws PGPException, IOException { - Tuple result = processor.process(SENC_LIT, ConsumerOptions.get().addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + Tuple result = processor.process(SENC_LIT, ConsumerOptions.get() + .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertNull(metadata.getCompressionAlgorithm()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); } - @ParameterizedTest(name = "Process PENC(LIT) using {0}") + @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") @MethodSource("provideMessageProcessors") public void testProcessPENC_COMP_LIT(Processor processor) throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); @@ -406,6 +410,9 @@ public class OpenPgpMessageInputStreamTest { .addDecryptionKey(secretKeys)); String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); } @ParameterizedTest(name = "Process OPS LIT SIG using {0}") @@ -416,6 +423,9 @@ public class OpenPgpMessageInputStreamTest { .addVerificationCert(cert)); String plain = result.getA(); assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertNull(metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); } private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java index 765221d3..d31c6009 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java @@ -9,6 +9,8 @@ import org.bouncycastle.openpgp.PGPException; import org.junit.jupiter.api.Test; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.util.ArmoredInputStreamFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -18,6 +20,7 @@ import java.nio.charset.StandardCharsets; public class TeeBCPGInputStreamTest { + private static final Logger LOGGER = LoggerFactory.getLogger(TeeBCPGInputStreamTest.class); private static final String INBAND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + @@ -48,11 +51,11 @@ public class TeeBCPGInputStreamTest { int tag; while ((tag = nestedTeeIn.nextPacketTag()) != -1) { - System.out.println(OpenPgpPacket.requireFromTag(tag)); + LOGGER.debug(OpenPgpPacket.requireFromTag(tag).toString()); Packet packet = nestedTeeIn.readPacket(); } nestedArmorOut.close(); - System.out.println(nestedOut); + LOGGER.debug(nestedOut.toString()); } } From 45555bf82de7e366a99dcc2ac30cad5092f56601 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 28 Sep 2022 16:55:08 +0200 Subject: [PATCH 0467/1204] Wip: Work on OPS verification --- .../OpenPgpMessageInputStream.java | 200 ++++++++++++------ 1 file changed, 133 insertions(+), 67 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 58699fcd..df3d6805 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -56,6 +56,7 @@ import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -64,15 +65,19 @@ public class OpenPgpMessageInputStream extends InputStream { private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); - protected final PDA automaton = new PDA(); + // Options to consume the data protected final ConsumerOptions options; protected final OpenPgpMetadata.Builder resultBuilder; - protected final BCPGInputStream bcpgIn; - protected InputStream in; + // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message + protected final PDA automaton = new PDA(); + // InputStream of OpenPGP packets of the current layer + protected final BCPGInputStream packetInputStream; + // InputStream of a nested data packet + protected InputStream nestedInputStream; private boolean closed = false; - private Signatures signatures; + private final Signatures signatures; private MessageMetadata.Layer metadata; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) @@ -82,31 +87,45 @@ public class OpenPgpMessageInputStream extends InputStream { OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, MessageMetadata.Layer metadata) throws PGPException, IOException { - // TODO: Use BCPGInputStream.wrap(inputStream); - if (inputStream instanceof BCPGInputStream) { - this.bcpgIn = (BCPGInputStream) inputStream; - } else { - this.bcpgIn = new BCPGInputStream(inputStream); - } this.options = options; this.metadata = metadata; this.resultBuilder = OpenPgpMetadata.getBuilder(); this.signatures = new Signatures(options); - this.signatures.addDetachedSignatures(options.getDetachedSignatures()); - consumePackets(); // nom nom nom + // Add detached signatures only on the outermost OpenPgpMessageInputStream + if (metadata instanceof MessageMetadata.Message) { + this.signatures.addDetachedSignatures(options.getDetachedSignatures()); + } + + // TODO: Use BCPGInputStream.wrap(inputStream); + BCPGInputStream bcpg = null; + if (inputStream instanceof BCPGInputStream) { + bcpg = (BCPGInputStream) inputStream; + } else { + bcpg = new BCPGInputStream(inputStream); + } + this.packetInputStream = new TeeBCPGInputStream(bcpg, signatures); + + // *omnomnom* + consumePackets(); } /** - * This method consumes OpenPGP packets from the current {@link BCPGInputStream}. - * Once it reaches a "nested" OpenPGP packet (Literal Data, Compressed Data, Encrypted Data), it sets
in
- * to the nested stream and breaks the loop. + * Consume OpenPGP packets from the current {@link BCPGInputStream}. + * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, + * set
nestedInputStream
to the nested stream and breaks the loop. * The nested stream is either a simple {@link InputStream} (in case of Literal Data), or another * {@link OpenPgpMessageInputStream} in case of Compressed and Encrypted Data. + * Once the nested data is processed, this method is called again to consume the remainder + * of packets following the nested data packet. * - * @throws IOException - * @throws PGPException + * @throws IOException in case of an IO error + * @throws PGPException in case of an OpenPGP error + * @throws MissingDecryptionMethodException if there is an encrypted data packet which cannot be decrypted + * due to missing decryption methods (no key, no password, no sessionkey) + * @throws MalformedOpenPgpMessageException if the message is made of an invalid packet sequence which + * does not follow the packet syntax of RFC4880. */ private void consumePackets() throws IOException, PGPException { @@ -127,17 +146,23 @@ public class OpenPgpMessageInputStream extends InputStream { processCompressedData(); break loop; - // One Pass Signatures + // One Pass Signature case OPS: automaton.next(InputAlphabet.OnePassSignatures); - signatures.addOnePassSignatures(readOnePassSignatures()); + signatures.addOnePassSignature(readOnePassSignature()); + // signatures.addOnePassSignatures(readOnePassSignatures()); break; - // Signatures - either prepended to the message, or corresponding to the One Pass Signatures + // Signature - either prepended to the message, or corresponding to a One Pass Signature case SIG: boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); - processSignature(isSigForOPS); + PGPSignature signature = readSignature(); + processSignature(signature, isSigForOPS); + /* + PGPSignatureList signatureList = readSignatures(); + processSignatures(signatureList, isSigForOPS); + */ break; // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) @@ -154,7 +179,7 @@ public class OpenPgpMessageInputStream extends InputStream { // Marker Packets need to be skipped and ignored case MARKER: - bcpgIn.readPacket(); // skip + packetInputStream.readPacket(); // skip break; // Key Packets are illegal in this context @@ -182,8 +207,15 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private void processSignature(boolean isSigForOPS) throws IOException { - PGPSignatureList signatureList = readSignatures(); + private void processSignature(PGPSignature signature, boolean isSigForOPS) { + if (isSigForOPS) { + signatures.addOnePassCorrespondingSignature(signature); + } else { + signatures.addPrependedSignature(signature); + } + } + + private void processSignatures(PGPSignatureList signatureList, boolean isSigForOPS) throws IOException { if (isSigForOPS) { signatures.addOnePassCorrespondingSignatures(signatureList); } else { @@ -192,21 +224,21 @@ public class OpenPgpMessageInputStream extends InputStream { } private void processCompressedData() throws IOException, PGPException { - PGPCompressedData compressedData = new PGPCompressedData(bcpgIn); + PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm())); - in = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); + nestedInputStream = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); } private void processLiteralData() throws IOException { - PGPLiteralData literalData = new PGPLiteralData(bcpgIn); + PGPLiteralData literalData = new PGPLiteralData(packetInputStream); this.metadata.setChild(new MessageMetadata.LiteralData(literalData.getFileName(), literalData.getModificationTime(), StreamEncoding.requireFromCode(literalData.getFormat()))); - in = literalData.getDataStream(); + nestedInputStream = literalData.getDataStream(); } private boolean processEncryptedData() throws IOException, PGPException { - PGPEncryptedDataList encDataList = new PGPEncryptedDataList(bcpgIn); + PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); // TODO: Replace with !encDataList.isIntegrityProtected() if (!encDataList.get(0).isIntegrityProtected()) { @@ -225,11 +257,11 @@ public class OpenPgpMessageInputStream extends InputStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(options.getSessionKey().getAlgorithm()); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; - in = new OpenPgpMessageInputStream(skesk.getDataStream(decryptorFactory), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(skesk.getDataStream(decryptorFactory), options, encryptedData); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - in = new OpenPgpMessageInputStream(pkesk.getDataStream(decryptorFactory), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(pkesk.getDataStream(decryptorFactory), options, encryptedData); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -248,7 +280,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = skesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(skesk.getSymmetricAlgorithm(decryptorFactory))); - in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // password mismatch? Try next password @@ -274,7 +306,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -293,7 +325,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - in = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -307,7 +339,7 @@ public class OpenPgpMessageInputStream extends InputStream { private int nextTag() throws IOException { try { - return bcpgIn.nextPacketTag(); + return packetInputStream.nextPacketTag(); } catch (IOException e) { if ("Stream closed".equals(e.getMessage())) { // ZipInflater Streams sometimes close under our feet -.- @@ -345,13 +377,23 @@ public class OpenPgpMessageInputStream extends InputStream { return null; } + private PGPOnePassSignature readOnePassSignature() + throws PGPException, IOException { + return new PGPOnePassSignature(packetInputStream); + } + + private PGPSignature readSignature() + throws PGPException, IOException { + return new PGPSignature(packetInputStream); + } + private PGPOnePassSignatureListWrapper readOnePassSignatures() throws IOException { List encapsulating = new ArrayList<>(); ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag; while ((tag = nextTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { - Packet packet = bcpgIn.readPacket(); + Packet packet = packetInputStream.readPacket(); if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; byte[] bytes = sigPacket.getEncoded(); @@ -371,7 +413,7 @@ public class OpenPgpMessageInputStream extends InputStream { BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag = nextTag(); while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { - Packet packet = bcpgIn.readPacket(); + Packet packet = packetInputStream.readPacket(); if (tag == PacketTags.SIGNATURE) { SignaturePacket sigPacket = (SignaturePacket) packet; sigPacket.encode(bcpgOut); @@ -387,14 +429,14 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public int read() throws IOException { - if (in == null) { + if (nestedInputStream == null) { automaton.assertValid(); return -1; } int r; try { - r = in.read(); + r = nestedInputStream.read(); } catch (IOException e) { r = -1; } @@ -403,9 +445,9 @@ public class OpenPgpMessageInputStream extends InputStream { byte b = (byte) r; signatures.update(b); } else { - in.close(); + nestedInputStream.close(); collectMetadata(); - in = null; + nestedInputStream = null; try { consumePackets(); @@ -421,16 +463,16 @@ public class OpenPgpMessageInputStream extends InputStream { public int read(byte[] b, int off, int len) throws IOException { - if (in == null) { + if (nestedInputStream == null) { automaton.assertValid(); return -1; } - int r = in.read(b, off, len); + int r = nestedInputStream.read(b, off, len); if (r == -1) { - in.close(); + nestedInputStream.close(); collectMetadata(); - in = null; + nestedInputStream = null; try { consumePackets(); @@ -449,10 +491,10 @@ public class OpenPgpMessageInputStream extends InputStream { return; } - if (in != null) { - in.close(); + if (nestedInputStream != null) { + nestedInputStream.close(); collectMetadata(); - in = null; + nestedInputStream = null; } try { @@ -467,8 +509,8 @@ public class OpenPgpMessageInputStream extends InputStream { } private void collectMetadata() { - if (in instanceof OpenPgpMessageInputStream) { - OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) in; + if (nestedInputStream instanceof OpenPgpMessageInputStream) { + OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) nestedInputStream; MessageMetadata.Layer childLayer = child.metadata; this.metadata.setChild((MessageMetadata.Nested) childLayer); } @@ -533,13 +575,14 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private static final class Signatures { + private static final class Signatures extends OutputStream { final ConsumerOptions options; List detachedSignatures = new ArrayList<>(); List prependedSignatures = new ArrayList<>(); - List onePassSignatures = new ArrayList<>(); + List> onePassSignatures = new ArrayList<>(); List correspondingSignatures = new ArrayList<>(); + boolean lastOpsIsContaining = true; private Signatures(ConsumerOptions options) { this.options = options; @@ -547,28 +590,44 @@ public class OpenPgpMessageInputStream extends InputStream { void addDetachedSignatures(Collection signatures) { for (PGPSignature signature : signatures) { - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing certificate = findCertificate(keyId); - initialize(signature, certificate, keyId); + addDetachedSignature(signature); } - this.detachedSignatures.addAll(signatures); + } + + void addDetachedSignature(PGPSignature signature) { + long keyId = SignatureUtils.determineIssuerKeyId(signature); + PGPPublicKeyRing certificate = findCertificate(keyId); + initialize(signature, certificate, keyId); + this.detachedSignatures.add(signature); } void addPrependedSignatures(PGPSignatureList signatures) { for (PGPSignature signature : signatures) { - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing certificate = findCertificate(keyId); - initialize(signature, certificate, keyId); - this.prependedSignatures.add(signature); + addPrependedSignature(signature); } } - void addOnePassSignatures(PGPOnePassSignatureListWrapper signatures) { - for (PGPOnePassSignature ops : signatures.list) { - PGPPublicKeyRing certificate = findCertificate(ops.getKeyID()); - initialize(ops, certificate); - this.onePassSignatures.add(ops); + void addPrependedSignature(PGPSignature signature) { + long keyId = SignatureUtils.determineIssuerKeyId(signature); + PGPPublicKeyRing certificate = findCertificate(keyId); + initialize(signature, certificate, keyId); + this.prependedSignatures.add(signature); + } + + void addOnePassSignature(PGPOnePassSignature signature) { + List list; + if (lastOpsIsContaining) { + list = new ArrayList<>(); + onePassSignatures.add(list); + } else { + list = onePassSignatures.get(onePassSignatures.size() - 1); } + + PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); + initialize(signature, certificate); + list.add(signature); + + // lastOpsIsContaining = signature.isContaining(); } void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { @@ -618,8 +677,10 @@ public class OpenPgpMessageInputStream extends InputStream { for (PGPSignature prepended : prependedSignatures) { prepended.update(b); } - for (PGPOnePassSignature ops : onePassSignatures) { - ops.update(b); + for (List opss : onePassSignatures) { + for (PGPOnePassSignature ops : opss) { + ops.update(b); + } } for (PGPSignature detached : detachedSignatures) { detached.update(b); @@ -660,5 +721,10 @@ public class OpenPgpMessageInputStream extends InputStream { LOGGER.debug("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } } + + @Override + public void write(int b) throws IOException { + update((byte) b); + } } } From 4e44691ef68295f96fdf6ae23456a414fe4f41c8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 28 Sep 2022 17:38:20 +0200 Subject: [PATCH 0468/1204] Wip --- .../OpenPgpMessageInputStream.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index df3d6805..36de2635 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -60,6 +60,7 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Stack; public class OpenPgpMessageInputStream extends InputStream { @@ -577,15 +578,20 @@ public class OpenPgpMessageInputStream extends InputStream { private static final class Signatures extends OutputStream { final ConsumerOptions options; - List detachedSignatures = new ArrayList<>(); - List prependedSignatures = new ArrayList<>(); - List> onePassSignatures = new ArrayList<>(); - List correspondingSignatures = new ArrayList<>(); + final List detachedSignatures; + final List prependedSignatures; + final Stack> onePassSignatures; + final List correspondingSignatures; boolean lastOpsIsContaining = true; private Signatures(ConsumerOptions options) { this.options = options; + this.detachedSignatures = new ArrayList<>(); + this.prependedSignatures = new ArrayList<>(); + this.onePassSignatures = new Stack<>(); + onePassSignatures.push(new ArrayList<>()); + this.correspondingSignatures = new ArrayList<>(); } void addDetachedSignatures(Collection signatures) { From ec28ba2924667a5ff3719f51c941f57a9275ca98 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 00:15:18 +0200 Subject: [PATCH 0469/1204] SIGNATURE VERIFICATION IN OPENPGP SUCKS BIG TIME --- .../OpenPgpMessageInputStream.java | 198 ++++++++++-------- .../OpenPgpMessageInputStreamTest.java | 22 +- 2 files changed, 128 insertions(+), 92 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 36de2635..a1364997 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -48,6 +48,7 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.SignatureUtils; +import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; import org.slf4j.Logger; @@ -138,19 +139,25 @@ public class OpenPgpMessageInputStream extends InputStream { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); + signatures.commitNested(); processLiteralData(); break loop; // Compressed Data - the content contains another OpenPGP message case COMP: automaton.next(InputAlphabet.CompressedData); + signatures.commitNested(); processCompressedData(); break loop; // One Pass Signature case OPS: automaton.next(InputAlphabet.OnePassSignatures); - signatures.addOnePassSignature(readOnePassSignature()); + // signatures.addOnePassSignature(readOnePassSignature()); + PGPOnePassSignatureList onePassSignatureList = readOnePassSignatures(); + for (PGPOnePassSignature ops : onePassSignatureList) { + signatures.addOnePassSignature(ops); + } // signatures.addOnePassSignatures(readOnePassSignatures()); break; @@ -158,12 +165,15 @@ public class OpenPgpMessageInputStream extends InputStream { case SIG: boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); - PGPSignature signature = readSignature(); - processSignature(signature, isSigForOPS); - /* + + // PGPSignature signature = readSignature(); + // processSignature(signature, isSigForOPS); + PGPSignatureList signatureList = readSignatures(); - processSignatures(signatureList, isSigForOPS); - */ + for (PGPSignature signature : signatureList) { + processSignature(signature, isSigForOPS); + } + break; // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) @@ -178,7 +188,7 @@ public class OpenPgpMessageInputStream extends InputStream { throw new MissingDecryptionMethodException("No working decryption method found."); - // Marker Packets need to be skipped and ignored + // Marker Packets need to be skipped and ignored case MARKER: packetInputStream.readPacket(); // skip break; @@ -193,8 +203,8 @@ public class OpenPgpMessageInputStream extends InputStream { case UATTR: throw new MalformedOpenPgpMessageException("Illegal Packet in Stream: " + nextPacket); - // MDC packet is usually processed by PGPEncryptedDataList, so it is very likely we encounter this - // packet out of order + // MDC packet is usually processed by PGPEncryptedDataList, so it is very likely we encounter this + // packet out of order case MDC: throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); @@ -210,20 +220,12 @@ public class OpenPgpMessageInputStream extends InputStream { private void processSignature(PGPSignature signature, boolean isSigForOPS) { if (isSigForOPS) { - signatures.addOnePassCorrespondingSignature(signature); + signatures.addCorrespondingOnePassSignature(signature); } else { signatures.addPrependedSignature(signature); } } - private void processSignatures(PGPSignatureList signatureList, boolean isSigForOPS) throws IOException { - if (isSigForOPS) { - signatures.addOnePassCorrespondingSignatures(signatureList); - } else { - signatures.addPrependedSignatures(signatureList); - } - } - private void processCompressedData() throws IOException, PGPException { PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( @@ -380,16 +382,17 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { - return new PGPOnePassSignature(packetInputStream); + //return new PGPOnePassSignature(packetInputStream); + return null; } private PGPSignature readSignature() throws PGPException, IOException { - return new PGPSignature(packetInputStream); + //return new PGPSignature(packetInputStream); + return null; } - private PGPOnePassSignatureListWrapper readOnePassSignatures() throws IOException { - List encapsulating = new ArrayList<>(); + private PGPOnePassSignatureList readOnePassSignatures() throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); int tag; @@ -398,7 +401,6 @@ public class OpenPgpMessageInputStream extends InputStream { if (tag == PacketTags.ONE_PASS_SIGNATURE) { OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; byte[] bytes = sigPacket.getEncoded(); - encapsulating.add(bytes[bytes.length - 1] == 1); bcpgOut.write(bytes); } } @@ -406,7 +408,7 @@ public class OpenPgpMessageInputStream extends InputStream { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); - return new PGPOnePassSignatureListWrapper(signatureList, encapsulating); + return signatureList; } private PGPSignatureList readSignatures() throws IOException { @@ -449,6 +451,7 @@ public class OpenPgpMessageInputStream extends InputStream { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; + signatures.popNested(); try { consumePackets(); @@ -474,6 +477,7 @@ public class OpenPgpMessageInputStream extends InputStream { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; + signatures.popNested(); try { consumePackets(); @@ -556,41 +560,29 @@ public class OpenPgpMessageInputStream extends InputStream { } } - /** - * Workaround for BC not exposing, whether an OPS is encapsulating or not. - * TODO: Remove once our PR is merged - * - * @see PR against BC - */ - private static class PGPOnePassSignatureListWrapper { - private final PGPOnePassSignatureList list; - private final List encapsulating; - - PGPOnePassSignatureListWrapper(PGPOnePassSignatureList signatures, List encapsulating) { - this.list = signatures; - this.encapsulating = encapsulating; - } - - public int size() { - return list.size(); - } - } - + // TODO: In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" + // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. + // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. + // For this we might want to provide two update entries into the Signatures class, one for OpenPGP packets and one + // for literal data. UUUUUGLY!!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; final List detachedSignatures; final List prependedSignatures; - final Stack> onePassSignatures; + final List onePassSignatures; + final Stack> opsUpdateStack; final List correspondingSignatures; - boolean lastOpsIsContaining = true; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + List opsCurrentNesting = new ArrayList<>(); private Signatures(ConsumerOptions options) { this.options = options; this.detachedSignatures = new ArrayList<>(); this.prependedSignatures = new ArrayList<>(); - this.onePassSignatures = new Stack<>(); - onePassSignatures.push(new ArrayList<>()); + this.onePassSignatures = new ArrayList<>(); + this.opsUpdateStack = new Stack<>(); this.correspondingSignatures = new ArrayList<>(); } @@ -607,12 +599,6 @@ public class OpenPgpMessageInputStream extends InputStream { this.detachedSignatures.add(signature); } - void addPrependedSignatures(PGPSignatureList signatures) { - for (PGPSignature signature : signatures) { - addPrependedSignature(signature); - } - } - void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); @@ -621,27 +607,61 @@ public class OpenPgpMessageInputStream extends InputStream { } void addOnePassSignature(PGPOnePassSignature signature) { - List list; - if (lastOpsIsContaining) { - list = new ArrayList<>(); - onePassSignatures.add(list); - } else { - list = onePassSignatures.get(onePassSignatures.size() - 1); - } - PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); initialize(signature, certificate); - list.add(signature); + onePassSignatures.add(signature); - // lastOpsIsContaining = signature.isContaining(); + opsCurrentNesting.add(signature); + if (isContaining(signature)) { + commitNested(); + } } - void addOnePassCorrespondingSignatures(PGPSignatureList signatures) { - for (PGPSignature signature : signatures) { - correspondingSignatures.add(signature); + boolean isContaining(PGPOnePassSignature ops) { + try { + byte[] bytes = ops.getEncoded(); + return bytes[bytes.length - 1] == 1; + } catch (IOException e) { + return false; } } + void addCorrespondingOnePassSignature(PGPSignature signature) { + for (PGPOnePassSignature onePassSignature : onePassSignatures) { + if (onePassSignature.getKeyID() != signature.getKeyID()) { + continue; + } + + boolean verified = false; + try { + verified = onePassSignature.verify(signature); + } catch (PGPException e) { + log("Cannot verify OPS signature.", e); + } + log("One-Pass-Signature by " + Long.toHexString(onePassSignature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + try { + log(ArmorUtils.toAsciiArmoredString(out.toByteArray())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + void commitNested() { + if (opsCurrentNesting.isEmpty()) { + return; + } + + log("Committing " + opsCurrentNesting.size() + " OPS sigs for updating"); + opsUpdateStack.push(opsCurrentNesting); + opsCurrentNesting = new ArrayList<>(); + } + + void popNested() { + log("Popping nested"); + opsUpdateStack.pop(); + } + private void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { if (certificate == null) { // SHIT @@ -680,14 +700,21 @@ public class OpenPgpMessageInputStream extends InputStream { } public void update(byte b) { + if (!opsUpdateStack.isEmpty()) { + log("Update"); + out.write(b); + } + for (PGPSignature prepended : prependedSignatures) { prepended.update(b); } - for (List opss : onePassSignatures) { + + for (List opss : opsUpdateStack) { for (PGPOnePassSignature ops : opss) { ops.update(b); } } + for (PGPSignature detached : detachedSignatures) { detached.update(b); } @@ -699,9 +726,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = detached.verify(); } catch (PGPException e) { - LOGGER.debug("Cannot verify detached signature.", e); + log("Cannot verify detached signature.", e); } - LOGGER.debug("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + log("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } for (PGPSignature prepended : prependedSignatures) { @@ -709,22 +736,9 @@ public class OpenPgpMessageInputStream extends InputStream { try { verified = prepended.verify(); } catch (PGPException e) { - LOGGER.debug("Cannot verify prepended signature.", e); + log("Cannot verify prepended signature.", e); } - LOGGER.debug("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - } - - - for (int i = 0; i < onePassSignatures.size(); i++) { - PGPOnePassSignature ops = onePassSignatures.get(i); - PGPSignature signature = correspondingSignatures.get(correspondingSignatures.size() - i - 1); - boolean verified = false; - try { - verified = ops.verify(signature); - } catch (PGPException e) { - LOGGER.debug("Cannot verify OPS signature.", e); - } - LOGGER.debug("One-Pass-Signature by " + Long.toHexString(ops.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + log("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); } } @@ -733,4 +747,18 @@ public class OpenPgpMessageInputStream extends InputStream { update((byte) b); } } + + static void log(String message) { + LOGGER.debug(message); + // CHECKSTYLE:OFF + System.out.println(message); + // CHECKSTYLE:ON + } + + static void log(String message, Throwable e) { + log(message); + // CHECKSTYLE:OFF + e.printStackTrace(); + // CHECKSTYLE:ON + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index 6962e279..e52a6016 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -377,7 +378,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessSIG_COMP_LIT(Processor processor) throws PGPException, IOException { + public void testProcessSIG_COMP_LIT(Processor processor) + throws PGPException, IOException { PGPPublicKeyRing cert = PGPainless.extractCertificate( PGPainless.readKeyRing().secretKeyRing(KEY)); @@ -392,7 +394,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process SENC(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessSENC_LIT(Processor processor) throws PGPException, IOException { + public void testProcessSENC_LIT(Processor processor) + throws PGPException, IOException { Tuple result = processor.process(SENC_LIT, ConsumerOptions.get() .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); String plain = result.getA(); @@ -404,7 +407,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessPENC_COMP_LIT(Processor processor) throws IOException, PGPException { + public void testProcessPENC_COMP_LIT(Processor processor) + throws IOException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); Tuple result = processor.process(PENC_COMP_LIT, ConsumerOptions.get() .addDecryptionKey(secretKeys)); @@ -417,7 +421,8 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process OPS LIT SIG using {0}") @MethodSource("provideMessageProcessors") - public void testProcessOPS_LIT_SIG(Processor processor) throws IOException, PGPException { + public void testProcessOPS_LIT_SIG(Processor processor) + throws IOException, PGPException { PGPPublicKeyRing cert = PGPainless.extractCertificate(PGPainless.readKeyRing().secretKeyRing(KEY)); Tuple result = processor.process(OPS_LIT_SIG, ConsumerOptions.get() .addVerificationCert(cert)); @@ -428,7 +433,8 @@ public class OpenPgpMessageInputStreamTest { assertNull(metadata.getCompressionAlgorithm()); } - private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) + throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(in, out); @@ -437,7 +443,8 @@ public class OpenPgpMessageInputStreamTest { return new Tuple<>(out.toString(), metadata); } - private static Tuple processReadSequential(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { + private static Tuple processReadSequential(String armoredMessage, ConsumerOptions options) + throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -451,7 +458,8 @@ public class OpenPgpMessageInputStreamTest { return new Tuple<>(out.toString(), metadata); } - private static OpenPgpMessageInputStream get(String armored, ConsumerOptions options) throws IOException, PGPException { + private static OpenPgpMessageInputStream get(String armored, ConsumerOptions options) + throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); OpenPgpMessageInputStream pgpIn = new OpenPgpMessageInputStream(armorIn, options); From 527aab922ee3f8cd1982a1f6083472e2bd83135f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 12:33:08 +0200 Subject: [PATCH 0470/1204] Fix ModificationDetectionException by not calling PGPUtil.getDecoderStream() --- .../DecryptionStreamFactory.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index a787280a..07be7c10 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -36,7 +36,6 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; @@ -241,14 +240,12 @@ public final class DecryptionStreamFactory { SessionKey sessionKey = options.getSessionKey(); if (sessionKey != null) { integrityProtectedEncryptedInputStream = decryptWithProvidedSessionKey(pgpEncryptedDataList, sessionKey); - InputStream decodedDataStream = PGPUtil.getDecoderStream(integrityProtectedEncryptedInputStream); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream); + PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(integrityProtectedEncryptedInputStream); return processPGPPackets(factory, ++depth); } InputStream decryptedDataStream = decryptSessionKey(pgpEncryptedDataList); - InputStream decodedDataStream = PGPUtil.getDecoderStream(decryptedDataStream); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream); + PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decryptedDataStream); return processPGPPackets(factory, ++depth); } @@ -299,8 +296,7 @@ public final class DecryptionStreamFactory { } InputStream inflatedDataStream = pgpCompressedData.getDataStream(); - InputStream decodedDataStream = PGPUtil.getDecoderStream(inflatedDataStream); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream); + PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(inflatedDataStream); return processPGPPackets(objectFactory, ++depth); } From 81bb8cba54c5d5040785e8e12829525acbf8ef71 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 12:50:32 +0200 Subject: [PATCH 0471/1204] Use BCs Arrays.constantTimeAreEqual(char[], char[]) --- .../main/java/org/pgpainless/util/BCUtil.java | 54 ------------------- .../java/org/pgpainless/util/Passphrase.java | 4 +- .../java/org/pgpainless/util/BCUtilTest.java | 17 +++--- 3 files changed, 10 insertions(+), 65 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java deleted file mode 100644 index bb811cea..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -public final class BCUtil { - - private BCUtil() { - - } - - /** - * A constant time equals comparison - does not terminate early if - * test will fail. For best results always pass the expected value - * as the first parameter. - * - * TODO: This method was proposed as a patch to BC: - * https://github.com/bcgit/bc-java/pull/1141 - * Replace usage of this method with upstream eventually. - * Remove once BC 172 gets released, given it contains the patch. - * - * @param expected first array - * @param supplied second array - * @return true if arrays equal, false otherwise. - */ - public static boolean constantTimeAreEqual( - char[] expected, - char[] supplied) { - if (expected == null || supplied == null) { - return false; - } - - if (expected == supplied) { - return true; - } - - int len = Math.min(expected.length, supplied.length); - - int nonEqual = expected.length ^ supplied.length; - - // do the char-wise comparison - for (int i = 0; i != len; i++) { - nonEqual |= (expected[i] ^ supplied[i]); - } - // If supplied is longer than expected, iterate over rest of supplied with NOPs - for (int i = len; i < supplied.length; i++) { - nonEqual |= ((byte) supplied[i] ^ (byte) ~supplied[i]); - } - - return nonEqual == 0; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java b/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java index 4cef145a..9576fb3e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java @@ -8,8 +8,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Arrays; -import static org.pgpainless.util.BCUtil.constantTimeAreEqual; - public class Passphrase { public final Object lock = new Object(); @@ -165,6 +163,6 @@ public class Passphrase { } Passphrase other = (Passphrase) obj; return (getChars() == null && other.getChars() == null) || - constantTimeAreEqual(getChars(), other.getChars()); + org.bouncycastle.util.Arrays.constantTimeAreEqual(getChars(), other.getChars()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java index 6fb90e63..82368762 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java @@ -19,6 +19,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.util.Arrays; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; @@ -94,14 +95,14 @@ public class BCUtilTest { @Test public void constantTimeAreEqualsTest() { char[] b = "Hello".toCharArray(); - assertTrue(BCUtil.constantTimeAreEqual(b, b)); - assertTrue(BCUtil.constantTimeAreEqual("Hello".toCharArray(), "Hello".toCharArray())); - assertTrue(BCUtil.constantTimeAreEqual(new char[0], new char[0])); - assertTrue(BCUtil.constantTimeAreEqual(new char[] {'H', 'e', 'l', 'l', 'o'}, "Hello".toCharArray())); + assertTrue(Arrays.constantTimeAreEqual(b, b)); + assertTrue(Arrays.constantTimeAreEqual("Hello".toCharArray(), "Hello".toCharArray())); + assertTrue(Arrays.constantTimeAreEqual(new char[0], new char[0])); + assertTrue(Arrays.constantTimeAreEqual(new char[] {'H', 'e', 'l', 'l', 'o'}, "Hello".toCharArray())); - assertFalse(BCUtil.constantTimeAreEqual("Hello".toCharArray(), "Hello World".toCharArray())); - assertFalse(BCUtil.constantTimeAreEqual(null, "Hello".toCharArray())); - assertFalse(BCUtil.constantTimeAreEqual("Hello".toCharArray(), null)); - assertFalse(BCUtil.constantTimeAreEqual(null, null)); + assertFalse(Arrays.constantTimeAreEqual("Hello".toCharArray(), "Hello World".toCharArray())); + assertFalse(Arrays.constantTimeAreEqual(null, "Hello".toCharArray())); + assertFalse(Arrays.constantTimeAreEqual("Hello".toCharArray(), null)); + assertFalse(Arrays.constantTimeAreEqual((char[]) null, null)); } } From 09f94944b3f270dd1956733dfef1a7be9a5a14fb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 13:02:22 +0200 Subject: [PATCH 0472/1204] Remove unnecessary throws declarations --- .../key/collection/PGPKeyRingCollection.java | 2 +- .../secretkeyring/SecretKeyRingEditor.java | 2 +- .../pgpainless/key/parsing/KeyRingReader.java | 6 ++---- .../org/pgpainless/key/util/KeyRingUtils.java | 17 +++-------------- .../ModificationDetectionTests.java | 2 +- .../EncryptionOptionsTest.java | 3 +-- .../certification/CertifyCertificateTest.java | 2 +- .../GenerateKeyWithAdditionalUserIdTest.java | 6 +++--- .../java/org/pgpainless/util/BCUtilTest.java | 4 +--- .../keyring/KeyRingsFromCollectionTest.java | 4 ++-- .../java/org/pgpainless/sop/DearmorImpl.java | 2 +- .../java/org/pgpainless/sop/InlineSignImpl.java | 2 +- .../org/pgpainless/sop/InlineVerifyImpl.java | 2 +- 13 files changed, 19 insertions(+), 35 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java b/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java index 46aa2baf..6cf7102d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java @@ -74,7 +74,7 @@ public class PGPKeyRingCollection { } public PGPKeyRingCollection(@Nonnull Collection collection, boolean isSilent) - throws IOException, PGPException { + throws PGPException { List secretKeyRings = new ArrayList<>(); List publicKeyRings = new ArrayList<>(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index 20876c36..01c09903 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -327,7 +327,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { @Nonnull SecretKeyRingProtector primaryKeyProtector, @Nonnull KeyFlag keyFlag, KeyFlag... additionalKeyFlags) - throws PGPException, IOException, NoSuchAlgorithmException { + throws PGPException, IOException { KeyFlag[] flags = concat(keyFlag, additionalKeyFlags); PublicKeyAlgorithm subkeyAlgorithm = PublicKeyAlgorithm.requireFromId(subkey.getPublicKey().getAlgorithm()); SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java index b5b8ea65..bbaecd4f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java @@ -232,10 +232,9 @@ public class KeyRingReader { * @return public key ring collection * * @throws IOException in case of an IO error or exceeding of max iterations - * @throws PGPException in case of a broken key */ public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream, int maxIterations) - throws IOException, PGPException { + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); @@ -317,11 +316,10 @@ public class KeyRingReader { * @return secret key ring collection * * @throws IOException in case of an IO error or exceeding of max iterations - * @throws PGPException in case of a broken secret key */ public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream, int maxIterations) - throws IOException, PGPException { + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 0bcdf8d1..013e283e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -167,12 +167,9 @@ public final class KeyRingUtils { * * @param secretKeyRings secret key ring collection * @return public key ring collection - * @throws PGPException TODO: remove - * @throws IOException TODO: remove */ @Nonnull - public static PGPPublicKeyRingCollection publicKeyRingCollectionFrom(@Nonnull PGPSecretKeyRingCollection secretKeyRings) - throws PGPException, IOException { + public static PGPPublicKeyRingCollection publicKeyRingCollectionFrom(@Nonnull PGPSecretKeyRingCollection secretKeyRings) { List certificates = new ArrayList<>(); for (PGPSecretKeyRing secretKey : secretKeyRings) { certificates.add(PGPainless.extractCertificate(secretKey)); @@ -200,13 +197,9 @@ public final class KeyRingUtils { * * @param rings array of public key rings * @return key ring collection - * - * @throws IOException in case of an io error - * @throws PGPException in case of a broken key */ @Nonnull - public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) - throws IOException, PGPException { + public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) { return new PGPPublicKeyRingCollection(Arrays.asList(rings)); } @@ -215,13 +208,9 @@ public final class KeyRingUtils { * * @param rings array of secret key rings * @return secret key ring collection - * - * @throws IOException in case of an io error - * @throws PGPException in case of a broken key */ @Nonnull - public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) - throws IOException, PGPException { + public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) { return new PGPSecretKeyRingCollection(Arrays.asList(rings)); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java index 2ca265ea..14a041ff 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java @@ -530,7 +530,7 @@ public class ModificationDetectionTests { ); } - private PGPSecretKeyRingCollection getDecryptionKey() throws IOException, PGPException { + private PGPSecretKeyRingCollection getDecryptionKey() throws IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(keyAscii); return new PGPSecretKeyRingCollection(Collections.singletonList(secretKeys)); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 3e7ccb4d..3436ba69 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -9,7 +9,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; @@ -165,7 +164,7 @@ public class EncryptionOptionsTest { } @Test - public void testAddRecipients_PGPPublicKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void testAddRecipients_PGPPublicKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPPublicKeyRing secondKeyRing = KeyRingUtils.publicKeyRingFrom( PGPainless.generateKeyRing().modernKeyRing("other@pgpainless.org")); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java index f837be1b..eb0069f5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java @@ -140,7 +140,7 @@ public class CertifyCertificateTest { } @Test - public void testScopedDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void testScopedDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing aliceKey = PGPainless.generateKeyRing() .modernKeyRing("Alice "); PGPSecretKeyRing caKey = PGPainless.generateKeyRing() diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java index 2e2ffc8d..e68d8e9b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java @@ -7,7 +7,6 @@ package org.pgpainless.key.generation; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Date; @@ -26,14 +25,15 @@ import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.UserId; import org.pgpainless.timeframe.TestTimeFrameProvider; +import org.pgpainless.util.DateUtil; import org.pgpainless.util.TestAllImplementations; public class GenerateKeyWithAdditionalUserIdTest { @TestTemplate @ExtendWith(TestAllImplementations.class) - public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { - Date now = new Date(); + public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + Date now = DateUtil.now(); Date expiration = TestTimeFrameProvider.defaultExpirationForCreationDate(now); PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java index 82368762..821d9e30 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java @@ -8,7 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Iterator; @@ -36,8 +35,7 @@ public class BCUtilTest { @Test public void keyRingToCollectionTest() - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, - IOException { + throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { PGPSecretKeyRing sec = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.RSA(RsaLength._3072), diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java index eb3510be..bf4955cd 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java @@ -56,7 +56,7 @@ public class KeyRingsFromCollectionTest { } @Test - public void selectPublicKeyRingFromPublicKeyRingCollectionTest() throws IOException, PGPException { + public void selectPublicKeyRingFromPublicKeyRingCollectionTest() throws IOException { PGPPublicKeyRing emil = TestKeys.getEmilPublicKeyRing(); PGPPublicKeyRing juliet = TestKeys.getJulietPublicKeyRing(); PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(Arrays.asList(emil, juliet)); @@ -68,7 +68,7 @@ public class KeyRingsFromCollectionTest { } @Test - public void selectPublicKeyRingMapFromPublicKeyRingCollectionMapTest() throws IOException, PGPException { + public void selectPublicKeyRingMapFromPublicKeyRingCollectionMapTest() throws IOException { PGPPublicKeyRing emil = TestKeys.getEmilPublicKeyRing(); PGPPublicKeyRing juliet = TestKeys.getJulietPublicKeyRing(); MultiMap map = new MultiMap<>(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java index ac53d415..f0b21a6d 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java @@ -18,7 +18,7 @@ import sop.operation.Dearmor; public class DearmorImpl implements Dearmor { @Override - public Ready data(InputStream data) throws IOException { + public Ready data(InputStream data) { InputStream decoder; try { decoder = PGPUtil.getDecoderStream(data); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index d0cfbfcd..30e1d71e 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -71,7 +71,7 @@ public class InlineSignImpl implements InlineSign { } @Override - public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, IOException, SOPGPException.ExpectedText { + public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { for (PGPSecretKeyRing key : signingKeys) { try { if (mode == InlineSignAs.clearsigned) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 4af53685..81d614cf 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -49,7 +49,7 @@ public class InlineVerifyImpl implements InlineVerify { } @Override - public ReadyWithResult> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { + public ReadyWithResult> data(InputStream data) throws SOPGPException.NoSignature, SOPGPException.BadData { return new ReadyWithResult>() { @Override public List writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { From babd1542e3113065270552568f2c96144348cb7b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 13:02:36 +0200 Subject: [PATCH 0473/1204] DO NOT MERGE: Disable broken test --- .../java/org/pgpainless/signature/IgnoreMarkerPackets.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java index c86efd05..87766dfe 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java @@ -20,6 +20,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; @@ -159,6 +160,8 @@ public class IgnoreMarkerPackets { } @Test + @Disabled + // TODO: Enable and fix public void markerPlusEncryptedMessage() throws IOException, PGPException { String msg = "-----BEGIN PGP MESSAGE-----\n" + "\n" + From 2ce4486e8936cbbbddf941858b2644b019e2034d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 13:14:32 +0200 Subject: [PATCH 0474/1204] Convert links in javadoc to html --- .../java/org/pgpainless/algorithm/PublicKeyAlgorithm.java | 6 +++--- .../decryption_verification/MissingPublicKeyCallback.java | 4 +++- .../key/util/PublicKeyParameterValidationUtil.java | 2 +- .../test/java/org/bouncycastle/PGPPublicKeyRingTest.java | 2 +- .../src/test/java/org/pgpainless/key/WeirdKeys.java | 2 +- .../key/generation/GenerateWithEmptyPassphraseTest.java | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java index baa8f92e..c7599db9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java @@ -28,7 +28,7 @@ public enum PublicKeyAlgorithm { /** * RSA with usage encryption. * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.5 + * @deprecated see Deprecation notice */ @Deprecated RSA_ENCRYPT (PublicKeyAlgorithmTags.RSA_ENCRYPT, false, true), @@ -36,7 +36,7 @@ public enum PublicKeyAlgorithm { /** * RSA with usage of creating signatures. * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.5 + * @deprecated see Deprecation notice */ @Deprecated RSA_SIGN (PublicKeyAlgorithmTags.RSA_SIGN, true, false), @@ -71,7 +71,7 @@ public enum PublicKeyAlgorithm { /** * ElGamal General. * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.8 + * @deprecated see Deprecation notice */ @Deprecated ELGAMAL_GENERAL (PublicKeyAlgorithmTags.ELGAMAL_GENERAL, true, true), diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java index f3eb949b..9b6f4a1e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java @@ -20,11 +20,13 @@ public interface MissingPublicKeyCallback { * you may not only search for the key-id on the key rings primary key! * * It would be super cool to provide the OpenPgp fingerprint here, but unfortunately one-pass-signatures - * only contain the key id (see https://datatracker.ietf.org/doc/html/rfc4880#section-5.4) + * only contain the key id. * * @param keyId ID of the missing signing (sub)key * * @return keyring containing the key or null + * + * @see RFC */ @Nullable PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java index d3c7fe68..69940691 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java @@ -237,7 +237,7 @@ public class PublicKeyParameterValidationUtil { * Validate ElGamal public key parameters. * * Original implementation by the openpgpjs authors: - * https://github.com/openpgpjs/openpgpjs/blob/main/src/crypto/public_key/elgamal.js#L76-L143 + * Stackexchange link */ @Test public void subkeysDoNotHaveUserIDsTest() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java b/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java index b02b07c5..7500bab4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java @@ -13,7 +13,7 @@ import org.pgpainless.PGPainless; * This class contains a set of slightly out of spec or weird keys. * Those are used to test whether implementations behave correctly when dealing with such keys. * - * Original source: https://gitlab.com/sequoia-pgp/weird-keys + * @see Original Source */ public class WeirdKeys { diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java index dbb3f49b..c1375883 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java @@ -20,7 +20,7 @@ import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.Passphrase; /** - * Reproduce behavior of https://github.com/pgpainless/pgpainless/issues/16 + * Reproduce behavior of Date: Thu, 29 Sep 2022 13:15:02 +0200 Subject: [PATCH 0476/1204] Reformat KeyRingReader --- .../pgpainless/key/parsing/KeyRingReader.java | 67 ++++++++++++------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java index bbaecd4f..50cacf05 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java @@ -41,7 +41,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public PGPKeyRing keyRing(@Nonnull InputStream inputStream) throws IOException { + public PGPKeyRing keyRing(@Nonnull InputStream inputStream) + throws IOException { return readKeyRing(inputStream); } @@ -53,7 +54,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public PGPKeyRing keyRing(@Nonnull byte[] bytes) throws IOException { + public PGPKeyRing keyRing(@Nonnull byte[] bytes) + throws IOException { return keyRing(new ByteArrayInputStream(bytes)); } @@ -65,19 +67,23 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public PGPKeyRing keyRing(@Nonnull String asciiArmored) throws IOException { + public PGPKeyRing keyRing(@Nonnull String asciiArmored) + throws IOException { return keyRing(asciiArmored.getBytes(UTF8)); } - public PGPPublicKeyRing publicKeyRing(@Nonnull InputStream inputStream) throws IOException { + public PGPPublicKeyRing publicKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readPublicKeyRing(inputStream); } - public PGPPublicKeyRing publicKeyRing(@Nonnull byte[] bytes) throws IOException { + public PGPPublicKeyRing publicKeyRing(@Nonnull byte[] bytes) + throws IOException { return publicKeyRing(new ByteArrayInputStream(bytes)); } - public PGPPublicKeyRing publicKeyRing(@Nonnull String asciiArmored) throws IOException { + public PGPPublicKeyRing publicKeyRing(@Nonnull String asciiArmored) + throws IOException { return publicKeyRing(asciiArmored.getBytes(UTF8)); } @@ -86,23 +92,28 @@ public class KeyRingReader { return readPublicKeyRingCollection(inputStream); } - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull byte[] bytes) throws IOException, PGPException { + public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull byte[] bytes) + throws IOException, PGPException { return publicKeyRingCollection(new ByteArrayInputStream(bytes)); } - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull String asciiArmored) throws IOException, PGPException { + public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull String asciiArmored) + throws IOException, PGPException { return publicKeyRingCollection(asciiArmored.getBytes(UTF8)); } - public PGPSecretKeyRing secretKeyRing(@Nonnull InputStream inputStream) throws IOException { + public PGPSecretKeyRing secretKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readSecretKeyRing(inputStream); } - public PGPSecretKeyRing secretKeyRing(@Nonnull byte[] bytes) throws IOException { + public PGPSecretKeyRing secretKeyRing(@Nonnull byte[] bytes) + throws IOException { return secretKeyRing(new ByteArrayInputStream(bytes)); } - public PGPSecretKeyRing secretKeyRing(@Nonnull String asciiArmored) throws IOException { + public PGPSecretKeyRing secretKeyRing(@Nonnull String asciiArmored) + throws IOException { return secretKeyRing(asciiArmored.getBytes(UTF8)); } @@ -111,11 +122,13 @@ public class KeyRingReader { return readSecretKeyRingCollection(inputStream); } - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull byte[] bytes) throws IOException, PGPException { + public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull byte[] bytes) + throws IOException, PGPException { return secretKeyRingCollection(new ByteArrayInputStream(bytes)); } - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull String asciiArmored) throws IOException, PGPException { + public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull String asciiArmored) + throws IOException, PGPException { return secretKeyRingCollection(asciiArmored.getBytes(UTF8)); } @@ -124,11 +137,13 @@ public class KeyRingReader { return readKeyRingCollection(inputStream, isSilent); } - public PGPKeyRingCollection keyRingCollection(@Nonnull byte[] bytes, boolean isSilent) throws IOException, PGPException { + public PGPKeyRingCollection keyRingCollection(@Nonnull byte[] bytes, boolean isSilent) + throws IOException, PGPException { return keyRingCollection(new ByteArrayInputStream(bytes), isSilent); } - public PGPKeyRingCollection keyRingCollection(@Nonnull String asciiArmored, boolean isSilent) throws IOException, PGPException { + public PGPKeyRingCollection keyRingCollection(@Nonnull String asciiArmored, boolean isSilent) + throws IOException, PGPException { return keyRingCollection(asciiArmored.getBytes(UTF8), isSilent); } @@ -142,7 +157,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream) throws IOException { + public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readKeyRing(inputStream, MAX_ITERATIONS); } @@ -157,7 +173,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { + public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream, int maxIterations) + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); int i = 0; @@ -181,7 +198,8 @@ public class KeyRingReader { throw new IOException("Loop exceeded max iteration count."); } - public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) throws IOException { + public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readPublicKeyRing(inputStream, MAX_ITERATIONS); } @@ -196,7 +214,8 @@ public class KeyRingReader { * * @throws IOException in case of an IO error or exceeding of max iterations */ - public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { + public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream, int maxIterations) + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); int i = 0; @@ -218,7 +237,7 @@ public class KeyRingReader { } public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException, PGPException { + throws IOException { return readPublicKeyRingCollection(inputStream, MAX_ITERATIONS); } @@ -264,7 +283,8 @@ public class KeyRingReader { throw new IOException("Loop exceeded max iteration count."); } - public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream) throws IOException { + public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readSecretKeyRing(inputStream, MAX_ITERATIONS); } @@ -279,7 +299,8 @@ public class KeyRingReader { * * @throws IOException in case of an IO error or exceeding of max iterations */ - public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { + public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream, int maxIterations) + throws IOException { InputStream decoderStream = ArmorUtils.getDecoderStream(inputStream); PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); int i = 0; @@ -302,7 +323,7 @@ public class KeyRingReader { } public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException, PGPException { + throws IOException { return readSecretKeyRingCollection(inputStream, MAX_ITERATIONS); } From 5e37d8038a4e241b11ca85324762f4d377a6585f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 29 Sep 2022 17:45:32 +0200 Subject: [PATCH 0477/1204] WIP: So close to working notarizations --- .../OpenPgpMessageInputStream.java | 359 +++++++++++------- .../TeeBCPGInputStream.java | 1 + .../automaton/PDA.java | 16 +- .../OpenPgpMessageInputStreamTest.java | 145 +++++++ version.gradle | 2 +- 5 files changed, 375 insertions(+), 148 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index a1364997..2d031edc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -4,12 +4,17 @@ package org.pgpainless.decryption_verification; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Stack; + import org.bouncycastle.bcpg.BCPGInputStream; -import org.bouncycastle.bcpg.BCPGOutputStream; -import org.bouncycastle.bcpg.OnePassSignaturePacket; -import org.bouncycastle.bcpg.Packet; -import org.bouncycastle.bcpg.PacketTags; -import org.bouncycastle.bcpg.SignaturePacket; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; @@ -17,7 +22,6 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPOnePassSignature; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; import org.bouncycastle.openpgp.PGPPBEEncryptedData; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; @@ -26,11 +30,12 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; +import org.bouncycastle.util.encoders.Hex; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; @@ -54,15 +59,6 @@ import org.pgpainless.util.Tuple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Stack; - public class OpenPgpMessageInputStream extends InputStream { private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); @@ -100,13 +96,7 @@ public class OpenPgpMessageInputStream extends InputStream { this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } - // TODO: Use BCPGInputStream.wrap(inputStream); - BCPGInputStream bcpg = null; - if (inputStream instanceof BCPGInputStream) { - bcpg = (BCPGInputStream) inputStream; - } else { - bcpg = new BCPGInputStream(inputStream); - } + BCPGInputStream bcpg = BCPGInputStream.wrap(inputStream); this.packetInputStream = new TeeBCPGInputStream(bcpg, signatures); // *omnomnom* @@ -133,13 +123,20 @@ public class OpenPgpMessageInputStream extends InputStream { throws IOException, PGPException { int tag; loop: while ((tag = nextTag()) != -1) { - OpenPgpPacket nextPacket = OpenPgpPacket.requireFromTag(tag); + OpenPgpPacket nextPacket; + try { + nextPacket = OpenPgpPacket.requireFromTag(tag); + } catch (NoSuchElementException e) { + log("Invalid tag: " + tag); + throw e; + } + log(nextPacket.toString()); + signatures.nextPacket(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); - signatures.commitNested(); processLiteralData(); break loop; @@ -153,12 +150,8 @@ public class OpenPgpMessageInputStream extends InputStream { // One Pass Signature case OPS: automaton.next(InputAlphabet.OnePassSignatures); - // signatures.addOnePassSignature(readOnePassSignature()); - PGPOnePassSignatureList onePassSignatureList = readOnePassSignatures(); - for (PGPOnePassSignature ops : onePassSignatureList) { - signatures.addOnePassSignature(ops); - } - // signatures.addOnePassSignatures(readOnePassSignatures()); + PGPOnePassSignature onePassSignature = readOnePassSignature(); + signatures.addOnePassSignature(onePassSignature); break; // Signature - either prepended to the message, or corresponding to a One Pass Signature @@ -166,13 +159,8 @@ public class OpenPgpMessageInputStream extends InputStream { boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signatures); - // PGPSignature signature = readSignature(); - // processSignature(signature, isSigForOPS); - - PGPSignatureList signatureList = readSignatures(); - for (PGPSignature signature : signatureList) { - processSignature(signature, isSigForOPS); - } + PGPSignature signature = readSignature(); + processSignature(signature, isSigForOPS); break; @@ -220,6 +208,7 @@ public class OpenPgpMessageInputStream extends InputStream { private void processSignature(PGPSignature signature, boolean isSigForOPS) { if (isSigForOPS) { + signatures.popNested(); signatures.addCorrespondingOnePassSignature(signature); } else { signatures.addPrependedSignature(signature); @@ -240,6 +229,41 @@ public class OpenPgpMessageInputStream extends InputStream { nestedInputStream = literalData.getDataStream(); } + private void debugEncryptedData() throws PGPException, IOException { + PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); + + // TODO: Replace with !encDataList.isIntegrityProtected() + if (!encDataList.get(0).isIntegrityProtected()) { + throw new MessageNotIntegrityProtectedException(); + } + + SortedESKs esks = new SortedESKs(encDataList); + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + long keyId = pkesk.getKeyID(); + PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); + if (decryptionKeys == null) { + continue; + } + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); + PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); + + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + InputStream decoder = PGPUtil.getDecoderStream(decrypted); + PGPObjectFactory objectFactory = ImplementationFactory.getInstance() + .getPGPObjectFactory(decoder); + objectFactory.nextObject(); + objectFactory.nextObject(); + objectFactory.nextObject(); + } catch (PGPException e) { + // hm :/ + } + } + } + private boolean processEncryptedData() throws IOException, PGPException { PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); @@ -309,7 +333,8 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + + nestedInputStream = new OpenPgpMessageInputStream(PGPUtil.getDecoderStream(decrypted), options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -382,52 +407,12 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { - //return new PGPOnePassSignature(packetInputStream); - return null; + return new PGPOnePassSignature(packetInputStream); } private PGPSignature readSignature() throws PGPException, IOException { - //return new PGPSignature(packetInputStream); - return null; - } - - private PGPOnePassSignatureList readOnePassSignatures() throws IOException { - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag; - while ((tag = nextTag()) == PacketTags.ONE_PASS_SIGNATURE || tag == PacketTags.MARKER) { - Packet packet = packetInputStream.readPacket(); - if (tag == PacketTags.ONE_PASS_SIGNATURE) { - OnePassSignaturePacket sigPacket = (OnePassSignaturePacket) packet; - byte[] bytes = sigPacket.getEncoded(); - bcpgOut.write(bytes); - } - } - bcpgOut.close(); - - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); - PGPOnePassSignatureList signatureList = (PGPOnePassSignatureList) objectFactory.nextObject(); - return signatureList; - } - - private PGPSignatureList readSignatures() throws IOException { - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - BCPGOutputStream bcpgOut = new BCPGOutputStream(buf); - int tag = nextTag(); - while (tag == PacketTags.SIGNATURE || tag == PacketTags.MARKER) { - Packet packet = packetInputStream.readPacket(); - if (tag == PacketTags.SIGNATURE) { - SignaturePacket sigPacket = (SignaturePacket) packet; - sigPacket.encode(bcpgOut); - tag = nextTag(); - } - } - bcpgOut.close(); - - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(buf.toByteArray()); - PGPSignatureList signatureList = (PGPSignatureList) objectFactory.nextObject(); - return signatureList; + return new PGPSignature(packetInputStream); } @Override @@ -446,12 +431,11 @@ public class OpenPgpMessageInputStream extends InputStream { boolean eos = r == -1; if (!eos) { byte b = (byte) r; - signatures.update(b); + signatures.updateLiteral(b); } else { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; - signatures.popNested(); try { consumePackets(); @@ -473,11 +457,13 @@ public class OpenPgpMessageInputStream extends InputStream { } int r = nestedInputStream.read(b, off, len); - if (r == -1) { + if (r != -1) { + signatures.updateLiteral(b, off, r); + } + else { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; - signatures.popNested(); try { consumePackets(); @@ -569,14 +555,11 @@ public class OpenPgpMessageInputStream extends InputStream { final ConsumerOptions options; final List detachedSignatures; final List prependedSignatures; - final List onePassSignatures; - final Stack> opsUpdateStack; + final List onePassSignatures; + final Stack> opsUpdateStack; + List literalOPS = new ArrayList<>(); final List correspondingSignatures; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - List opsCurrentNesting = new ArrayList<>(); - private Signatures(ConsumerOptions options) { this.options = options; this.detachedSignatures = new ArrayList<>(); @@ -608,57 +591,42 @@ public class OpenPgpMessageInputStream extends InputStream { void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); - initialize(signature, certificate); - onePassSignatures.add(signature); + OPS ops = new OPS(signature); + ops.init(certificate); + onePassSignatures.add(ops); - opsCurrentNesting.add(signature); - if (isContaining(signature)) { + literalOPS.add(ops); + if (signature.isContaining()) { commitNested(); } } - boolean isContaining(PGPOnePassSignature ops) { - try { - byte[] bytes = ops.getEncoded(); - return bytes[bytes.length - 1] == 1; - } catch (IOException e) { - return false; - } - } - void addCorrespondingOnePassSignature(PGPSignature signature) { - for (PGPOnePassSignature onePassSignature : onePassSignatures) { - if (onePassSignature.getKeyID() != signature.getKeyID()) { + for (int i = onePassSignatures.size() - 1; i >= 0; i--) { + OPS onePassSignature = onePassSignatures.get(i); + if (onePassSignature.signature.getKeyID() != signature.getKeyID()) { + continue; + } + if (onePassSignature.finished) { continue; } - boolean verified = false; - try { - verified = onePassSignature.verify(signature); - } catch (PGPException e) { - log("Cannot verify OPS signature.", e); - } - log("One-Pass-Signature by " + Long.toHexString(onePassSignature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - try { - log(ArmorUtils.toAsciiArmoredString(out.toByteArray())); - } catch (IOException e) { - throw new RuntimeException(e); - } + boolean verified = onePassSignature.verify(signature); + log("One-Pass-Signature by " + Long.toHexString(onePassSignature.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + System.out.println(onePassSignature); + break; } } void commitNested() { - if (opsCurrentNesting.isEmpty()) { - return; - } - - log("Committing " + opsCurrentNesting.size() + " OPS sigs for updating"); - opsUpdateStack.push(opsCurrentNesting); - opsCurrentNesting = new ArrayList<>(); + opsUpdateStack.push(literalOPS); + literalOPS = new ArrayList<>(); } void popNested() { - log("Popping nested"); + if (opsUpdateStack.isEmpty()) { + return; + } opsUpdateStack.pop(); } @@ -676,7 +644,7 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private void initialize(PGPOnePassSignature ops, PGPPublicKeyRing certificate) { + private static void initialize(PGPOnePassSignature ops, PGPPublicKeyRing certificate) { if (certificate == null) { return; } @@ -699,20 +667,9 @@ public class OpenPgpMessageInputStream extends InputStream { return null; // TODO: Missing cert for sig } - public void update(byte b) { - if (!opsUpdateStack.isEmpty()) { - log("Update"); - out.write(b); - } - - for (PGPSignature prepended : prependedSignatures) { - prepended.update(b); - } - - for (List opss : opsUpdateStack) { - for (PGPOnePassSignature ops : opss) { - ops.update(b); - } + public void updateLiteral(byte b) { + for (OPS ops : literalOPS) { + ops.update(b); } for (PGPSignature detached : detachedSignatures) { @@ -720,6 +677,33 @@ public class OpenPgpMessageInputStream extends InputStream { } } + public void updateLiteral(byte[] b, int off, int len) { + for (OPS ops : literalOPS) { + ops.update(b, off, len); + } + + for (PGPSignature detached : detachedSignatures) { + detached.update(b, off, len); + } + } + + public void updatePacket(byte b) { + for (List nestedOPSs : opsUpdateStack) { + for (OPS ops : nestedOPSs) { + ops.update(b); + } + } + } + + public void updatePacket(byte[] buf, int off, int len) { + for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { + List nestedOPSs = opsUpdateStack.get(i); + for (OPS ops : nestedOPSs) { + ops.update(buf, off, len); + } + } + } + public void finish() { for (PGPSignature detached : detachedSignatures) { boolean verified = false; @@ -743,8 +727,97 @@ public class OpenPgpMessageInputStream extends InputStream { } @Override - public void write(int b) throws IOException { - update((byte) b); + public void write(int b) { + updatePacket((byte) b); + } + + @Override + public void write(byte[] b, int off, int len) { + updatePacket(b, off, len); + } + + public void nextPacket(OpenPgpPacket nextPacket) { + if (nextPacket == OpenPgpPacket.LIT) { + if (literalOPS.isEmpty() && !opsUpdateStack.isEmpty()) { + literalOPS = opsUpdateStack.pop(); + } + } + } + + static class OPS { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + PGPOnePassSignature signature; + boolean finished; + boolean valid; + + public OPS(PGPOnePassSignature signature) { + this.signature = signature; + } + + public void init(PGPPublicKeyRing certificate) { + initialize(signature, certificate); + } + + public boolean verify(PGPSignature signature) { + if (this.signature.getKeyID() != signature.getKeyID()) { + // nope + return false; + } + finished = true; + try { + valid = this.signature.verify(signature); + } catch (PGPException e) { + log("Cannot verify OPS " + signature.getKeyID()); + } + return valid; + } + + public void update(byte b) { + if (finished) { + log("Updating finished sig!"); + return; + } + signature.update(b); + bytes.write(b); + } + + public void update(byte[] bytes, int off, int len) { + if (finished) { + log("Updating finished sig!"); + return; + } + signature.update(bytes, off, len); + this.bytes.write(bytes, off, len); + } + + @Override + public String toString() { + String OPS = "c40d03000a01fbfcc82a015e733001"; + String LIT_H = "cb28620000000000"; + String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; + String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; + String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; + String out = signature.getKeyID() + " last=" + signature.isContaining() + "\n"; + + String hex = Hex.toHexString(bytes.toByteArray()); + while (hex.contains(OPS)) { + hex = hex.replace(OPS, "[OPS]"); + } + while (hex.contains(LIT_H)) { + hex = hex.replace(LIT_H, "[LIT]"); + } + while (hex.contains(LIT)) { + hex = hex.replace(LIT, ""); + } + while (hex.contains(SIG1)) { + hex = hex.replace(SIG1, "[SIG1]"); + } + while (hex.contains(SIG2)) { + hex = hex.replace(SIG2, "[SIG2]"); + } + + return out + hex; + } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index ab24f22e..cce21051 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -1,6 +1,7 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.bcpg.BCPGInputStream; +import org.pgpainless.util.ArmorUtils; import java.io.IOException; import java.io.InputStream; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index dda2adce..1d4b1189 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -149,11 +149,19 @@ public class PDA { @Override State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); - if (stackItem == terminus && input == InputAlphabet.EndOfSequence && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); + if (input == InputAlphabet.EndOfSequence) { + if (stackItem == terminus && automaton.stack.isEmpty()) { + return Valid; + } else { + // premature end of stream + throw new MalformedOpenPgpMessageException(this, input, stackItem); + } + } else if (input == InputAlphabet.Signatures) { + if (stackItem == ops) { + return CorrespondingSignature; + } } + throw new MalformedOpenPgpMessageException(this, input, stackItem); } }, diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index e52a6016..f780b2eb 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -433,6 +433,151 @@ public class OpenPgpMessageInputStreamTest { assertNull(metadata.getCompressionAlgorithm()); } + @ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessOPS_OPS_OPS_LIT_SIG_SIG_SIG(Processor processor) throws IOException, PGPException { + String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "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\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "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\n" + + "=JP9T\n" + + "-----END PGP MESSAGE-----"; + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + Tuple result = processor.process(MSG, ConsumerOptions.get() + .addVerificationCert(certificate) + .addDecryptionKey(secretKeys)); + String plain = result.getA(); + assertEquals("encrypt ∘ sign ∘ sign ∘ sign", plain); + MessageMetadata metadata = result.getB(); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); + } + private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); diff --git a/version.gradle b/version.gradle index dc50f086..7d25632e 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '1.3.14' + shortVersion = '1.4.0' isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 From e420678076fb33bad2fd94e6928d760098786115 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 6 Oct 2022 21:52:23 +0200 Subject: [PATCH 0478/1204] 2/3 the way to working sig verification --- .../DelayedTeeInputStreamInputStream.java | 38 +++ .../OpenPgpMessageInputStream.java | 263 ++++++++++++------ .../TeeBCPGInputStream.java | 36 --- .../OpenPgpMessageInputStreamTest.java | 234 ++++++++++------ .../TeeBCPGInputStreamTest.java | 61 ---- 5 files changed, 353 insertions(+), 279 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java new file mode 100644 index 00000000..5cbabf89 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java @@ -0,0 +1,38 @@ +package org.pgpainless.decryption_verification; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class DelayedTeeInputStreamInputStream extends InputStream { + + private int last = -1; + private final InputStream inputStream; + private final OutputStream outputStream; + + public DelayedTeeInputStreamInputStream(InputStream inputStream, OutputStream outputStream) { + this.inputStream = inputStream; + this.outputStream = outputStream; + } + + @Override + public int read() throws IOException { + if (last != -1) { + outputStream.write(last); + } + last = inputStream.read(); + return last; + } + + /** + * Squeeze the last byte out and update the output stream. + * + * @throws IOException in case of an IO error + */ + public void squeeze() throws IOException { + if (last != -1) { + outputStream.write(last); + } + last = -1; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 2d031edc..8d370965 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -20,7 +20,6 @@ import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPOnePassSignature; import org.bouncycastle.openpgp.PGPPBEEncryptedData; import org.bouncycastle.openpgp.PGPPrivateKey; @@ -53,7 +52,6 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.Tuple; import org.slf4j.Logger; @@ -68,6 +66,7 @@ public class OpenPgpMessageInputStream extends InputStream { protected final OpenPgpMetadata.Builder resultBuilder; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message protected final PDA automaton = new PDA(); + protected final DelayedTeeInputStreamInputStream delayedTee; // InputStream of OpenPGP packets of the current layer protected final BCPGInputStream packetInputStream; // InputStream of a nested data packet @@ -96,8 +95,9 @@ public class OpenPgpMessageInputStream extends InputStream { this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } - BCPGInputStream bcpg = BCPGInputStream.wrap(inputStream); - this.packetInputStream = new TeeBCPGInputStream(bcpg, signatures); + delayedTee = new DelayedTeeInputStreamInputStream(inputStream, signatures); + BCPGInputStream bcpg = BCPGInputStream.wrap(delayedTee); + this.packetInputStream = bcpg; // *omnomnom* consumePackets(); @@ -121,22 +121,15 @@ public class OpenPgpMessageInputStream extends InputStream { */ private void consumePackets() throws IOException, PGPException { - int tag; - loop: while ((tag = nextTag()) != -1) { - OpenPgpPacket nextPacket; - try { - nextPacket = OpenPgpPacket.requireFromTag(tag); - } catch (NoSuchElementException e) { - log("Invalid tag: " + tag); - throw e; - } - log(nextPacket.toString()); + OpenPgpPacket nextPacket; + loop: while ((nextPacket = nextPacketTag()) != null) { signatures.nextPacket(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); + delayedTee.squeeze(); processLiteralData(); break loop; @@ -145,12 +138,14 @@ public class OpenPgpMessageInputStream extends InputStream { automaton.next(InputAlphabet.CompressedData); signatures.commitNested(); processCompressedData(); + delayedTee.squeeze(); break loop; // One Pass Signature case OPS: automaton.next(InputAlphabet.OnePassSignatures); PGPOnePassSignature onePassSignature = readOnePassSignature(); + delayedTee.squeeze(); signatures.addOnePassSignature(onePassSignature); break; @@ -160,6 +155,7 @@ public class OpenPgpMessageInputStream extends InputStream { automaton.next(InputAlphabet.Signatures); PGPSignature signature = readSignature(); + delayedTee.squeeze(); processSignature(signature, isSigForOPS); break; @@ -170,6 +166,7 @@ public class OpenPgpMessageInputStream extends InputStream { case SED: case SEIPD: automaton.next(InputAlphabet.EncryptedData); + delayedTee.squeeze(); if (processEncryptedData()) { break loop; } @@ -179,6 +176,7 @@ public class OpenPgpMessageInputStream extends InputStream { // Marker Packets need to be skipped and ignored case MARKER: packetInputStream.readPacket(); // skip + delayedTee.squeeze(); break; // Key Packets are illegal in this context @@ -206,6 +204,23 @@ public class OpenPgpMessageInputStream extends InputStream { } } + private OpenPgpPacket nextPacketTag() throws IOException { + int tag = nextTag(); + if (tag == -1) { + log("EOF"); + return null; + } + OpenPgpPacket packet; + try { + packet = OpenPgpPacket.requireFromTag(tag); + } catch (NoSuchElementException e) { + log("Invalid tag: " + tag); + throw e; + } + log(packet.toString()); + return packet; + } + private void processSignature(PGPSignature signature, boolean isSigForOPS) { if (isSigForOPS) { signatures.popNested(); @@ -229,41 +244,6 @@ public class OpenPgpMessageInputStream extends InputStream { nestedInputStream = literalData.getDataStream(); } - private void debugEncryptedData() throws PGPException, IOException { - PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); - - // TODO: Replace with !encDataList.isIntegrityProtected() - if (!encDataList.get(0).isIntegrityProtected()) { - throw new MessageNotIntegrityProtectedException(); - } - - SortedESKs esks = new SortedESKs(encDataList); - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - long keyId = pkesk.getKeyID(); - PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); - if (decryptionKeys == null) { - continue; - } - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); - PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); - - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - InputStream decoder = PGPUtil.getDecoderStream(decrypted); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance() - .getPGPObjectFactory(decoder); - objectFactory.nextObject(); - objectFactory.nextObject(); - objectFactory.nextObject(); - } catch (PGPException e) { - // hm :/ - } - } - } - private boolean processEncryptedData() throws IOException, PGPException { PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); @@ -553,12 +533,13 @@ public class OpenPgpMessageInputStream extends InputStream { // for literal data. UUUUUGLY!!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; - final List detachedSignatures; - final List prependedSignatures; + final List detachedSignatures; + final List prependedSignatures; final List onePassSignatures; final Stack> opsUpdateStack; List literalOPS = new ArrayList<>(); final List correspondingSignatures; + boolean isLiteral = true; private Signatures(ConsumerOptions options) { this.options = options; @@ -579,14 +560,14 @@ public class OpenPgpMessageInputStream extends InputStream { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.detachedSignatures.add(signature); + this.detachedSignatures.add(new SIG(signature)); } void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.prependedSignatures.add(signature); + this.prependedSignatures.add(new SIG(signature)); } void addOnePassSignature(PGPOnePassSignature signature) { @@ -630,7 +611,7 @@ public class OpenPgpMessageInputStream extends InputStream { opsUpdateStack.pop(); } - private void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { + private static void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { if (certificate == null) { // SHIT return; @@ -672,7 +653,7 @@ public class OpenPgpMessageInputStream extends InputStream { ops.update(b); } - for (PGPSignature detached : detachedSignatures) { + for (SIG detached : detachedSignatures) { detached.update(b); } } @@ -682,13 +663,22 @@ public class OpenPgpMessageInputStream extends InputStream { ops.update(b, off, len); } - for (PGPSignature detached : detachedSignatures) { + for (SIG detached : detachedSignatures) { detached.update(b, off, len); } } public void updatePacket(byte b) { - for (List nestedOPSs : opsUpdateStack) { + for (SIG detached : detachedSignatures) { + detached.update(b); + } + + for (SIG prepended : prependedSignatures) { + prepended.update(b); + } + + for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { + List nestedOPSs = opsUpdateStack.get(i); for (OPS ops : nestedOPSs) { ops.update(b); } @@ -696,6 +686,14 @@ public class OpenPgpMessageInputStream extends InputStream { } public void updatePacket(byte[] buf, int off, int len) { + for (SIG detached : detachedSignatures) { + detached.update(buf, off, len); + } + + for (SIG prepended : prependedSignatures) { + prepended.update(buf, off, len); + } + for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { List nestedOPSs = opsUpdateStack.get(i); for (OPS ops : nestedOPSs) { @@ -705,24 +703,16 @@ public class OpenPgpMessageInputStream extends InputStream { } public void finish() { - for (PGPSignature detached : detachedSignatures) { - boolean verified = false; - try { - verified = detached.verify(); - } catch (PGPException e) { - log("Cannot verify detached signature.", e); - } - log("Detached Signature by " + Long.toHexString(detached.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + for (SIG detached : detachedSignatures) { + boolean verified = detached.verify(); + log("Detached Signature by " + Long.toHexString(detached.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + System.out.println(detached); } - for (PGPSignature prepended : prependedSignatures) { - boolean verified = false; - try { - verified = prepended.verify(); - } catch (PGPException e) { - log("Cannot verify prepended signature.", e); - } - log("Prepended Signature by " + Long.toHexString(prepended.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + for (SIG prepended : prependedSignatures) { + boolean verified = prepended.verify(); + log("Prepended Signature by " + Long.toHexString(prepended.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + System.out.println(prepended); } } @@ -738,9 +728,92 @@ public class OpenPgpMessageInputStream extends InputStream { public void nextPacket(OpenPgpPacket nextPacket) { if (nextPacket == OpenPgpPacket.LIT) { + isLiteral = true; if (literalOPS.isEmpty() && !opsUpdateStack.isEmpty()) { literalOPS = opsUpdateStack.pop(); } + } else { + isLiteral = false; + } + } + + static class SIG { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + PGPSignature signature; + boolean finished; + boolean valid; + + public SIG(PGPSignature signature) { + this.signature = signature; + } + + public void init(PGPPublicKeyRing certificate) { + initialize(signature, certificate, signature.getKeyID()); + } + + public boolean verify() { + finished = true; + try { + valid = this.signature.verify(); + } catch (PGPException e) { + log("Cannot verify SIG " + signature.getKeyID()); + } + return valid; + } + + public void update(byte b) { + if (finished) { + log("Updating finished sig!"); + return; + } + signature.update(b); + bytes.write(b); + } + + public void update(byte[] bytes, int off, int len) { + if (finished) { + log("Updating finished sig!"); + return; + } + signature.update(bytes, off, len); + this.bytes.write(bytes, off, len); + } + + @Override + public String toString() { + String OPS = "c40d03000a01fbfcc82a015e733001"; + String LIT_H = "cb28620000000000"; + String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; + String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; + String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; + String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; + String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; + String out = ""; + + String hex = Hex.toHexString(bytes.toByteArray()); + while (hex.contains(OPS)) { + hex = hex.replace(OPS, "[OPS]"); + } + while (hex.contains(LIT_H)) { + hex = hex.replace(LIT_H, "[LIT]"); + } + while (hex.contains(LIT)) { + hex = hex.replace(LIT, ""); + } + while (hex.contains(SIG1)) { + hex = hex.replace(SIG1, "[SIG1]"); + } + while (hex.contains(SIG1f)) { + hex = hex.replace(SIG1f, "[SIG1f]"); + } + while (hex.contains(SIG2)) { + hex = hex.replace(SIG2, "[SIG2]"); + } + while (hex.contains(SIG2f)) { + hex = hex.replace(SIG2f, "[SIG2f]"); + } + + return out + hex; } } @@ -796,27 +869,35 @@ public class OpenPgpMessageInputStream extends InputStream { String LIT_H = "cb28620000000000"; String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; + String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String out = signature.getKeyID() + " last=" + signature.isContaining() + "\n"; + String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; + String out = "last=" + signature.isContaining() + "\n"; - String hex = Hex.toHexString(bytes.toByteArray()); - while (hex.contains(OPS)) { - hex = hex.replace(OPS, "[OPS]"); - } - while (hex.contains(LIT_H)) { - hex = hex.replace(LIT_H, "[LIT]"); - } - while (hex.contains(LIT)) { - hex = hex.replace(LIT, ""); - } - while (hex.contains(SIG1)) { - hex = hex.replace(SIG1, "[SIG1]"); - } - while (hex.contains(SIG2)) { - hex = hex.replace(SIG2, "[SIG2]"); - } + String hex = Hex.toHexString(bytes.toByteArray()); + while (hex.contains(OPS)) { + hex = hex.replace(OPS, "[OPS]"); + } + while (hex.contains(LIT_H)) { + hex = hex.replace(LIT_H, "[LIT]"); + } + while (hex.contains(LIT)) { + hex = hex.replace(LIT, ""); + } + while (hex.contains(SIG1)) { + hex = hex.replace(SIG1, "[SIG1]"); + } + while (hex.contains(SIG1f)) { + hex = hex.replace(SIG1f, "[SIG1f]"); + } + while (hex.contains(SIG2)) { + hex = hex.replace(SIG2, "[SIG2]"); + } + while (hex.contains(SIG2f)) { + hex = hex.replace(SIG2f, "[SIG2f]"); + } - return out + hex; + return out + hex; } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java deleted file mode 100644 index cce21051..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.pgpainless.decryption_verification; - -import org.bouncycastle.bcpg.BCPGInputStream; -import org.pgpainless.util.ArmorUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public class TeeBCPGInputStream extends BCPGInputStream { - - private final OutputStream out; - - public TeeBCPGInputStream(InputStream in, OutputStream outputStream) { - super(in); - this.out = outputStream; - } - - @Override - public int read() throws IOException { - int r = super.read(); - if (r != -1) { - out.write(r); - } - return r; - } - - @Override - public int read(byte[] buf, int off, int len) throws IOException { - int r = super.read(buf, off, len); - if (r > 0) { - out.write(buf, off, r); - } - return r; - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index f780b2eb..46c521ef 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -143,11 +142,11 @@ public class OpenPgpMessageInputStreamTest { "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + - "hF4Dyqa/GWUy6WsSAQdAQ62BwmUt8Iby0+jvrLhMgST79KR/as+dyl0nf1uki2sw\n" + - "Thg1Ojtf0hOyJgcpQ4nP2Q0wYFR0F1sCydaIlTGreYZHlGtybP7/Ml6KNZILTRWP\n" + - "0kYBkGBgK7oQWRIVyoF2POvEP6EX1X8nvQk7O3NysVdRVbnia7gE3AzRYuha4kxs\n" + - "pI6xJkntLMS3K6him1Y9FHINIASFSB+C\n" + - "=5p00\n" + + "hF4Dyqa/GWUy6WsSAQdAuGt49sQwdAHH3jPx11V3wSh7Amur3TbnONiQYJmMo3Qw\n" + + "87yBnZCsaB7evxLBgi6PpF3tiytHM60xlrPeKKPpJhu60vNafRM2OOwqk7AdcZw4\n" + + "0kYBEhiioO2btSuafNrQEjYzAgC7K6l7aPCcQObNp4ofryXu1P5vN+vUZp357hyS\n" + + "6zZqP+0wJQ9yJZMvFTtFeSaSi0oMP2sb\n" + + "=LvRL\n" + "-----END PGP MESSAGE-----"; public static final String OPS_LIT_SIG = "" + @@ -170,8 +169,8 @@ public class OpenPgpMessageInputStreamTest { // genKey(); // genSIG_LIT(); // genSENC_LIT(); - // genPENC_COMP_LIT(); - genOPS_LIT_SIG(); + genPENC_COMP_LIT(); + // genOPS_LIT_SIG(); } public static void genLIT() throws IOException { @@ -433,91 +432,144 @@ public class OpenPgpMessageInputStreamTest { assertNull(metadata.getCompressionAlgorithm()); } + String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "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\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @ParameterizedTest(name = "Process PENC(OPS OPS LIT SIG SIG) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessPENC_OPS_OPS_LIT_SIG_SIG(Processor processor) throws IOException, PGPException { + String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "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\n" + + "=mdYs\n" + + "-----END PGP MESSAGE-----\n"; + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + Tuple result = processor.process(MSG, ConsumerOptions.get() + .addVerificationCert(certificate) + .addDecryptionKey(secretKeys)); + String plain = result.getA(); + assertEquals("encrypt ∘ sign ∘ sign", plain); + MessageMetadata metadata = result.getB(); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); + } + @ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}") @MethodSource("provideMessageProcessors") public void testProcessOPS_OPS_OPS_LIT_SIG_SIG_SIG(Processor processor) throws IOException, PGPException { - String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Comment: Bob's OpenPGP Transferable Secret Key\n" + - "\n" + - "lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + - "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + - "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + - "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + - "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + - "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + - "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + - "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + - "vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM\n" + - "cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK\n" + - "3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z\n" + - "Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs\n" + - "hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ\n" + - "bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4\n" + - "i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI\n" + - "1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP\n" + - "fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6\n" + - "fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E\n" + - "LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx\n" + - "+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL\n" + - "hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN\n" + - "WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/\n" + - "MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC\n" + - "mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC\n" + - "YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E\n" + - "he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8\n" + - "zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P\n" + - "NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT\n" + - "t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w\n" + - "ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\n" + - "F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U\n" + - "2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX\n" + - "yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe\n" + - "doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3\n" + - "BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl\n" + - "sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN\n" + - "4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+\n" + - "L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG\n" + - "ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad\n" + - "BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD\n" + - "bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar\n" + - "29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2\n" + - "WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB\n" + - "leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te\n" + - "g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj\n" + - "Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn\n" + - "JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx\n" + - "IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp\n" + - "SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h\n" + - "OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np\n" + - "Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c\n" + - "+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0\n" + - "tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o\n" + - "BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny\n" + - "zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK\n" + - "clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl\n" + - "zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr\n" + - "gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ\n" + - "aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5\n" + - "fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/\n" + - "ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5\n" + - "HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf\n" + - "SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd\n" + - "5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ\n" + - "E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM\n" + - "GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY\n" + - "vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ\n" + - "26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP\n" + - "eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX\n" + - "c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief\n" + - "rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0\n" + - "JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg\n" + - "71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH\n" + - "s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd\n" + - "NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91\n" + - "6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7\n" + - "xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=\n" + - "=miES\n" + - "-----END PGP PRIVATE KEY BLOCK-----"; String MSG = "-----BEGIN PGP MESSAGE-----\n" + "\n" + "wcDMA3wvqk35PDeyAQwA0yaEgydkAMEfl7rDTYVGanLKiFiWIs34mkF+LB8qR5eY\n" + @@ -565,7 +617,7 @@ public class OpenPgpMessageInputStreamTest { "x12WVuyITVU3fCfHp6/0A6wPtJezCvoodqPlw/3fd5eSVYzb5C3v564uhz4=\n" + "=JP9T\n" + "-----END PGP MESSAGE-----"; - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); Tuple result = processor.process(MSG, ConsumerOptions.get() diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java deleted file mode 100644 index d31c6009..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TeeBCPGInputStreamTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.pgpainless.decryption_verification; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.bcpg.BCPGInputStream; -import org.bouncycastle.bcpg.Packet; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPException; -import org.junit.jupiter.api.Test; -import org.pgpainless.algorithm.OpenPgpPacket; -import org.pgpainless.util.ArmoredInputStreamFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -public class TeeBCPGInputStreamTest { - - private static final Logger LOGGER = LoggerFactory.getLogger(TeeBCPGInputStreamTest.class); - private static final String INBAND_SIGNED = "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "owGbwMvMyCUWdXSHvVTUtXbG0yJJDCDgkZqTk6+jEJ5flJOiyNVRysIoxsXAxsqU\n" + - "GDiVjUGRUwCmQUyRRWnOn9Z/PIseF3Yz6cCEL05nZDj1OClo75WVTjNmJPemW6qV\n" + - "6ki//1K1++2s0qTP+0N11O4z/BVLDDdxnmQryS+5VXjBX7/0Hxnm/eqeX6Zum35r\n" + - "M8e7ufwA\n" + - "=RDiy\n" + - "-----END PGP MESSAGE-----"; - - @Test - public void test() throws IOException, PGPException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ArmoredOutputStream armorOut = new ArmoredOutputStream(out); - - ByteArrayInputStream bytesIn = new ByteArrayInputStream(INBAND_SIGNED.getBytes(StandardCharsets.UTF_8)); - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - BCPGInputStream bcpgIn = new BCPGInputStream(armorIn); - TeeBCPGInputStream teeIn = new TeeBCPGInputStream(bcpgIn, armorOut); - - ByteArrayOutputStream nestedOut = new ByteArrayOutputStream(); - ArmoredOutputStream nestedArmorOut = new ArmoredOutputStream(nestedOut); - - PGPCompressedData compressedData = new PGPCompressedData(teeIn); - InputStream nestedStream = compressedData.getDataStream(); - BCPGInputStream nestedBcpgIn = new BCPGInputStream(nestedStream); - TeeBCPGInputStream nestedTeeIn = new TeeBCPGInputStream(nestedBcpgIn, nestedArmorOut); - - int tag; - while ((tag = nestedTeeIn.nextPacketTag()) != -1) { - LOGGER.debug(OpenPgpPacket.requireFromTag(tag).toString()); - Packet packet = nestedTeeIn.readPacket(); - } - - nestedArmorOut.close(); - LOGGER.debug(nestedOut.toString()); - } -} From bdc968dd430712c80ccbe8aacdbb4411a8a8b9e2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 8 Oct 2022 13:57:53 +0200 Subject: [PATCH 0479/1204] Create TeeBCPGInputStream to move teeing logic out of OpenPgpMessageInputStream --- .../DelayedTeeInputStreamInputStream.java | 38 ----- .../OpenPgpMessageInputStream.java | 107 +++++--------- .../TeeBCPGInputStream.java | 131 ++++++++++++++++++ .../automaton/InputAlphabet.java | 4 +- .../automaton/PDA.java | 18 +-- .../OpenPgpMessageInputStreamTest.java | 3 +- .../automaton/PDATest.java | 10 +- 7 files changed, 182 insertions(+), 129 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java deleted file mode 100644 index 5cbabf89..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DelayedTeeInputStreamInputStream.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public class DelayedTeeInputStreamInputStream extends InputStream { - - private int last = -1; - private final InputStream inputStream; - private final OutputStream outputStream; - - public DelayedTeeInputStreamInputStream(InputStream inputStream, OutputStream outputStream) { - this.inputStream = inputStream; - this.outputStream = outputStream; - } - - @Override - public int read() throws IOException { - if (last != -1) { - outputStream.write(last); - } - last = inputStream.read(); - return last; - } - - /** - * Squeeze the last byte out and update the output stream. - * - * @throws IOException in case of an IO error - */ - public void squeeze() throws IOException { - if (last != -1) { - outputStream.write(last); - } - last = -1; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 8d370965..75359de1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -11,7 +11,6 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.NoSuchElementException; import java.util.Stack; import org.bouncycastle.bcpg.BCPGInputStream; @@ -57,6 +56,8 @@ import org.pgpainless.util.Tuple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; + public class OpenPgpMessageInputStream extends InputStream { private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); @@ -66,16 +67,15 @@ public class OpenPgpMessageInputStream extends InputStream { protected final OpenPgpMetadata.Builder resultBuilder; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message protected final PDA automaton = new PDA(); - protected final DelayedTeeInputStreamInputStream delayedTee; - // InputStream of OpenPGP packets of the current layer - protected final BCPGInputStream packetInputStream; + // InputStream of OpenPGP packets + protected TeeBCPGInputStream packetInputStream; // InputStream of a nested data packet protected InputStream nestedInputStream; private boolean closed = false; private final Signatures signatures; - private MessageMetadata.Layer metadata; + private final MessageMetadata.Layer metadata; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { @@ -95,9 +95,7 @@ public class OpenPgpMessageInputStream extends InputStream { this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } - delayedTee = new DelayedTeeInputStreamInputStream(inputStream, signatures); - BCPGInputStream bcpg = BCPGInputStream.wrap(delayedTee); - this.packetInputStream = bcpg; + packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); // *omnomnom* consumePackets(); @@ -122,41 +120,36 @@ public class OpenPgpMessageInputStream extends InputStream { private void consumePackets() throws IOException, PGPException { OpenPgpPacket nextPacket; - loop: while ((nextPacket = nextPacketTag()) != null) { + loop: while ((nextPacket = packetInputStream.nextPacketTag()) != null) { signatures.nextPacket(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: automaton.next(InputAlphabet.LiteralData); - delayedTee.squeeze(); processLiteralData(); break loop; // Compressed Data - the content contains another OpenPGP message case COMP: automaton.next(InputAlphabet.CompressedData); - signatures.commitNested(); processCompressedData(); - delayedTee.squeeze(); break loop; // One Pass Signature case OPS: - automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.OnePassSignature); PGPOnePassSignature onePassSignature = readOnePassSignature(); - delayedTee.squeeze(); signatures.addOnePassSignature(onePassSignature); break; // Signature - either prepended to the message, or corresponding to a One Pass Signature case SIG: + // true if Signature corresponds to OnePassSignature boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; - automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.Signature); - PGPSignature signature = readSignature(); - delayedTee.squeeze(); - processSignature(signature, isSigForOPS); + processSignature(isSigForOPS); break; @@ -166,7 +159,6 @@ public class OpenPgpMessageInputStream extends InputStream { case SED: case SEIPD: automaton.next(InputAlphabet.EncryptedData); - delayedTee.squeeze(); if (processEncryptedData()) { break loop; } @@ -175,8 +167,7 @@ public class OpenPgpMessageInputStream extends InputStream { // Marker Packets need to be skipped and ignored case MARKER: - packetInputStream.readPacket(); // skip - delayedTee.squeeze(); + packetInputStream.readMarker(); break; // Key Packets are illegal in this context @@ -204,26 +195,10 @@ public class OpenPgpMessageInputStream extends InputStream { } } - private OpenPgpPacket nextPacketTag() throws IOException { - int tag = nextTag(); - if (tag == -1) { - log("EOF"); - return null; - } - OpenPgpPacket packet; - try { - packet = OpenPgpPacket.requireFromTag(tag); - } catch (NoSuchElementException e) { - log("Invalid tag: " + tag); - throw e; - } - log(packet.toString()); - return packet; - } - - private void processSignature(PGPSignature signature, boolean isSigForOPS) { + private void processSignature(boolean isSigForOPS) throws PGPException, IOException { + PGPSignature signature = readSignature(); if (isSigForOPS) { - signatures.popNested(); + signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs are dealt with signatures.addCorrespondingOnePassSignature(signature); } else { signatures.addPrependedSignature(signature); @@ -231,21 +206,22 @@ public class OpenPgpMessageInputStream extends InputStream { } private void processCompressedData() throws IOException, PGPException { - PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); + signatures.enterNesting(); + PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm())); nestedInputStream = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); } private void processLiteralData() throws IOException { - PGPLiteralData literalData = new PGPLiteralData(packetInputStream); + PGPLiteralData literalData = packetInputStream.readLiteralData(); this.metadata.setChild(new MessageMetadata.LiteralData(literalData.getFileName(), literalData.getModificationTime(), StreamEncoding.requireFromCode(literalData.getFormat()))); nestedInputStream = literalData.getDataStream(); } private boolean processEncryptedData() throws IOException, PGPException { - PGPEncryptedDataList encDataList = new PGPEncryptedDataList(packetInputStream); + PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() if (!encDataList.get(0).isIntegrityProtected()) { @@ -345,19 +321,6 @@ public class OpenPgpMessageInputStream extends InputStream { return false; } - private int nextTag() throws IOException { - try { - return packetInputStream.nextPacketTag(); - } catch (IOException e) { - if ("Stream closed".equals(e.getMessage())) { - // ZipInflater Streams sometimes close under our feet -.- - // Therefore we catch resulting IOEs and return -1 instead. - return -1; - } - throw e; - } - } - private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { int algorithm = pkesk.getAlgorithm(); List> decryptionKeyCandidates = new ArrayList<>(); @@ -387,12 +350,12 @@ public class OpenPgpMessageInputStream extends InputStream { private PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { - return new PGPOnePassSignature(packetInputStream); + return packetInputStream.readOnePassSignature(); } private PGPSignature readSignature() throws PGPException, IOException { - return new PGPSignature(packetInputStream); + return packetInputStream.readSignature(); } @Override @@ -428,7 +391,7 @@ public class OpenPgpMessageInputStream extends InputStream { } @Override - public int read(byte[] b, int off, int len) + public int read(@Nonnull byte[] b, int off, int len) throws IOException { if (nestedInputStream == null) { @@ -482,8 +445,7 @@ public class OpenPgpMessageInputStream extends InputStream { private void collectMetadata() { if (nestedInputStream instanceof OpenPgpMessageInputStream) { OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) nestedInputStream; - MessageMetadata.Layer childLayer = child.metadata; - this.metadata.setChild((MessageMetadata.Nested) childLayer); + this.metadata.setChild((MessageMetadata.Nested) child.metadata); } } @@ -496,9 +458,9 @@ public class OpenPgpMessageInputStream extends InputStream { private static class SortedESKs { - private List skesks = new ArrayList<>(); - private List pkesks = new ArrayList<>(); - private List anonPkesks = new ArrayList<>(); + private final List skesks = new ArrayList<>(); + private final List pkesks = new ArrayList<>(); + private final List anonPkesks = new ArrayList<>(); SortedESKs(PGPEncryptedDataList esks) { for (PGPEncryptedData esk : esks) { @@ -526,11 +488,10 @@ public class OpenPgpMessageInputStream extends InputStream { } } - // TODO: In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" - // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. - // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. - // For this we might want to provide two update entries into the Signatures class, one for OpenPGP packets and one - // for literal data. UUUUUGLY!!!! + // In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" + // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. + // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. + // Furthermore, For 'OPS COMP(LIT("Foo")) SIG', the signature is updated with "Foo". CHAOS!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; final List detachedSignatures; @@ -578,7 +539,7 @@ public class OpenPgpMessageInputStream extends InputStream { literalOPS.add(ops); if (signature.isContaining()) { - commitNested(); + enterNesting(); } } @@ -599,12 +560,12 @@ public class OpenPgpMessageInputStream extends InputStream { } } - void commitNested() { + void enterNesting() { opsUpdateStack.push(literalOPS); literalOPS = new ArrayList<>(); } - void popNested() { + void leaveNesting() { if (opsUpdateStack.isEmpty()) { return; } @@ -722,7 +683,7 @@ public class OpenPgpMessageInputStream extends InputStream { } @Override - public void write(byte[] b, int off, int len) { + public void write(@Nonnull byte[] b, int off, int len) { updatePacket(b, off, len); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java new file mode 100644 index 00000000..f80793f0 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.MarkerPacket; +import org.bouncycastle.bcpg.Packet; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.algorithm.OpenPgpPacket; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.NoSuchElementException; + +/** + * Since we need to update signatures with data from the underlying stream, this class is used to tee out the data. + * Unfortunately we cannot simply override {@link BCPGInputStream#read()} to tee the data out though, since + * {@link BCPGInputStream#readPacket()} inconsistently calls a mix of {@link BCPGInputStream#read()} and + * {@link InputStream#read()} of the underlying stream. This would cause the second length byte to get swallowed up. + * + * Therefore, this class delegates the teeing to an {@link DelayedTeeInputStreamInputStream} which wraps the underlying + * stream. Since calling {@link BCPGInputStream#nextPacketTag()} reads up to and including the next packets tag, + * we need to delay teeing out that byte to signature verifiers. + * Hence, the reading methods of the {@link TeeBCPGInputStream} handle pushing this byte to the output stream using + * {@link DelayedTeeInputStreamInputStream#squeeze()}. + */ +public class TeeBCPGInputStream { + + protected final DelayedTeeInputStreamInputStream delayedTee; + // InputStream of OpenPGP packets of the current layer + protected final BCPGInputStream packetInputStream; + + public TeeBCPGInputStream(BCPGInputStream inputStream, OutputStream outputStream) { + this.delayedTee = new DelayedTeeInputStreamInputStream(inputStream, outputStream); + this.packetInputStream = BCPGInputStream.wrap(delayedTee); + } + + public OpenPgpPacket nextPacketTag() throws IOException { + int tag = packetInputStream.nextPacketTag(); + if (tag == -1) { + return null; + } + + OpenPgpPacket packet; + try { + packet = OpenPgpPacket.requireFromTag(tag); + } catch (NoSuchElementException e) { + throw e; + } + return packet; + } + + public Packet readPacket() throws IOException { + return packetInputStream.readPacket(); + } + + public PGPCompressedData readCompressedData() throws IOException { + delayedTee.squeeze(); + PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); + return compressedData; + } + + public PGPLiteralData readLiteralData() throws IOException { + delayedTee.squeeze(); + return new PGPLiteralData(packetInputStream); + } + + public PGPEncryptedDataList readEncryptedDataList() throws IOException { + delayedTee.squeeze(); + return new PGPEncryptedDataList(packetInputStream); + } + + public PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { + PGPOnePassSignature onePassSignature = new PGPOnePassSignature(packetInputStream); + delayedTee.squeeze(); + return onePassSignature; + } + + public PGPSignature readSignature() throws PGPException, IOException { + PGPSignature signature = new PGPSignature(packetInputStream); + delayedTee.squeeze(); + return signature; + } + + public MarkerPacket readMarker() throws IOException { + MarkerPacket markerPacket = (MarkerPacket) readPacket(); + delayedTee.squeeze(); + return markerPacket; + } + + public static class DelayedTeeInputStreamInputStream extends InputStream { + + private int last = -1; + private final InputStream inputStream; + private final OutputStream outputStream; + + public DelayedTeeInputStreamInputStream(InputStream inputStream, OutputStream outputStream) { + this.inputStream = inputStream; + this.outputStream = outputStream; + } + + @Override + public int read() throws IOException { + if (last != -1) { + outputStream.write(last); + } + last = inputStream.read(); + return last; + } + + /** + * Squeeze the last byte out and update the output stream. + * + * @throws IOException in case of an IO error + */ + public void squeeze() throws IOException { + if (last != -1) { + outputStream.write(last); + } + last = -1; + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java index 8e795f5b..ad2a8c55 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java @@ -18,11 +18,11 @@ public enum InputAlphabet { /** * A {@link PGPSignatureList} object. */ - Signatures, + Signature, /** * A {@link PGPOnePassSignatureList} object. */ - OnePassSignatures, + OnePassSignature, /** * A {@link PGPCompressedData} packet. * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index 1d4b1189..156dc2ed 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -35,11 +35,11 @@ public class PDA { case LiteralData: return LiteralMessage; - case Signatures: + case Signature: automaton.pushStack(msg); return OpenPgpMessage; - case OnePassSignatures: + case OnePassSignature: automaton.pushStack(ops); automaton.pushStack(msg); return OpenPgpMessage; @@ -63,7 +63,7 @@ public class PDA { StackAlphabet stackItem = automaton.popStack(); switch (input) { - case Signatures: + case Signature: if (stackItem == ops) { return CorrespondingSignature; } else { @@ -78,7 +78,7 @@ public class PDA { } case LiteralData: - case OnePassSignatures: + case OnePassSignature: case CompressedData: case EncryptedData: default: @@ -92,7 +92,7 @@ public class PDA { State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { - case Signatures: + case Signature: if (stackItem == ops) { return CorrespondingSignature; } else { @@ -107,7 +107,7 @@ public class PDA { } case LiteralData: - case OnePassSignatures: + case OnePassSignature: case CompressedData: case EncryptedData: default: @@ -121,7 +121,7 @@ public class PDA { State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { StackAlphabet stackItem = automaton.popStack(); switch (input) { - case Signatures: + case Signature: if (stackItem == ops) { return CorrespondingSignature; } else { @@ -136,7 +136,7 @@ public class PDA { } case LiteralData: - case OnePassSignatures: + case OnePassSignature: case CompressedData: case EncryptedData: default: @@ -156,7 +156,7 @@ public class PDA { // premature end of stream throw new MalformedOpenPgpMessageException(this, input, stackItem); } - } else if (input == InputAlphabet.Signatures) { + } else if (input == InputAlphabet.Signature) { if (stackItem == ops) { return CorrespondingSignature; } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index 46c521ef..1955eebe 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -49,7 +49,6 @@ import org.pgpainless.util.Tuple; public class OpenPgpMessageInputStreamTest { - public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + @@ -241,7 +240,7 @@ public class OpenPgpMessageInputStreamTest { System.out); } - public static void genSIG_LIT() throws PGPException, IOException { + public static void genSIG_COMP_LIT() throws PGPException, IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); EncryptionStream signer = PGPainless.encryptAndOrSign() diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java index 6e8a38d6..6dbb2cd6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java @@ -30,9 +30,9 @@ public class PDATest { @Test public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { PDA automaton = new PDA(); - automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.OnePassSignature); automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.Signature); automaton.next(InputAlphabet.EndOfSequence); assertTrue(automaton.isValid()); @@ -47,7 +47,7 @@ public class PDATest { @Test public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { PDA automaton = new PDA(); - automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.Signature); automaton.next(InputAlphabet.LiteralData); automaton.next(InputAlphabet.EndOfSequence); @@ -63,10 +63,10 @@ public class PDATest { @Test public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { PDA automaton = new PDA(); - automaton.next(InputAlphabet.OnePassSignatures); + automaton.next(InputAlphabet.OnePassSignature); automaton.next(InputAlphabet.CompressedData); // Here would be a nested PDA for the LiteralData packet - automaton.next(InputAlphabet.Signatures); + automaton.next(InputAlphabet.Signature); automaton.next(InputAlphabet.EndOfSequence); assertTrue(automaton.isValid()); From 43c369f1f9314c16190796e0fbf8277512248021 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 16 Oct 2022 15:47:47 +0200 Subject: [PATCH 0480/1204] It was the buffering. --- .../DecryptionBuilderInterface.java | 2 +- .../DecryptionStream.java | 60 +------ .../DecryptionStreamFactory.java | 33 +++- .../DecryptionStreamImpl.java | 65 +++++++ .../MessageMetadata.java | 22 +++ .../OpenPgpMessageInputStream.java | 166 ++++++++++++++---- ...vestigateMultiSEIPMessageHandlingTest.java | 17 +- ...ntDecryptionUsingNonEncryptionKeyTest.java | 4 +- 8 files changed, 256 insertions(+), 113 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java index 07db42f0..b35911de 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java @@ -13,7 +13,7 @@ import org.bouncycastle.openpgp.PGPException; public interface DecryptionBuilderInterface { /** - * Create a {@link DecryptionStream} on an {@link InputStream} which contains the encrypted and/or signed data. + * Create a {@link DecryptionStreamImpl} on an {@link InputStream} which contains the encrypted and/or signed data. * * @param inputStream encrypted and/or signed data. * @return api handle diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index e3f1e720..c531f487 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -1,65 +1,9 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - package org.pgpainless.decryption_verification; -import java.io.IOException; -import java.io.InputStream; import javax.annotation.Nonnull; -import org.bouncycastle.util.io.Streams; - -/** - * Decryption Stream that handles updating and verification of detached signatures, - * as well as verification of integrity-protected input streams once the stream gets closed. - */ -public class DecryptionStream extends CloseForResultInputStream { - - private final InputStream inputStream; - private final IntegrityProtectedInputStream integrityProtectedInputStream; - private final InputStream armorStream; - - /** - * Create an input stream that handles decryption and - if necessary - integrity protection verification. - * - * @param wrapped underlying input stream - * @param resultBuilder builder for decryption metadata like algorithms, recipients etc. - * @param integrityProtectedInputStream in case of data encrypted using SEIP packet close this stream to check integrity - * @param armorStream armor stream to verify CRC checksums - */ - DecryptionStream(@Nonnull InputStream wrapped, - @Nonnull OpenPgpMetadata.Builder resultBuilder, - IntegrityProtectedInputStream integrityProtectedInputStream, - InputStream armorStream) { +public abstract class DecryptionStream extends CloseForResultInputStream { + public DecryptionStream(@Nonnull OpenPgpMetadata.Builder resultBuilder) { super(resultBuilder); - this.inputStream = wrapped; - this.integrityProtectedInputStream = integrityProtectedInputStream; - this.armorStream = armorStream; } - - @Override - public void close() throws IOException { - if (armorStream != null) { - Streams.drain(armorStream); - } - inputStream.close(); - if (integrityProtectedInputStream != null) { - integrityProtectedInputStream.close(); - } - super.close(); - } - - @Override - public int read() throws IOException { - int r = inputStream.read(); - return r; - } - - @Override - public int read(@Nonnull byte[] bytes, int offset, int length) throws IOException { - int read = inputStream.read(bytes, offset, length); - return read; - } - } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 07be7c10..680f33e4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -40,6 +40,7 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; +import org.graalvm.compiler.lir.amd64.AMD64BinaryConsumer; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; @@ -88,8 +89,28 @@ public final class DecryptionStreamFactory { public static DecryptionStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) + @Nonnull ConsumerOptions options) throws PGPException, IOException { + OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(inputStream); + openPgpInputStream.reset(); + if (openPgpInputStream.isBinaryOpenPgp()) { + return new OpenPgpMessageInputStream(openPgpInputStream, options); + } else if (openPgpInputStream.isAsciiArmored()) { + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpInputStream); + if (armorIn.isClearText()) { + return createOld(openPgpInputStream, options); + } else { + return new OpenPgpMessageInputStream(armorIn, options); + } + } else if (openPgpInputStream.isNonOpenPgp()) { + return createOld(openPgpInputStream, options); + } else { + throw new IOException("What?"); + } + } + + public static DecryptionStream createOld(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options) throws IOException, PGPException { DecryptionStreamFactory factory = new DecryptionStreamFactory(options); OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); return factory.parseOpenPGPDataAndCreateDecryptionStream(openPgpIn); @@ -136,7 +157,7 @@ public final class DecryptionStreamFactory { if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) { outerDecodingStream = openPgpIn; pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null); - return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); + return new DecryptionStreamImpl(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); } // Data appears to be OpenPGP message, @@ -147,7 +168,7 @@ public final class DecryptionStreamFactory { objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); // Parse OpenPGP message pgpInStream = processPGPPackets(objectFactory, 1); - return new DecryptionStream(pgpInStream, + return new DecryptionStreamImpl(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); } @@ -161,7 +182,7 @@ public final class DecryptionStreamFactory { objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); // Parse OpenPGP message pgpInStream = processPGPPackets(objectFactory, 1); - return new DecryptionStream(pgpInStream, + return new DecryptionStreamImpl(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, outerDecodingStream); } @@ -170,7 +191,7 @@ public final class DecryptionStreamFactory { throw new PGPException("Not sure how to handle the input stream."); } - private DecryptionStream parseCleartextSignedMessage(ArmoredInputStream armorIn) + private DecryptionStreamImpl parseCleartextSignedMessage(ArmoredInputStream armorIn) throws IOException, PGPException { resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) .setFileEncoding(StreamEncoding.TEXT); @@ -185,7 +206,7 @@ public final class DecryptionStreamFactory { initializeDetachedSignatures(options.getDetachedSignatures()); InputStream verifyIn = wrapInVerifySignatureStream(multiPassStrategy.getMessageInputStream(), null); - return new DecryptionStream(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream, + return new DecryptionStreamImpl(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream, null); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java new file mode 100644 index 00000000..27ace697 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2018 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import java.io.IOException; +import java.io.InputStream; +import javax.annotation.Nonnull; + +import org.bouncycastle.util.io.Streams; + +/** + * Decryption Stream that handles updating and verification of detached signatures, + * as well as verification of integrity-protected input streams once the stream gets closed. + */ +public class DecryptionStreamImpl extends DecryptionStream { + + private final InputStream inputStream; + private final IntegrityProtectedInputStream integrityProtectedInputStream; + private final InputStream armorStream; + + /** + * Create an input stream that handles decryption and - if necessary - integrity protection verification. + * + * @param wrapped underlying input stream + * @param resultBuilder builder for decryption metadata like algorithms, recipients etc. + * @param integrityProtectedInputStream in case of data encrypted using SEIP packet close this stream to check integrity + * @param armorStream armor stream to verify CRC checksums + */ + DecryptionStreamImpl(@Nonnull InputStream wrapped, + @Nonnull OpenPgpMetadata.Builder resultBuilder, + IntegrityProtectedInputStream integrityProtectedInputStream, + InputStream armorStream) { + super(resultBuilder); + this.inputStream = wrapped; + this.integrityProtectedInputStream = integrityProtectedInputStream; + this.armorStream = armorStream; + } + + @Override + public void close() throws IOException { + if (armorStream != null) { + Streams.drain(armorStream); + } + inputStream.close(); + if (integrityProtectedInputStream != null) { + integrityProtectedInputStream.close(); + } + super.close(); + } + + @Override + public int read() throws IOException { + int r = inputStream.read(); + return r; + } + + @Override + public int read(@Nonnull byte[] bytes, int offset, int length) throws IOException { + int read = inputStream.read(bytes, offset, length); + return read; + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index cbf251a8..87f6d7f6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -69,6 +69,28 @@ public class MessageMetadata { }; } + public @Nullable SessionKey getSessionKey() { + Iterator sessionKeys = getSessionKeys(); + if (sessionKeys.hasNext()) { + return sessionKeys.next(); + } + return null; + } + + public @Nonnull Iterator getSessionKeys() { + return new LayerIterator(message) { + @Override + boolean matches(Nested layer) { + return layer instanceof EncryptedData; + } + + @Override + SessionKey getProperty(Layer last) { + return ((EncryptedData) last).getSessionKey(); + } + }; + } + public String getFilename() { return findLiteralData().getFileName(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 75359de1..7e34d329 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -4,6 +4,7 @@ package org.pgpainless.decryption_verification; +import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -12,6 +13,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Stack; +import javax.annotation.Nonnull; import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.openpgp.PGPCompressedData; @@ -28,7 +30,6 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; @@ -46,25 +47,27 @@ import org.pgpainless.decryption_verification.automaton.StackAlphabet; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; +import org.pgpainless.exception.SignatureValidationException; +import org.pgpainless.exception.UnacceptableAlgorithmException; import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.SessionKey; import org.pgpainless.util.Tuple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; - -public class OpenPgpMessageInputStream extends InputStream { +public class OpenPgpMessageInputStream extends DecryptionStream { private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); // Options to consume the data protected final ConsumerOptions options; - protected final OpenPgpMetadata.Builder resultBuilder; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message protected final PDA automaton = new PDA(); // InputStream of OpenPGP packets @@ -76,6 +79,7 @@ public class OpenPgpMessageInputStream extends InputStream { private final Signatures signatures; private final MessageMetadata.Layer metadata; + private final Policy policy; public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) throws IOException, PGPException { @@ -84,10 +88,11 @@ public class OpenPgpMessageInputStream extends InputStream { OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, MessageMetadata.Layer metadata) throws PGPException, IOException { + super(OpenPgpMetadata.getBuilder()); + this.policy = PGPainless.getPolicy(); this.options = options; this.metadata = metadata; - this.resultBuilder = OpenPgpMetadata.getBuilder(); this.signatures = new Signatures(options); // Add detached signatures only on the outermost OpenPgpMessageInputStream @@ -210,7 +215,8 @@ public class OpenPgpMessageInputStream extends InputStream { PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm())); - nestedInputStream = new OpenPgpMessageInputStream(compressedData.getDataStream(), options, compressionLayer); + InputStream decompressed = compressedData.getDataStream(); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer); } private void processLiteralData() throws IOException { @@ -232,19 +238,25 @@ public class OpenPgpMessageInputStream extends InputStream { // Try session key if (options.getSessionKey() != null) { + SessionKey sessionKey = options.getSessionKey(); + if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(sessionKey.getAlgorithm())) { + throw new UnacceptableAlgorithmException("Symmetric algorithm " + sessionKey.getAlgorithm() + " is not acceptable."); + } SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getSessionKeyDataDecryptorFactory(options.getSessionKey()); + .getSessionKeyDataDecryptorFactory(sessionKey); // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) PGPEncryptedData esk = esks.all().get(0); try { - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(options.getSessionKey().getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; - nestedInputStream = new OpenPgpMessageInputStream(skesk.getDataStream(decryptorFactory), options, encryptedData); + InputStream decrypted = skesk.getDataStream(decryptorFactory); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - nestedInputStream = new OpenPgpMessageInputStream(pkesk.getDataStream(decryptorFactory), options, encryptedData); + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -263,7 +275,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = skesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(skesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } catch (PGPException e) { // password mismatch? Try next password @@ -286,14 +298,20 @@ public class OpenPgpMessageInputStream extends InputStream { PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); try { + SymmetricKeyAlgorithm symAlg = SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)); + if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(symAlg)) { + throw new UnacceptableAlgorithmException("Symmetric-key algorithm " + symAlg + " is not acceptable."); + } InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(PGPUtil.getDecoderStream(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } catch (PGPException e) { - // hm :/ + if (e instanceof UnacceptableAlgorithmException) { + throw e; + } } } @@ -309,7 +327,7 @@ public class OpenPgpMessageInputStream extends InputStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(decrypted, options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); return true; } catch (PGPException e) { // hm :/ @@ -321,6 +339,10 @@ public class OpenPgpMessageInputStream extends InputStream { return false; } + private static InputStream buffer(InputStream inputStream) { + return new BufferedInputStream(inputStream); + } + private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { int algorithm = pkesk.getAlgorithm(); List> decryptionKeyCandidates = new ArrayList<>(); @@ -420,6 +442,7 @@ public class OpenPgpMessageInputStream extends InputStream { @Override public void close() throws IOException { + super.close(); if (closed) { automaton.assertValid(); return; @@ -502,6 +525,8 @@ public class OpenPgpMessageInputStream extends InputStream { final List correspondingSignatures; boolean isLiteral = true; + final List verified = new ArrayList<>(); + private Signatures(ConsumerOptions options) { this.options = options; this.detachedSignatures = new ArrayList<>(); @@ -521,19 +546,19 @@ public class OpenPgpMessageInputStream extends InputStream { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.detachedSignatures.add(new SIG(signature)); + this.detachedSignatures.add(new SIG(signature, certificate, keyId)); } void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.prependedSignatures.add(new SIG(signature)); + this.prependedSignatures.add(new SIG(signature, certificate, keyId)); } void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); - OPS ops = new OPS(signature); + OPS ops = new OPS(signature, certificate, signature.getKeyID()); ops.init(certificate); onePassSignatures.add(ops); @@ -546,7 +571,7 @@ public class OpenPgpMessageInputStream extends InputStream { void addCorrespondingOnePassSignature(PGPSignature signature) { for (int i = onePassSignatures.size() - 1; i >= 0; i--) { OPS onePassSignature = onePassSignatures.get(i); - if (onePassSignature.signature.getKeyID() != signature.getKeyID()) { + if (onePassSignature.opSignature.getKeyID() != signature.getKeyID()) { continue; } if (onePassSignature.finished) { @@ -554,8 +579,8 @@ public class OpenPgpMessageInputStream extends InputStream { } boolean verified = onePassSignature.verify(signature); - log("One-Pass-Signature by " + Long.toHexString(onePassSignature.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - System.out.println(onePassSignature); + log("One-Pass-Signature by " + Long.toHexString(onePassSignature.opSignature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); + log(onePassSignature.toString()); break; } } @@ -666,14 +691,20 @@ public class OpenPgpMessageInputStream extends InputStream { public void finish() { for (SIG detached : detachedSignatures) { boolean verified = detached.verify(); + if (verified) { + this.verified.add(detached.signature); + } log("Detached Signature by " + Long.toHexString(detached.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - System.out.println(detached); + log(detached.toString()); } for (SIG prepended : prependedSignatures) { boolean verified = prepended.verify(); + if (verified) { + this.verified.add(prepended.signature); + } log("Prepended Signature by " + Long.toHexString(prepended.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - System.out.println(prepended); + log(prepended.toString()); } } @@ -701,11 +732,15 @@ public class OpenPgpMessageInputStream extends InputStream { static class SIG { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); PGPSignature signature; + PGPPublicKeyRing certificate; + long keyId; boolean finished; boolean valid; - public SIG(PGPSignature signature) { + public SIG(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { this.signature = signature; + this.certificate = certificate; + this.keyId = keyId; } public void init(PGPPublicKeyRing certificate) { @@ -713,6 +748,9 @@ public class OpenPgpMessageInputStream extends InputStream { } public boolean verify() { + if (finished) { + throw new IllegalStateException("Already finished."); + } finished = true; try { valid = this.signature.verify(); @@ -780,26 +818,32 @@ public class OpenPgpMessageInputStream extends InputStream { static class OPS { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - PGPOnePassSignature signature; + PGPOnePassSignature opSignature; + PGPSignature signature; + PGPPublicKeyRing certificate; + long keyId; boolean finished; boolean valid; - public OPS(PGPOnePassSignature signature) { - this.signature = signature; + public OPS(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { + this.opSignature = signature; + this.certificate = certificate; + this.keyId = keyId; } public void init(PGPPublicKeyRing certificate) { - initialize(signature, certificate); + initialize(opSignature, certificate); } public boolean verify(PGPSignature signature) { - if (this.signature.getKeyID() != signature.getKeyID()) { + if (this.opSignature.getKeyID() != signature.getKeyID()) { // nope return false; } + this.signature = signature; finished = true; try { - valid = this.signature.verify(signature); + valid = this.opSignature.verify(signature); } catch (PGPException e) { log("Cannot verify OPS " + signature.getKeyID()); } @@ -811,7 +855,7 @@ public class OpenPgpMessageInputStream extends InputStream { log("Updating finished sig!"); return; } - signature.update(b); + opSignature.update(b); bytes.write(b); } @@ -820,7 +864,7 @@ public class OpenPgpMessageInputStream extends InputStream { log("Updating finished sig!"); return; } - signature.update(bytes, off, len); + opSignature.update(bytes, off, len); this.bytes.write(bytes, off, len); } @@ -833,7 +877,7 @@ public class OpenPgpMessageInputStream extends InputStream { String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String out = "last=" + signature.isContaining() + "\n"; + String out = "last=" + opSignature.isContaining() + "\n"; String hex = Hex.toHexString(bytes.toByteArray()); while (hex.contains(OPS)) { @@ -863,10 +907,64 @@ public class OpenPgpMessageInputStream extends InputStream { } } + @Override + public OpenPgpMetadata getResult() { + MessageMetadata m = getMetadata(); + resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); + resultBuilder.setModificationDate(m.getModificationDate()); + resultBuilder.setFileName(m.getFilename()); + resultBuilder.setFileEncoding(m.getFormat()); + resultBuilder.setSessionKey(m.getSessionKey()); + + for (Signatures.OPS ops : signatures.onePassSignatures) { + if (!ops.finished) { + continue; + } + + SubkeyIdentifier identifier = new SubkeyIdentifier(ops.certificate, ops.keyId); + SignatureVerification verification = new SignatureVerification(ops.signature, identifier); + if (ops.valid) { + resultBuilder.addVerifiedInbandSignature(verification); + } else { + resultBuilder.addInvalidInbandSignature(verification, new SignatureValidationException("Incorrect signature.")); + } + } + + for (Signatures.SIG prep : signatures.prependedSignatures) { + if (!prep.finished) { + continue; + } + + SubkeyIdentifier identifier = new SubkeyIdentifier(prep.certificate, prep.keyId); + SignatureVerification verification = new SignatureVerification(prep.signature, identifier); + if (prep.valid) { + resultBuilder.addVerifiedInbandSignature(verification); + } else { + resultBuilder.addInvalidInbandSignature(verification, new SignatureValidationException("Incorrect signature.")); + } + } + + for (Signatures.SIG det : signatures.detachedSignatures) { + if (!det.finished) { + continue; + } + + SubkeyIdentifier identifier = new SubkeyIdentifier(det.certificate, det.keyId); + SignatureVerification verification = new SignatureVerification(det.signature, identifier); + if (det.valid) { + resultBuilder.addVerifiedDetachedSignature(verification); + } else { + resultBuilder.addInvalidDetachedSignature(verification, new SignatureValidationException("Incorrect signature.")); + } + } + + return resultBuilder.build(); + } + static void log(String message) { LOGGER.debug(message); // CHECKSTYLE:OFF - System.out.println(message); + // System.out.println(message); // CHECKSTYLE:ON } diff --git a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java index 3350203a..d31a0e0f 100644 --- a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java +++ b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java @@ -6,6 +6,7 @@ package investigations; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -32,9 +33,8 @@ import org.pgpainless.algorithm.SignatureType; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; @@ -177,7 +177,7 @@ public class InvestigateMultiSEIPMessageHandlingTest { } @Test - public void testDecryptAndVerifyDoesIgnoreAppendedSEIPData() throws IOException, PGPException { + public void testDecryptAndVerifyDetectsAppendedSEIPData() throws IOException, PGPException { PGPSecretKeyRing ring1 = PGPainless.readKeyRing().secretKeyRing(KEY1); PGPSecretKeyRing ring2 = PGPainless.readKeyRing().secretKeyRing(KEY2); @@ -191,15 +191,6 @@ public class InvestigateMultiSEIPMessageHandlingTest { .withOptions(options); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - decryptionStream.close(); - - assertArrayEquals(data1.getBytes(StandardCharsets.UTF_8), out.toByteArray()); - OpenPgpMetadata metadata = decryptionStream.getResult(); - assertEquals(1, metadata.getVerifiedSignatures().size(), - "The first SEIP packet is signed exactly only by the signing key of ring1."); - assertEquals( - new SubkeyIdentifier(ring1, new KeyRingInfo(ring1).getSigningSubkeys().get(0).getKeyID()), - metadata.getVerifiedSignatures().keySet().iterator().next()); + assertThrows(MalformedOpenPgpMessageException.class, () -> Streams.pipeAll(decryptionStream, out)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java index 851a1bef..7474301b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java @@ -8,6 +8,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -185,7 +187,7 @@ public class PreventDecryptionUsingNonEncryptionKeyTest { decryptionStream.close(); OpenPgpMetadata metadata = decryptionStream.getResult(); - assertEquals(metadata.getDecryptionKey(), new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID())); + assertEquals(new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID()), metadata.getDecryptionKey()); } @Test From a9f77ea10019ca3889198d4dc3f1253aa3e81073 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 16 Oct 2022 18:15:31 +0200 Subject: [PATCH 0481/1204] Cleaning up and collect signature verifications --- .../MessageMetadata.java | 85 +++- .../OpenPgpMessageInputStream.java | 375 ++++++++---------- .../OpenPgpMessageInputStreamTest.java | 19 + 3 files changed, 262 insertions(+), 217 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 87f6d7f6..bb2f5c76 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -91,6 +91,62 @@ public class MessageMetadata { }; } + public @Nonnull List getVerifiedSignatures() { + List verifications = new ArrayList<>(); + Iterator> verificationsByLayer = getVerifiedSignaturesByLayer(); + while (verificationsByLayer.hasNext()) { + verifications.addAll(verificationsByLayer.next()); + } + return verifications; + } + + public @Nonnull Iterator> getVerifiedSignaturesByLayer() { + return new LayerIterator>(message) { + @Override + boolean matches(Nested layer) { + return layer instanceof Layer; + } + + @Override + boolean matches(Layer layer) { + return true; + } + + @Override + List getProperty(Layer last) { + return new ArrayList<>(last.getVerifiedSignatures()); + } + }; + } + + public @Nonnull List getRejectedSignatures() { + List rejected = new ArrayList<>(); + Iterator> rejectedByLayer = getRejectedSignaturesByLayer(); + while (rejectedByLayer.hasNext()) { + rejected.addAll(rejectedByLayer.next()); + } + return rejected; + } + + public @Nonnull Iterator> getRejectedSignaturesByLayer() { + return new LayerIterator>(message) { + @Override + boolean matches(Nested layer) { + return layer instanceof Layer; + } + + @Override + boolean matches(Layer layer) { + return true; + } + + @Override + List getProperty(Layer last) { + return new ArrayList<>(last.getFailedSignatures()); + } + }; + } + public String getFilename() { return findLiteralData().getFileName(); } @@ -132,6 +188,14 @@ public class MessageMetadata { public List getFailedSignatures() { return new ArrayList<>(failedSignatures); } + + void addVerifiedSignature(SignatureVerification signatureVerification) { + verifiedSignatures.add(signatureVerification); + } + + void addFailedSignature(SignatureVerification.Failure failure) { + failedSignatures.add(failure); + } } public interface Nested { @@ -223,9 +287,11 @@ public class MessageMetadata { private abstract static class LayerIterator implements Iterator { private Nested current; Layer last = null; + Message parent; LayerIterator(Message message) { super(); + this.parent = message; this.current = message.child; if (matches(current)) { last = (Layer) current; @@ -234,6 +300,9 @@ public class MessageMetadata { @Override public boolean hasNext() { + if (parent != null && matches(parent)) { + return true; + } if (last == null) { findNext(); } @@ -242,6 +311,11 @@ public class MessageMetadata { @Override public O next() { + if (parent != null && matches(parent)) { + O property = getProperty(parent); + parent = null; + return property; + } if (last == null) { findNext(); } @@ -263,7 +337,16 @@ public class MessageMetadata { } } - abstract boolean matches(Nested layer); + boolean matches(Nested layer) { + return false; + } + + boolean matches(Layer layer) { + if (layer instanceof Nested) { + return matches((Nested) layer); + } + return false; + } abstract O getProperty(Layer last); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 7e34d329..9e26dc20 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -34,7 +34,6 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.bouncycastle.util.encoders.Hex; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; @@ -81,16 +80,27 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private final MessageMetadata.Layer metadata; private final Policy policy; - public OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options) + public OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options) throws IOException, PGPException { - this(inputStream, options, new MessageMetadata.Message()); + this(inputStream, options, PGPainless.getPolicy()); } - OpenPgpMessageInputStream(InputStream inputStream, ConsumerOptions options, MessageMetadata.Layer metadata) + public OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull Policy policy) + throws PGPException, IOException { + this(inputStream, options, new MessageMetadata.Message(), policy); + } + + protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) throws PGPException, IOException { super(OpenPgpMetadata.getBuilder()); - this.policy = PGPainless.getPolicy(); + this.policy = policy; this.options = options; this.metadata = metadata; this.signatures = new Signatures(options); @@ -100,6 +110,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } + // tee out packet bytes for signature verification packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); // *omnomnom* @@ -125,37 +136,30 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void consumePackets() throws IOException, PGPException { OpenPgpPacket nextPacket; - loop: while ((nextPacket = packetInputStream.nextPacketTag()) != null) { + + loop: // we break this when we go deeper. + while ((nextPacket = packetInputStream.nextPacketTag()) != null) { signatures.nextPacket(nextPacket); switch (nextPacket) { // Literal Data - the literal data content is the new input stream case LIT: - automaton.next(InputAlphabet.LiteralData); processLiteralData(); break loop; // Compressed Data - the content contains another OpenPGP message case COMP: - automaton.next(InputAlphabet.CompressedData); processCompressedData(); break loop; // One Pass Signature case OPS: - automaton.next(InputAlphabet.OnePassSignature); - PGPOnePassSignature onePassSignature = readOnePassSignature(); - signatures.addOnePassSignature(onePassSignature); + processOnePassSignature(); break; // Signature - either prepended to the message, or corresponding to a One Pass Signature case SIG: - // true if Signature corresponds to OnePassSignature - boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; - automaton.next(InputAlphabet.Signature); - - processSignature(isSigForOPS); - + processSignature(); break; // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) @@ -163,14 +167,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { case SKESK: case SED: case SEIPD: - automaton.next(InputAlphabet.EncryptedData); if (processEncryptedData()) { break loop; } throw new MissingDecryptionMethodException("No working decryption method found."); - // Marker Packets need to be skipped and ignored + // Marker Packets need to be skipped and ignored case MARKER: packetInputStream.readMarker(); break; @@ -200,36 +203,51 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - private void processSignature(boolean isSigForOPS) throws PGPException, IOException { - PGPSignature signature = readSignature(); - if (isSigForOPS) { - signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs are dealt with - signatures.addCorrespondingOnePassSignature(signature); - } else { - signatures.addPrependedSignature(signature); - } + private void processLiteralData() throws IOException { + automaton.next(InputAlphabet.LiteralData); + PGPLiteralData literalData = packetInputStream.readLiteralData(); + this.metadata.setChild(new MessageMetadata.LiteralData( + literalData.getFileName(), + literalData.getModificationTime(), + StreamEncoding.requireFromCode(literalData.getFormat()))); + nestedInputStream = literalData.getDataStream(); } private void processCompressedData() throws IOException, PGPException { + automaton.next(InputAlphabet.CompressedData); signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm())); InputStream decompressed = compressedData.getDataStream(); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer, policy); } - private void processLiteralData() throws IOException { - PGPLiteralData literalData = packetInputStream.readLiteralData(); - this.metadata.setChild(new MessageMetadata.LiteralData(literalData.getFileName(), literalData.getModificationTime(), - StreamEncoding.requireFromCode(literalData.getFormat()))); - nestedInputStream = literalData.getDataStream(); + private void processOnePassSignature() throws PGPException, IOException { + automaton.next(InputAlphabet.OnePassSignature); + PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); + signatures.addOnePassSignature(onePassSignature); + } + + private void processSignature() throws PGPException, IOException { + // true if Signature corresponds to OnePassSignature + boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; + automaton.next(InputAlphabet.Signature); + PGPSignature signature = packetInputStream.readSignature(); + if (isSigForOPS) { + signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with + signatures.addCorrespondingOnePassSignature(signature, metadata); + } else { + signatures.addPrependedSignature(signature); + } } private boolean processEncryptedData() throws IOException, PGPException { + automaton.next(InputAlphabet.EncryptedData); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() + // once BC ships it if (!encDataList.get(0).isIntegrityProtected()) { throw new MessageNotIntegrityProtectedException(); } @@ -239,24 +257,25 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Try session key if (options.getSessionKey() != null) { SessionKey sessionKey = options.getSessionKey(); - if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(sessionKey.getAlgorithm())) { - throw new UnacceptableAlgorithmException("Symmetric algorithm " + sessionKey.getAlgorithm() + " is not acceptable."); - } + + throwIfUnacceptable(sessionKey.getAlgorithm()); + SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getSessionKeyDataDecryptorFactory(sessionKey); - // TODO: Replace with encDataList.addSessionKeyDecryptionMethod(sessionKey) - PGPEncryptedData esk = esks.all().get(0); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + try { - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + // TODO: Use BCs new API once shipped + PGPEncryptedData esk = esks.all().get(0); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; InputStream decrypted = skesk.getDataStream(decryptorFactory); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; InputStream decrypted = pkesk.getDataStream(decryptorFactory); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -268,19 +287,25 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Try passwords for (PGPPBEEncryptedData skesk : esks.skesks) { + SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); + throwIfUnacceptable(kekAlgorithm); for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decrypted = skesk.getDataStream(decryptorFactory); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(skesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); - return true; - } catch (PGPException e) { - // password mismatch? Try next password - } + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + SymmetricKeyAlgorithm sessionKeyAlgorithm = SymmetricKeyAlgorithm.requireFromId( + skesk.getSymmetricAlgorithm(decryptorFactory)); + throwIfUnacceptable(sessionKeyAlgorithm); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKeyAlgorithm); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + return true; + } catch (UnacceptableAlgorithmException e) { + throw e; + } catch (PGPException e) { + // Password mismatch? + } } } @@ -299,19 +324,17 @@ public class OpenPgpMessageInputStream extends DecryptionStream { .getPublicKeyDataDecryptorFactory(privateKey); try { SymmetricKeyAlgorithm symAlg = SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)); - if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(symAlg)) { - throw new UnacceptableAlgorithmException("Symmetric-key algorithm " + symAlg + " is not acceptable."); - } + throwIfUnacceptable(symAlg); InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; + } catch (UnacceptableAlgorithmException e) { + throw e; } catch (PGPException e) { - if (e instanceof UnacceptableAlgorithmException) { - throw e; - } + } } @@ -327,7 +350,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData); + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } catch (PGPException e) { // hm :/ @@ -339,6 +362,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } + private void throwIfUnacceptable(SymmetricKeyAlgorithm algorithm) + throws UnacceptableAlgorithmException { + if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { + throw new UnacceptableAlgorithmException("Symmetric-Key algorithm " + algorithm + " is not acceptable for message decryption."); + } + } + private static InputStream buffer(InputStream inputStream) { return new BufferedInputStream(inputStream); } @@ -370,16 +400,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return null; } - private PGPOnePassSignature readOnePassSignature() - throws PGPException, IOException { - return packetInputStream.readOnePassSignature(); - } - - private PGPSignature readSignature() - throws PGPException, IOException { - return packetInputStream.readSignature(); - } - @Override public int read() throws IOException { if (nestedInputStream == null) { @@ -407,7 +427,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } catch (PGPException e) { throw new RuntimeException(e); } - signatures.finish(); + signatures.finish(metadata); } return r; } @@ -435,7 +455,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } catch (PGPException e) { throw new RuntimeException(e); } - signatures.finish(); + signatures.finish(metadata); } return r; } @@ -517,11 +537,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Furthermore, For 'OPS COMP(LIT("Foo")) SIG', the signature is updated with "Foo". CHAOS!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; - final List detachedSignatures; - final List prependedSignatures; - final List onePassSignatures; - final Stack> opsUpdateStack; - List literalOPS = new ArrayList<>(); + final List detachedSignatures; + final List prependedSignatures; + final List onePassSignatures; + final Stack> opsUpdateStack; + List literalOPS = new ArrayList<>(); final List correspondingSignatures; boolean isLiteral = true; @@ -546,19 +566,19 @@ public class OpenPgpMessageInputStream extends DecryptionStream { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.detachedSignatures.add(new SIG(signature, certificate, keyId)); + this.detachedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); } void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); initialize(signature, certificate, keyId); - this.prependedSignatures.add(new SIG(signature, certificate, keyId)); + this.prependedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); } void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); - OPS ops = new OPS(signature, certificate, signature.getKeyID()); + OnePassSignature ops = new OnePassSignature(signature, certificate, signature.getKeyID()); ops.init(certificate); onePassSignatures.add(ops); @@ -568,9 +588,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - void addCorrespondingOnePassSignature(PGPSignature signature) { + void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer) { for (int i = onePassSignatures.size() - 1; i >= 0; i--) { - OPS onePassSignature = onePassSignatures.get(i); + OnePassSignature onePassSignature = onePassSignatures.get(i); if (onePassSignature.opSignature.getKeyID() != signature.getKeyID()) { continue; } @@ -579,8 +599,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } boolean verified = onePassSignature.verify(signature); - log("One-Pass-Signature by " + Long.toHexString(onePassSignature.opSignature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - log(onePassSignature.toString()); + SignatureVerification verification = new SignatureVerification(signature, + new SubkeyIdentifier(onePassSignature.certificate, onePassSignature.keyId)); + if (verified) { + layer.addVerifiedSignature(verification); + } else { + layer.addFailedSignature(new SignatureVerification.Failure(verification, + new SignatureValidationException("Incorrect Signature."))); + } break; } } @@ -597,11 +623,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { opsUpdateStack.pop(); } - private static void initialize(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { - if (certificate == null) { - // SHIT - return; - } + private static void initialize(@Nonnull PGPSignature signature, @Nonnull PGPPublicKeyRing certificate, long keyId) { PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() .getPGPContentVerifierBuilderProvider(); try { @@ -611,10 +633,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - private static void initialize(PGPOnePassSignature ops, PGPPublicKeyRing certificate) { - if (certificate == null) { - return; - } + private static void initialize(@Nonnull PGPOnePassSignature ops, @Nonnull PGPPublicKeyRing certificate) { PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() .getPGPContentVerifierBuilderProvider(); try { @@ -635,76 +654,74 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } public void updateLiteral(byte b) { - for (OPS ops : literalOPS) { + for (OnePassSignature ops : literalOPS) { ops.update(b); } - for (SIG detached : detachedSignatures) { + for (DetachedOrPrependedSignature detached : detachedSignatures) { detached.update(b); } + + for (DetachedOrPrependedSignature prepended : prependedSignatures) { + prepended.update(b); + } } public void updateLiteral(byte[] b, int off, int len) { - for (OPS ops : literalOPS) { + for (OnePassSignature ops : literalOPS) { ops.update(b, off, len); } - for (SIG detached : detachedSignatures) { + for (DetachedOrPrependedSignature detached : detachedSignatures) { detached.update(b, off, len); } + + for (DetachedOrPrependedSignature prepended : prependedSignatures) { + prepended.update(b, off, len); + } } public void updatePacket(byte b) { - for (SIG detached : detachedSignatures) { - detached.update(b); - } - - for (SIG prepended : prependedSignatures) { - prepended.update(b); - } - for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OPS ops : nestedOPSs) { + List nestedOPSs = opsUpdateStack.get(i); + for (OnePassSignature ops : nestedOPSs) { ops.update(b); } } } public void updatePacket(byte[] buf, int off, int len) { - for (SIG detached : detachedSignatures) { - detached.update(buf, off, len); - } - - for (SIG prepended : prependedSignatures) { - prepended.update(buf, off, len); - } - for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OPS ops : nestedOPSs) { + List nestedOPSs = opsUpdateStack.get(i); + for (OnePassSignature ops : nestedOPSs) { ops.update(buf, off, len); } } } - public void finish() { - for (SIG detached : detachedSignatures) { + public void finish(MessageMetadata.Layer layer) { + for (DetachedOrPrependedSignature detached : detachedSignatures) { boolean verified = detached.verify(); + SignatureVerification verification = new SignatureVerification( + detached.signature, new SubkeyIdentifier(detached.certificate, detached.keyId)); if (verified) { - this.verified.add(detached.signature); + layer.addVerifiedSignature(verification); + } else { + layer.addFailedSignature(new SignatureVerification.Failure( + verification, new SignatureValidationException("Incorrect Signature."))); } - log("Detached Signature by " + Long.toHexString(detached.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - log(detached.toString()); } - for (SIG prepended : prependedSignatures) { + for (DetachedOrPrependedSignature prepended : prependedSignatures) { boolean verified = prepended.verify(); + SignatureVerification verification = new SignatureVerification( + prepended.signature, new SubkeyIdentifier(prepended.certificate, prepended.keyId)); if (verified) { - this.verified.add(prepended.signature); + layer.addVerifiedSignature(verification); + } else { + layer.addFailedSignature(new SignatureVerification.Failure( + verification, new SignatureValidationException("Incorrect Signature."))); } - log("Prepended Signature by " + Long.toHexString(prepended.signature.getKeyID()) + " is " + (verified ? "verified" : "unverified")); - log(prepended.toString()); } } @@ -729,7 +746,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - static class SIG { + static class DetachedOrPrependedSignature { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); PGPSignature signature; PGPPublicKeyRing certificate; @@ -737,7 +754,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean finished; boolean valid; - public SIG(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { + public DetachedOrPrependedSignature(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { this.signature = signature; this.certificate = certificate; this.keyId = keyId; @@ -762,8 +779,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void update(byte b) { if (finished) { - log("Updating finished sig!"); - return; + throw new IllegalStateException("Already finished."); } signature.update(b); bytes.write(b); @@ -771,52 +787,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void update(byte[] bytes, int off, int len) { if (finished) { - log("Updating finished sig!"); - return; + throw new IllegalStateException("Already finished."); } signature.update(bytes, off, len); this.bytes.write(bytes, off, len); } - - @Override - public String toString() { - String OPS = "c40d03000a01fbfcc82a015e733001"; - String LIT_H = "cb28620000000000"; - String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; - String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; - String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; - String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String out = ""; - - String hex = Hex.toHexString(bytes.toByteArray()); - while (hex.contains(OPS)) { - hex = hex.replace(OPS, "[OPS]"); - } - while (hex.contains(LIT_H)) { - hex = hex.replace(LIT_H, "[LIT]"); - } - while (hex.contains(LIT)) { - hex = hex.replace(LIT, ""); - } - while (hex.contains(SIG1)) { - hex = hex.replace(SIG1, "[SIG1]"); - } - while (hex.contains(SIG1f)) { - hex = hex.replace(SIG1f, "[SIG1f]"); - } - while (hex.contains(SIG2)) { - hex = hex.replace(SIG2, "[SIG2]"); - } - while (hex.contains(SIG2f)) { - hex = hex.replace(SIG2f, "[SIG2f]"); - } - - return out + hex; - } } - static class OPS { + static class OnePassSignature { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); PGPOnePassSignature opSignature; PGPSignature signature; @@ -825,7 +803,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean finished; boolean valid; - public OPS(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { + public OnePassSignature(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { this.opSignature = signature; this.certificate = certificate; this.keyId = keyId; @@ -836,6 +814,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } public boolean verify(PGPSignature signature) { + if (finished) { + throw new IllegalStateException("Already finished."); + } + if (this.opSignature.getKeyID() != signature.getKeyID()) { // nope return false; @@ -852,8 +834,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void update(byte b) { if (finished) { - log("Updating finished sig!"); - return; + throw new IllegalStateException("Already finished."); } opSignature.update(b); bytes.write(b); @@ -861,49 +842,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void update(byte[] bytes, int off, int len) { if (finished) { - log("Updating finished sig!"); - return; + throw new IllegalStateException("Already finished."); } opSignature.update(bytes, off, len); this.bytes.write(bytes, off, len); } - - @Override - public String toString() { - String OPS = "c40d03000a01fbfcc82a015e733001"; - String LIT_H = "cb28620000000000"; - String LIT = "656e637279707420e28898207369676e20e28898207369676e20e28898207369676e"; - String SIG1 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; - String SIG1f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267b0409ed8ea96dac66447bdff5b7b60c9f80a0ab91d257029153dc3b6d8c27b98162104d1a66e1a23b182c9980f788cfbfcc82a015e7330000029640c00846b5096d92474fd446cc7edaf9f14572cab93a80e12384c1e829f95debc6e8373c2ce5402be53dc1a18cf92a0ed909e0fb38855713ef8ffb13502ffac7c830fa254cc1aa6c666a97b0cc3bc176538f6913d3b8e8981a65cc42df10e0f39e4d0a06dfe961437b59a71892f4fca1116aed15123ea0d86a7b2ce47dd9d3ef22d920631bc011e82babe03ad5d72b3ba7f95bf646f20ccf6f7a4d95de37397c76c7d53741458e51ab6074007f61181c7b88b7c98f5b7510c8dfa3be01f4841501679478b15c5249d928e2a10d15ec63efa1500b994d5bfb32ffb174a976116930eb97a111e6dfd4c5e43e04a5d76ba74806a62fda63a8c3f53f6eebaf852892340e81dd08bbf348454a2cf525aeb512cf33aeeee78465ee4c442e41cc45ac4e3bb0c3333677aa60332ee7f464d9020f8554b82d619872477cca18d8431888f4ae8abe5894e9720f759c410cd7991db12703dc147040dd0d3758223e0b75de6ceae49c1a0c2c45efedeb7114ae785cc886afdc45c82172e4476e1ab5b86dc4314dd76"; - String SIG2 = "c2c10400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String SIG2f = "c2c13b0400010a006f058262c806350910fbfcc82a015e7330471400000000001e002073616c74406e6f746174696f6e732e736571756f69612d7067702e6f7267a4d9c117dc7ba3a7e9270856f128d2ab271743eac3cb5750b22a89bd5fd60753162104d1a66e1a23b182c9980f788cfbfcc82a015e73300000b8400bff796c20fa8b25ff7a42686338e06417a2966e85a0fc2723c928bef6cd19d34cf5e7d55ada33080613012dadb79e0278e59d9e7ed7d2d6102912a5f768c2e75b60099225c3d8bfe0c123240188b80dbee89b9b3bd5b13ccc662abc37e2129b6968adac9aba43aa778c0fe4fe337591ee87a96a29a013debc83555293c877144fc676aa1b03782c501949521a320adf6ad96c4f2e036b52a18369c637fdc49033696a84d03a69580b953187fce5aca6fb26fc8815da9f3b513bfe8e304f33ecb4b521aeb7d09c4a284ea66123bd0d6a358b2526d762ca110e1f7f20b3038d774b64d5cfd34e2213765828359d7afc5bf24d5270e99d80c3c1568fa01624b6ea1e9ce4e6890ce9bacf6611a45d41e2671f68f5b096446bf08d27ce75608425b2e3ab92146229ad1fcd8224aca5b5f73960506e7df07bfbf3664348e8ecbfb2eb467b9cfe412cb377a6ee2eb5fd11be9cf9208fe9a74c296f52cfa02a1eb0519ad9a8349bf6ccd6495feb7e391451bf96e08a0798883dee5974e47cbf3b51f111b6d3"; - String out = "last=" + opSignature.isContaining() + "\n"; - - String hex = Hex.toHexString(bytes.toByteArray()); - while (hex.contains(OPS)) { - hex = hex.replace(OPS, "[OPS]"); - } - while (hex.contains(LIT_H)) { - hex = hex.replace(LIT_H, "[LIT]"); - } - while (hex.contains(LIT)) { - hex = hex.replace(LIT, ""); - } - while (hex.contains(SIG1)) { - hex = hex.replace(SIG1, "[SIG1]"); - } - while (hex.contains(SIG1f)) { - hex = hex.replace(SIG1f, "[SIG1f]"); - } - while (hex.contains(SIG2)) { - hex = hex.replace(SIG2, "[SIG2]"); - } - while (hex.contains(SIG2f)) { - hex = hex.replace(SIG2f, "[SIG2f]"); - } - - return out + hex; - } } } @@ -916,7 +859,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { resultBuilder.setFileEncoding(m.getFormat()); resultBuilder.setSessionKey(m.getSessionKey()); - for (Signatures.OPS ops : signatures.onePassSignatures) { + for (Signatures.OnePassSignature ops : signatures.onePassSignatures) { if (!ops.finished) { continue; } @@ -930,7 +873,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - for (Signatures.SIG prep : signatures.prependedSignatures) { + for (Signatures.DetachedOrPrependedSignature prep : signatures.prependedSignatures) { if (!prep.finished) { continue; } @@ -944,7 +887,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - for (Signatures.SIG det : signatures.detachedSignatures) { + for (Signatures.DetachedOrPrependedSignature det : signatures.detachedSignatures) { if (!det.finished) { continue; } @@ -964,7 +907,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { static void log(String message) { LOGGER.debug(message); // CHECKSTYLE:OFF - // System.out.println(message); + System.out.println(message); // CHECKSTYLE:ON } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index 1955eebe..87ae27f8 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -331,6 +332,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals("", metadata.getFilename()); JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process LIT LIT using {0}") @@ -349,6 +352,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals(PLAINTEXT, plain); MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process COMP using {0}") @@ -372,6 +377,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next()); assertFalse(compressionAlgorithms.hasNext()); assertNull(metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") @@ -388,6 +395,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); assertNull(metadata.getEncryptionAlgorithm()); + assertFalse(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process SENC(LIT) using {0}") @@ -401,6 +410,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertNull(metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") @@ -415,6 +426,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process OPS LIT SIG using {0}") @@ -429,6 +442,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertNull(metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -564,6 +579,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } @ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}") @@ -627,6 +644,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedSignatures().isEmpty()); + assertTrue(metadata.getRejectedSignatures().isEmpty()); } private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) From 654493dfccb2e16f3327c98c402d82757132f0ca Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 16 Oct 2022 18:54:22 +0200 Subject: [PATCH 0482/1204] Properly expose signatures --- .../MessageMetadata.java | 87 ++++++++-- .../OpenPgpMessageInputStream.java | 162 +++++++++--------- .../OpenPgpMessageInputStreamTest.java | 36 ++-- 3 files changed, 171 insertions(+), 114 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index bb2f5c76..2cd2a6f2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -91,16 +91,24 @@ public class MessageMetadata { }; } - public @Nonnull List getVerifiedSignatures() { + public List getVerifiedDetachedSignatures() { + return new ArrayList<>(message.verifiedDetachedSignatures); + } + + public List getRejectedDetachedSignatures() { + return new ArrayList<>(message.rejectedDetachedSignatures); + } + + public @Nonnull List getVerifiedInlineSignatures() { List verifications = new ArrayList<>(); - Iterator> verificationsByLayer = getVerifiedSignaturesByLayer(); + Iterator> verificationsByLayer = getVerifiedInlineSignaturesByLayer(); while (verificationsByLayer.hasNext()) { verifications.addAll(verificationsByLayer.next()); } return verifications; } - public @Nonnull Iterator> getVerifiedSignaturesByLayer() { + public @Nonnull Iterator> getVerifiedInlineSignaturesByLayer() { return new LayerIterator>(message) { @Override boolean matches(Nested layer) { @@ -114,21 +122,24 @@ public class MessageMetadata { @Override List getProperty(Layer last) { - return new ArrayList<>(last.getVerifiedSignatures()); + List list = new ArrayList<>(); + list.addAll(last.getVerifiedOnePassSignatures()); + list.addAll(last.getVerifiedPrependedSignatures()); + return list; } }; } - public @Nonnull List getRejectedSignatures() { + public @Nonnull List getRejectedInlineSignatures() { List rejected = new ArrayList<>(); - Iterator> rejectedByLayer = getRejectedSignaturesByLayer(); + Iterator> rejectedByLayer = getRejectedInlineSignaturesByLayer(); while (rejectedByLayer.hasNext()) { rejected.addAll(rejectedByLayer.next()); } return rejected; } - public @Nonnull Iterator> getRejectedSignaturesByLayer() { + public @Nonnull Iterator> getRejectedInlineSignaturesByLayer() { return new LayerIterator>(message) { @Override boolean matches(Nested layer) { @@ -142,7 +153,10 @@ public class MessageMetadata { @Override List getProperty(Layer last) { - return new ArrayList<>(last.getFailedSignatures()); + List list = new ArrayList<>(); + list.addAll(last.getRejectedOnePassSignatures()); + list.addAll(last.getRejectedPrependedSignatures()); + return list; } }; } @@ -169,8 +183,12 @@ public class MessageMetadata { } public abstract static class Layer { - protected final List verifiedSignatures = new ArrayList<>(); - protected final List failedSignatures = new ArrayList<>(); + protected final List verifiedDetachedSignatures = new ArrayList<>(); + protected final List rejectedDetachedSignatures = new ArrayList<>(); + protected final List verifiedOnePassSignatures = new ArrayList<>(); + protected final List rejectedOnePassSignatures = new ArrayList<>(); + protected final List verifiedPrependedSignatures = new ArrayList<>(); + protected final List rejectedPrependedSignatures = new ArrayList<>(); protected Nested child; public Nested getChild() { @@ -181,21 +199,54 @@ public class MessageMetadata { this.child = child; } - public List getVerifiedSignatures() { - return new ArrayList<>(verifiedSignatures); + public List getVerifiedDetachedSignatures() { + return new ArrayList<>(verifiedDetachedSignatures); } - public List getFailedSignatures() { - return new ArrayList<>(failedSignatures); + public List getRejectedDetachedSignatures() { + return new ArrayList<>(rejectedDetachedSignatures); } - void addVerifiedSignature(SignatureVerification signatureVerification) { - verifiedSignatures.add(signatureVerification); + void addVerifiedDetachedSignature(SignatureVerification signatureVerification) { + verifiedDetachedSignatures.add(signatureVerification); } - void addFailedSignature(SignatureVerification.Failure failure) { - failedSignatures.add(failure); + void addRejectedDetachedSignature(SignatureVerification.Failure failure) { + rejectedDetachedSignatures.add(failure); } + + public List getVerifiedOnePassSignatures() { + return new ArrayList<>(verifiedOnePassSignatures); + } + + public List getRejectedOnePassSignatures() { + return new ArrayList<>(rejectedOnePassSignatures); + } + + void addVerifiedOnePassSignature(SignatureVerification verifiedOnePassSignature) { + this.verifiedOnePassSignatures.add(verifiedOnePassSignature); + } + + void addRejectedOnePassSignature(SignatureVerification.Failure rejected) { + this.rejectedOnePassSignatures.add(rejected); + } + + public List getVerifiedPrependedSignatures() { + return new ArrayList<>(verifiedPrependedSignatures); + } + + public List getRejectedPrependedSignatures() { + return new ArrayList<>(rejectedPrependedSignatures); + } + + void addVerifiedPrependedSignature(SignatureVerification verified) { + this.verifiedPrependedSignatures.add(verified); + } + + void addRejectedPrependedSignature(SignatureVerification.Failure rejected) { + this.rejectedPrependedSignatures.add(rejected); + } + } public interface Nested { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 9e26dc20..a4b05bbe 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -29,6 +29,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; @@ -55,6 +56,7 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; +import org.pgpainless.signature.consumer.SignatureValidator; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; import org.pgpainless.util.Tuple; @@ -94,9 +96,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options, - @Nonnull MessageMetadata.Layer metadata, - @Nonnull Policy policy) + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) throws PGPException, IOException { super(OpenPgpMetadata.getBuilder()); @@ -173,7 +175,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throw new MissingDecryptionMethodException("No working decryption method found."); - // Marker Packets need to be skipped and ignored + // Marker Packets need to be skipped and ignored case MARKER: packetInputStream.readMarker(); break; @@ -236,7 +238,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSignature signature = packetInputStream.readSignature(); if (isSigForOPS) { signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with - signatures.addCorrespondingOnePassSignature(signature, metadata); + signatures.addCorrespondingOnePassSignature(signature, metadata, policy); } else { signatures.addPrependedSignature(signature); } @@ -257,7 +259,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Try session key if (options.getSessionKey() != null) { SessionKey sessionKey = options.getSessionKey(); - throwIfUnacceptable(sessionKey.getAlgorithm()); SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() @@ -270,11 +271,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; InputStream decrypted = skesk.getDataStream(decryptorFactory); + encryptedData.sessionKey = sessionKey; nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; InputStream decrypted = pkesk.getDataStream(decryptorFactory); + encryptedData.sessionKey = sessionKey; nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } else { @@ -290,22 +293,22 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); throwIfUnacceptable(kekAlgorithm); for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decrypted = skesk.getDataStream(decryptorFactory); - SymmetricKeyAlgorithm sessionKeyAlgorithm = SymmetricKeyAlgorithm.requireFromId( - skesk.getSymmetricAlgorithm(decryptorFactory)); - throwIfUnacceptable(sessionKeyAlgorithm); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKeyAlgorithm); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); - return true; - } catch (UnacceptableAlgorithmException e) { - throw e; - } catch (PGPException e) { - // Password mismatch? - } + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + encryptedData.sessionKey = sessionKey; + nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + return true; + } catch (UnacceptableAlgorithmException e) { + throw e; + } catch (PGPException e) { + // Password mismatch? + } } } @@ -323,11 +326,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); try { - SymmetricKeyAlgorithm symAlg = SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)); - throwIfUnacceptable(symAlg); InputStream decrypted = pkesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + encryptedData.sessionKey = sessionKey; nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; @@ -348,8 +353,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { InputStream decrypted = pkesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + encryptedData.sessionKey = sessionKey; nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); return true; } catch (PGPException e) { @@ -427,7 +436,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } catch (PGPException e) { throw new RuntimeException(e); } - signatures.finish(metadata); + signatures.finish(metadata, policy); } return r; } @@ -455,7 +464,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } catch (PGPException e) { throw new RuntimeException(e); } - signatures.finish(metadata); + signatures.finish(metadata, policy); } return r; } @@ -588,7 +597,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer) { + void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer, Policy policy) { for (int i = onePassSignatures.size() - 1; i >= 0; i--) { OnePassSignature onePassSignature = onePassSignatures.get(i); if (onePassSignature.opSignature.getKeyID() != signature.getKeyID()) { @@ -598,19 +607,32 @@ public class OpenPgpMessageInputStream extends DecryptionStream { continue; } - boolean verified = onePassSignature.verify(signature); + boolean correct = onePassSignature.verify(signature); SignatureVerification verification = new SignatureVerification(signature, new SubkeyIdentifier(onePassSignature.certificate, onePassSignature.keyId)); - if (verified) { - layer.addVerifiedSignature(verification); + if (correct) { + PGPPublicKey signingKey = onePassSignature.certificate.getPublicKey(onePassSignature.keyId); + try { + checkSignatureValidity(signature, signingKey, policy); + layer.addVerifiedOnePassSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); + } } else { - layer.addFailedSignature(new SignatureVerification.Failure(verification, - new SignatureValidationException("Incorrect Signature."))); + layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, + new SignatureValidationException("Bad Signature."))); } break; } } + boolean checkSignatureValidity(PGPSignature signature, PGPPublicKey signingKey, Policy policy) throws SignatureValidationException { + SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); + SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); + SignatureValidator.signatureIsEffective().verify(signature); + return true; + } + void enterNesting() { opsUpdateStack.push(literalOPS); literalOPS = new ArrayList<>(); @@ -699,27 +721,39 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - public void finish(MessageMetadata.Layer layer) { + public void finish(MessageMetadata.Layer layer, Policy policy) { for (DetachedOrPrependedSignature detached : detachedSignatures) { - boolean verified = detached.verify(); + boolean correct = detached.verify(); SignatureVerification verification = new SignatureVerification( detached.signature, new SubkeyIdentifier(detached.certificate, detached.keyId)); - if (verified) { - layer.addVerifiedSignature(verification); + if (correct) { + try { + PGPPublicKey signingKey = detached.certificate.getPublicKey(detached.keyId); + checkSignatureValidity(detached.signature, signingKey, policy); + layer.addVerifiedDetachedSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); + } } else { - layer.addFailedSignature(new SignatureVerification.Failure( - verification, new SignatureValidationException("Incorrect Signature."))); + layer.addRejectedDetachedSignature(new SignatureVerification.Failure( + verification, new SignatureValidationException("Incorrect Signature."))); } } for (DetachedOrPrependedSignature prepended : prependedSignatures) { - boolean verified = prepended.verify(); + boolean correct = prepended.verify(); SignatureVerification verification = new SignatureVerification( prepended.signature, new SubkeyIdentifier(prepended.certificate, prepended.keyId)); - if (verified) { - layer.addVerifiedSignature(verification); + if (correct) { + try { + PGPPublicKey signingKey = prepended.certificate.getPublicKey(prepended.keyId); + checkSignatureValidity(prepended.signature, signingKey, policy); + layer.addVerifiedPrependedSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); + } } else { - layer.addFailedSignature(new SignatureVerification.Failure( + layer.addRejectedPrependedSignature(new SignatureVerification.Failure( verification, new SignatureValidationException("Incorrect Signature."))); } } @@ -859,46 +893,18 @@ public class OpenPgpMessageInputStream extends DecryptionStream { resultBuilder.setFileEncoding(m.getFormat()); resultBuilder.setSessionKey(m.getSessionKey()); - for (Signatures.OnePassSignature ops : signatures.onePassSignatures) { - if (!ops.finished) { - continue; - } - - SubkeyIdentifier identifier = new SubkeyIdentifier(ops.certificate, ops.keyId); - SignatureVerification verification = new SignatureVerification(ops.signature, identifier); - if (ops.valid) { - resultBuilder.addVerifiedInbandSignature(verification); - } else { - resultBuilder.addInvalidInbandSignature(verification, new SignatureValidationException("Incorrect signature.")); - } + for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { + resultBuilder.addVerifiedDetachedSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { + resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); } - for (Signatures.DetachedOrPrependedSignature prep : signatures.prependedSignatures) { - if (!prep.finished) { - continue; - } - - SubkeyIdentifier identifier = new SubkeyIdentifier(prep.certificate, prep.keyId); - SignatureVerification verification = new SignatureVerification(prep.signature, identifier); - if (prep.valid) { - resultBuilder.addVerifiedInbandSignature(verification); - } else { - resultBuilder.addInvalidInbandSignature(verification, new SignatureValidationException("Incorrect signature.")); - } + for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { + resultBuilder.addVerifiedInbandSignature(accepted); } - - for (Signatures.DetachedOrPrependedSignature det : signatures.detachedSignatures) { - if (!det.finished) { - continue; - } - - SubkeyIdentifier identifier = new SubkeyIdentifier(det.certificate, det.keyId); - SignatureVerification verification = new SignatureVerification(det.signature, identifier); - if (det.valid) { - resultBuilder.addVerifiedDetachedSignature(verification); - } else { - resultBuilder.addInvalidDetachedSignature(verification, new SignatureValidationException("Incorrect signature.")); - } + for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { + resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); } return resultBuilder.build(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index 87ae27f8..a8969047 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -332,8 +332,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals("", metadata.getFilename()); JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); assertEquals(StreamEncoding.BINARY, metadata.getFormat()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process LIT LIT using {0}") @@ -352,8 +352,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals(PLAINTEXT, plain); MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process COMP using {0}") @@ -377,8 +377,8 @@ public class OpenPgpMessageInputStreamTest { assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next()); assertFalse(compressionAlgorithms.hasNext()); assertNull(metadata.getEncryptionAlgorithm()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") @@ -395,8 +395,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); assertNull(metadata.getEncryptionAlgorithm()); - assertFalse(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process SENC(LIT) using {0}") @@ -410,8 +410,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertNull(metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") @@ -426,8 +426,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); - assertTrue(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process OPS LIT SIG using {0}") @@ -442,8 +442,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertNull(metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); - assertFalse(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -579,8 +579,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); - assertFalse(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } @ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}") @@ -644,8 +644,8 @@ public class OpenPgpMessageInputStreamTest { MessageMetadata metadata = result.getB(); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); - assertFalse(metadata.getVerifiedSignatures().isEmpty()); - assertTrue(metadata.getRejectedSignatures().isEmpty()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) From fbcde13df37bfd471d191b817ff5e794c39e9d23 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 16 Oct 2022 19:12:17 +0200 Subject: [PATCH 0483/1204] Reinstate integrity-protection and fix tests Integrity Protection is now checked when reading from the stream, not only when closing. --- .../OpenPgpMessageInputStream.java | 17 +++++++++---- .../TeeBCPGInputStream.java | 20 ++++++++++++++-- .../ModificationDetectionTests.java | 24 ++++++++++++------- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index a4b05bbe..54172e7f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -272,13 +272,15 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; InputStream decrypted = skesk.getDataStream(decryptorFactory); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; InputStream decrypted = pkesk.getDataStream(decryptorFactory); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); @@ -302,7 +304,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throwIfUnacceptable(sessionKey.getAlgorithm()); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; @@ -334,7 +337,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; @@ -359,7 +363,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); encryptedData.sessionKey = sessionKey; - nestedInputStream = new OpenPgpMessageInputStream(buffer(decrypted), options, encryptedData, policy); + + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (PGPException e) { // hm :/ @@ -491,6 +497,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { automaton.next(InputAlphabet.EndOfSequence); automaton.assertValid(); + packetInputStream.close(); closed = true; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index f80793f0..2efcfc43 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -96,6 +96,12 @@ public class TeeBCPGInputStream { return markerPacket; } + + public void close() throws IOException { + this.packetInputStream.close(); + this.delayedTee.close(); + } + public static class DelayedTeeInputStreamInputStream extends InputStream { private int last = -1; @@ -112,8 +118,12 @@ public class TeeBCPGInputStream { if (last != -1) { outputStream.write(last); } - last = inputStream.read(); - return last; + try { + last = inputStream.read(); + return last; + } catch (IOException e) { + return -1; + } } /** @@ -127,5 +137,11 @@ public class TeeBCPGInputStream { } last = -1; } + + @Override + public void close() throws IOException { + inputStream.close(); + outputStream.close(); + } } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java index 14a041ff..9ecaa38a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java @@ -238,8 +238,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate @@ -269,8 +271,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate @@ -313,8 +317,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate @@ -344,8 +350,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate From 6fd705b1dc6de71ea78939d6a1edeae03dfb39cd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Oct 2022 00:53:50 +0200 Subject: [PATCH 0484/1204] Fix checkstyle issues --- .../decryption_verification/DecryptionStreamFactory.java | 1 - .../decryption_verification/OpenPgpMessageInputStream.java | 5 ++--- .../InvestigateMultiSEIPMessageHandlingTest.java | 2 -- .../PreventDecryptionUsingNonEncryptionKeyTest.java | 2 -- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 680f33e4..5e6b2aec 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -40,7 +40,6 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.graalvm.compiler.lir.amd64.AMD64BinaryConsumer; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 54172e7f..37ccff37 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -29,7 +29,6 @@ import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; @@ -795,7 +794,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean finished; boolean valid; - public DetachedOrPrependedSignature(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { + DetachedOrPrependedSignature(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { this.signature = signature; this.certificate = certificate; this.keyId = keyId; @@ -844,7 +843,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean finished; boolean valid; - public OnePassSignature(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { + OnePassSignature(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { this.opSignature = signature; this.certificate = certificate; this.keyId = keyId; diff --git a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java index d31a0e0f..a0ea747a 100644 --- a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java +++ b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java @@ -4,8 +4,6 @@ package investigations; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java index 7474301b..ba80c69d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java @@ -8,8 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.EOFException; import java.io.IOException; import java.nio.charset.StandardCharsets; From 7097d449169283751471631f5969d50ee2e41e60 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Oct 2022 02:47:11 +0200 Subject: [PATCH 0485/1204] Fix NPEs and expose decryption keys --- .../MessageMetadata.java | 20 +++++++++++++ .../OpenPgpMessageInputStream.java | 30 ++++++++++++------- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 2cd2a6f2..1be47112 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -7,6 +7,7 @@ package org.pgpainless.decryption_verification; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.util.SessionKey; import javax.annotation.Nonnull; @@ -182,6 +183,24 @@ public class MessageMetadata { return (LiteralData) nested; } + public SubkeyIdentifier getDecryptionKey() { + Iterator iterator = new LayerIterator(message) { + @Override + public boolean matches(Nested layer) { + return layer instanceof EncryptedData; + } + + @Override + public SubkeyIdentifier getProperty(Layer last) { + return ((EncryptedData) last).decryptionKey; + } + }; + if (iterator.hasNext()) { + return iterator.next(); + } + return null; + } + public abstract static class Layer { protected final List verifiedDetachedSignatures = new ArrayList<>(); protected final List rejectedDetachedSignatures = new ArrayList<>(); @@ -309,6 +328,7 @@ public class MessageMetadata { public static class EncryptedData extends Layer implements Nested { protected final SymmetricKeyAlgorithm algorithm; + protected SubkeyIdentifier decryptionKey; protected SessionKey sessionKey; protected List recipients; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 37ccff37..944ce6e4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -334,6 +334,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); @@ -361,6 +362,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); @@ -560,8 +562,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { final List correspondingSignatures; boolean isLiteral = true; - final List verified = new ArrayList<>(); - private Signatures(ConsumerOptions options) { this.options = options; this.detachedSignatures = new ArrayList<>(); @@ -580,24 +580,33 @@ public class OpenPgpMessageInputStream extends DecryptionStream { void addDetachedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); - initialize(signature, certificate, keyId); - this.detachedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + + if (certificate != null) { + initialize(signature, certificate, keyId); + this.detachedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + } } void addPrependedSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); - initialize(signature, certificate, keyId); - this.prependedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + + if (certificate != null) { + initialize(signature, certificate, keyId); + this.prependedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + } } void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); - OnePassSignature ops = new OnePassSignature(signature, certificate, signature.getKeyID()); - ops.init(certificate); - onePassSignatures.add(ops); - literalOPS.add(ops); + if (certificate != null) { + OnePassSignature ops = new OnePassSignature(signature, certificate, signature.getKeyID()); + ops.init(certificate); + onePassSignatures.add(ops); + + literalOPS.add(ops); + } if (signature.isContaining()) { enterNesting(); } @@ -898,6 +907,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { resultBuilder.setFileName(m.getFilename()); resultBuilder.setFileEncoding(m.getFormat()); resultBuilder.setSessionKey(m.getSessionKey()); + resultBuilder.setDecryptionKey(m.getDecryptionKey()); for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { resultBuilder.addVerifiedDetachedSignature(accepted); From dfbb01d61cc063ff849d64aa7f21441a7c9facd9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Oct 2022 15:55:47 +0200 Subject: [PATCH 0486/1204] Enfore max recursion depth and fix CRC test --- .../MessageMetadata.java | 19 ++++++++- .../OpenPgpMessageInputStream.java | 39 ++++++++++++++++--- .../org/bouncycastle/AsciiArmorCRCTests.java | 20 +++++----- .../MessageMetadataTest.java | 6 +-- .../RecursionDepthTest.java | 4 +- 5 files changed, 66 insertions(+), 22 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 1be47112..59f8052a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -7,6 +7,7 @@ package org.pgpainless.decryption_verification; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.util.SessionKey; @@ -202,6 +203,8 @@ public class MessageMetadata { } public abstract static class Layer { + public static final int MAX_LAYER_DEPTH = 16; + protected final int depth; protected final List verifiedDetachedSignatures = new ArrayList<>(); protected final List rejectedDetachedSignatures = new ArrayList<>(); protected final List verifiedOnePassSignatures = new ArrayList<>(); @@ -210,6 +213,13 @@ public class MessageMetadata { protected final List rejectedPrependedSignatures = new ArrayList<>(); protected Nested child; + public Layer(int depth) { + this.depth = depth; + if (depth > MAX_LAYER_DEPTH) { + throw new MalformedOpenPgpMessageException("Maximum nesting depth exceeded."); + } + } + public Nested getChild() { return child; } @@ -274,6 +284,9 @@ public class MessageMetadata { public static class Message extends Layer { + public Message() { + super(0); + } } public static class LiteralData implements Nested { @@ -312,7 +325,8 @@ public class MessageMetadata { public static class CompressedData extends Layer implements Nested { protected final CompressionAlgorithm algorithm; - public CompressedData(CompressionAlgorithm zip) { + public CompressedData(CompressionAlgorithm zip, int depth) { + super(depth); this.algorithm = zip; } @@ -332,7 +346,8 @@ public class MessageMetadata { protected SessionKey sessionKey; protected List recipients; - public EncryptedData(SymmetricKeyAlgorithm algorithm) { + public EncryptedData(SymmetricKeyAlgorithm algorithm, int depth) { + super(depth); this.algorithm = algorithm; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 944ce6e4..f5ed556a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Stack; import javax.annotation.Nonnull; +import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; @@ -56,6 +57,7 @@ import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.consumer.SignatureValidator; +import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; import org.pgpainless.util.Tuple; @@ -91,7 +93,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Nonnull ConsumerOptions options, @Nonnull Policy policy) throws PGPException, IOException { - this(inputStream, options, new MessageMetadata.Message(), policy); + this(prepareInputStream(inputStream, options), options, new MessageMetadata.Message(), policy); } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, @@ -118,6 +120,26 @@ public class OpenPgpMessageInputStream extends DecryptionStream { consumePackets(); } + private static InputStream prepareInputStream(InputStream inputStream, ConsumerOptions options) throws IOException { + OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); + openPgpIn.reset(); + + if (openPgpIn.isBinaryOpenPgp()) { + return openPgpIn; + } + + if (openPgpIn.isAsciiArmored()) { + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); + if (armorIn.isClearText()) { + return armorIn; + } else { + return armorIn; + } + } else { + return openPgpIn; + } + } + /** * Consume OpenPGP packets from the current {@link BCPGInputStream}. * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, @@ -219,7 +241,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( - CompressionAlgorithm.fromId(compressedData.getAlgorithm())); + CompressionAlgorithm.fromId(compressedData.getAlgorithm()), + metadata.depth + 1); InputStream decompressed = compressedData.getDataStream(); nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer, policy); } @@ -262,7 +285,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getSessionKeyDataDecryptorFactory(sessionKey); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + sessionKey.getAlgorithm(), metadata.depth + 1); try { // TODO: Use BCs new API once shipped @@ -301,7 +325,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { InputStream decrypted = skesk.getDataStream(decryptorFactory); SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); throwIfUnacceptable(sessionKey.getAlgorithm()); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + sessionKey.getAlgorithm(), metadata.depth + 1); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); @@ -333,7 +358,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throwIfUnacceptable(sessionKey.getAlgorithm()); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + metadata.depth + 1); encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); encryptedData.sessionKey = sessionKey; @@ -361,7 +387,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throwIfUnacceptable(sessionKey.getAlgorithm()); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory))); + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + metadata.depth + 1); encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); encryptedData.sessionKey = sessionKey; diff --git a/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java b/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java index c3af0bd6..f9bd7a61 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java @@ -489,7 +489,7 @@ public class AsciiArmorCRCTests { Passphrase passphrase = Passphrase.fromPassword("flowcrypt compatibility tests"); @Test - public void testInvalidArmorCRCThrowsOnClose() throws PGPException, IOException { + public void testInvalidArmorCRCThrowsOnClose() throws IOException { String message = "-----BEGIN PGP MESSAGE-----\n" + "Version: FlowCrypt 5.0.4 Gmail Encryption flowcrypt.com\n" + "Comment: Seamlessly send, receive and search encrypted email\n" + @@ -542,14 +542,16 @@ public class AsciiArmorCRCTests { "-----END PGP MESSAGE-----\n"; PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(ASCII_KEY); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions().addDecryptionKey( - key, SecretKeyRingProtector.unlockAnyKeyWith(passphrase) - )); + assertThrows(IOException.class, () -> { + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions().addDecryptionKey( + key, SecretKeyRingProtector.unlockAnyKeyWith(passphrase) + )); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, outputStream); - assertThrows(IOException.class, decryptionStream::close); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, outputStream); + decryptionStream.close(); + }); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java index 9f887eb9..d87fc6bf 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java @@ -28,9 +28,9 @@ public class MessageMetadataTest { // For the sake of testing though, this is okay. MessageMetadata.Message message = new MessageMetadata.Message(); - MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128); - MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256); + MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP, message.depth + 1); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128, compressedData.depth + 1); + MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256, encryptedData.depth + 1); MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData(); message.setChild(compressedData); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java index b253cf7f..52c5a906 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java @@ -11,12 +11,12 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.util.TestAllImplementations; public class RecursionDepthTest { @@ -143,7 +143,7 @@ public class RecursionDepthTest { "-----END PGP ARMORED FILE-----\n"; - assertThrows(PGPException.class, () -> { + assertThrows(MalformedOpenPgpMessageException.class, () -> { DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8))) .withOptions(new ConsumerOptions().addDecryptionKey(secretKey)); From d3f07a2250d13eab224efc6d08b1c4c5ebebeac9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 14:05:21 +0200 Subject: [PATCH 0487/1204] Reuse *SignatureCheck class --- .../DecryptionStreamFactory.java | 16 +- .../OpenPgpMessageInputStream.java | 391 +++++++----------- .../SignatureInputStream.java | 12 +- ...ignatureCheck.java => SignatureCheck.java} | 6 +- 4 files changed, 170 insertions(+), 255 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/signature/consumer/{DetachedSignatureCheck.java => SignatureCheck.java} (90%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 5e6b2aec..0739eb19 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -60,7 +60,7 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.consumer.DetachedSignatureCheck; +import org.pgpainless.signature.consumer.SignatureCheck; import org.pgpainless.signature.consumer.OnePassSignatureCheck; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; @@ -79,7 +79,7 @@ public final class DecryptionStreamFactory { private final ConsumerOptions options; private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); private final List onePassSignatureChecks = new ArrayList<>(); - private final List detachedSignatureChecks = new ArrayList<>(); + private final List signatureChecks = new ArrayList<>(); private final Map onePassSignaturesWithMissingCert = new HashMap<>(); private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = @@ -88,7 +88,7 @@ public final class DecryptionStreamFactory { public static DecryptionStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) + @Nonnull ConsumerOptions options) throws PGPException, IOException { OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(inputStream); openPgpInputStream.reset(); @@ -134,9 +134,9 @@ public final class DecryptionStreamFactory { SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(signingKeyRing, signingKey.getKeyID()); try { signature.init(verifierBuilderProvider, signingKey); - DetachedSignatureCheck detachedSignature = - new DetachedSignatureCheck(signature, signingKeyRing, signingKeyIdentifier); - detachedSignatureChecks.add(detachedSignature); + SignatureCheck detachedSignature = + new SignatureCheck(signature, signingKeyRing, signingKeyIdentifier); + signatureChecks.add(detachedSignature); } catch (PGPException e) { SignatureValidationException ex = new SignatureValidationException( "Cannot verify detached signature made by " + signingKeyIdentifier + ".", e); @@ -212,7 +212,7 @@ public final class DecryptionStreamFactory { private InputStream wrapInVerifySignatureStream(InputStream bufferedIn, @Nullable PGPObjectFactory objectFactory) { return new SignatureInputStream.VerifySignatures( bufferedIn, objectFactory, onePassSignatureChecks, - onePassSignaturesWithMissingCert, detachedSignatureChecks, options, + onePassSignaturesWithMissingCert, signatureChecks, options, resultBuilder); } @@ -348,7 +348,7 @@ public final class DecryptionStreamFactory { } return new SignatureInputStream.VerifySignatures(literalDataInputStream, objectFactory, - onePassSignatureChecks, onePassSignaturesWithMissingCert, detachedSignatureChecks, options, resultBuilder) { + onePassSignatureChecks, onePassSignaturesWithMissingCert, signatureChecks, options, resultBuilder) { }; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index f5ed556a..101a60c8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -5,7 +5,6 @@ package org.pgpainless.decryption_verification; import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -31,6 +30,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; @@ -44,6 +44,8 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.automaton.InputAlphabet; import org.pgpainless.decryption_verification.automaton.PDA; import org.pgpainless.decryption_verification.automaton.StackAlphabet; +import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; +import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; @@ -56,7 +58,9 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.consumer.SignatureValidator; +import org.pgpainless.signature.consumer.OnePassSignatureCheck; +import org.pgpainless.signature.consumer.SignatureCheck; +import org.pgpainless.signature.consumer.SignatureVerifier; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; @@ -93,7 +97,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Nonnull ConsumerOptions options, @Nonnull Policy policy) throws PGPException, IOException { - this(prepareInputStream(inputStream, options), options, new MessageMetadata.Message(), policy); + this( + prepareInputStream(inputStream, options, policy), + options, new MessageMetadata.Message(), policy); } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, @@ -120,7 +126,20 @@ public class OpenPgpMessageInputStream extends DecryptionStream { consumePackets(); } - private static InputStream prepareInputStream(InputStream inputStream, ConsumerOptions options) throws IOException { + protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + @Nonnull Policy policy, + @Nonnull ConsumerOptions options) { + super(OpenPgpMetadata.getBuilder()); + this.policy = policy; + this.options = options; + this.metadata = new MessageMetadata.Message(); + this.signatures = new Signatures(options); + this.signatures.addDetachedSignatures(options.getDetachedSignatures()); + this.packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); + } + + private static InputStream prepareInputStream(InputStream inputStream, ConsumerOptions options, Policy policy) + throws IOException, PGPException { OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); openPgpIn.reset(); @@ -131,7 +150,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (openPgpIn.isAsciiArmored()) { ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); if (armorIn.isClearText()) { - return armorIn; + return parseCleartextSignedMessage(armorIn, options, policy); } else { return armorIn; } @@ -140,6 +159,19 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } + private static DecryptionStream parseCleartextSignedMessage(ArmoredInputStream armorIn, ConsumerOptions options, Policy policy) + throws IOException, PGPException { + MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); + PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream()); + + for (PGPSignature signature : signatures) { + options.addVerificationOfDetachedSignature(signature); + } + + options.forceNonOpenPgpData(); + return new OpenPgpMessageInputStream(multiPassStrategy.getMessageInputStream(), policy, options); + } + /** * Consume OpenPGP packets from the current {@link BCPGInputStream}. * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, @@ -575,17 +607,58 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } + @Override + public OpenPgpMetadata getResult() { + MessageMetadata m = getMetadata(); + resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); + resultBuilder.setModificationDate(m.getModificationDate()); + resultBuilder.setFileName(m.getFilename()); + resultBuilder.setFileEncoding(m.getFormat()); + resultBuilder.setSessionKey(m.getSessionKey()); + resultBuilder.setDecryptionKey(m.getDecryptionKey()); + + for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { + resultBuilder.addVerifiedDetachedSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { + resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { + resultBuilder.addVerifiedInbandSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { + resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + return resultBuilder.build(); + } + + static void log(String message) { + LOGGER.debug(message); + // CHECKSTYLE:OFF + System.out.println(message); + // CHECKSTYLE:ON + } + + static void log(String message, Throwable e) { + log(message); + // CHECKSTYLE:OFF + e.printStackTrace(); + // CHECKSTYLE:ON + } + // In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. // Furthermore, For 'OPS COMP(LIT("Foo")) SIG', the signature is updated with "Foo". CHAOS!!! private static final class Signatures extends OutputStream { final ConsumerOptions options; - final List detachedSignatures; - final List prependedSignatures; - final List onePassSignatures; - final Stack> opsUpdateStack; - List literalOPS = new ArrayList<>(); + final List detachedSignatures; + final List prependedSignatures; + final List onePassSignatures; + final Stack> opsUpdateStack; + List literalOPS = new ArrayList<>(); final List correspondingSignatures; boolean isLiteral = true; @@ -605,31 +678,37 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } void addDetachedSignature(PGPSignature signature) { - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing certificate = findCertificate(keyId); - - if (certificate != null) { - initialize(signature, certificate, keyId); - this.detachedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + SignatureCheck check = initializeSignature(signature); + if (check != null) { + detachedSignatures.add(check); } } void addPrependedSignature(PGPSignature signature) { + SignatureCheck check = initializeSignature(signature); + if (check != null) { + this.prependedSignatures.add(check); + } + } + + SignatureCheck initializeSignature(PGPSignature signature) { long keyId = SignatureUtils.determineIssuerKeyId(signature); PGPPublicKeyRing certificate = findCertificate(keyId); - - if (certificate != null) { - initialize(signature, certificate, keyId); - this.prependedSignatures.add(new DetachedOrPrependedSignature(signature, certificate, keyId)); + if (certificate == null) { + return null; } + + SubkeyIdentifier verifierKey = new SubkeyIdentifier(certificate, keyId); + initialize(signature, certificate, keyId); + return new SignatureCheck(signature, certificate, verifierKey); } void addOnePassSignature(PGPOnePassSignature signature) { PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); if (certificate != null) { - OnePassSignature ops = new OnePassSignature(signature, certificate, signature.getKeyID()); - ops.init(certificate); + OnePassSignatureCheck ops = new OnePassSignatureCheck(signature, certificate); + initialize(signature, certificate); onePassSignatures.add(ops); literalOPS.add(ops); @@ -641,40 +720,29 @@ public class OpenPgpMessageInputStream extends DecryptionStream { void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer, Policy policy) { for (int i = onePassSignatures.size() - 1; i >= 0; i--) { - OnePassSignature onePassSignature = onePassSignatures.get(i); - if (onePassSignature.opSignature.getKeyID() != signature.getKeyID()) { - continue; - } - if (onePassSignature.finished) { + OnePassSignatureCheck onePassSignature = onePassSignatures.get(i); + if (onePassSignature.getOnePassSignature().getKeyID() != signature.getKeyID()) { continue; } - boolean correct = onePassSignature.verify(signature); + if (onePassSignature.getSignature() != null) { + continue; + } + + onePassSignature.setSignature(signature); SignatureVerification verification = new SignatureVerification(signature, - new SubkeyIdentifier(onePassSignature.certificate, onePassSignature.keyId)); - if (correct) { - PGPPublicKey signingKey = onePassSignature.certificate.getPublicKey(onePassSignature.keyId); - try { - checkSignatureValidity(signature, signingKey, policy); - layer.addVerifiedOnePassSignature(verification); - } catch (SignatureValidationException e) { - layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); - } - } else { - layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, - new SignatureValidationException("Bad Signature."))); + new SubkeyIdentifier(onePassSignature.getVerificationKeys(), onePassSignature.getOnePassSignature().getKeyID())); + + try { + SignatureVerifier.verifyOnePassSignature(signature, onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()), onePassSignature, policy); + layer.addVerifiedOnePassSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); } break; } } - boolean checkSignatureValidity(PGPSignature signature, PGPPublicKey signingKey, Policy policy) throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective().verify(signature); - return true; - } - void enterNesting() { opsUpdateStack.push(literalOPS); literalOPS = new ArrayList<>(); @@ -718,85 +786,75 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } public void updateLiteral(byte b) { - for (OnePassSignature ops : literalOPS) { - ops.update(b); + for (OnePassSignatureCheck ops : literalOPS) { + ops.getOnePassSignature().update(b); } - for (DetachedOrPrependedSignature detached : detachedSignatures) { - detached.update(b); + for (SignatureCheck detached : detachedSignatures) { + detached.getSignature().update(b); } - for (DetachedOrPrependedSignature prepended : prependedSignatures) { - prepended.update(b); + for (SignatureCheck prepended : prependedSignatures) { + prepended.getSignature().update(b); } } public void updateLiteral(byte[] b, int off, int len) { - for (OnePassSignature ops : literalOPS) { - ops.update(b, off, len); + for (OnePassSignatureCheck ops : literalOPS) { + ops.getOnePassSignature().update(b, off, len); } - for (DetachedOrPrependedSignature detached : detachedSignatures) { - detached.update(b, off, len); + for (SignatureCheck detached : detachedSignatures) { + detached.getSignature().update(b, off, len); } - for (DetachedOrPrependedSignature prepended : prependedSignatures) { - prepended.update(b, off, len); + for (SignatureCheck prepended : prependedSignatures) { + prepended.getSignature().update(b, off, len); } } public void updatePacket(byte b) { for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OnePassSignature ops : nestedOPSs) { - ops.update(b); + List nestedOPSs = opsUpdateStack.get(i); + for (OnePassSignatureCheck ops : nestedOPSs) { + ops.getOnePassSignature().update(b); } } } public void updatePacket(byte[] buf, int off, int len) { for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OnePassSignature ops : nestedOPSs) { - ops.update(buf, off, len); + List nestedOPSs = opsUpdateStack.get(i); + for (OnePassSignatureCheck ops : nestedOPSs) { + ops.getOnePassSignature().update(buf, off, len); } } } public void finish(MessageMetadata.Layer layer, Policy policy) { - for (DetachedOrPrependedSignature detached : detachedSignatures) { - boolean correct = detached.verify(); - SignatureVerification verification = new SignatureVerification( - detached.signature, new SubkeyIdentifier(detached.certificate, detached.keyId)); - if (correct) { - try { - PGPPublicKey signingKey = detached.certificate.getPublicKey(detached.keyId); - checkSignatureValidity(detached.signature, signingKey, policy); - layer.addVerifiedDetachedSignature(verification); - } catch (SignatureValidationException e) { - layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); - } - } else { - layer.addRejectedDetachedSignature(new SignatureVerification.Failure( - verification, new SignatureValidationException("Incorrect Signature."))); + for (SignatureCheck detached : detachedSignatures) { + SignatureVerification verification = new SignatureVerification(detached.getSignature(), detached.getSigningKeyIdentifier()); + try { + SignatureVerifier.verifyInitializedSignature( + detached.getSignature(), + detached.getSigningKeyRing().getPublicKey(detached.getSigningKeyIdentifier().getKeyId()), + policy, detached.getSignature().getCreationTime()); + layer.addVerifiedDetachedSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); } } - for (DetachedOrPrependedSignature prepended : prependedSignatures) { - boolean correct = prepended.verify(); - SignatureVerification verification = new SignatureVerification( - prepended.signature, new SubkeyIdentifier(prepended.certificate, prepended.keyId)); - if (correct) { - try { - PGPPublicKey signingKey = prepended.certificate.getPublicKey(prepended.keyId); - checkSignatureValidity(prepended.signature, signingKey, policy); - layer.addVerifiedPrependedSignature(verification); - } catch (SignatureValidationException e) { - layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); - } - } else { - layer.addRejectedPrependedSignature(new SignatureVerification.Failure( - verification, new SignatureValidationException("Incorrect Signature."))); + for (SignatureCheck prepended : prependedSignatures) { + SignatureVerification verification = new SignatureVerification(prepended.getSignature(), prepended.getSigningKeyIdentifier()); + try { + SignatureVerifier.verifyInitializedSignature( + prepended.getSignature(), + prepended.getSigningKeyRing().getPublicKey(prepended.getSigningKeyIdentifier().getKeyId()), + policy, prepended.getSignature().getCreationTime()); + layer.addVerifiedPrependedSignature(verification); + } catch (SignatureValidationException e) { + layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); } } } @@ -822,148 +880,5 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - static class DetachedOrPrependedSignature { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - PGPSignature signature; - PGPPublicKeyRing certificate; - long keyId; - boolean finished; - boolean valid; - - DetachedOrPrependedSignature(PGPSignature signature, PGPPublicKeyRing certificate, long keyId) { - this.signature = signature; - this.certificate = certificate; - this.keyId = keyId; - } - - public void init(PGPPublicKeyRing certificate) { - initialize(signature, certificate, signature.getKeyID()); - } - - public boolean verify() { - if (finished) { - throw new IllegalStateException("Already finished."); - } - finished = true; - try { - valid = this.signature.verify(); - } catch (PGPException e) { - log("Cannot verify SIG " + signature.getKeyID()); - } - return valid; - } - - public void update(byte b) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - signature.update(b); - bytes.write(b); - } - - public void update(byte[] bytes, int off, int len) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - signature.update(bytes, off, len); - this.bytes.write(bytes, off, len); - } - } - - static class OnePassSignature { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - PGPOnePassSignature opSignature; - PGPSignature signature; - PGPPublicKeyRing certificate; - long keyId; - boolean finished; - boolean valid; - - OnePassSignature(PGPOnePassSignature signature, PGPPublicKeyRing certificate, long keyId) { - this.opSignature = signature; - this.certificate = certificate; - this.keyId = keyId; - } - - public void init(PGPPublicKeyRing certificate) { - initialize(opSignature, certificate); - } - - public boolean verify(PGPSignature signature) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - - if (this.opSignature.getKeyID() != signature.getKeyID()) { - // nope - return false; - } - this.signature = signature; - finished = true; - try { - valid = this.opSignature.verify(signature); - } catch (PGPException e) { - log("Cannot verify OPS " + signature.getKeyID()); - } - return valid; - } - - public void update(byte b) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - opSignature.update(b); - bytes.write(b); - } - - public void update(byte[] bytes, int off, int len) { - if (finished) { - throw new IllegalStateException("Already finished."); - } - opSignature.update(bytes, off, len); - this.bytes.write(bytes, off, len); - } - } - } - - @Override - public OpenPgpMetadata getResult() { - MessageMetadata m = getMetadata(); - resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); - resultBuilder.setModificationDate(m.getModificationDate()); - resultBuilder.setFileName(m.getFilename()); - resultBuilder.setFileEncoding(m.getFormat()); - resultBuilder.setSessionKey(m.getSessionKey()); - resultBuilder.setDecryptionKey(m.getDecryptionKey()); - - for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { - resultBuilder.addVerifiedDetachedSignature(accepted); - } - for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { - resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { - resultBuilder.addVerifiedInbandSignature(accepted); - } - for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { - resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - return resultBuilder.build(); - } - - static void log(String message) { - LOGGER.debug(message); - // CHECKSTYLE:OFF - System.out.println(message); - // CHECKSTYLE:ON - } - - static void log(String message, Throwable e) { - log(message); - // CHECKSTYLE:OFF - e.printStackTrace(); - // CHECKSTYLE:ON } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java index 275acc17..70a2f4ef 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java @@ -22,7 +22,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.policy.Policy; import org.pgpainless.signature.consumer.CertificateValidator; -import org.pgpainless.signature.consumer.DetachedSignatureCheck; +import org.pgpainless.signature.consumer.SignatureCheck; import org.pgpainless.signature.consumer.OnePassSignatureCheck; import org.pgpainless.signature.SignatureUtils; import org.slf4j.Logger; @@ -41,7 +41,7 @@ public abstract class SignatureInputStream extends FilterInputStream { private final PGPObjectFactory objectFactory; private final List opSignatures; private final Map opSignaturesWithMissingCert; - private final List detachedSignatures; + private final List detachedSignatures; private final ConsumerOptions options; private final OpenPgpMetadata.Builder resultBuilder; @@ -50,7 +50,7 @@ public abstract class SignatureInputStream extends FilterInputStream { @Nullable PGPObjectFactory objectFactory, List opSignatures, Map onePassSignaturesWithMissingCert, - List detachedSignatures, + List detachedSignatures, ConsumerOptions options, OpenPgpMetadata.Builder resultBuilder) { super(literalDataStream); @@ -170,7 +170,7 @@ public abstract class SignatureInputStream extends FilterInputStream { private void verifyDetachedSignatures() { Policy policy = PGPainless.getPolicy(); - for (DetachedSignatureCheck s : detachedSignatures) { + for (SignatureCheck s : detachedSignatures) { try { signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()).verify(s.getSignature()); @@ -200,13 +200,13 @@ public abstract class SignatureInputStream extends FilterInputStream { } private void updateDetachedSignatures(byte b) { - for (DetachedSignatureCheck detachedSignature : detachedSignatures) { + for (SignatureCheck detachedSignature : detachedSignatures) { detachedSignature.getSignature().update(b); } } private void updateDetachedSignatures(byte[] b, int off, int read) { - for (DetachedSignatureCheck detachedSignature : detachedSignatures) { + for (SignatureCheck detachedSignature : detachedSignatures) { detachedSignature.getSignature().update(b, off, read); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/DetachedSignatureCheck.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java similarity index 90% rename from pgpainless-core/src/main/java/org/pgpainless/signature/consumer/DetachedSignatureCheck.java rename to pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java index a431c5de..bc9f1f0b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/DetachedSignatureCheck.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java @@ -13,19 +13,19 @@ import org.pgpainless.key.SubkeyIdentifier; * Tuple-class which bundles together a signature, the signing key that created the signature, * an identifier of the signing key and a record of whether the signature was verified. */ -public class DetachedSignatureCheck { +public class SignatureCheck { private final PGPSignature signature; private final PGPKeyRing signingKeyRing; private final SubkeyIdentifier signingKeyIdentifier; /** - * Create a new {@link DetachedSignatureCheck} object. + * Create a new {@link SignatureCheck} object. * * @param signature signature * @param signingKeyRing signing key that created the signature * @param signingKeyIdentifier identifier of the used signing key */ - public DetachedSignatureCheck(PGPSignature signature, PGPKeyRing signingKeyRing, SubkeyIdentifier signingKeyIdentifier) { + public SignatureCheck(PGPSignature signature, PGPKeyRing signingKeyRing, SubkeyIdentifier signingKeyIdentifier) { this.signature = signature; this.signingKeyRing = signingKeyRing; this.signingKeyIdentifier = signingKeyIdentifier; From 7da34c8329badb17e243dc4ebb937d480164822b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 15:35:15 +0200 Subject: [PATCH 0488/1204] Work on postponed keys --- .../OpenPgpMessageInputStream.java | 121 +++++++++++++++--- 1 file changed, 104 insertions(+), 17 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 101a60c8..dfb0df91 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -10,7 +10,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.Stack; import javax.annotation.Nonnull; @@ -49,6 +51,7 @@ import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStra import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; +import org.pgpainless.exception.MissingPassphraseException; import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.exception.UnacceptableAlgorithmException; import org.pgpainless.implementation.ImplementationFactory; @@ -60,6 +63,7 @@ import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.consumer.OnePassSignatureCheck; import org.pgpainless.signature.consumer.SignatureCheck; +import org.pgpainless.signature.consumer.SignatureValidator; import org.pgpainless.signature.consumer.SignatureVerifier; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; @@ -127,8 +131,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, - @Nonnull Policy policy, - @Nonnull ConsumerOptions options) { + @Nonnull Policy policy, + @Nonnull ConsumerOptions options) { super(OpenPgpMetadata.getBuilder()); this.policy = policy; this.options = options; @@ -371,6 +375,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } + List> postponedDueToMissingPassphrase = new ArrayList<>(); // Try (known) secret keys for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { long keyId = pkesk.getKeyID(); @@ -378,7 +383,15 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (decryptionKeys == null) { continue; } + PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); + // Postpone keys with missing passphrase + if (!protector.hasPassphraseFor(keyId)) { + postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); + continue; + } + PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); @@ -408,7 +421,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // try anonymous secret keys for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { + PGPSecretKey secretKey = decryptionKeyCandidate.getB(); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); + if (!protector.hasPassphraseFor(secretKey.getKeyID())) { + postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); + continue; + } PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); @@ -433,10 +451,64 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } + if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { + // Non-interactive mode: Throw an exception with all locked decryption keys + Set keyIds = new HashSet<>(); + for (Tuple k : postponedDueToMissingPassphrase) { + PGPSecretKey key = k.getA(); + keyIds.add(new SubkeyIdentifier(getDecryptionKey(key.getKeyID()), key.getKeyID())); + } + if (!keyIds.isEmpty()) { + throw new MissingPassphraseException(keyIds); + } + } else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + // Interactive mode: Fire protector callbacks to get passphrases interactively + for (Tuple missingPassphrases : postponedDueToMissingPassphrase) { + PGPSecretKey secretKey = missingPassphrases.getA(); + long keyId = secretKey.getKeyID(); + PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); + + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + metadata.depth + 1); + encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKey, keyId); + encryptedData.sessionKey = sessionKey; + + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + return true; + } catch (PGPException e) { + // hm :/ + } + } + } + } else { + throw new IllegalStateException("Invalid PostponedKeysStrategy set in consumer options."); + } + // we did not yet succeed in decrypting any session key :/ return false; } + private PGPSecretKey getDecryptionKey(PGPSecretKeyRing decryptionKeys, long keyId) { + KeyRingInfo info = PGPainless.inspectKeyRing(decryptionKeys); + if (info.getEncryptionSubkeys(EncryptionPurpose.ANY).contains(info.getPublicKey(keyId))) { + return info.getSecretKey(keyId); + } + return null; + } + private void throwIfUnacceptable(SymmetricKeyAlgorithm algorithm) throws UnacceptableAlgorithmException { if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { @@ -497,10 +569,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { collectMetadata(); nestedInputStream = null; - try { - consumePackets(); - } catch (PGPException e) { - throw new RuntimeException(e); + if (packetInputStream != null) { + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } } signatures.finish(metadata, policy); } @@ -512,23 +586,26 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throws IOException { if (nestedInputStream == null) { - automaton.assertValid(); + if (packetInputStream != null) { + automaton.assertValid(); + } return -1; } int r = nestedInputStream.read(b, off, len); if (r != -1) { signatures.updateLiteral(b, off, r); - } - else { + } else { nestedInputStream.close(); collectMetadata(); nestedInputStream = null; - try { - consumePackets(); - } catch (PGPException e) { - throw new RuntimeException(e); + if (packetInputStream != null) { + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } } signatures.finish(metadata, policy); } @@ -539,7 +616,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { public void close() throws IOException { super.close(); if (closed) { - automaton.assertValid(); + if (packetInputStream != null) { + automaton.assertValid(); + } return; } @@ -555,9 +634,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throw new RuntimeException(e); } - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); - packetInputStream.close(); + if (packetInputStream != null) { + automaton.next(InputAlphabet.EndOfSequence); + automaton.assertValid(); + packetInputStream.close(); + } closed = true; } @@ -734,6 +815,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { new SubkeyIdentifier(onePassSignature.getVerificationKeys(), onePassSignature.getOnePassSignature().getKeyID())); try { + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(signature); SignatureVerifier.verifyOnePassSignature(signature, onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()), onePassSignature, policy); layer.addVerifiedOnePassSignature(verification); } catch (SignatureValidationException e) { @@ -835,6 +918,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (SignatureCheck detached : detachedSignatures) { SignatureVerification verification = new SignatureVerification(detached.getSignature(), detached.getSigningKeyIdentifier()); try { + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(detached.getSignature()); SignatureVerifier.verifyInitializedSignature( detached.getSignature(), detached.getSigningKeyRing().getPublicKey(detached.getSigningKeyIdentifier().getKeyId()), @@ -848,6 +933,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (SignatureCheck prepended : prependedSignatures) { SignatureVerification verification = new SignatureVerification(prepended.getSignature(), prepended.getSigningKeyIdentifier()); try { + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(prepended.getSignature()); SignatureVerifier.verifyInitializedSignature( prepended.getSignature(), prepended.getSigningKeyRing().getPublicKey(prepended.getSigningKeyIdentifier().getKeyId()), From d39d062a0df05b9a4b67ec6d4bb55a56e6cff334 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 21 Sep 2022 15:03:45 +0200 Subject: [PATCH 0489/1204] WIP: Explore Hardware Decryption --- .../ConsumerOptions.java | 6 +++ .../HardwareSecurity.java | 27 +++++++++++++ .../HardwareSecurityCallbackTest.java | 39 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index b6117a60..d57eff48 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -48,6 +48,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; + private HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = null; private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -238,6 +239,11 @@ public class ConsumerOptions { return this; } + public ConsumerOptions setHardwareDecryptionCallback(HardwareSecurity.DecryptionCallback callback) { + this.hardwareDecryptionCallback = callback; + return this; + } + public @Nonnull Set getDecryptionKeys() { return Collections.unmodifiableSet(decryptionKeys.keySet()); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java new file mode 100644 index 00000000..bea14aef --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -0,0 +1,27 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.pgpainless.util.SessionKey; + +public class HardwareSecurity { + + public interface DecryptionCallback { + + /** + * Delegate decryption of a Public-Key-Encrypted-Session-Key (PKESK) to an external API for dealing with + * hardware security modules such as smartcards or TPMs. + * + * If decryption fails for some reason, a subclass of the {@link HardwareSecurityException} is thrown. + * + * @param pkesk public-key-encrypted session key + * @return decrypted session key + * @throws HardwareSecurityException exception + */ + SessionKey decryptSessionKey(PGPPublicKeyEncryptedData pkesk) throws HardwareSecurityException; + + } + + public static class HardwareSecurityException extends Exception { + + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java new file mode 100644 index 00000000..d731277e --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java @@ -0,0 +1,39 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.util.SessionKey; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class HardwareSecurityCallbackTest { + + @Test + public void test() throws PGPException, IOException { + PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(new byte[0])) + .withOptions(ConsumerOptions.get() + .setHardwareDecryptionCallback(new HardwareSecurity.DecryptionCallback() { + @Override + public SessionKey decryptSessionKey(PGPPublicKeyEncryptedData pkesk) throws HardwareSecurity.HardwareSecurityException { + /* + pkesk.getSessionKey(new PublicKeyDataDecryptorFactory() { + @Override + public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { + return new byte[0]; + } + + @Override + public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) throws PGPException { + return null; + } + }); + */ + return null; + } + })); + } +} From 529c64cf43d62d9764d2ffa540a61b88aaafd622 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 23 Sep 2022 16:17:42 +0200 Subject: [PATCH 0490/1204] Implement exploratory support for custom decryption factories This may enable decryption of messages with hardware-backed keys --- .../ConsumerOptions.java | 24 ++++- .../DecryptionStreamFactory.java | 28 ++++++ .../HardwareSecurity.java | 91 ++++++++++++++++++- ...stomPublicKeyDataDecryptorFactoryTest.java | 86 ++++++++++++++++++ .../HardwareSecurityCallbackTest.java | 39 -------- 5 files changed, 221 insertions(+), 47 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index d57eff48..8f0576ee 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -22,6 +22,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -48,7 +49,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; - private HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = null; + private Map, PublicKeyDataDecryptorFactory> customPublicKeyDataDecryptorFactories = new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -239,11 +240,28 @@ public class ConsumerOptions { return this; } - public ConsumerOptions setHardwareDecryptionCallback(HardwareSecurity.DecryptionCallback callback) { - this.hardwareDecryptionCallback = callback; + /** + * Add a custom {@link PublicKeyDataDecryptorFactory} which enable decryption of messages, e.g. using + * hardware-backed secret keys. + * (See e.g. {@link org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory}). + * + * The set of key-ids determines, whether the decryptor factory shall be consulted to decrypt a given session key. + * See for example {@link HardwareSecurity#getIdsOfHardwareBackedKeys(PGPSecretKeyRing)}. + * + * @param keyIds set of key-ids for which the factory shall be consulted + * @param decryptorFactory decryptor factory + * @return options + */ + public ConsumerOptions addCustomDecryptorFactory( + @Nonnull Set keyIds, @Nonnull PublicKeyDataDecryptorFactory decryptorFactory) { + this.customPublicKeyDataDecryptorFactories.put(keyIds, decryptorFactory); return this; } + Map, PublicKeyDataDecryptorFactory> getCustomDecryptorFactories() { + return new HashMap<>(customPublicKeyDataDecryptorFactories); + } + public @Nonnull Set getDecryptionKeys() { return Collections.unmodifiableSet(decryptionKeys.keySet()); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 0739eb19..ba837940 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -408,6 +408,34 @@ public final class DecryptionStreamFactory { } } + // Try custom PublicKeyDataDecryptorFactories (e.g. hardware-backed). + Map, PublicKeyDataDecryptorFactory> customFactories = options.getCustomDecryptorFactories(); + for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { + Long keyId = publicKeyEncryptedData.getKeyID(); + for (Set keyIds : customFactories.keySet()) { + if (!keyIds.contains(keyId)) { + continue; + } + + PublicKeyDataDecryptorFactory decryptorFactory = customFactories.get(keyIds); + try { + InputStream decryptedDataStream = publicKeyEncryptedData.getDataStream(decryptorFactory); + PGPSessionKey pgpSessionKey = publicKeyEncryptedData.getSessionKey(decryptorFactory); + SessionKey sessionKey = new SessionKey(pgpSessionKey); + resultBuilder.setSessionKey(sessionKey); + + throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); + + integrityProtectedEncryptedInputStream = + new IntegrityProtectedInputStream(decryptedDataStream, publicKeyEncryptedData, options); + + return integrityProtectedEncryptedInputStream; + } catch (PGPException e) { + LOGGER.debug("Decryption with custom PublicKeyDataDecryptorFactory failed", e); + } + } + } + // Then try decryption with public key encryption for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { PGPPrivateKey privateKey = null; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index bea14aef..cc8cf598 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -1,8 +1,23 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.pgpainless.util.SessionKey; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.PGPDataDecryptor; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import java.util.HashSet; +import java.util.Set; + +/** + * Enable integration of hardware-backed OpenPGP keys. + */ public class HardwareSecurity { public interface DecryptionCallback { @@ -13,15 +28,81 @@ public class HardwareSecurity { * * If decryption fails for some reason, a subclass of the {@link HardwareSecurityException} is thrown. * - * @param pkesk public-key-encrypted session key + * @param keyAlgorithm algorithm + * @param sessionKeyData encrypted session key + * * @return decrypted session key * @throws HardwareSecurityException exception */ - SessionKey decryptSessionKey(PGPPublicKeyEncryptedData pkesk) throws HardwareSecurityException; + byte[] decryptSessionKey(int keyAlgorithm, byte[] sessionKeyData) + throws HardwareSecurityException; } - public static class HardwareSecurityException extends Exception { + /** + * Return the key-ids of all keys which appear to be stored on a hardware token / smartcard. + * + * @param secretKeys secret keys + * @return set of keys with S2K type DIVERT_TO_CARD or GNU_DUMMY_S2K + */ + public static Set getIdsOfHardwareBackedKeys(PGPSecretKeyRing secretKeys) { + Set hardwareBackedKeys = new HashSet<>(); + for (PGPSecretKey secretKey : secretKeys) { + S2K s2K = secretKey.getS2K(); + if (s2K == null) { + continue; + } + + int type = s2K.getType(); + // TODO: Is GNU_DUMMY_S2K appropriate? + if (type == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD || type == S2K.GNU_DUMMY_S2K) { + hardwareBackedKeys.add(secretKey.getKeyID()); + } + } + return hardwareBackedKeys; + } + + /** + * Implementation of {@link PublicKeyDataDecryptorFactory} which delegates decryption of encrypted session keys + * to a {@link DecryptionCallback}. + * Users can provide such a callback to delegate decryption of messages to hardware security SDKs. + */ + public static class HardwareDataDecryptorFactory implements PublicKeyDataDecryptorFactory { + + private final DecryptionCallback callback; + // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. + private final PublicKeyDataDecryptorFactory factory = + new BcPublicKeyDataDecryptorFactory(null); + + /** + * Create a new {@link HardwareDataDecryptorFactory}. + * + * @param callback decryption callback + */ + public HardwareDataDecryptorFactory(DecryptionCallback callback) { + this.callback = callback; + } + + @Override + public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) + throws PGPException { + try { + // delegate decryption to the callback + return callback.decryptSessionKey(keyAlgorithm, secKeyData[0]); + } catch (HardwareSecurityException e) { + throw new PGPException("Hardware-backed decryption failed.", e); + } + } + + @Override + public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) + throws PGPException { + return factory.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); + } + } + + public static class HardwareSecurityException + extends Exception { } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java new file mode 100644 index 00000000..f507e02e --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CustomPublicKeyDataDecryptorFactoryTest { + + @Test + public void testDecryptionWithEmulatedHardwareDecryptionCallback() + throws PGPException, IOException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKey = PGPainless.generateKeyRing().modernKeyRing("Alice"); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKey); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + PGPPublicKey encryptionKey = info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); + + // Encrypt a test message + String plaintext = "Hello, World!\n"; + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addRecipient(cert))); + encryptionStream.write(plaintext.getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + + HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = new HardwareSecurity.DecryptionCallback() { + @Override + public byte[] decryptSessionKey(int keyAlgorithm, byte[] sessionKeyData) + throws HardwareSecurity.HardwareSecurityException { + // Emulate hardware decryption. + try { + PGPSecretKey decryptionKey = secretKey.getSecretKey(encryptionKey.getKeyID()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, Passphrase.emptyPassphrase()); + PublicKeyDataDecryptorFactory internal = new BcPublicKeyDataDecryptorFactory(privateKey); + return internal.recoverSessionData(keyAlgorithm, new byte[][] {sessionKeyData}); + } catch (PGPException e) { + throw new HardwareSecurity.HardwareSecurityException(); + } + } + }; + + // Decrypt + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ciphertextOut.toByteArray())) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory( + Collections.singleton(encryptionKey.getKeyID()), + new HardwareSecurity.HardwareDataDecryptorFactory(hardwareDecryptionCallback))); + + ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, decryptedOut); + decryptionStream.close(); + + assertEquals(plaintext, decryptedOut.toString()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java deleted file mode 100644 index d731277e..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityCallbackTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.pgpainless.decryption_verification; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.util.SessionKey; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -public class HardwareSecurityCallbackTest { - - @Test - public void test() throws PGPException, IOException { - PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(new byte[0])) - .withOptions(ConsumerOptions.get() - .setHardwareDecryptionCallback(new HardwareSecurity.DecryptionCallback() { - @Override - public SessionKey decryptSessionKey(PGPPublicKeyEncryptedData pkesk) throws HardwareSecurity.HardwareSecurityException { - /* - pkesk.getSessionKey(new PublicKeyDataDecryptorFactory() { - @Override - public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { - return new byte[0]; - } - - @Override - public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) throws PGPException { - return null; - } - }); - */ - return null; - } - })); - } -} From 228918f96b3669c852919bf0160e9295dbdd7dd1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 10 Oct 2022 18:01:30 +0200 Subject: [PATCH 0491/1204] Change HardwareSecurity DecryptionCallback to emit key-id --- .../ConsumerOptions.java | 15 +++----- .../CustomPublicKeyDataDecryptorFactory.java | 13 +++++++ .../DecryptionStreamFactory.java | 34 +++++++++---------- .../HardwareSecurity.java | 17 +++++++--- ...stomPublicKeyDataDecryptorFactoryTest.java | 8 ++--- 5 files changed, 51 insertions(+), 36 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index 8f0576ee..dba9fca7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -49,7 +49,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; - private Map, PublicKeyDataDecryptorFactory> customPublicKeyDataDecryptorFactories = new HashMap<>(); + private Map customPublicKeyDataDecryptorFactories = new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -245,20 +245,15 @@ public class ConsumerOptions { * hardware-backed secret keys. * (See e.g. {@link org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory}). * - * The set of key-ids determines, whether the decryptor factory shall be consulted to decrypt a given session key. - * See for example {@link HardwareSecurity#getIdsOfHardwareBackedKeys(PGPSecretKeyRing)}. - * - * @param keyIds set of key-ids for which the factory shall be consulted - * @param decryptorFactory decryptor factory + * @param factory decryptor factory * @return options */ - public ConsumerOptions addCustomDecryptorFactory( - @Nonnull Set keyIds, @Nonnull PublicKeyDataDecryptorFactory decryptorFactory) { - this.customPublicKeyDataDecryptorFactories.put(keyIds, decryptorFactory); + public ConsumerOptions addCustomDecryptorFactory(@Nonnull CustomPublicKeyDataDecryptorFactory factory) { + this.customPublicKeyDataDecryptorFactories.put(factory.getKeyId(), factory); return this; } - Map, PublicKeyDataDecryptorFactory> getCustomDecryptorFactories() { + Map getCustomDecryptorFactories() { return new HashMap<>(customPublicKeyDataDecryptorFactories); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java new file mode 100644 index 00000000..43e8e723 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; + +public interface CustomPublicKeyDataDecryptorFactory extends PublicKeyDataDecryptorFactory { + + long getKeyId(); + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index ba837940..23e3e47d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -409,30 +409,28 @@ public final class DecryptionStreamFactory { } // Try custom PublicKeyDataDecryptorFactories (e.g. hardware-backed). - Map, PublicKeyDataDecryptorFactory> customFactories = options.getCustomDecryptorFactories(); + Map customFactories = options.getCustomDecryptorFactories(); for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { Long keyId = publicKeyEncryptedData.getKeyID(); - for (Set keyIds : customFactories.keySet()) { - if (!keyIds.contains(keyId)) { - continue; - } + if (!customFactories.containsKey(keyId)) { + continue; + } - PublicKeyDataDecryptorFactory decryptorFactory = customFactories.get(keyIds); - try { - InputStream decryptedDataStream = publicKeyEncryptedData.getDataStream(decryptorFactory); - PGPSessionKey pgpSessionKey = publicKeyEncryptedData.getSessionKey(decryptorFactory); - SessionKey sessionKey = new SessionKey(pgpSessionKey); - resultBuilder.setSessionKey(sessionKey); + PublicKeyDataDecryptorFactory decryptorFactory = customFactories.get(keyId); + try { + InputStream decryptedDataStream = publicKeyEncryptedData.getDataStream(decryptorFactory); + PGPSessionKey pgpSessionKey = publicKeyEncryptedData.getSessionKey(decryptorFactory); + SessionKey sessionKey = new SessionKey(pgpSessionKey); + resultBuilder.setSessionKey(sessionKey); - throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); + throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); - integrityProtectedEncryptedInputStream = - new IntegrityProtectedInputStream(decryptedDataStream, publicKeyEncryptedData, options); + integrityProtectedEncryptedInputStream = + new IntegrityProtectedInputStream(decryptedDataStream, publicKeyEncryptedData, options); - return integrityProtectedEncryptedInputStream; - } catch (PGPException e) { - LOGGER.debug("Decryption with custom PublicKeyDataDecryptorFactory failed", e); - } + return integrityProtectedEncryptedInputStream; + } catch (PGPException e) { + LOGGER.debug("Decryption with custom PublicKeyDataDecryptorFactory failed", e); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index cc8cf598..d5bf60db 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -28,13 +28,14 @@ public class HardwareSecurity { * * If decryption fails for some reason, a subclass of the {@link HardwareSecurityException} is thrown. * + * @param keyId id of the key * @param keyAlgorithm algorithm * @param sessionKeyData encrypted session key * * @return decrypted session key * @throws HardwareSecurityException exception */ - byte[] decryptSessionKey(int keyAlgorithm, byte[] sessionKeyData) + byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData) throws HardwareSecurityException; } @@ -67,20 +68,22 @@ public class HardwareSecurity { * to a {@link DecryptionCallback}. * Users can provide such a callback to delegate decryption of messages to hardware security SDKs. */ - public static class HardwareDataDecryptorFactory implements PublicKeyDataDecryptorFactory { + public static class HardwareDataDecryptorFactory implements CustomPublicKeyDataDecryptorFactory { private final DecryptionCallback callback; // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. private final PublicKeyDataDecryptorFactory factory = new BcPublicKeyDataDecryptorFactory(null); + private long keyId; /** * Create a new {@link HardwareDataDecryptorFactory}. * * @param callback decryption callback */ - public HardwareDataDecryptorFactory(DecryptionCallback callback) { + public HardwareDataDecryptorFactory(long keyId, DecryptionCallback callback) { this.callback = callback; + this.keyId = keyId; } @Override @@ -88,7 +91,7 @@ public class HardwareSecurity { throws PGPException { try { // delegate decryption to the callback - return callback.decryptSessionKey(keyAlgorithm, secKeyData[0]); + return callback.decryptSessionKey(keyId, keyAlgorithm, secKeyData[0]); } catch (HardwareSecurityException e) { throw new PGPException("Hardware-backed decryption failed.", e); } @@ -99,10 +102,16 @@ public class HardwareSecurity { throws PGPException { return factory.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); } + + @Override + public long getKeyId() { + return keyId; + } } public static class HardwareSecurityException extends Exception { } + } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java index f507e02e..4ea894e5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java @@ -29,7 +29,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -55,7 +54,7 @@ public class CustomPublicKeyDataDecryptorFactoryTest { HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = new HardwareSecurity.DecryptionCallback() { @Override - public byte[] decryptSessionKey(int keyAlgorithm, byte[] sessionKeyData) + public byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData) throws HardwareSecurity.HardwareSecurityException { // Emulate hardware decryption. try { @@ -74,8 +73,9 @@ public class CustomPublicKeyDataDecryptorFactoryTest { .onInputStream(new ByteArrayInputStream(ciphertextOut.toByteArray())) .withOptions(ConsumerOptions.get() .addCustomDecryptorFactory( - Collections.singleton(encryptionKey.getKeyID()), - new HardwareSecurity.HardwareDataDecryptorFactory(hardwareDecryptionCallback))); + new HardwareSecurity.HardwareDataDecryptorFactory( + encryptionKey.getKeyID(), + hardwareDecryptionCallback))); ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); Streams.pipeAll(decryptionStream, decryptedOut); From cfd3f77491e6b0b15b01ffc2f466c6fd8c2a3827 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 10 Oct 2022 18:08:43 +0200 Subject: [PATCH 0492/1204] Make map final --- .../org/pgpainless/decryption_verification/ConsumerOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index dba9fca7..0f02aba0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -49,7 +49,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; - private Map customPublicKeyDataDecryptorFactories = new HashMap<>(); + private final Map customPublicKeyDataDecryptorFactories = new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); From a39c6bc881f2763d6ea812b95d94341918efe3f7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 16:07:31 +0200 Subject: [PATCH 0493/1204] Identify custom decryptor factories by subkey id --- .../ConsumerOptions.java | 7 ++++--- .../CustomPublicKeyDataDecryptorFactory.java | 3 ++- .../DecryptionStreamFactory.java | 2 +- .../HardwareSecurity.java | 19 +++++++++++++------ ...stomPublicKeyDataDecryptorFactoryTest.java | 3 ++- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index 0f02aba0..f8f59d36 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -25,6 +25,7 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; @@ -49,7 +50,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; - private final Map customPublicKeyDataDecryptorFactories = new HashMap<>(); + private final Map customPublicKeyDataDecryptorFactories = new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -249,11 +250,11 @@ public class ConsumerOptions { * @return options */ public ConsumerOptions addCustomDecryptorFactory(@Nonnull CustomPublicKeyDataDecryptorFactory factory) { - this.customPublicKeyDataDecryptorFactories.put(factory.getKeyId(), factory); + this.customPublicKeyDataDecryptorFactories.put(factory.getSubkeyIdentifier(), factory); return this; } - Map getCustomDecryptorFactories() { + Map getCustomDecryptorFactories() { return new HashMap<>(customPublicKeyDataDecryptorFactories); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java index 43e8e723..37dc10a9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java @@ -5,9 +5,10 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.pgpainless.key.SubkeyIdentifier; public interface CustomPublicKeyDataDecryptorFactory extends PublicKeyDataDecryptorFactory { - long getKeyId(); + SubkeyIdentifier getSubkeyIdentifier(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 23e3e47d..451750ee 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -409,7 +409,7 @@ public final class DecryptionStreamFactory { } // Try custom PublicKeyDataDecryptorFactories (e.g. hardware-backed). - Map customFactories = options.getCustomDecryptorFactories(); + Map customFactories = options.getCustomDecryptorFactories(); for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { Long keyId = publicKeyEncryptedData.getKeyID(); if (!customFactories.containsKey(keyId)) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index d5bf60db..f234ff00 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -11,6 +11,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.operator.PGPDataDecryptor; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import org.pgpainless.key.SubkeyIdentifier; import java.util.HashSet; import java.util.Set; @@ -74,16 +75,16 @@ public class HardwareSecurity { // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. private final PublicKeyDataDecryptorFactory factory = new BcPublicKeyDataDecryptorFactory(null); - private long keyId; + private SubkeyIdentifier subkey; /** * Create a new {@link HardwareDataDecryptorFactory}. * * @param callback decryption callback */ - public HardwareDataDecryptorFactory(long keyId, DecryptionCallback callback) { + public HardwareDataDecryptorFactory(SubkeyIdentifier subkeyIdentifier, DecryptionCallback callback) { this.callback = callback; - this.keyId = keyId; + this.subkey = subkeyIdentifier; } @Override @@ -91,7 +92,7 @@ public class HardwareSecurity { throws PGPException { try { // delegate decryption to the callback - return callback.decryptSessionKey(keyId, keyAlgorithm, secKeyData[0]); + return callback.decryptSessionKey(subkey.getSubkeyId(), keyAlgorithm, secKeyData[0]); } catch (HardwareSecurityException e) { throw new PGPException("Hardware-backed decryption failed.", e); } @@ -104,8 +105,14 @@ public class HardwareSecurity { } @Override - public long getKeyId() { - return keyId; + public PGPDataDecryptor createDataDecryptor(int aeadAlgorithm, byte[] iv, int chunkSize, int encAlgorithm, byte[] key) + throws PGPException { + return factory.createDataDecryptor(aeadAlgorithm, iv, chunkSize, encAlgorithm, key); + } + + @Override + public SubkeyIdentifier getSubkeyIdentifier() { + return subkey; } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java index 4ea894e5..73c3bf56 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java @@ -19,6 +19,7 @@ import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; @@ -74,7 +75,7 @@ public class CustomPublicKeyDataDecryptorFactoryTest { .withOptions(ConsumerOptions.get() .addCustomDecryptorFactory( new HardwareSecurity.HardwareDataDecryptorFactory( - encryptionKey.getKeyID(), + new SubkeyIdentifier(cert, encryptionKey.getKeyID()), hardwareDecryptionCallback))); ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); From 3d5916c545a125369141e5cde0fe535a7507febb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 16:08:01 +0200 Subject: [PATCH 0494/1204] Implement custom decryptor factories in pda --- .../OpenPgpMessageInputStream.java | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index dfb0df91..bba7b396 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -314,6 +314,20 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SortedESKs esks = new SortedESKs(encDataList); + // Try custom decryptor factory + for (SubkeyIdentifier subkeyIdentifier : options.getCustomDecryptorFactories().keySet()) { + PublicKeyDataDecryptorFactory decryptorFactory = options.getCustomDecryptorFactories().get(subkeyIdentifier); + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + if (pkesk.getKeyID() != subkeyIdentifier.getSubkeyId()) { + continue; + } + + if (decryptPKESKAndStream(subkeyIdentifier, decryptorFactory, pkesk)) { + return true; + } + } + } + // Try session key if (options.getSessionKey() != null) { SessionKey sessionKey = options.getSessionKey(); @@ -357,20 +371,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decrypted = skesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - sessionKey.getAlgorithm(), metadata.depth + 1); - encryptedData.sessionKey = sessionKey; - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + if (decryptSKESKAndStream(skesk, decryptorFactory)) { return true; - } catch (UnacceptableAlgorithmException e) { - throw e; - } catch (PGPException e) { - // Password mismatch? } } } @@ -393,28 +395,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), - metadata.depth + 1); - encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); - encryptedData.sessionKey = sessionKey; - - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { return true; - } catch (UnacceptableAlgorithmException e) { - throw e; - } catch (PGPException e) { - } } @@ -430,23 +417,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), - metadata.depth + 1); - encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); - encryptedData.sessionKey = sessionKey; - - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { return true; - } catch (PGPException e) { - // hm :/ } } } @@ -470,26 +444,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); - + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); - try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), - metadata.depth + 1); - encryptedData.decryptionKey = new SubkeyIdentifier(decryptionKey, keyId); - encryptedData.sessionKey = sessionKey; - - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { return true; - } catch (PGPException e) { - // hm :/ } } } @@ -501,6 +461,46 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } + private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) throws IOException, UnacceptableAlgorithmException { + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + sessionKey.getAlgorithm(), metadata.depth + 1); + encryptedData.sessionKey = sessionKey; + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + return true; + } catch (UnacceptableAlgorithmException e) { + throw e; + } catch (PGPException e) { + // Password mismatch? + } + return false; + } + + private boolean decryptPKESKAndStream(SubkeyIdentifier subkeyIdentifier, PublicKeyDataDecryptorFactory decryptorFactory, PGPPublicKeyEncryptedData pkesk) throws IOException { + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + metadata.depth + 1); + encryptedData.decryptionKey = subkeyIdentifier; + encryptedData.sessionKey = sessionKey; + + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + return true; + } catch (PGPException e) { + + } + return false; + } + private PGPSecretKey getDecryptionKey(PGPSecretKeyRing decryptionKeys, long keyId) { KeyRingInfo info = PGPainless.inspectKeyRing(decryptionKeys); if (info.getEncryptionSubkeys(EncryptionPurpose.ANY).contains(info.getPublicKey(keyId))) { From a9993fd86699bcaf0790dc7f5d8944d2a0f4ccbd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 16:13:18 +0200 Subject: [PATCH 0495/1204] Throw UnacceptableAlgEx for unencrypted encData --- .../decryption_verification/OpenPgpMessageInputStream.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index bba7b396..969f3717 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -480,7 +480,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } - private boolean decryptPKESKAndStream(SubkeyIdentifier subkeyIdentifier, PublicKeyDataDecryptorFactory decryptorFactory, PGPPublicKeyEncryptedData pkesk) throws IOException { + private boolean decryptPKESKAndStream(SubkeyIdentifier subkeyIdentifier, + PublicKeyDataDecryptorFactory decryptorFactory, + PGPPublicKeyEncryptedData pkesk) + throws IOException, UnacceptableAlgorithmException { try { InputStream decrypted = pkesk.getDataStream(decryptorFactory); SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); @@ -495,6 +498,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; + } catch (UnacceptableAlgorithmException e) { + throw e; } catch (PGPException e) { } From b7acb2a59c98690a5582db351730afe33a3c5311 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 19:35:25 +0200 Subject: [PATCH 0496/1204] Enable logging in tests --- pgpainless-core/src/test/resources/logback-test.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/test/resources/logback-test.xml b/pgpainless-core/src/test/resources/logback-test.xml index 7e4c3194..bb16e293 100644 --- a/pgpainless-core/src/test/resources/logback-test.xml +++ b/pgpainless-core/src/test/resources/logback-test.xml @@ -19,7 +19,7 @@ SPDX-License-Identifier: Apache-2.0 - + From a27c0ff36e10842ffc8c956e5a3b67afda549242 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Oct 2022 19:35:50 +0200 Subject: [PATCH 0497/1204] Add detailled logging to OpenPgpMessageInputStream --- .../OpenPgpMessageInputStream.java | 84 +++++++++++++++---- .../automaton/PDA.java | 13 ++- .../org/pgpainless/key/util/KeyIdUtil.java | 4 + 3 files changed, 82 insertions(+), 19 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 969f3717..1dbadbbe 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -59,6 +59,7 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.key.util.KeyIdUtil; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.consumer.OnePassSignatureCheck; @@ -234,6 +235,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Marker Packets need to be skipped and ignored case MARKER: + LOGGER.debug("Skipping Marker Packet"); packetInputStream.readMarker(); break; @@ -263,6 +265,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processLiteralData() throws IOException { + LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); automaton.next(InputAlphabet.LiteralData); PGPLiteralData literalData = packetInputStream.readLiteralData(); this.metadata.setChild(new MessageMetadata.LiteralData( @@ -279,6 +282,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm()), metadata.depth + 1); + LOGGER.debug("Compressed Data Packet (" + compressionLayer.algorithm + ") at depth " + metadata.depth + " encountered"); InputStream decompressed = compressedData.getDataStream(); nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer, policy); } @@ -286,6 +290,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processOnePassSignature() throws PGPException, IOException { automaton.next(InputAlphabet.OnePassSignature); PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); + LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + + "at depth " + metadata.depth + " encountered"); signatures.addOnePassSignature(onePassSignature); } @@ -294,42 +300,59 @@ public class OpenPgpMessageInputStream extends DecryptionStream { boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; automaton.next(InputAlphabet.Signature); PGPSignature signature = packetInputStream.readSignature(); + long keyId = SignatureUtils.determineIssuerKeyId(signature); if (isSigForOPS) { + LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key " + + KeyIdUtil.formatKeyId(keyId) + + " at depth " + metadata.depth + " encountered"); signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with signatures.addCorrespondingOnePassSignature(signature, metadata, policy); } else { + LOGGER.debug("Prepended Signature Packet by key " + + KeyIdUtil.formatKeyId(keyId) + + " at depth " + metadata.depth + " encountered"); signatures.addPrependedSignature(signature); } } private boolean processEncryptedData() throws IOException, PGPException { + LOGGER.debug("Symmetrically Encrypted Data Packet at depth " + metadata.depth + " encountered"); automaton.next(InputAlphabet.EncryptedData); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() // once BC ships it if (!encDataList.get(0).isIntegrityProtected()) { + LOGGER.debug("Symmetrically Encrypted Data Packet is not integrity-protected and is therefore rejected."); throw new MessageNotIntegrityProtectedException(); } SortedESKs esks = new SortedESKs(encDataList); + LOGGER.debug("Symmetrically Encrypted Integrity-Protected Data has " + + esks.skesks.size() + " SKESK(s) and " + + (esks.pkesks.size() + esks.anonPkesks.size()) + " PKESK(s) from which " + + esks.anonPkesks.size() + " PKESK(s) have an anonymous recipient"); - // Try custom decryptor factory + // Try custom decryptor factories for (SubkeyIdentifier subkeyIdentifier : options.getCustomDecryptorFactories().keySet()) { + LOGGER.debug("Attempt decryption with custom decryptor factory with key " + subkeyIdentifier); PublicKeyDataDecryptorFactory decryptorFactory = options.getCustomDecryptorFactories().get(subkeyIdentifier); for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + // find matching PKESK if (pkesk.getKeyID() != subkeyIdentifier.getSubkeyId()) { continue; } + // attempt decryption if (decryptPKESKAndStream(subkeyIdentifier, decryptorFactory, pkesk)) { return true; } } } - // Try session key + // Try provided session key if (options.getSessionKey() != null) { + LOGGER.debug("Attempt decryption with provided session key"); SessionKey sessionKey = options.getSessionKey(); throwIfUnacceptable(sessionKey.getAlgorithm()); @@ -340,6 +363,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { // TODO: Use BCs new API once shipped + // see https://github.com/bcgit/bc-java/pull/1228 (discussion) PGPEncryptedData esk = esks.all().get(0); if (esk instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; @@ -347,6 +371,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + LOGGER.debug("Successfully decrypted data with provided session key"); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; @@ -354,20 +379,29 @@ public class OpenPgpMessageInputStream extends DecryptionStream { encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + LOGGER.debug("Successfully decrypted data with provided session key"); return true; } else { throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); } } catch (PGPException e) { // Session key mismatch? + LOGGER.debug("Decryption using provided session key failed. Mismatched session key and message?", e); } } // Try passwords - for (PGPPBEEncryptedData skesk : esks.skesks) { - SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); - throwIfUnacceptable(kekAlgorithm); - for (Passphrase passphrase : options.getDecryptionPassphrases()) { + for (Passphrase passphrase : options.getDecryptionPassphrases()) { + for (PGPPBEEncryptedData skesk : esks.skesks) { + LOGGER.debug("Attempt decryption with provided passphrase"); + SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); + try { + throwIfUnacceptable(kekAlgorithm); + } catch (UnacceptableAlgorithmException e) { + LOGGER.debug("Skipping SKESK with unacceptable encapsulation algorithm", e); + continue; + } + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPBEDataDecryptorFactory(passphrase); @@ -381,22 +415,25 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Try (known) secret keys for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { long keyId = pkesk.getKeyID(); + LOGGER.debug("Encountered PKESK with recipient " + KeyIdUtil.formatKeyId(keyId)); PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); if (decryptionKeys == null) { + LOGGER.debug("Skipping PKESK because no matching key " + KeyIdUtil.formatKeyId(keyId) + " was provided"); continue; } PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); + LOGGER.debug("Attempt decryption using secret key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); // Postpone keys with missing passphrase if (!protector.hasPassphraseFor(keyId)) { + LOGGER.debug("Missing passphrase for key " + decryptionKeyId + ". Postponing decryption until all other keys were tried"); postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); continue; } - PGPSecretKey decryptionKey = decryptionKeys.getSecretKey(keyId); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, decryptionKey.getKeyID()); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, protector); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); @@ -408,16 +445,19 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // try anonymous secret keys for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { + PGPSecretKeyRing decryptionKeys = decryptionKeyCandidate.getA(); PGPSecretKey secretKey = decryptionKeyCandidate.getB(); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); + LOGGER.debug("Attempt decryption of anonymous PKESK with key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); if (!protector.hasPassphraseFor(secretKey.getKeyID())) { + LOGGER.debug("Missing passphrase for key " + decryptionKeyId + ". Postponing decryption until all other keys were tried."); postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); continue; } PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeyCandidate.getA(), privateKey.getKeyID()); if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { return true; @@ -442,9 +482,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSecretKey secretKey = missingPassphrases.getA(); long keyId = secretKey.getKeyID(); PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); + LOGGER.debug("Attempt decryption with key " + decryptionKeyId + " while interactively requesting its passphrase"); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); @@ -458,10 +499,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } // we did not yet succeed in decrypting any session key :/ + + LOGGER.debug("Failed to decrypt encrypted data packet"); return false; } - private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) throws IOException, UnacceptableAlgorithmException { + private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) + throws IOException, UnacceptableAlgorithmException { try { InputStream decrypted = skesk.getDataStream(decryptorFactory); SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); @@ -469,18 +513,19 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( sessionKey.getAlgorithm(), metadata.depth + 1); encryptedData.sessionKey = sessionKey; + LOGGER.debug("Successfully decrypted data with passphrase"); IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; } catch (PGPException e) { - // Password mismatch? + LOGGER.debug("Decryption of encrypted data packet using password failed. Password mismatch?", e); } return false; } - private boolean decryptPKESKAndStream(SubkeyIdentifier subkeyIdentifier, + private boolean decryptPKESKAndStream(SubkeyIdentifier decryptionKeyId, PublicKeyDataDecryptorFactory decryptorFactory, PGPPublicKeyEncryptedData pkesk) throws IOException, UnacceptableAlgorithmException { @@ -492,16 +537,17 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), metadata.depth + 1); - encryptedData.decryptionKey = subkeyIdentifier; + encryptedData.decryptionKey = decryptionKeyId; encryptedData.sessionKey = sessionKey; + LOGGER.debug("Successfully decrypted data with key " + decryptionKeyId); IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; } catch (PGPException e) { - + LOGGER.debug("Decryption of encrypted data packet using secret key failed.", e); } return false; } @@ -823,8 +869,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(signature); SignatureVerifier.verifyOnePassSignature(signature, onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()), onePassSignature, policy); + LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedOnePassSignature(verification); } catch (SignatureValidationException e) { + LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); } break; @@ -929,8 +977,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { detached.getSignature(), detached.getSigningKeyRing().getPublicKey(detached.getSigningKeyIdentifier().getKeyId()), policy, detached.getSignature().getCreationTime()); + LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedDetachedSignature(verification); } catch (SignatureValidationException e) { + LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); } } @@ -944,8 +994,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { prepended.getSignature(), prepended.getSigningKeyRing().getPublicKey(prepended.getSigningKeyIdentifier().getKeyId()), policy, prepended.getSignature().getCreationTime()); + LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedPrependedSignature(verification); } catch (SignatureValidationException e) { + LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java index 156dc2ed..3df3c5b5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java @@ -5,6 +5,8 @@ package org.pgpainless.decryption_verification.automaton; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Stack; @@ -15,11 +17,11 @@ import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ter public class PDA { private static int ID = 0; + private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); /** * Set of states of the automaton. - * Each state defines its valid transitions in their {@link NestingPDA.State#transition(InputAlphabet, NestingPDA)} - * method. + * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, PDA)} method. */ public enum State { @@ -199,7 +201,12 @@ public class PDA { } public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { - state = state.transition(input, this); + try { + state = state.transition(input, this); + } catch (MalformedOpenPgpMessageException e) { + LOGGER.debug("Unexpected Packet or Token '" + input + "' encountered. Message is malformed.", e); + throw e; + } } /** diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java index e6ab4f48..7ebac8fd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java @@ -30,4 +30,8 @@ public final class KeyIdUtil { return new BigInteger(longKeyId, 16).longValue(); } + + public static String formatKeyId(long keyId) { + return Long.toHexString(keyId).toUpperCase(); + } } From 977f8c4101fdf5e2cd42049d1c2ac9f116a7b108 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 00:31:25 +0200 Subject: [PATCH 0498/1204] Rename automaton package to syntax_check --- .../OpenPgpMessageInputStream.java | 33 ++++++++++--------- .../InputAlphabet.java | 2 +- .../{automaton => syntax_check}/PDA.java | 8 ++--- .../StackAlphabet.java | 2 +- .../package-info.java | 2 +- .../MalformedOpenPgpMessageException.java | 6 ++-- .../{automaton => syntax_check}/PDATest.java | 2 +- 7 files changed, 28 insertions(+), 27 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/InputAlphabet.java (95%) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/PDA.java (96%) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/StackAlphabet.java (89%) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/package-info.java (78%) rename pgpainless-core/src/test/java/org/pgpainless/decryption_verification/{automaton => syntax_check}/PDATest.java (97%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 1dbadbbe..a2fa2351 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -43,9 +43,9 @@ import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.decryption_verification.automaton.InputAlphabet; -import org.pgpainless.decryption_verification.automaton.PDA; -import org.pgpainless.decryption_verification.automaton.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.InputAlphabet; +import org.pgpainless.decryption_verification.syntax_check.PDA; +import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.exception.MalformedOpenPgpMessageException; @@ -80,7 +80,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Options to consume the data protected final ConsumerOptions options; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message - protected final PDA automaton = new PDA(); + protected final PDA syntaxVerifier = new PDA(); // InputStream of OpenPGP packets protected TeeBCPGInputStream packetInputStream; // InputStream of a nested data packet @@ -266,7 +266,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processLiteralData() throws IOException { LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); - automaton.next(InputAlphabet.LiteralData); + syntaxVerifier.next(InputAlphabet.LiteralData); PGPLiteralData literalData = packetInputStream.readLiteralData(); this.metadata.setChild(new MessageMetadata.LiteralData( literalData.getFileName(), @@ -276,7 +276,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processCompressedData() throws IOException, PGPException { - automaton.next(InputAlphabet.CompressedData); + syntaxVerifier.next(InputAlphabet.CompressedData); signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( @@ -288,7 +288,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processOnePassSignature() throws PGPException, IOException { - automaton.next(InputAlphabet.OnePassSignature); + syntaxVerifier.next(InputAlphabet.OnePassSignature); PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + "at depth " + metadata.depth + " encountered"); @@ -297,8 +297,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processSignature() throws PGPException, IOException { // true if Signature corresponds to OnePassSignature - boolean isSigForOPS = automaton.peekStack() == StackAlphabet.ops; - automaton.next(InputAlphabet.Signature); + boolean isSigForOPS = syntaxVerifier.peekStack() == StackAlphabet.ops; + syntaxVerifier.next(InputAlphabet.Signature); PGPSignature signature = packetInputStream.readSignature(); long keyId = SignatureUtils.determineIssuerKeyId(signature); if (isSigForOPS) { @@ -317,7 +317,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private boolean processEncryptedData() throws IOException, PGPException { LOGGER.debug("Symmetrically Encrypted Data Packet at depth " + metadata.depth + " encountered"); - automaton.next(InputAlphabet.EncryptedData); + syntaxVerifier.next(InputAlphabet.EncryptedData); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() @@ -601,7 +601,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Override public int read() throws IOException { if (nestedInputStream == null) { - automaton.assertValid(); + syntaxVerifier.assertValid(); return -1; } @@ -638,7 +638,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (nestedInputStream == null) { if (packetInputStream != null) { - automaton.assertValid(); + syntaxVerifier.assertValid(); } return -1; } @@ -668,7 +668,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { super.close(); if (closed) { if (packetInputStream != null) { - automaton.assertValid(); + syntaxVerifier.assertValid(); } return; } @@ -686,8 +686,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } if (packetInputStream != null) { - automaton.next(InputAlphabet.EndOfSequence); - automaton.assertValid(); + syntaxVerifier.next(InputAlphabet.EndOfSequence); + syntaxVerifier.assertValid(); packetInputStream.close(); } closed = true; @@ -717,7 +717,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (PGPEncryptedData esk : esks) { if (esk instanceof PGPPBEEncryptedData) { skesks.add((PGPPBEEncryptedData) esk); - } else if (esk instanceof PGPPublicKeyEncryptedData) { + } + else if (esk instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; if (pkesk.getKeyID() != 0) { pkesks.add(pkesk); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java similarity index 95% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java index ad2a8c55..f73ede34 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/InputAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java similarity index 96% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 3df3c5b5..0d6ba28c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.slf4j.Logger; @@ -10,9 +10,9 @@ import org.slf4j.LoggerFactory; import java.util.Stack; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.ops; -import static org.pgpainless.decryption_verification.automaton.StackAlphabet.terminus; +import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.msg; +import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.ops; +import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.terminus; public class PDA { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java similarity index 89% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java index 09865f31..6030fbc8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/StackAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; public enum StackAlphabet { /** diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java similarity index 78% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java index 80a79e85..4df6af5a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/automaton/package-info.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java @@ -5,4 +5,4 @@ /** * Pushdown Automaton to verify validity of packet sequences according to the OpenPGP Message format. */ -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index 0069209c..cbe78c05 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -4,9 +4,9 @@ package org.pgpainless.exception; -import org.pgpainless.decryption_verification.automaton.InputAlphabet; -import org.pgpainless.decryption_verification.automaton.PDA; -import org.pgpainless.decryption_verification.automaton.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.InputAlphabet; +import org.pgpainless.decryption_verification.syntax_check.PDA; +import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; /** * Exception that gets thrown if the OpenPGP message is malformed. diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java similarity index 97% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 6dbb2cd6..346e1f3b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/automaton/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -1,4 +1,4 @@ -package org.pgpainless.decryption_verification.automaton; +package org.pgpainless.decryption_verification.syntax_check; import org.junit.jupiter.api.Test; import org.pgpainless.exception.MalformedOpenPgpMessageException; From 3977d1f40785ef71fdafa9b41d6a3bd3cdc2a899 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 00:40:40 +0200 Subject: [PATCH 0499/1204] Add more direct PDA tests --- .../syntax_check/PDATest.java | 86 ++++++++++++++----- 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 346e1f3b..e4877d94 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -1,10 +1,11 @@ package org.pgpainless.decryption_verification.syntax_check; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + import org.junit.jupiter.api.Test; import org.pgpainless.exception.MalformedOpenPgpMessageException; -import static org.junit.jupiter.api.Assertions.assertTrue; - public class PDATest { @@ -15,11 +16,11 @@ public class PDATest { */ @Test public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { - PDA automaton = new PDA(); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); + PDA check = new PDA(); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.EndOfSequence); - assertTrue(automaton.isValid()); + assertTrue(check.isValid()); } /** @@ -29,13 +30,13 @@ public class PDATest { */ @Test public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { - PDA automaton = new PDA(); - automaton.next(InputAlphabet.OnePassSignature); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.Signature); - automaton.next(InputAlphabet.EndOfSequence); + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); - assertTrue(automaton.isValid()); + assertTrue(check.isValid()); } @@ -46,12 +47,12 @@ public class PDATest { */ @Test public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { - PDA automaton = new PDA(); - automaton.next(InputAlphabet.Signature); - automaton.next(InputAlphabet.LiteralData); - automaton.next(InputAlphabet.EndOfSequence); + PDA check = new PDA(); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.EndOfSequence); - assertTrue(automaton.isValid()); + assertTrue(check.isValid()); } @@ -62,14 +63,53 @@ public class PDATest { */ @Test public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { - PDA automaton = new PDA(); - automaton.next(InputAlphabet.OnePassSignature); - automaton.next(InputAlphabet.CompressedData); + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.CompressedData); // Here would be a nested PDA for the LiteralData packet - automaton.next(InputAlphabet.Signature); - automaton.next(InputAlphabet.EndOfSequence); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); - assertTrue(automaton.isValid()); + assertTrue(check.isValid()); } + @Test + public void testTwoLiteralDataIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.LiteralData)); + } + + @Test + public void testTrailingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.Signature)); + } + + @Test + public void testOPSWithPrependedSigIsValid() { + PDA check = new PDA(); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); + + assertTrue(check.isValid()); + } + + @Test + public void testPrependedSigInsideOPSSignedMessageIsValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); + + assertTrue(check.isValid()); + } } From 54cb9dad713fb466ce234490838f1e670c75f0b6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 13:46:33 +0200 Subject: [PATCH 0500/1204] Further increase coverage of PDA class --- .../syntax_check/PDATest.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index e4877d94..8fa107f6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -73,6 +73,42 @@ public class PDATest { assertTrue(check.isValid()); } + @Test + public void testOPSSignedEncryptedMessageIsValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.EncryptedData); + check.next(InputAlphabet.Signature); + check.next(InputAlphabet.EndOfSequence); + assertTrue(check.isValid()); + } + + @Test + public void anyInputAfterEOSIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.LiteralData); + check.next(InputAlphabet.EndOfSequence); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.Signature)); + } + + @Test + public void testEncryptedMessageWithAppendedStandalongSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.EncryptedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.Signature)); + } + + @Test + public void testOPSSignedEncryptedMessageWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.EncryptedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.EndOfSequence)); + } + @Test public void testTwoLiteralDataIsNotValid() { PDA check = new PDA(); @@ -89,6 +125,48 @@ public class PDATest { () -> check.next(InputAlphabet.Signature)); } + @Test + public void testOPSAloneIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.EndOfSequence)); + } + + @Test + public void testOPSLitWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.EndOfSequence)); + } + + @Test + public void testCompressedMessageWithStandalongAppendedSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.Signature)); + } + + @Test + public void testOPSCompressedDataWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.OnePassSignature); + check.next(InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.EndOfSequence)); + } + + @Test + public void testCompressedMessageFollowedByTrailingLiteralDataIsNotValid() { + PDA check = new PDA(); + check.next(InputAlphabet.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputAlphabet.LiteralData)); + } + @Test public void testOPSWithPrependedSigIsValid() { PDA check = new PDA(); From 3f8653cf2ea831d9d68bc40f6d1b72cd510d8531 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 16:16:45 +0200 Subject: [PATCH 0501/1204] Fix CRCing test and fully depend on new stream for decryption --- .../DecryptionBuilder.java | 2 +- .../MessageMetadata.java | 22 ++- .../OpenPgpMessageInputStream.java | 152 +++++++++++++----- .../TeeBCPGInputStream.java | 14 +- .../OpenPgpMessageInputStreamTest.java | 2 +- 5 files changed, 137 insertions(+), 55 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java index 68a68847..0baf4124 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java @@ -31,7 +31,7 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { throw new IllegalArgumentException("Consumer options cannot be null."); } - return DecryptionStreamFactory.create(inputStream, consumerOptions); + return OpenPgpMessageInputStream.create(inputStream, consumerOptions); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 59f8052a..7811578e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -164,19 +164,35 @@ public class MessageMetadata { } public String getFilename() { - return findLiteralData().getFileName(); + LiteralData literalData = findLiteralData(); + if (literalData == null) { + return null; + } + return literalData.getFileName(); } public Date getModificationDate() { - return findLiteralData().getModificationDate(); + LiteralData literalData = findLiteralData(); + if (literalData == null) { + return null; + } + return literalData.getModificationDate(); } public StreamEncoding getFormat() { - return findLiteralData().getFormat(); + LiteralData literalData = findLiteralData(); + if (literalData == null) { + return null; + } + return literalData.getFormat(); } private LiteralData findLiteralData() { Nested nested = message.child; + if (nested == null) { + return null; + } + while (nested.hasNestedChild()) { Layer layer = (Layer) nested; nested = layer.child; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index a2fa2351..dee59058 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -37,6 +37,8 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; +import org.bouncycastle.util.io.Streams; +import org.bouncycastle.util.io.TeeInputStream; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; @@ -79,32 +81,87 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Options to consume the data protected final ConsumerOptions options; + + private final Policy policy; // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message protected final PDA syntaxVerifier = new PDA(); // InputStream of OpenPGP packets protected TeeBCPGInputStream packetInputStream; - // InputStream of a nested data packet + // InputStream of a data packet containing nested data protected InputStream nestedInputStream; private boolean closed = false; private final Signatures signatures; private final MessageMetadata.Layer metadata; - private final Policy policy; - public OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + /** + * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of + * OpenPGP messages and signatures. + * This constructor will use the global PGPainless {@link Policy}. + * + * @param inputStream underlying input stream + * @param options options for consuming the stream + * + * @throws IOException in case of an IO error + * @throws PGPException in case of an OpenPGP error + */ + public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, @Nonnull ConsumerOptions options) throws IOException, PGPException { - this(inputStream, options, PGPainless.getPolicy()); + return create(inputStream, options, PGPainless.getPolicy()); } - public OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + /** + * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of + * OpenPGP messages and signatures. + * + * @param inputStream underlying input stream containing the OpenPGP message + * @param options options for consuming the message + * @param policy policy for acceptable algorithms etc. + * + * @throws PGPException in case of an OpenPGP error + * @throws IOException in case of an IO error + */ + public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, @Nonnull ConsumerOptions options, @Nonnull Policy policy) throws PGPException, IOException { - this( - prepareInputStream(inputStream, options, policy), - options, new MessageMetadata.Message(), policy); + return create(inputStream, options, new MessageMetadata.Message(), policy); + } + + protected static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) + throws IOException, PGPException { + OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); + openPgpIn.reset(); + + if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) { + return new OpenPgpMessageInputStream(Type.non_openpgp, + openPgpIn, options, metadata, policy); + } + + if (openPgpIn.isBinaryOpenPgp()) { + // Simply consume OpenPGP message + return new OpenPgpMessageInputStream(Type.standard, + openPgpIn, options, metadata, policy); + } + + if (openPgpIn.isAsciiArmored()) { + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); + if (armorIn.isClearText()) { + return new OpenPgpMessageInputStream(Type.cleartext_signed, + armorIn, options, metadata, policy); + } else { + // Simply consume dearmored OpenPGP message + return new OpenPgpMessageInputStream(Type.standard, + armorIn, options, metadata, policy); + } + } else { + throw new AssertionError("Huh?"); + } } protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, @@ -131,52 +188,56 @@ public class OpenPgpMessageInputStream extends DecryptionStream { consumePackets(); } - protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, - @Nonnull Policy policy, - @Nonnull ConsumerOptions options) { + enum Type { + standard, + cleartext_signed, + non_openpgp + } + + protected OpenPgpMessageInputStream(@Nonnull Type type, + @Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) throws PGPException, IOException { super(OpenPgpMetadata.getBuilder()); this.policy = policy; this.options = options; - this.metadata = new MessageMetadata.Message(); + this.metadata = metadata; this.signatures = new Signatures(options); - this.signatures.addDetachedSignatures(options.getDetachedSignatures()); - this.packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); - } - private static InputStream prepareInputStream(InputStream inputStream, ConsumerOptions options, Policy policy) - throws IOException, PGPException { - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); - openPgpIn.reset(); - - if (openPgpIn.isBinaryOpenPgp()) { - return openPgpIn; + if (metadata instanceof MessageMetadata.Message) { + this.signatures.addDetachedSignatures(options.getDetachedSignatures()); } - if (openPgpIn.isAsciiArmored()) { - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); - if (armorIn.isClearText()) { - return parseCleartextSignedMessage(armorIn, options, policy); - } else { - return armorIn; - } - } else { - return openPgpIn; + switch (type) { + case standard: + // tee out packet bytes for signature verification + packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), this.signatures); + + // *omnomnom* + consumePackets(); + break; + case cleartext_signed: + MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); + PGPSignatureList detachedSignatures = ClearsignedMessageUtil + .detachSignaturesFromInbandClearsignedMessage( + inputStream, multiPassStrategy.getMessageOutputStream()); + + for (PGPSignature signature : detachedSignatures) { + options.addVerificationOfDetachedSignature(signature); + } + + options.forceNonOpenPgpData(); + packetInputStream = null; + nestedInputStream = new TeeInputStream(multiPassStrategy.getMessageInputStream(), this.signatures); + break; + case non_openpgp: + packetInputStream = null; + nestedInputStream = new TeeInputStream(inputStream, this.signatures); + break; } } - private static DecryptionStream parseCleartextSignedMessage(ArmoredInputStream armorIn, ConsumerOptions options, Policy policy) - throws IOException, PGPException { - MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); - PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream()); - - for (PGPSignature signature : signatures) { - options.addVerificationOfDetachedSignature(signature); - } - - options.forceNonOpenPgpData(); - return new OpenPgpMessageInputStream(multiPassStrategy.getMessageInputStream(), policy, options); - } - /** * Consume OpenPGP packets from the current {@link BCPGInputStream}. * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, @@ -196,6 +257,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void consumePackets() throws IOException, PGPException { OpenPgpPacket nextPacket; + if (packetInputStream == null) { + return; + } loop: // we break this when we go deeper. while ((nextPacket = packetInputStream.nextPacketTag()) != null) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index 2efcfc43..1ca1b3f9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -4,6 +4,11 @@ package org.pgpainless.decryption_verification; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.NoSuchElementException; + import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.bcpg.MarkerPacket; import org.bouncycastle.bcpg.Packet; @@ -15,11 +20,6 @@ import org.bouncycastle.openpgp.PGPOnePassSignature; import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.algorithm.OpenPgpPacket; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.NoSuchElementException; - /** * Since we need to update signatures with data from the underlying stream, this class is used to tee out the data. * Unfortunately we cannot simply override {@link BCPGInputStream#read()} to tee the data out though, since @@ -96,7 +96,6 @@ public class TeeBCPGInputStream { return markerPacket; } - public void close() throws IOException { this.packetInputStream.close(); this.delayedTee.close(); @@ -122,6 +121,9 @@ public class TeeBCPGInputStream { last = inputStream.read(); return last; } catch (IOException e) { + if ("crc check failed in armored message.".equals(e.getMessage())) { + throw e; + } return -1; } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index a8969047..f9539149 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -677,7 +677,7 @@ public class OpenPgpMessageInputStreamTest { throws IOException, PGPException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - OpenPgpMessageInputStream pgpIn = new OpenPgpMessageInputStream(armorIn, options); + OpenPgpMessageInputStream pgpIn = OpenPgpMessageInputStream.create(armorIn, options); return pgpIn; } } From e281143d4873f4f2d3f2623b57471a622c54dba3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Oct 2022 16:17:30 +0200 Subject: [PATCH 0502/1204] Delete old DecryptionStreamFactory --- .../DecryptionStreamFactory.java | 692 ------------------ 1 file changed, 692 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java deleted file mode 100644 index 451750ee..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ /dev/null @@ -1,692 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPOnePassSignature; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPPBEEncryptedData; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSessionKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.EncryptionPurpose; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; -import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; -import org.pgpainless.exception.FinalIOException; -import org.pgpainless.exception.MessageNotIntegrityProtectedException; -import org.pgpainless.exception.MissingDecryptionMethodException; -import org.pgpainless.exception.MissingLiteralDataException; -import org.pgpainless.exception.MissingPassphraseException; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.exception.UnacceptableAlgorithmException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.consumer.SignatureCheck; -import org.pgpainless.signature.consumer.OnePassSignatureCheck; -import org.pgpainless.util.ArmoredInputStreamFactory; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.SessionKey; -import org.pgpainless.util.Tuple; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class DecryptionStreamFactory { - - - private static final Logger LOGGER = LoggerFactory.getLogger(DecryptionStreamFactory.class); - // Maximum nesting depth of packets (e.g. compression, encryption...) - private static final int MAX_PACKET_NESTING_DEPTH = 16; - - private final ConsumerOptions options; - private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - private final List onePassSignatureChecks = new ArrayList<>(); - private final List signatureChecks = new ArrayList<>(); - private final Map onePassSignaturesWithMissingCert = new HashMap<>(); - - private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = - ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(); - private IntegrityProtectedInputStream integrityProtectedEncryptedInputStream; - - - public static DecryptionStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) - throws PGPException, IOException { - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(inputStream); - openPgpInputStream.reset(); - if (openPgpInputStream.isBinaryOpenPgp()) { - return new OpenPgpMessageInputStream(openPgpInputStream, options); - } else if (openPgpInputStream.isAsciiArmored()) { - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpInputStream); - if (armorIn.isClearText()) { - return createOld(openPgpInputStream, options); - } else { - return new OpenPgpMessageInputStream(armorIn, options); - } - } else if (openPgpInputStream.isNonOpenPgp()) { - return createOld(openPgpInputStream, options); - } else { - throw new IOException("What?"); - } - } - - public static DecryptionStream createOld(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) throws IOException, PGPException { - DecryptionStreamFactory factory = new DecryptionStreamFactory(options); - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); - return factory.parseOpenPGPDataAndCreateDecryptionStream(openPgpIn); - } - - public DecryptionStreamFactory(ConsumerOptions options) { - this.options = options; - initializeDetachedSignatures(options.getDetachedSignatures()); - } - - private void initializeDetachedSignatures(Set signatures) { - for (PGPSignature signature : signatures) { - long issuerKeyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing signingKeyRing = findSignatureVerificationKeyRing(issuerKeyId); - if (signingKeyRing == null) { - SignatureValidationException ex = new SignatureValidationException( - "Missing verification certificate " + Long.toHexString(issuerKeyId)); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, null), ex); - continue; - } - PGPPublicKey signingKey = signingKeyRing.getPublicKey(issuerKeyId); - SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(signingKeyRing, signingKey.getKeyID()); - try { - signature.init(verifierBuilderProvider, signingKey); - SignatureCheck detachedSignature = - new SignatureCheck(signature, signingKeyRing, signingKeyIdentifier); - signatureChecks.add(detachedSignature); - } catch (PGPException e) { - SignatureValidationException ex = new SignatureValidationException( - "Cannot verify detached signature made by " + signingKeyIdentifier + ".", e); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, signingKeyIdentifier), ex); - } - } - } - - private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(OpenPgpInputStream openPgpIn) - throws IOException, PGPException { - - InputStream pgpInStream; - InputStream outerDecodingStream; - PGPObjectFactory objectFactory; - - // Non-OpenPGP data. We are probably verifying detached signatures - if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) { - outerDecodingStream = openPgpIn; - pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null); - return new DecryptionStreamImpl(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); - } - - // Data appears to be OpenPGP message, - // or we handle it as such, since user provided a session-key for decryption - if (openPgpIn.isLikelyOpenPgpMessage() || - (openPgpIn.isBinaryOpenPgp() && options.getSessionKey() != null)) { - outerDecodingStream = openPgpIn; - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); - // Parse OpenPGP message - pgpInStream = processPGPPackets(objectFactory, 1); - return new DecryptionStreamImpl(pgpInStream, - resultBuilder, integrityProtectedEncryptedInputStream, null); - } - - if (openPgpIn.isAsciiArmored()) { - ArmoredInputStream armoredInputStream = ArmoredInputStreamFactory.get(openPgpIn); - if (armoredInputStream.isClearText()) { - resultBuilder.setCleartextSigned(); - return parseCleartextSignedMessage(armoredInputStream); - } else { - outerDecodingStream = armoredInputStream; - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); - // Parse OpenPGP message - pgpInStream = processPGPPackets(objectFactory, 1); - return new DecryptionStreamImpl(pgpInStream, - resultBuilder, integrityProtectedEncryptedInputStream, - outerDecodingStream); - } - } - - throw new PGPException("Not sure how to handle the input stream."); - } - - private DecryptionStreamImpl parseCleartextSignedMessage(ArmoredInputStream armorIn) - throws IOException, PGPException { - resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) - .setFileEncoding(StreamEncoding.TEXT); - - MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); - PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream()); - - for (PGPSignature signature : signatures) { - options.addVerificationOfDetachedSignature(signature); - } - - initializeDetachedSignatures(options.getDetachedSignatures()); - - InputStream verifyIn = wrapInVerifySignatureStream(multiPassStrategy.getMessageInputStream(), null); - return new DecryptionStreamImpl(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream, - null); - } - - private InputStream wrapInVerifySignatureStream(InputStream bufferedIn, @Nullable PGPObjectFactory objectFactory) { - return new SignatureInputStream.VerifySignatures( - bufferedIn, objectFactory, onePassSignatureChecks, - onePassSignaturesWithMissingCert, signatureChecks, options, - resultBuilder); - } - - private InputStream processPGPPackets(@Nonnull PGPObjectFactory objectFactory, int depth) - throws IOException, PGPException { - if (depth >= MAX_PACKET_NESTING_DEPTH) { - throw new PGPException("Maximum depth of nested packages exceeded."); - } - Object nextPgpObject; - try { - while ((nextPgpObject = objectFactory.nextObject()) != null) { - if (nextPgpObject instanceof PGPEncryptedDataList) { - return processPGPEncryptedDataList((PGPEncryptedDataList) nextPgpObject, depth); - } - if (nextPgpObject instanceof PGPCompressedData) { - return processPGPCompressedData((PGPCompressedData) nextPgpObject, depth); - } - if (nextPgpObject instanceof PGPOnePassSignatureList) { - return processOnePassSignatureList(objectFactory, (PGPOnePassSignatureList) nextPgpObject, depth); - } - if (nextPgpObject instanceof PGPLiteralData) { - return processPGPLiteralData(objectFactory, (PGPLiteralData) nextPgpObject, depth); - } - } - } catch (FinalIOException e) { - throw e; - } catch (IOException e) { - if (depth == 1 && e.getMessage().contains("invalid armor")) { - throw e; - } - if (depth == 1 && e.getMessage().contains("unknown object in stream:")) { - throw new MissingLiteralDataException("No Literal Data Packet found."); - } else { - throw new FinalIOException(e); - } - } - - throw new MissingLiteralDataException("No Literal Data Packet found"); - } - - private InputStream processPGPEncryptedDataList(PGPEncryptedDataList pgpEncryptedDataList, int depth) - throws PGPException, IOException { - LOGGER.debug("Depth {}: Encountered PGPEncryptedDataList", depth); - - SessionKey sessionKey = options.getSessionKey(); - if (sessionKey != null) { - integrityProtectedEncryptedInputStream = decryptWithProvidedSessionKey(pgpEncryptedDataList, sessionKey); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(integrityProtectedEncryptedInputStream); - return processPGPPackets(factory, ++depth); - } - - InputStream decryptedDataStream = decryptSessionKey(pgpEncryptedDataList); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decryptedDataStream); - return processPGPPackets(factory, ++depth); - } - - private IntegrityProtectedInputStream decryptWithProvidedSessionKey( - PGPEncryptedDataList pgpEncryptedDataList, - SessionKey sessionKey) - throws PGPException { - SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getSessionKeyDataDecryptorFactory(sessionKey); - InputStream decryptedDataStream = null; - PGPEncryptedData encryptedData = null; - for (PGPEncryptedData pgpEncryptedData : pgpEncryptedDataList) { - encryptedData = pgpEncryptedData; - if (!options.isIgnoreMDCErrors() && !encryptedData.isIntegrityProtected()) { - throw new MessageNotIntegrityProtectedException(); - } - - if (encryptedData instanceof PGPPBEEncryptedData) { - PGPPBEEncryptedData pbeEncrypted = (PGPPBEEncryptedData) encryptedData; - decryptedDataStream = pbeEncrypted.getDataStream(decryptorFactory); - break; - } else if (encryptedData instanceof PGPPublicKeyEncryptedData) { - PGPPublicKeyEncryptedData pkEncrypted = (PGPPublicKeyEncryptedData) encryptedData; - decryptedDataStream = pkEncrypted.getDataStream(decryptorFactory); - break; - } - } - - if (decryptedDataStream == null) { - throw new PGPException("No valid PGP data encountered."); - } - - resultBuilder.setSessionKey(sessionKey); - throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); - integrityProtectedEncryptedInputStream = - new IntegrityProtectedInputStream(decryptedDataStream, encryptedData, options); - return integrityProtectedEncryptedInputStream; - } - - private InputStream processPGPCompressedData(PGPCompressedData pgpCompressedData, int depth) - throws PGPException, IOException { - try { - CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.requireFromId(pgpCompressedData.getAlgorithm()); - LOGGER.debug("Depth {}: Encountered PGPCompressedData: {}", depth, compressionAlgorithm); - resultBuilder.setCompressionAlgorithm(compressionAlgorithm); - } catch (NoSuchElementException e) { - throw new PGPException("Unknown compression algorithm encountered.", e); - } - - InputStream inflatedDataStream = pgpCompressedData.getDataStream(); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(inflatedDataStream); - - return processPGPPackets(objectFactory, ++depth); - } - - private InputStream processOnePassSignatureList( - @Nonnull PGPObjectFactory objectFactory, - PGPOnePassSignatureList onePassSignatures, - int depth) - throws PGPException, IOException { - LOGGER.debug("Depth {}: Encountered PGPOnePassSignatureList of size {}", depth, onePassSignatures.size()); - initOnePassSignatures(onePassSignatures); - return processPGPPackets(objectFactory, depth); - } - - private InputStream processPGPLiteralData( - @Nonnull PGPObjectFactory objectFactory, - PGPLiteralData pgpLiteralData, - int depth) { - LOGGER.debug("Depth {}: Found PGPLiteralData", depth); - InputStream literalDataInputStream = pgpLiteralData.getInputStream(); - - resultBuilder.setFileName(pgpLiteralData.getFileName()) - .setModificationDate(pgpLiteralData.getModificationTime()) - .setFileEncoding(StreamEncoding.requireFromCode(pgpLiteralData.getFormat())); - - if (onePassSignatureChecks.isEmpty() && onePassSignaturesWithMissingCert.isEmpty()) { - LOGGER.debug("No OnePassSignatures found -> We are done"); - return literalDataInputStream; - } - - return new SignatureInputStream.VerifySignatures(literalDataInputStream, objectFactory, - onePassSignatureChecks, onePassSignaturesWithMissingCert, signatureChecks, options, resultBuilder) { - }; - } - - private InputStream decryptSessionKey(@Nonnull PGPEncryptedDataList encryptedDataList) - throws PGPException { - Iterator encryptedDataIterator = encryptedDataList.getEncryptedDataObjects(); - if (!encryptedDataIterator.hasNext()) { - throw new PGPException("Decryption failed - EncryptedDataList has no items"); - } - - PGPPrivateKey decryptionKey = null; - PGPPublicKeyEncryptedData encryptedSessionKey = null; - - List passphraseProtected = new ArrayList<>(); - List publicKeyProtected = new ArrayList<>(); - List> postponedDueToMissingPassphrase = new ArrayList<>(); - - // Sort PKESK and SKESK packets - while (encryptedDataIterator.hasNext()) { - PGPEncryptedData encryptedData = encryptedDataIterator.next(); - - if (!encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) { - throw new MessageNotIntegrityProtectedException(); - } - - // SKESK - if (encryptedData instanceof PGPPBEEncryptedData) { - passphraseProtected.add((PGPPBEEncryptedData) encryptedData); - } - // PKESK - else if (encryptedData instanceof PGPPublicKeyEncryptedData) { - publicKeyProtected.add((PGPPublicKeyEncryptedData) encryptedData); - } - } - - // Try decryption with passphrases first - for (PGPPBEEncryptedData pbeEncryptedData : passphraseProtected) { - for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory passphraseDecryptor = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decryptedDataStream = pbeEncryptedData.getDataStream(passphraseDecryptor); - - PGPSessionKey pgpSessionKey = pbeEncryptedData.getSessionKey(passphraseDecryptor); - SessionKey sessionKey = new SessionKey(pgpSessionKey); - resultBuilder.setSessionKey(sessionKey); - - throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); - - integrityProtectedEncryptedInputStream = - new IntegrityProtectedInputStream(decryptedDataStream, pbeEncryptedData, options); - - return integrityProtectedEncryptedInputStream; - } catch (PGPException e) { - LOGGER.debug("Probable passphrase mismatch, skip PBE encrypted data block", e); - } - } - } - - // Try custom PublicKeyDataDecryptorFactories (e.g. hardware-backed). - Map customFactories = options.getCustomDecryptorFactories(); - for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { - Long keyId = publicKeyEncryptedData.getKeyID(); - if (!customFactories.containsKey(keyId)) { - continue; - } - - PublicKeyDataDecryptorFactory decryptorFactory = customFactories.get(keyId); - try { - InputStream decryptedDataStream = publicKeyEncryptedData.getDataStream(decryptorFactory); - PGPSessionKey pgpSessionKey = publicKeyEncryptedData.getSessionKey(decryptorFactory); - SessionKey sessionKey = new SessionKey(pgpSessionKey); - resultBuilder.setSessionKey(sessionKey); - - throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); - - integrityProtectedEncryptedInputStream = - new IntegrityProtectedInputStream(decryptedDataStream, publicKeyEncryptedData, options); - - return integrityProtectedEncryptedInputStream; - } catch (PGPException e) { - LOGGER.debug("Decryption with custom PublicKeyDataDecryptorFactory failed", e); - } - } - - // Then try decryption with public key encryption - for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { - PGPPrivateKey privateKey = null; - if (options.getDecryptionKeys().isEmpty()) { - break; - } - - long keyId = publicKeyEncryptedData.getKeyID(); - // Wildcard KeyID - if (keyId == 0L) { - LOGGER.debug("Hidden recipient detected. Try to decrypt with all available secret keys."); - for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { - if (privateKey != null) { - break; - } - KeyRingInfo info = new KeyRingInfo(secretKeys); - List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); - for (PGPPublicKey pubkey : encryptionSubkeys) { - PGPSecretKey secretKey = secretKeys.getSecretKey(pubkey.getKeyID()); - // Skip missing secret key - if (secretKey == null) { - continue; - } - - privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, - postponedDueToMissingPassphrase, true); - } - } - } - // Non-wildcard key-id - else { - LOGGER.debug("PGPEncryptedData is encrypted for key {}", Long.toHexString(keyId)); - resultBuilder.addRecipientKeyId(keyId); - - PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId); - if (secretKeys == null) { - LOGGER.debug("Missing certificate of {}. Skip.", Long.toHexString(keyId)); - continue; - } - - // Make sure that the recipient key is encryption capable and non-expired - KeyRingInfo info = new KeyRingInfo(secretKeys); - List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); - - PGPSecretKey secretKey = null; - for (PGPPublicKey pubkey : encryptionSubkeys) { - if (pubkey.getKeyID() == keyId) { - secretKey = secretKeys.getSecretKey(keyId); - break; - } - } - - if (secretKey == null) { - LOGGER.debug("Key " + Long.toHexString(keyId) + " is not valid or not capable for decryption."); - } else { - privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, - postponedDueToMissingPassphrase, true); - } - } - if (privateKey == null) { - continue; - } - decryptionKey = privateKey; - encryptedSessionKey = publicKeyEncryptedData; - break; - } - - // Try postponed keys with missing passphrases (will cause missing passphrase callbacks to fire) - if (encryptedSessionKey == null) { - - if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { - // Non-interactive mode: Throw an exception with all locked decryption keys - Set keyIds = new HashSet<>(); - for (Tuple k : postponedDueToMissingPassphrase) { - keyIds.add(k.getA()); - } - if (!keyIds.isEmpty()) { - throw new MissingPassphraseException(keyIds); - } - } - else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { - // Interactive mode: Fire protector callbacks to get passphrases interactively - for (Tuple missingPassphrases : postponedDueToMissingPassphrase) { - SubkeyIdentifier keyId = missingPassphrases.getA(); - PGPPublicKeyEncryptedData publicKeyEncryptedData = missingPassphrases.getB(); - PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId.getKeyId()); - PGPSecretKey secretKey = secretKeys.getSecretKey(keyId.getSubkeyId()); - - PGPPrivateKey privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, - postponedDueToMissingPassphrase, false); - if (privateKey == null) { - continue; - } - - decryptionKey = privateKey; - encryptedSessionKey = publicKeyEncryptedData; - break; - } - } else { - throw new IllegalStateException("Invalid PostponedKeysStrategy set in consumer options."); - } - - } - - return decryptWith(encryptedSessionKey, decryptionKey); - } - - /** - * Try decryption of the provided public-key-encrypted-data using the given secret key. - * If the secret key is encrypted and the secret key protector does not have a passphrase available and the boolean - * postponeIfMissingPassphrase is true, data decryption is postponed by pushing a tuple of the encrypted data decryption key - * identifier to the postponed list. - * - * This method only returns a non-null private key, if the private key is able to decrypt the message successfully. - * - * @param secretKeys secret key ring - * @param secretKey secret key - * @param publicKeyEncryptedData encrypted data which is tried to decrypt using the secret key - * @param postponed list of postponed decryptions due to missing secret key passphrases - * @param postponeIfMissingPassphrase flag to specify whether missing secret key passphrases should result in postponed decryption - * @return private key if decryption is successful, null if decryption is unsuccessful or postponed - * - * @throws PGPException in case of an OpenPGP error - */ - private PGPPrivateKey tryPublicKeyDecryption( - PGPSecretKeyRing secretKeys, - PGPSecretKey secretKey, - PGPPublicKeyEncryptedData publicKeyEncryptedData, - List> postponed, - boolean postponeIfMissingPassphrase) throws PGPException { - SecretKeyRingProtector protector = options.getSecretKeyProtector(secretKeys); - - if (postponeIfMissingPassphrase && !protector.hasPassphraseFor(secretKey.getKeyID())) { - // Postpone decryption with key with missing passphrase - SubkeyIdentifier identifier = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); - postponed.add(new Tuple<>(identifier, publicKeyEncryptedData)); - return null; - } - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey( - secretKey, protector.getDecryptor(secretKey.getKeyID())); - - // test if we have the right private key - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - try { - publicKeyEncryptedData.getSymmetricAlgorithm(decryptorFactory); // will only succeed if we have the right secret key - LOGGER.debug("Found correct decryption key {}.", Long.toHexString(secretKey.getKeyID())); - resultBuilder.setDecryptionKey(new SubkeyIdentifier(secretKeys, privateKey.getKeyID())); - return privateKey; - } catch (PGPException | ClassCastException e) { - return null; - } - } - - private InputStream decryptWith(PGPPublicKeyEncryptedData encryptedSessionKey, PGPPrivateKey decryptionKey) - throws PGPException { - if (decryptionKey == null || encryptedSessionKey == null) { - throw new MissingDecryptionMethodException("Decryption failed - No suitable decryption key or passphrase found"); - } - - PublicKeyDataDecryptorFactory dataDecryptor = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(decryptionKey); - - PGPSessionKey pgpSessionKey = encryptedSessionKey.getSessionKey(dataDecryptor); - SessionKey sessionKey = new SessionKey(pgpSessionKey); - resultBuilder.setSessionKey(sessionKey); - - SymmetricKeyAlgorithm symmetricKeyAlgorithm = sessionKey.getAlgorithm(); - if (symmetricKeyAlgorithm == SymmetricKeyAlgorithm.NULL) { - LOGGER.debug("Message is unencrypted"); - } else { - LOGGER.debug("Message is encrypted using {}", symmetricKeyAlgorithm); - } - throwIfAlgorithmIsRejected(symmetricKeyAlgorithm); - - integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream( - encryptedSessionKey.getDataStream(dataDecryptor), encryptedSessionKey, options); - return integrityProtectedEncryptedInputStream; - } - - private void throwIfAlgorithmIsRejected(SymmetricKeyAlgorithm algorithm) - throws UnacceptableAlgorithmException { - if (!PGPainless.getPolicy().getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { - throw new UnacceptableAlgorithmException("Data is " - + (algorithm == SymmetricKeyAlgorithm.NULL ? - "unencrypted" : - "encrypted with symmetric algorithm " + algorithm) + " which is not acceptable as per PGPainless' policy.\n" + - "To mark this algorithm as acceptable, use PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy()."); - } - } - - private void initOnePassSignatures(@Nonnull PGPOnePassSignatureList onePassSignatureList) - throws PGPException { - Iterator iterator = onePassSignatureList.iterator(); - if (!iterator.hasNext()) { - throw new PGPException("Verification failed - No OnePassSignatures found"); - } - - processOnePassSignatures(iterator); - } - - private void processOnePassSignatures(Iterator signatures) - throws PGPException { - while (signatures.hasNext()) { - PGPOnePassSignature signature = signatures.next(); - processOnePassSignature(signature); - } - } - - private void processOnePassSignature(PGPOnePassSignature signature) - throws PGPException { - final long keyId = signature.getKeyID(); - - LOGGER.debug("Encountered OnePassSignature from {}", Long.toHexString(keyId)); - - // Find public key - PGPPublicKeyRing verificationKeyRing = findSignatureVerificationKeyRing(keyId); - if (verificationKeyRing == null) { - onePassSignaturesWithMissingCert.put(keyId, new OnePassSignatureCheck(signature, null)); - return; - } - PGPPublicKey verificationKey = verificationKeyRing.getPublicKey(keyId); - - signature.init(verifierBuilderProvider, verificationKey); - OnePassSignatureCheck onePassSignature = new OnePassSignatureCheck(signature, verificationKeyRing); - onePassSignatureChecks.add(onePassSignature); - } - - private PGPSecretKeyRing findDecryptionKeyRing(long keyId) { - for (PGPSecretKeyRing key : options.getDecryptionKeys()) { - if (key.getSecretKey(keyId) != null) { - return key; - } - } - return null; - } - - private PGPPublicKeyRing findSignatureVerificationKeyRing(long keyId) { - PGPPublicKeyRing verificationKeyRing = null; - for (PGPPublicKeyRing publicKeyRing : options.getCertificates()) { - PGPPublicKey verificationKey = publicKeyRing.getPublicKey(keyId); - if (verificationKey != null) { - LOGGER.debug("Found public key {} for signature verification", Long.toHexString(keyId)); - verificationKeyRing = publicKeyRing; - break; - } - } - - if (verificationKeyRing == null && options.getMissingCertificateCallback() != null) { - verificationKeyRing = options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId); - } - - return verificationKeyRing; - } -} From aa398f9963a7704c0da74c9227b561f495f85fcf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 17:49:30 +0200 Subject: [PATCH 0503/1204] Only check message integrity once --- .../IntegrityProtectedInputStream.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java index 4da52d0f..286160e8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java @@ -11,12 +11,17 @@ import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPException; import org.pgpainless.exception.ModificationDetectionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class IntegrityProtectedInputStream extends InputStream { + private static final Logger LOGGER = LoggerFactory.getLogger(IntegrityProtectedInputStream.class); + private final InputStream inputStream; private final PGPEncryptedData encryptedData; private final ConsumerOptions options; + private boolean closed = false; public IntegrityProtectedInputStream(InputStream inputStream, PGPEncryptedData encryptedData, ConsumerOptions options) { this.inputStream = inputStream; @@ -36,11 +41,17 @@ public class IntegrityProtectedInputStream extends InputStream { @Override public void close() throws IOException { + if (closed) { + return; + } + closed = true; + if (encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) { try { if (!encryptedData.verify()) { throw new ModificationDetectionException(); } + LOGGER.debug("Integrity Protection check passed"); } catch (PGPException e) { throw new IOException("Failed to verify integrity protection", e); } From e0b21457931aa024a93ff02aa020e7f3022a4e44 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 17:56:33 +0200 Subject: [PATCH 0504/1204] Fix more tests --- .../OpenPgpMessageInputStream.java | 76 ++++++++++++++----- .../TeeBCPGInputStream.java | 1 - 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index dee59058..de80f7ff 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -4,7 +4,6 @@ package org.pgpainless.decryption_verification; -import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -37,7 +36,6 @@ import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.bouncycastle.util.io.Streams; import org.bouncycastle.util.io.TeeInputStream; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; @@ -210,6 +208,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } switch (type) { + case standard: // tee out packet bytes for signature verification packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), this.signatures); @@ -217,20 +216,22 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // *omnomnom* consumePackets(); break; + case cleartext_signed: + resultBuilder.setCleartextSigned(); MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); PGPSignatureList detachedSignatures = ClearsignedMessageUtil .detachSignaturesFromInbandClearsignedMessage( inputStream, multiPassStrategy.getMessageOutputStream()); for (PGPSignature signature : detachedSignatures) { - options.addVerificationOfDetachedSignature(signature); + signatures.addDetachedSignature(signature); } options.forceNonOpenPgpData(); - packetInputStream = null; nestedInputStream = new TeeInputStream(multiPassStrategy.getMessageInputStream(), this.signatures); break; + case non_openpgp: packetInputStream = null; nestedInputStream = new TeeInputStream(inputStream, this.signatures); @@ -348,14 +349,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { metadata.depth + 1); LOGGER.debug("Compressed Data Packet (" + compressionLayer.algorithm + ") at depth " + metadata.depth + " encountered"); InputStream decompressed = compressedData.getDataStream(); - nestedInputStream = new OpenPgpMessageInputStream(buffer(decompressed), options, compressionLayer, policy); + nestedInputStream = new OpenPgpMessageInputStream(decompressed, options, compressionLayer, policy); } private void processOnePassSignature() throws PGPException, IOException { syntaxVerifier.next(InputAlphabet.OnePassSignature); PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + - "at depth " + metadata.depth + " encountered"); + " at depth " + metadata.depth + " encountered"); signatures.addOnePassSignature(onePassSignature); } @@ -434,7 +435,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { InputStream decrypted = skesk.getDataStream(decryptorFactory); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); LOGGER.debug("Successfully decrypted data with provided session key"); return true; } else if (esk instanceof PGPPublicKeyEncryptedData) { @@ -442,7 +443,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { InputStream decrypted = pkesk.getDataStream(decryptorFactory); encryptedData.sessionKey = sessionKey; IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); LOGGER.debug("Successfully decrypted data with provided session key"); return true; } else { @@ -579,7 +580,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { encryptedData.sessionKey = sessionKey; LOGGER.debug("Successfully decrypted data with passphrase"); IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; @@ -606,7 +607,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { LOGGER.debug("Successfully decrypted data with key " + decryptionKeyId); IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(buffer(integrityProtected), options, encryptedData, policy); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { throw e; @@ -631,10 +632,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - private static InputStream buffer(InputStream inputStream) { - return new BufferedInputStream(inputStream); - } - private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { int algorithm = pkesk.getAlgorithm(); List> decryptionKeyCandidates = new ArrayList<>(); @@ -665,7 +662,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Override public int read() throws IOException { if (nestedInputStream == null) { - syntaxVerifier.assertValid(); + if (packetInputStream != null) { + syntaxVerifier.assertValid(); + } return -1; } @@ -699,7 +698,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Override public int read(@Nonnull byte[] b, int off, int len) throws IOException { - if (nestedInputStream == null) { if (packetInputStream != null) { syntaxVerifier.assertValid(); @@ -768,6 +766,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (!closed) { throw new IllegalStateException("Stream must be closed before access to metadata can be granted."); } + return new MessageMetadata((MessageMetadata.Message) metadata); } @@ -857,6 +856,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { final Stack> opsUpdateStack; List literalOPS = new ArrayList<>(); final List correspondingSignatures; + final List prependedSignaturesWithMissingCert = new ArrayList<>(); + final List inbandSignaturesWithMissingCert = new ArrayList<>(); + final List detachedSignaturesWithMissingCert = new ArrayList<>(); boolean isLiteral = true; private Signatures(ConsumerOptions options) { @@ -876,15 +878,29 @@ public class OpenPgpMessageInputStream extends DecryptionStream { void addDetachedSignature(PGPSignature signature) { SignatureCheck check = initializeSignature(signature); + long keyId = SignatureUtils.determineIssuerKeyId(signature); if (check != null) { detachedSignatures.add(check); + } else { + LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); + this.detachedSignaturesWithMissingCert.add(new SignatureVerification.Failure( + new SignatureVerification(signature, null), + new SignatureValidationException("Missing verification key") + )); } } void addPrependedSignature(PGPSignature signature) { SignatureCheck check = initializeSignature(signature); + long keyId = SignatureUtils.determineIssuerKeyId(signature); if (check != null) { this.prependedSignatures.add(check); + } else { + LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); + this.prependedSignaturesWithMissingCert.add(new SignatureVerification.Failure( + new SignatureVerification(signature, null), + new SignatureValidationException("Missing verification key") + )); } } @@ -916,11 +932,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer, Policy policy) { + boolean found = false; + long keyId = SignatureUtils.determineIssuerKeyId(signature); for (int i = onePassSignatures.size() - 1; i >= 0; i--) { OnePassSignatureCheck onePassSignature = onePassSignatures.get(i); - if (onePassSignature.getOnePassSignature().getKeyID() != signature.getKeyID()) { + if (onePassSignature.getOnePassSignature().getKeyID() != keyId) { continue; } + found = true; if (onePassSignature.getSignature() != null) { continue; @@ -942,6 +961,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } break; } + + if (!found) { + LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); + inbandSignaturesWithMissingCert.add(new SignatureVerification.Failure( + new SignatureVerification(signature, null), + new SignatureValidationException("Missing verification key."))); + } } void enterNesting() { @@ -983,6 +1009,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return cert; } } + + if (options.getMissingCertificateCallback() != null) { + return options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId); + } return null; // TODO: Missing cert for sig } @@ -1066,6 +1096,18 @@ public class OpenPgpMessageInputStream extends DecryptionStream { layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); } } + + for (SignatureVerification.Failure rejected : inbandSignaturesWithMissingCert) { + layer.addRejectedOnePassSignature(rejected); + } + + for (SignatureVerification.Failure rejected : prependedSignaturesWithMissingCert) { + layer.addRejectedPrependedSignature(rejected); + } + + for (SignatureVerification.Failure rejected : detachedSignaturesWithMissingCert) { + layer.addRejectedDetachedSignature(rejected); + } } @Override diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index 1ca1b3f9..bbcf593e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -98,7 +98,6 @@ public class TeeBCPGInputStream { public void close() throws IOException { this.packetInputStream.close(); - this.delayedTee.close(); } public static class DelayedTeeInputStreamInputStream extends InputStream { From 8097c87b7f8d3f9e310965cf79566db4a5d60663 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 18:30:40 +0200 Subject: [PATCH 0505/1204] Fix last two broken tests --- .../OpenPgpMessageInputStream.java | 31 ++++++++++++------- .../org/pgpainless/key/util/KeyRingUtils.java | 11 +++++++ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index de80f7ff..11a61e8c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -9,6 +9,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -60,12 +61,13 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.key.util.KeyIdUtil; +import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; +import org.pgpainless.signature.consumer.CertificateValidator; import org.pgpainless.signature.consumer.OnePassSignatureCheck; import org.pgpainless.signature.consumer.SignatureCheck; import org.pgpainless.signature.consumer.SignatureValidator; -import org.pgpainless.signature.consumer.SignatureVerifier; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; @@ -654,7 +656,16 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (decryptionKey == null) { continue; } - return secretKeys; + + KeyRingInfo info = new KeyRingInfo(secretKeys, policy, new Date()); + List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); + for (PGPPublicKey key : encryptionKeys) { + if (key.getKeyID() == keyID) { + return secretKeys; + } + } + + LOGGER.debug("Subkey " + Long.toHexString(keyID) + " cannot be used for decryption."); } return null; } @@ -951,8 +962,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(signature); - SignatureVerifier.verifyOnePassSignature(signature, onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()), onePassSignature, policy); + .verify(signature); + CertificateValidator.validateCertificateAndVerifyOnePassSignature(onePassSignature, policy); LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedOnePassSignature(verification); } catch (SignatureValidationException e) { @@ -1068,10 +1079,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(detached.getSignature()); - SignatureVerifier.verifyInitializedSignature( - detached.getSignature(), - detached.getSigningKeyRing().getPublicKey(detached.getSigningKeyIdentifier().getKeyId()), - policy, detached.getSignature().getCreationTime()); + CertificateValidator.validateCertificateAndVerifyInitializedSignature( + detached.getSignature(), KeyRingUtils.publicKeys(detached.getSigningKeyRing()), policy); LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedDetachedSignature(verification); } catch (SignatureValidationException e) { @@ -1085,10 +1094,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(prepended.getSignature()); - SignatureVerifier.verifyInitializedSignature( - prepended.getSignature(), - prepended.getSigningKeyRing().getPublicKey(prepended.getSigningKeyIdentifier().getKeyId()), - policy, prepended.getSignature().getCreationTime()); + CertificateValidator.validateCertificateAndVerifyInitializedSignature( + prepended.getSignature(), KeyRingUtils.publicKeys(prepended.getSigningKeyRing()), policy); LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedPrependedSignature(verification); } catch (SignatureValidationException e) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 013e283e..2ce058fd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -144,6 +144,17 @@ public final class KeyRingUtils { return secretKey; } + @Nonnull + public static PGPPublicKeyRing publicKeys(@Nonnull PGPKeyRing keys) { + if (keys instanceof PGPPublicKeyRing) { + return (PGPPublicKeyRing) keys; + } else if (keys instanceof PGPSecretKeyRing) { + return publicKeyRingFrom((PGPSecretKeyRing) keys); + } else { + throw new IllegalArgumentException("Unknown keys class: " + keys.getClass().getName()); + } + } + /** * Extract a {@link PGPPublicKeyRing} containing all public keys from the provided {@link PGPSecretKeyRing}. * From a013ab4ebbc3803dcf513897b1cabb1d8620b4d0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 18:32:56 +0200 Subject: [PATCH 0506/1204] Wrap MalformedOpenPgpMessageException in BadData --- .../decryption_verification/OpenPgpMessageInputStream.java | 2 +- .../src/main/java/org/pgpainless/sop/DecryptImpl.java | 3 ++- .../src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java | 3 ++- .../src/main/java/org/pgpainless/sop/InlineVerifyImpl.java | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 11a61e8c..f081e021 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -656,7 +656,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (decryptionKey == null) { continue; } - + KeyRingInfo info = new KeyRingInfo(secretKeys, policy, new Date()); List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); for (PGPPublicKey key : encryptionKeys) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 11b20f82..ee86f5e8 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -23,6 +23,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.util.Passphrase; @@ -123,7 +124,7 @@ public class DecryptImpl implements Decrypt { throw new SOPGPException.CannotDecrypt("No usable decryption key or password provided.", e); } catch (WrongPassphraseException e) { throw new SOPGPException.KeyIsProtected(); - } catch (PGPException | IOException e) { + } catch (MalformedOpenPgpMessageException | PGPException | IOException e) { throw new SOPGPException.BadData(e); } finally { // Forget passphrases after decryption diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index 3065addf..b563e704 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -18,6 +18,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import sop.Verification; import sop.exception.SOPGPException; import sop.operation.DetachedVerify; @@ -82,7 +83,7 @@ public class DetachedVerifyImpl implements DetachedVerify { } return verificationList; - } catch (PGPException e) { + } catch (MalformedOpenPgpMessageException | PGPException e) { throw new SOPGPException.BadData(e); } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 81d614cf..4948712c 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -19,6 +19,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MissingDecryptionMethodException; import sop.ReadyWithResult; import sop.Verification; @@ -82,7 +83,7 @@ public class InlineVerifyImpl implements InlineVerify { return verificationList; } catch (MissingDecryptionMethodException e) { throw new SOPGPException.BadData("Cannot verify encrypted message.", e); - } catch (PGPException e) { + } catch (MalformedOpenPgpMessageException | PGPException e) { throw new SOPGPException.BadData(e); } } From 192aa9832621ec759f08656681d7a0c6340f8a63 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 18:38:30 +0200 Subject: [PATCH 0507/1204] Add missing REUSE license headers --- .../pgpainless/decryption_verification/DecryptionStream.java | 4 ++++ .../OpenPgpMessageInputStreamTest.java | 4 ++++ .../decryption_verification/syntax_check/PDATest.java | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index c531f487..0f221ad7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification; import javax.annotation.Nonnull; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index f9539149..d2c62fb2 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 8fa107f6..9250acfa 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification.syntax_check; import static org.junit.jupiter.api.Assertions.assertThrows; From 7e8841abf3097a569ff4e9e5285c26adf635b4b0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Oct 2022 19:23:52 +0200 Subject: [PATCH 0508/1204] Handle unknown packet versions gracefully --- .../OpenPgpMessageInputStream.java | 9 +- .../UnsupportedPacketVersionsTest.java | 410 ++++++++++++++++++ 2 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index f081e021..75b78294 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -18,6 +18,7 @@ import javax.annotation.Nonnull; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.UnsupportedPacketVersionException; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; @@ -366,7 +367,13 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // true if Signature corresponds to OnePassSignature boolean isSigForOPS = syntaxVerifier.peekStack() == StackAlphabet.ops; syntaxVerifier.next(InputAlphabet.Signature); - PGPSignature signature = packetInputStream.readSignature(); + PGPSignature signature; + try { + signature = packetInputStream.readSignature(); + } catch (UnsupportedPacketVersionException e) { + LOGGER.debug("Unsupported Signature at depth " + metadata.depth + " encountered.", e); + return; + } long keyId = SignatureUtils.determineIssuerKeyId(signature); if (isSigForOPS) { LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key " + diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java new file mode 100644 index 00000000..68916348 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java @@ -0,0 +1,410 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +public class UnsupportedPacketVersionsTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "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\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final String PKESK3_PKESK23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51UgwUoXQUFBQUFBQUEJYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYdJMAQZo\n" + + "tUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5QDO4\n" + + "wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=VS1M\n" + + "-----END PGP MESSAGE-----"; + + private static final String PKESK23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wUoXQUFBQUFBQUEJYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL/2D/nw1r\n" + + "Yn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr191IicIcY\n" + + "0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9UKDDQdKE\n" + + "95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6TGasJF7VH\n" + + "8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDarkzUahT33s\n" + + "46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71oB47I407\n" + + "exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGEgQbTjUgD\n" + + "2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+pzI0ZJthE\n" + + "RNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dVINJMAQZo\n" + + "tUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5QDO4\n" + + "wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=EhNy\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SKESK23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51Ugw00XCQMINQp7MFzAc6T/YWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYdJM\n" + + "AQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5\n" + + "QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=pvWj\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String SKESK23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "w00XCQMINQp7MFzAc6T/YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL/2D/\n" + + "nw1rYn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr191Ii\n" + + "cIcY0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9UKDD\n" + + "QdKE95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6TGasJ\n" + + "F7VH8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDarkzUah\n" + + "T33s46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71oB47\n" + + "I407exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGEgQbT\n" + + "jUgD2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+pzI0Z\n" + + "JthERNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dVINJM\n" + + "AQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5\n" + + "QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=STOd\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SKESK4wS2K23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51Ugw1AECRcIYWFhYWFhYWFBQUFBYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YdJMAQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNV\n" + + "CHg5QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=/uxY\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String SKESK4wS2K23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "w1AECRcIYWFhYWFhYWFBQUFBYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL\n" + + "/2D/nw1rYn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr1\n" + + "91IicIcY0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9\n" + + "UKDDQdKE95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6T\n" + + "GasJF7VH8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDark\n" + + "zUahT33s46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71\n" + + "oB47I407exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGE\n" + + "gQbTjUgD2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+p\n" + + "zI0ZJthERNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dV\n" + + "INJMAQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNV\n" + + "CHg5QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=cIwV\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS3_LIT_SIG4 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv+IhkCMhbdRcMnIPZNPGU6OK1Jk5xuRdIEIBsvv7b8jmAr\n" + + "9IwjfnV/RDMtH+xR/T9K7qJGGFYnhLY5w0CmYHQcDKpcBqk0Dw6l/eKCNhgRXKAk\n" + + "gfaKL1Utt1Pw0nz0mOwHyPEN/pGc0xlVhsjVkRvIOsKcpfuc1EpSZMFgDcBQDhe/\n" + + "jAsR/MvRugkW8xLpyQyfeGLJUOEYVrkpam3rLKB1KywAgBmpr9WDwfYITW/VE9k5\n" + + "cKIOPMDJFU+u9lzBx6nSS4JRBuCO2mhR4gjcRaGPWiiz+0qZfS+AXYV/MAU5OwK/\n" + + "6nhX97zwS4r1Avztjh4taBhLVsY4pw6PuLtACJNwPrev63Yc+a4hJJ4pm1AnHV58\n" + + "Y1pZKQL8vA61+/tbhFQ18vbJ8E1NOka/euFLQu9Mg58jhpcscqMouyr3JFMwgH2Y\n" + + "eFuRJncJAKotXxlfnF37qz5LG3bamACXWZSObjp9d4quIAoCDUteZlDWQ1xq5R4y\n" + + "QYXtk9ZuHsHsmY9A0CiI0sGYAUBtLubJ5qhLLr/GqKAmy8jTSA3MjgtrB55NWj9J\n" + + "bjgFNsd1BNGklgnwhtmApHJWY8skAAQkJj0rXj/aOMc734ypiEWDiU1quRbEeRLR\n" + + "kDvBNUXx2j2rVF+MmQS/sm5Yk/op+4lH/Wounsci3qWH76GaNZoIlvNE3mdFoVTe\n" + + "cRh4W2Em8uAUH4bKwazltRJUhZmXvuGfUnQCmolJTpyPl4DaQQgzdBXLTRcPxwdU\n" + + "30e7HnxZWESKx1LnGxp3Oan1k1lXyHwvnEk26EXhve+dhsQ6YsKgvtSLNFqGsfKe\n" + + "MVOq37cpOGFQsYStWHZd0tcHjIjWmAeZ8kH+ZzR9tgYKxxjimsxafLS/lo415SkC\n" + + "LnOCz6hywI7CufSUcXUlHGJuobZ5HDJcygsQhQmNVLDmKh4xUJDZrORS0ciMy2kc\n" + + "XAnxCDYbltVQktc/F/Gl1lTx0UNmV5d9G0utVmxxGbXna3BLkB+6qMuux/ngC+cI\n" + + "+0GjeuXDVzUqhKDDC3Sq4T2nwix4CjKvawnHC+vpHzRmdZSkXz3nrSdhJS5JnOap\n" + + "I4CdyYkP3jQs5P03dkv5RZAoqNPkeftwadu0cjZj8HEC8bVSd7YCS/0Gbgvp/eK3\n" + + "aOXZAWhELyQON4bbXiWzMTcO2AA5soBmP2QnBNdq7NxbEkBec8aAXZiyXn17JYit\n" + + "HRtJ7beptXjN0y2bEhOvsBHbDpjGO22fZfQ0aywP2k4/XanDf4WJolEFgLj2Qp8F\n" + + "pw1Olc2UhApxhAqjkme09hggli3wUhthQrpBlmntfbvBcmmO+p/jV8Zn\n" + + "=Roko\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS23_LIT_SIG23 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/byYGOehnfgZu/HJSSDEQhYE27lh5j+oQPktYBxsTI3Ii\n" + + "AJ0pyJggtUM3c/e6z4HioAs4oQiaH1eoGBIl7noROhHJgT6E/oKdHJHmQndo0gT7\n" + + "xzSHKrEZYEqT45LD6jT5teOiwGX/B7as6jaQOT+Nh+M0ZqpPrBdWDeUoY/I/lx9j\n" + + "IcXQUhKuqJwZ16xsnv0JJ80rdp5qv0g2NHT1hK1JEONyT2fefov3EXaSpQZHRBEi\n" + + "XfwToHcJrgemFoGwZBQhXsPWBgKH92aW6r7ZJZMQ4BE2SwqEw+cbaaqwfFRJ9puj\n" + + "ZUBi2JGwnYImZyD7jYverkjH05vI7d5qQDhCT6GPD8Q0WKfY225LFTj/zzbC23lp\n" + + "VLlbT2Ap8ZFOESpM+crOUaOguBhnTOF05s1eXhQYxIKzJTW8UVrzbwI8ut3BnEDj\n" + + "0aDqUR+QDYgYmz8hnEjanHk2McvfaNdV6uOPZNph2RuPzhZeoNcz1PLy+XPFJsQT\n" + + "EdqURJ2D35qmrBvq6klN0sGYAXEtW4hxMaKhH7/SIk2m3kUAChjtzr4XRcE2i8h3\n" + + "9uD6CRWxokArfRVAp5RpXt3ywoYrl1Mp5prVwWcrTzYVPZwwe/bYAFTIfavi0Ezb\n" + + "A/ah2e1EYCTWxLY0Klzil2Xw9/Dc/JPTRqzWJxIn0AU4DVfYNwlH3QimDUDKOKbu\n" + + "bw4bLEEBKRr5BcNMA2rJOw/n3AmKxQcdSFJh3ZNtDPWzRwflIzE2qB686hpeOs34\n" + + "T2iJcfr9W9rKcYI7+WYcZA3fWokaWUfXdWrPMXBVJuPdGezGLSfe/OM4kw/8s2vR\n" + + "Bk88WZ1ZiFXE2CRaHP80fHFpxAZioWTuC5UGhF7NgZ7Q1E85GaGVe1fQeqmeX3mo\n" + + "gwAWwq9WFhPQQPwdrDz+1h/pzD0RVW7D+zfWdF//vesc4z1Bpi5prbMdpVdmyvO9\n" + + "8Rcc42GtmhtYSB9SPjzPuN8PrWvD3AKgw5vQro6oiNh0TGmj5Se4lXCfRfCl4tak\n" + + "cmvdi+1wAvn5OFdxYKKHNvjavwj2SY70nx0ACasBpbMEwoQ0StZmOxCaofkgvEwN\n" + + "t8jMq/MVrIfunhMjx0/GDpBZBb8kze8zrvWxlbTnoIfh2yVEqmTZWue9HX5Mnh8P\n" + + "wexxNrPaafTjA3nUgXXzlItJ/Wa43pYw2sgcBPlF/jRKSFiD+pDLysqwpH0ANsJ3\n" + + "G7t8Qavq1DlrHFgV5jhfR1tjA5ohjxx7yzceQBvZUFxKM1WEWR+9dRb3bpfZJr0g\n" + + "qgO36bpCeCEej+ubXpXXTN28LQLXjQHlE2o1NGLoGl+G72tXOTx30kPS\n" + + "=wUZn\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS23_OPS3_LIT_SIG4_SIG23 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQwAmftqzbRiMRk7YbpJO7uqtWQIq2uZj7nYYOVpQgR777Zb\n" + + "QT1J3pbPoCceWFSuHH/D0IeP84T95O+A630dtj3ZWhh4EGnBIKm13R6bEA4jKtcx\n" + + "rK3HZyHo85N5TFn/gLOqCc2v/0cxUoygBqFpHFwBG/e4uuQoxgD4RsBGFrdYlyK7\n" + + "MMkZ/fgaKhawTCVQ2dtBcp6WvKX/8u6FXSh35zFJJTM30LE7BFDgWEyGPLv0bWnl\n" + + "37oCo8zcK+SIK4JooNm1ZxIowWTOxUz1pWPtmtGXA8XoGJtggXt+HpVxNE8bDCgV\n" + + "f717+R0HMwQKP4F50Wq3Js9ZJrPjRvXLeiZe6nvqn5AQCOsG8osIIg2YElyW/uIe\n" + + "C38OxaZbql297cYzzdEZtRYCTTY7j99pG8ZD1nNdd5IPm027dfPl+JJ5nzn/u3iT\n" + + "+FKxgArZ9cYkeEpNB+BfWoWyNfbA830s3Y+G5wt3s4cmH1z8JvDeepUYhyqvA/6M\n" + + "RInjwCUwSQCAuI+QhnPi0sOlAYOwkgN8RyqOP4vR4Kd1rJfm7rg2h40ag7tJtu5y\n" + + "YXa4sU9DLpspTYvYgzRtsNbYHrH+LFkQlpG+PFnJAI6Qn+Q1zZsZ2HbbK7GOZFug\n" + + "0wCg5E3DJQxBNlOx7h7xacpGb2QsmBTLUPvWFiYoq8He5XOx1MleLxO+E5l40kT4\n" + + "ZE8RxfRyVreQdp3rZ6RMiIfnM8VljMxteaLmaIzqsLTlvKlf1w2DBRL9reI2oCP6\n" + + "BukX8zba16ITsLELnuQ5L8EmQwcH8Yj8Mg37foyFHa2fIvZRg+0tz3b5nJKteWQn\n" + + "u/qR/RGNAXT1D/YBQ+Tgnq6qIUd4Da/XEXAi8R6FKprc2yqCNzMSA1wolaq2DUGJ\n" + + "ASyRN7uQJpVc1DlxTRTMQLpuoxljQtc6dmn/HKr7DF8jUKcM8cRk6PCqWd6PRYPq\n" + + "WJTiHh2FoyDaR5+HuSbRCOr7i9jZXh/TctM70itLIvQlw/x9WEm2ZxwS7+0mHMvP\n" + + "h9U4Wi70mfR0WllDNpWm5ZEeksoUF7aCQ2lVIQH8E6YGmWUSCYUgjgiIsfSqn9kh\n" + + "tG8WGCrM1sPSIzQG70d1fuirRg5H7oeVRTPzpYN/cSXqRULk31Z9RneXwgZZZgPB\n" + + "Q1hE3oJmP4LJEfRhL4P7TL2Xp+1Kvius53my2zKnVXoBNlAUHSdidXsd+xVaOEkE\n" + + "cNyhLg4cZmlyuz5Ew/NHPAD70Cd9qXQraOf3dqZ77yhG0y/FCwXxnnfW1FnTLe14\n" + + "3RWuNAFhbuNuYrXn0Zq+SFz3UnNNKMoNejwDcvkxxZ92KQXJcB7zCRnEehjBz/At\n" + + "iNgsVfiOVRxzzp12iV+ljtM8A3KJHnnBQypPIeq4yKsXxtumVhryAc5k2neRZkvc\n" + + "Wo1x3T/EY0SSlFFSYsiyDgbaj0SguiVNTrJbLQd62a1S4ZCYB5k2hlzm22eIKHIa\n" + + "lb+sYaTGbSkxVH2xMvjxgO4dx9YvTlH6rsTIktmhvYxnF27Y6Bfhp+x8I3RoYPRC\n" + + "ImMgllybYE9AOHLI5uogvoe133OfHAmHVm36qx+S24r8YTMdZ6iJKLCd0Hav6aS9\n" + + "b4ptBiKQQQR2mtxaQNyBVEjfbpt2/ATnzRg5D/TAJATvhoeByWRYNP21iATnWU5c\n" + + "H3uK3dNDLnZAbaEf2XfqEG2fcw/Bn9mbXUodEay+EQl0Z11kWKOBSwMyGwSxdMhw\n" + + "S+9tTnfFZ73B/fyD41p3Ft02cUJcD2yW/j3+5JLOqZJJlTEhtAFvixkhcfR7VJTl\n" + + "arZfECPXOOMbiBxQmFA4+AZfP+9bMFPz9/guZTkIWsjKO4JI6ge6ayl6Eel2Qsbo\n" + + "MzsYA6m9h4a0VQPmHf1Itg5kiEpecG4rEqzJC2ov4mTiD4kVlPhUj6Je+VU3mEgT\n" + + "geMf/8JkD6+IParbR7iaEQF2wPgrR/VcBX/5Y8AI4mW8eiTCybtPt9z4X6w326Uy\n" + + "UkaqeswhQmd2sODDqxxrdjVmYQEWqVKIRRBLsR5fvDiqyiVFbPEO\n" + + "=qFHk\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS3_OPS23_LIT_SIG23_SIG4 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "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\n" + + "=jGpv\n" + + "-----END PGP MESSAGE-----\n"; + + private final PGPSecretKeyRing key; + private final PGPPublicKeyRing cert; + + public UnsupportedPacketVersionsTest() throws IOException { + key = PGPainless.readKeyRing().secretKeyRing(KEY); + cert = PGPainless.extractCertificate(key); + } + + @Test + public void pkesk3_pkesk23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_PKESK23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk23_pkesk_seip() throws PGPException, IOException { + decryptAndCompare(PKESK23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk3_skesk23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_SKESK23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void skesk23_pkesk3_seip() throws PGPException, IOException { + decryptAndCompare(SKESK23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + @Disabled("Enable once https://github.com/bcgit/bc-java/pull/1268 is available") + public void pkesk3_skesk4Ws2k23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_SKESK4wS2K23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + @Disabled("Enable once https://github.com/bcgit/bc-java/pull/1268 is available") + public void skesk4Ws2k23_pkesk3_seip() throws PGPException, IOException { + decryptAndCompare(SKESK4wS2K23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk3_seip_ops3_lit_sig4() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS3_LIT_SIG4, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops23_lit_sig23() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS23_LIT_SIG23, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops23_ops3_lit_sig4_sig23() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS23_OPS3_LIT_SIG4_SIG23, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops3_ops23_lit_sig23_sig4() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS3_OPS23_LIT_SIG23_SIG4, "Encrypted, signed message."); + } + + public void decryptAndCompare(String msg, String plain) throws IOException, PGPException { + // noinspection CharsetObjectCanBeUsed + ByteArrayInputStream inputStream = new ByteArrayInputStream(msg.getBytes(Charset.forName("UTF8"))); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(inputStream) + .withOptions(ConsumerOptions.get() + .addDecryptionKey(key) + .addVerificationCert(cert)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + assertEquals(plain, out.toString()); + } +} From a0ba6828c982bfce4192162e45f6c853a976790a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Oct 2022 18:22:50 +0200 Subject: [PATCH 0509/1204] Remove superfluous states --- .../syntax_check/PDA.java | 26 +++---------------- .../syntax_check/StackAlphabet.java | 4 --- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 0d6ba28c..c77b50cc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -67,7 +67,7 @@ public class PDA { case Signature: if (stackItem == ops) { - return CorrespondingSignature; + return LiteralMessage; } else { throw new MalformedOpenPgpMessageException(this, input, stackItem); } @@ -96,7 +96,7 @@ public class PDA { switch (input) { case Signature: if (stackItem == ops) { - return CorrespondingSignature; + return CompressedMessage; } else { throw new MalformedOpenPgpMessageException(this, input, stackItem); } @@ -125,7 +125,7 @@ public class PDA { switch (input) { case Signature: if (stackItem == ops) { - return CorrespondingSignature; + return EncryptedMessage; } else { throw new MalformedOpenPgpMessageException(this, input, stackItem); } @@ -147,26 +147,6 @@ public class PDA { } }, - CorrespondingSignature { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - if (input == InputAlphabet.EndOfSequence) { - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - // premature end of stream - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } else if (input == InputAlphabet.Signature) { - if (stackItem == ops) { - return CorrespondingSignature; - } - } - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - }, - Valid { @Override State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java index 6030fbc8..a8a2a213 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java @@ -13,10 +13,6 @@ public enum StackAlphabet { * OnePassSignature (in case of BC this represents a OnePassSignatureList). */ ops, - /** - * ESK. Not used, as BC combines encrypted data with their encrypted session keys. - */ - esk, /** * Special symbol representing the end of the message. */ From a2a5c9223eaff33c9ec0f2d6ea7336c2e25d657f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Oct 2022 18:24:19 +0200 Subject: [PATCH 0510/1204] Remove debugging fields --- .../pgpainless/decryption_verification/syntax_check/PDA.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index c77b50cc..40021734 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -16,7 +16,6 @@ import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet. public class PDA { - private static int ID = 0; private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); /** @@ -171,13 +170,11 @@ public class PDA { private final Stack stack = new Stack<>(); private State state; - private int id; public PDA() { state = State.OpenPgpMessage; stack.push(terminus); stack.push(msg); - this.id = ID++; } public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { @@ -240,6 +237,6 @@ public class PDA { @Override public String toString() { - return "PDA " + id + ": State: " + state + " Stack: " + stack; + return "State: " + state + " Stack: " + stack; } } From 798e68e87f4c76239564a988a887dc6beaa92c4c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Oct 2022 18:39:20 +0200 Subject: [PATCH 0511/1204] Improve syntax error reporting --- .../decryption_verification/syntax_check/PDA.java | 11 +++++++++-- .../exception/MalformedOpenPgpMessageException.java | 6 +++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 40021734..550cb855 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -8,6 +8,9 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Stack; import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.msg; @@ -169,6 +172,7 @@ public class PDA { } private final Stack stack = new Stack<>(); + private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting private State state; public PDA() { @@ -180,9 +184,12 @@ public class PDA { public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { try { state = state.transition(input, this); + inputs.add(input); } catch (MalformedOpenPgpMessageException e) { - LOGGER.debug("Unexpected Packet or Token '" + input + "' encountered. Message is malformed.", e); - throw e; + MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException("Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + + ", token '" + input + "' is unexpected and illegal.", e); + LOGGER.debug("Invalid input '" + input + "'", wrapped); + throw wrapped; } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index cbe78c05..aa158746 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -21,6 +21,10 @@ public class MalformedOpenPgpMessageException extends RuntimeException { } public MalformedOpenPgpMessageException(PDA.State state, InputAlphabet input, StackAlphabet stackItem) { - this("Invalid input: There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); + this("There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); + } + + public MalformedOpenPgpMessageException(String s, MalformedOpenPgpMessageException e) { + super(s, e); } } From b3d61b0494a9c63d6037436fe600fa2769f1999c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 11:56:10 +0200 Subject: [PATCH 0512/1204] Separate out syntax logic --- .../syntax_check/OpenPgpMessageSyntax.java | 119 +++++++++++++ .../syntax_check/PDA.java | 165 ++---------------- .../syntax_check/State.java | 16 ++ .../syntax_check/Syntax.java | 13 ++ .../syntax_check/Transition.java | 28 +++ .../MalformedOpenPgpMessageException.java | 4 +- 6 files changed, 189 insertions(+), 156 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java new file mode 100644 index 00000000..ab45a896 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +public class OpenPgpMessageSyntax implements Syntax { + + @Override + public Transition transition(State from, InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + switch (from) { + case OpenPgpMessage: + return fromOpenPgpMessage(input, stackItem); + case LiteralMessage: + return fromLiteralMessage(input, stackItem); + case CompressedMessage: + return fromCompressedMessage(input, stackItem); + case EncryptedMessage: + return fromEncryptedMessage(input, stackItem); + case Valid: + return fromValid(input, stackItem); + } + + throw new MalformedOpenPgpMessageException(from, input, stackItem); + } + + Transition fromOpenPgpMessage(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + if (stackItem != StackAlphabet.msg) { + throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); + } + + switch (input) { + case LiteralData: + return new Transition(State.LiteralMessage); + + case Signature: + return new Transition(State.OpenPgpMessage, StackAlphabet.msg); + + case OnePassSignature: + return new Transition(State.OpenPgpMessage, StackAlphabet.ops, StackAlphabet.msg); + + case CompressedData: + return new Transition(State.CompressedMessage); + + case EncryptedData: + return new Transition(State.EncryptedMessage); + + case EndOfSequence: + default: + throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); + } + } + + Transition fromLiteralMessage(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + switch (input) { + case Signature: + if (stackItem == StackAlphabet.ops) { + return new Transition(State.LiteralMessage); + } + break; + + case EndOfSequence: + if (stackItem == StackAlphabet.terminus) { + return new Transition(State.Valid); + } + break; + } + + throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem); + } + + Transition fromCompressedMessage(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + switch (input) { + case Signature: + if (stackItem == StackAlphabet.ops) { + return new Transition(State.CompressedMessage); + } + break; + + case EndOfSequence: + if (stackItem == StackAlphabet.terminus) { + return new Transition(State.Valid); + } + break; + } + + throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem); + } + + Transition fromEncryptedMessage(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + switch (input) { + case Signature: + if (stackItem == StackAlphabet.ops) { + return new Transition(State.EncryptedMessage); + } + break; + + case EndOfSequence: + if (stackItem == StackAlphabet.terminus) { + return new Transition(State.Valid); + } + break; + } + + throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem); + } + + Transition fromValid(InputAlphabet input, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException { + throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 550cb855..94c1739e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -14,177 +14,31 @@ import java.util.List; import java.util.Stack; import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.ops; import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.terminus; public class PDA { private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); - /** - * Set of states of the automaton. - * Each state defines its valid transitions in their {@link State#transition(InputAlphabet, PDA)} method. - */ - public enum State { - - OpenPgpMessage { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - if (stackItem != msg) { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - switch (input) { - - case LiteralData: - return LiteralMessage; - - case Signature: - automaton.pushStack(msg); - return OpenPgpMessage; - - case OnePassSignature: - automaton.pushStack(ops); - automaton.pushStack(msg); - return OpenPgpMessage; - - case CompressedData: - return CompressedMessage; - - case EncryptedData: - return EncryptedMessage; - - case EndOfSequence: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - LiteralMessage { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - - case Signature: - if (stackItem == ops) { - return LiteralMessage; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignature: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - CompressedMessage { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - case Signature: - if (stackItem == ops) { - return CompressedMessage; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignature: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - EncryptedMessage { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - StackAlphabet stackItem = automaton.popStack(); - switch (input) { - case Signature: - if (stackItem == ops) { - return EncryptedMessage; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case EndOfSequence: - if (stackItem == terminus && automaton.stack.isEmpty()) { - return Valid; - } else { - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - - case LiteralData: - case OnePassSignature: - case CompressedData: - case EncryptedData: - default: - throw new MalformedOpenPgpMessageException(this, input, stackItem); - } - } - }, - - Valid { - @Override - State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException { - throw new MalformedOpenPgpMessageException(this, input, null); - } - }, - ; - - /** - * Pop the automatons stack and transition to another state. - * If no valid transition from the current state is available given the popped stack item and input symbol, - * a {@link MalformedOpenPgpMessageException} is thrown. - * Otherwise, the stack is manipulated according to the valid transition and the new state is returned. - * - * @param input input symbol - * @param automaton automaton - * @return new state of the automaton - * @throws MalformedOpenPgpMessageException in case of an illegal input symbol - */ - abstract State transition(InputAlphabet input, PDA automaton) throws MalformedOpenPgpMessageException; - } - private final Stack stack = new Stack<>(); private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting private State state; + private Syntax syntax = new OpenPgpMessageSyntax(); public PDA() { state = State.OpenPgpMessage; - stack.push(terminus); - stack.push(msg); + pushStack(terminus); + pushStack(msg); } public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { try { - state = state.transition(input, this); + Transition transition = syntax.transition(state, input, popStack()); inputs.add(input); + state = transition.getNewState(); + for (StackAlphabet item : transition.getPushedItems()) { + pushStack(item); + } } catch (MalformedOpenPgpMessageException e) { MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException("Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + ", token '" + input + "' is unexpected and illegal.", e); @@ -230,6 +84,9 @@ public class PDA { * @return stack item */ private StackAlphabet popStack() { + if (stack.isEmpty()) { + return null; + } return stack.pop(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java new file mode 100644 index 00000000..9dee9af1 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +/** + * Set of states of the automaton. + */ +public enum State { + OpenPgpMessage, + LiteralMessage, + CompressedMessage, + EncryptedMessage, + Valid +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java new file mode 100644 index 00000000..47813a9e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +public interface Syntax { + + Transition transition(State from, InputAlphabet inputAlphabet, StackAlphabet stackItem) + throws MalformedOpenPgpMessageException; +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java new file mode 100644 index 00000000..bbc28e58 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Transition { + + private final List pushedItems = new ArrayList<>(); + private final State newState; + + public Transition(State newState, StackAlphabet... pushedItems) { + this.newState = newState; + this.pushedItems.addAll(Arrays.asList(pushedItems)); + } + + public State getNewState() { + return newState; + } + + public List getPushedItems() { + return new ArrayList<>(pushedItems); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index aa158746..db8d0df6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -5,8 +5,8 @@ package org.pgpainless.exception; import org.pgpainless.decryption_verification.syntax_check.InputAlphabet; -import org.pgpainless.decryption_verification.syntax_check.PDA; import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.State; /** * Exception that gets thrown if the OpenPGP message is malformed. @@ -20,7 +20,7 @@ public class MalformedOpenPgpMessageException extends RuntimeException { super(message); } - public MalformedOpenPgpMessageException(PDA.State state, InputAlphabet input, StackAlphabet stackItem) { + public MalformedOpenPgpMessageException(State state, InputAlphabet input, StackAlphabet stackItem) { this("There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } From 8ca0cfd3ae9dff01ded689c434dd5d6cb9f6d6e7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 12:07:22 +0200 Subject: [PATCH 0513/1204] Rename *Alphabet to *Symbol and add javadoc --- .../OpenPgpMessageInputStream.java | 18 ++-- .../{InputAlphabet.java => InputSymbol.java} | 2 +- .../syntax_check/OpenPgpMessageSyntax.java | 41 ++++--- .../syntax_check/PDA.java | 18 ++-- .../{StackAlphabet.java => StackSymbol.java} | 2 +- .../syntax_check/Syntax.java | 19 +++- .../syntax_check/Transition.java | 6 +- .../MalformedOpenPgpMessageException.java | 6 +- .../syntax_check/PDATest.java | 102 +++++++++--------- 9 files changed, 121 insertions(+), 93 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/{InputAlphabet.java => InputSymbol.java} (98%) rename pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/{StackAlphabet.java => StackSymbol.java} (93%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 75b78294..a0f17210 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -45,9 +45,9 @@ import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.decryption_verification.syntax_check.InputAlphabet; +import org.pgpainless.decryption_verification.syntax_check.InputSymbol; import org.pgpainless.decryption_verification.syntax_check.PDA; -import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.StackSymbol; import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.exception.MalformedOpenPgpMessageException; @@ -334,7 +334,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processLiteralData() throws IOException { LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); - syntaxVerifier.next(InputAlphabet.LiteralData); + syntaxVerifier.next(InputSymbol.LiteralData); PGPLiteralData literalData = packetInputStream.readLiteralData(); this.metadata.setChild(new MessageMetadata.LiteralData( literalData.getFileName(), @@ -344,7 +344,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processCompressedData() throws IOException, PGPException { - syntaxVerifier.next(InputAlphabet.CompressedData); + syntaxVerifier.next(InputSymbol.CompressedData); signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( @@ -356,7 +356,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processOnePassSignature() throws PGPException, IOException { - syntaxVerifier.next(InputAlphabet.OnePassSignature); + syntaxVerifier.next(InputSymbol.OnePassSignature); PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + " at depth " + metadata.depth + " encountered"); @@ -365,8 +365,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processSignature() throws PGPException, IOException { // true if Signature corresponds to OnePassSignature - boolean isSigForOPS = syntaxVerifier.peekStack() == StackAlphabet.ops; - syntaxVerifier.next(InputAlphabet.Signature); + boolean isSigForOPS = syntaxVerifier.peekStack() == StackSymbol.ops; + syntaxVerifier.next(InputSymbol.Signature); PGPSignature signature; try { signature = packetInputStream.readSignature(); @@ -391,7 +391,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private boolean processEncryptedData() throws IOException, PGPException { LOGGER.debug("Symmetrically Encrypted Data Packet at depth " + metadata.depth + " encountered"); - syntaxVerifier.next(InputAlphabet.EncryptedData); + syntaxVerifier.next(InputSymbol.EncryptedData); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); // TODO: Replace with !encDataList.isIntegrityProtected() @@ -766,7 +766,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } if (packetInputStream != null) { - syntaxVerifier.next(InputAlphabet.EndOfSequence); + syntaxVerifier.next(InputSymbol.EndOfSequence); syntaxVerifier.assertValid(); packetInputStream.close(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java similarity index 98% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java index f73ede34..854c3305 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java @@ -10,7 +10,7 @@ import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPOnePassSignatureList; import org.bouncycastle.openpgp.PGPSignatureList; -public enum InputAlphabet { +public enum InputSymbol { /** * A {@link PGPLiteralData} packet. */ diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java index ab45a896..4c811e9f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -6,10 +6,20 @@ package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; +/** + * This class describes the syntax for OpenPGP messages as specified by rfc4880. + * + * @see + * rfc4880 - §11.3. OpenPGP Messages + * @see + * Blog post about theoretic background and translation of grammar to PDA syntax + * @see + * Blog post about practically implementing the PDA for packet syntax validation + */ public class OpenPgpMessageSyntax implements Syntax { @Override - public Transition transition(State from, InputAlphabet input, StackAlphabet stackItem) + public Transition transition(State from, InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (from) { case OpenPgpMessage: @@ -27,9 +37,9 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(from, input, stackItem); } - Transition fromOpenPgpMessage(InputAlphabet input, StackAlphabet stackItem) + Transition fromOpenPgpMessage(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { - if (stackItem != StackAlphabet.msg) { + if (stackItem != StackSymbol.msg) { throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); } @@ -38,10 +48,10 @@ public class OpenPgpMessageSyntax implements Syntax { return new Transition(State.LiteralMessage); case Signature: - return new Transition(State.OpenPgpMessage, StackAlphabet.msg); + return new Transition(State.OpenPgpMessage, StackSymbol.msg); case OnePassSignature: - return new Transition(State.OpenPgpMessage, StackAlphabet.ops, StackAlphabet.msg); + return new Transition(State.OpenPgpMessage, StackSymbol.ops, StackSymbol.msg); case CompressedData: return new Transition(State.CompressedMessage); @@ -55,17 +65,17 @@ public class OpenPgpMessageSyntax implements Syntax { } } - Transition fromLiteralMessage(InputAlphabet input, StackAlphabet stackItem) + Transition fromLiteralMessage(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: - if (stackItem == StackAlphabet.ops) { + if (stackItem == StackSymbol.ops) { return new Transition(State.LiteralMessage); } break; case EndOfSequence: - if (stackItem == StackAlphabet.terminus) { + if (stackItem == StackSymbol.terminus) { return new Transition(State.Valid); } break; @@ -74,17 +84,17 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem); } - Transition fromCompressedMessage(InputAlphabet input, StackAlphabet stackItem) + Transition fromCompressedMessage(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: - if (stackItem == StackAlphabet.ops) { + if (stackItem == StackSymbol.ops) { return new Transition(State.CompressedMessage); } break; case EndOfSequence: - if (stackItem == StackAlphabet.terminus) { + if (stackItem == StackSymbol.terminus) { return new Transition(State.Valid); } break; @@ -93,17 +103,17 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem); } - Transition fromEncryptedMessage(InputAlphabet input, StackAlphabet stackItem) + Transition fromEncryptedMessage(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: - if (stackItem == StackAlphabet.ops) { + if (stackItem == StackSymbol.ops) { return new Transition(State.EncryptedMessage); } break; case EndOfSequence: - if (stackItem == StackAlphabet.terminus) { + if (stackItem == StackSymbol.terminus) { return new Transition(State.Valid); } break; @@ -112,8 +122,9 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem); } - Transition fromValid(InputAlphabet input, StackAlphabet stackItem) + Transition fromValid(InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException { + // There is no applicable transition rule out of Valid throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 94c1739e..68a5e2c4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -13,15 +13,15 @@ import java.util.Arrays; import java.util.List; import java.util.Stack; -import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.msg; -import static org.pgpainless.decryption_verification.syntax_check.StackAlphabet.terminus; +import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.msg; +import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.terminus; public class PDA { private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); - private final Stack stack = new Stack<>(); - private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting + private final Stack stack = new Stack<>(); + private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting private State state; private Syntax syntax = new OpenPgpMessageSyntax(); @@ -31,12 +31,12 @@ public class PDA { pushStack(msg); } - public void next(InputAlphabet input) throws MalformedOpenPgpMessageException { + public void next(InputSymbol input) throws MalformedOpenPgpMessageException { try { Transition transition = syntax.transition(state, input, popStack()); inputs.add(input); state = transition.getNewState(); - for (StackAlphabet item : transition.getPushedItems()) { + for (StackSymbol item : transition.getPushedItems()) { pushStack(item); } } catch (MalformedOpenPgpMessageException e) { @@ -56,7 +56,7 @@ public class PDA { return state; } - public StackAlphabet peekStack() { + public StackSymbol peekStack() { if (stack.isEmpty()) { return null; } @@ -83,7 +83,7 @@ public class PDA { * * @return stack item */ - private StackAlphabet popStack() { + private StackSymbol popStack() { if (stack.isEmpty()) { return null; } @@ -95,7 +95,7 @@ public class PDA { * * @param item item */ - private void pushStack(StackAlphabet item) { + private void pushStack(StackSymbol item) { stack.push(item); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java similarity index 93% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java rename to pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java index a8a2a213..120458e5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackAlphabet.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java @@ -4,7 +4,7 @@ package org.pgpainless.decryption_verification.syntax_check; -public enum StackAlphabet { +public enum StackSymbol { /** * OpenPGP Message. */ diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java index 47813a9e..63d63fed 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java @@ -6,8 +6,25 @@ package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; +/** + * This interface can be used to define a custom syntax for the {@link PDA}. + */ public interface Syntax { - Transition transition(State from, InputAlphabet inputAlphabet, StackAlphabet stackItem) + /** + * Describe a transition rule from {@link State}
from
for {@link InputSymbol}
input
+ * with {@link StackSymbol}
stackItem
from the top of the {@link PDA PDAs} stack. + * The resulting {@link Transition} contains the new {@link State}, as well as a list of + * {@link StackSymbol StackSymbols} that get pushed onto the stack by the transition rule. + * If there is no applicable rule, a {@link MalformedOpenPgpMessageException} is thrown, since in this case + * the {@link InputSymbol} must be considered illegal. + * + * @param from current state of the PDA + * @param input input symbol + * @param stackItem item that got popped from the top of the stack + * @return applicable transition rule containing the new state and pushed stack symbols + * @throws MalformedOpenPgpMessageException if there is no applicable transition rule (the input symbol is illegal) + */ + Transition transition(State from, InputSymbol input, StackSymbol stackItem) throws MalformedOpenPgpMessageException; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java index bbc28e58..a0e58cf0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java @@ -10,10 +10,10 @@ import java.util.List; public class Transition { - private final List pushedItems = new ArrayList<>(); + private final List pushedItems = new ArrayList<>(); private final State newState; - public Transition(State newState, StackAlphabet... pushedItems) { + public Transition(State newState, StackSymbol... pushedItems) { this.newState = newState; this.pushedItems.addAll(Arrays.asList(pushedItems)); } @@ -22,7 +22,7 @@ public class Transition { return newState; } - public List getPushedItems() { + public List getPushedItems() { return new ArrayList<>(pushedItems); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java index db8d0df6..f98a4048 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -4,8 +4,8 @@ package org.pgpainless.exception; -import org.pgpainless.decryption_verification.syntax_check.InputAlphabet; -import org.pgpainless.decryption_verification.syntax_check.StackAlphabet; +import org.pgpainless.decryption_verification.syntax_check.InputSymbol; +import org.pgpainless.decryption_verification.syntax_check.StackSymbol; import org.pgpainless.decryption_verification.syntax_check.State; /** @@ -20,7 +20,7 @@ public class MalformedOpenPgpMessageException extends RuntimeException { super(message); } - public MalformedOpenPgpMessageException(State state, InputAlphabet input, StackAlphabet stackItem) { + public MalformedOpenPgpMessageException(State state, InputSymbol input, StackSymbol stackItem) { this("There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 9250acfa..2b66eee0 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -21,8 +21,8 @@ public class PDATest { @Test public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -35,10 +35,10 @@ public class PDATest { @Test public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -52,9 +52,9 @@ public class PDATest { @Test public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.Signature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -68,11 +68,11 @@ public class PDATest { @Test public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.CompressedData); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.CompressedData); // Here would be a nested PDA for the LiteralData packet - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -80,105 +80,105 @@ public class PDATest { @Test public void testOPSSignedEncryptedMessageIsValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.EncryptedData); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.EncryptedData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @Test public void anyInputAfterEOSIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.Signature)); + () -> check.next(InputSymbol.Signature)); } @Test public void testEncryptedMessageWithAppendedStandalongSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.EncryptedData); + check.next(InputSymbol.EncryptedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.Signature)); + () -> check.next(InputSymbol.Signature)); } @Test public void testOPSSignedEncryptedMessageWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.EncryptedData); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.EncryptedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.EndOfSequence)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testTwoLiteralDataIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.LiteralData); + check.next(InputSymbol.LiteralData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.LiteralData)); + () -> check.next(InputSymbol.LiteralData)); } @Test public void testTrailingSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.LiteralData); + check.next(InputSymbol.LiteralData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.Signature)); + () -> check.next(InputSymbol.Signature)); } @Test public void testOPSAloneIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); + check.next(InputSymbol.OnePassSignature); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.EndOfSequence)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testOPSLitWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.LiteralData); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.EndOfSequence)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testCompressedMessageWithStandalongAppendedSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.CompressedData); + check.next(InputSymbol.CompressedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.Signature)); + () -> check.next(InputSymbol.Signature)); } @Test public void testOPSCompressedDataWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.CompressedData); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.CompressedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.EndOfSequence)); + () -> check.next(InputSymbol.EndOfSequence)); } @Test public void testCompressedMessageFollowedByTrailingLiteralDataIsNotValid() { PDA check = new PDA(); - check.next(InputAlphabet.CompressedData); + check.next(InputSymbol.CompressedData); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputAlphabet.LiteralData)); + () -> check.next(InputSymbol.LiteralData)); } @Test public void testOPSWithPrependedSigIsValid() { PDA check = new PDA(); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.Signature); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } @@ -186,11 +186,11 @@ public class PDATest { @Test public void testPrependedSigInsideOPSSignedMessageIsValid() { PDA check = new PDA(); - check.next(InputAlphabet.OnePassSignature); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.LiteralData); - check.next(InputAlphabet.Signature); - check.next(InputAlphabet.EndOfSequence); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.Signature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); assertTrue(check.isValid()); } From ec793c66ffab343ab32cb659e32c279be9279a90 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 12:42:30 +0200 Subject: [PATCH 0514/1204] More cleanup and better error reporting --- .../syntax_check/OpenPgpMessageSyntax.java | 15 ++++++++----- .../syntax_check/PDA.java | 19 +++++++++++----- .../syntax_check/Syntax.java | 5 ++++- .../syntax_check/Transition.java | 22 ++++++++++++++++++- .../syntax_check/PDATest.java | 2 +- 5 files changed, 48 insertions(+), 15 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java index 4c811e9f..c6de8765 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -6,6 +6,9 @@ package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * This class describes the syntax for OpenPGP messages as specified by rfc4880. * @@ -19,7 +22,7 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; public class OpenPgpMessageSyntax implements Syntax { @Override - public Transition transition(State from, InputSymbol input, StackSymbol stackItem) + public @Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (from) { case OpenPgpMessage: @@ -37,7 +40,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(from, input, stackItem); } - Transition fromOpenPgpMessage(InputSymbol input, StackSymbol stackItem) + Transition fromOpenPgpMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { if (stackItem != StackSymbol.msg) { throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); @@ -65,7 +68,7 @@ public class OpenPgpMessageSyntax implements Syntax { } } - Transition fromLiteralMessage(InputSymbol input, StackSymbol stackItem) + Transition fromLiteralMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: @@ -84,7 +87,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem); } - Transition fromCompressedMessage(InputSymbol input, StackSymbol stackItem) + Transition fromCompressedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: @@ -103,7 +106,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem); } - Transition fromEncryptedMessage(InputSymbol input, StackSymbol stackItem) + Transition fromEncryptedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { case Signature: @@ -122,7 +125,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem); } - Transition fromValid(InputSymbol input, StackSymbol stackItem) + Transition fromValid(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { // There is no applicable transition rule out of Valid throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 68a5e2c4..ed9175fd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -20,10 +20,13 @@ public class PDA { private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); + // right now we implement what rfc4880 specifies. + // TODO: Consider implementing what we proposed here: + // https://mailarchive.ietf.org/arch/msg/openpgp/uepOF6XpSegMO4c59tt9e5H1i4g/ + private final Syntax syntax = new OpenPgpMessageSyntax(); private final Stack stack = new Stack<>(); - private final List inputs = new ArrayList<>(); // keep track of inputs for debugging / error reporting + private final List inputs = new ArrayList<>(); // Track inputs for debugging / error reporting private State state; - private Syntax syntax = new OpenPgpMessageSyntax(); public PDA() { state = State.OpenPgpMessage; @@ -32,16 +35,20 @@ public class PDA { } public void next(InputSymbol input) throws MalformedOpenPgpMessageException { + StackSymbol stackSymbol = popStack(); try { - Transition transition = syntax.transition(state, input, popStack()); - inputs.add(input); + Transition transition = syntax.transition(state, input, stackSymbol); state = transition.getNewState(); for (StackSymbol item : transition.getPushedItems()) { pushStack(item); } + inputs.add(input); } catch (MalformedOpenPgpMessageException e) { - MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException("Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + - ", token '" + input + "' is unexpected and illegal.", e); + MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException( + "Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + + ", token '" + input + "' is not allowed." + + "\nNo transition from state '" + state + "' with stack " + Arrays.toString(stack.toArray()) + + (stackSymbol != null ? "||'" + stackSymbol + "'." : "."), e); LOGGER.debug("Invalid input '" + input + "'", wrapped); throw wrapped; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java index 63d63fed..2f3d0a57 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java @@ -6,6 +6,9 @@ package org.pgpainless.decryption_verification.syntax_check; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * This interface can be used to define a custom syntax for the {@link PDA}. */ @@ -25,6 +28,6 @@ public interface Syntax { * @return applicable transition rule containing the new state and pushed stack symbols * @throws MalformedOpenPgpMessageException if there is no applicable transition rule (the input symbol is illegal) */ - Transition transition(State from, InputSymbol input, StackSymbol stackItem) + @Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java index a0e58cf0..ab0db5ef 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java @@ -4,24 +4,44 @@ package org.pgpainless.decryption_verification.syntax_check; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +/** + * Result of applying a transition rule. + * Transition rules can be described by implementing the {@link Syntax} interface. + */ public class Transition { private final List pushedItems = new ArrayList<>(); private final State newState; - public Transition(State newState, StackSymbol... pushedItems) { + public Transition(@Nonnull State newState, @Nonnull StackSymbol... pushedItems) { this.newState = newState; this.pushedItems.addAll(Arrays.asList(pushedItems)); } + /** + * Return the new {@link State} that is reached by applying the transition. + * + * @return new state + */ + @Nonnull public State getNewState() { return newState; } + /** + * Return a list of {@link StackSymbol StackSymbols} that are pushed onto the stack + * by applying the transition. + * The list contains items in the order in which they are pushed onto the stack. + * The list may be empty. + * + * @return list of items to be pushed onto the stack + */ + @Nonnull public List getPushedItems() { return new ArrayList<>(pushedItems); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index 2b66eee0..d0486a3e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -97,7 +97,7 @@ public class PDATest { } @Test - public void testEncryptedMessageWithAppendedStandalongSigIsNotValid() { + public void testEncryptedMessageWithAppendedStandaloneSigIsNotValid() { PDA check = new PDA(); check.next(InputSymbol.EncryptedData); assertThrows(MalformedOpenPgpMessageException.class, From 161ce577115f20c797f79d83d36655c3d0834c6e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 12:47:42 +0200 Subject: [PATCH 0515/1204] Clean up old unused code --- .../DecryptionBuilderInterface.java | 2 +- .../DecryptionStreamImpl.java | 65 ------ .../SignatureInputStream.java | 215 ------------------ .../exception/FinalIOException.java | 17 -- .../MissingLiteralDataException.java | 17 -- 5 files changed, 1 insertion(+), 315 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java index b35911de..07db42f0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java @@ -13,7 +13,7 @@ import org.bouncycastle.openpgp.PGPException; public interface DecryptionBuilderInterface { /** - * Create a {@link DecryptionStreamImpl} on an {@link InputStream} which contains the encrypted and/or signed data. + * Create a {@link DecryptionStream} on an {@link InputStream} which contains the encrypted and/or signed data. * * @param inputStream encrypted and/or signed data. * @return api handle diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java deleted file mode 100644 index 27ace697..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamImpl.java +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nonnull; - -import org.bouncycastle.util.io.Streams; - -/** - * Decryption Stream that handles updating and verification of detached signatures, - * as well as verification of integrity-protected input streams once the stream gets closed. - */ -public class DecryptionStreamImpl extends DecryptionStream { - - private final InputStream inputStream; - private final IntegrityProtectedInputStream integrityProtectedInputStream; - private final InputStream armorStream; - - /** - * Create an input stream that handles decryption and - if necessary - integrity protection verification. - * - * @param wrapped underlying input stream - * @param resultBuilder builder for decryption metadata like algorithms, recipients etc. - * @param integrityProtectedInputStream in case of data encrypted using SEIP packet close this stream to check integrity - * @param armorStream armor stream to verify CRC checksums - */ - DecryptionStreamImpl(@Nonnull InputStream wrapped, - @Nonnull OpenPgpMetadata.Builder resultBuilder, - IntegrityProtectedInputStream integrityProtectedInputStream, - InputStream armorStream) { - super(resultBuilder); - this.inputStream = wrapped; - this.integrityProtectedInputStream = integrityProtectedInputStream; - this.armorStream = armorStream; - } - - @Override - public void close() throws IOException { - if (armorStream != null) { - Streams.drain(armorStream); - } - inputStream.close(); - if (integrityProtectedInputStream != null) { - integrityProtectedInputStream.close(); - } - super.close(); - } - - @Override - public int read() throws IOException { - int r = inputStream.read(); - return r; - } - - @Override - public int read(@Nonnull byte[] bytes, int offset, int length) throws IOException { - int read = inputStream.read(bytes, offset, length); - return read; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java deleted file mode 100644 index 70a2f4ef..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import static org.pgpainless.signature.consumer.SignatureValidator.signatureWasCreatedInBounds; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Map; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.pgpainless.PGPainless; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.consumer.CertificateValidator; -import org.pgpainless.signature.consumer.SignatureCheck; -import org.pgpainless.signature.consumer.OnePassSignatureCheck; -import org.pgpainless.signature.SignatureUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public abstract class SignatureInputStream extends FilterInputStream { - - protected SignatureInputStream(InputStream inputStream) { - super(inputStream); - } - - public static class VerifySignatures extends SignatureInputStream { - - private static final Logger LOGGER = LoggerFactory.getLogger(VerifySignatures.class); - - private final PGPObjectFactory objectFactory; - private final List opSignatures; - private final Map opSignaturesWithMissingCert; - private final List detachedSignatures; - private final ConsumerOptions options; - private final OpenPgpMetadata.Builder resultBuilder; - - public VerifySignatures( - InputStream literalDataStream, - @Nullable PGPObjectFactory objectFactory, - List opSignatures, - Map onePassSignaturesWithMissingCert, - List detachedSignatures, - ConsumerOptions options, - OpenPgpMetadata.Builder resultBuilder) { - super(literalDataStream); - this.objectFactory = objectFactory; - this.opSignatures = opSignatures; - this.opSignaturesWithMissingCert = onePassSignaturesWithMissingCert; - this.detachedSignatures = detachedSignatures; - this.options = options; - this.resultBuilder = resultBuilder; - } - - @Override - public int read() throws IOException { - final int data = super.read(); - final boolean endOfStream = data == -1; - if (endOfStream) { - finalizeSignatures(); - } else { - byte b = (byte) data; - updateOnePassSignatures(b); - updateDetachedSignatures(b); - } - return data; - } - - @Override - public int read(@Nonnull byte[] b, int off, int len) throws IOException { - int read = super.read(b, off, len); - - final boolean endOfStream = read == -1; - if (endOfStream) { - finalizeSignatures(); - } else { - updateOnePassSignatures(b, off, read); - updateDetachedSignatures(b, off, read); - } - return read; - } - - private void finalizeSignatures() { - parseAndCombineSignatures(); - verifyOnePassSignatures(); - verifyDetachedSignatures(); - } - - public void parseAndCombineSignatures() { - if (objectFactory == null) { - return; - } - // Parse signatures from message - PGPSignatureList signatures; - try { - signatures = parseSignatures(objectFactory); - } catch (IOException e) { - return; - } - List signatureList = SignatureUtils.toList(signatures); - // Set signatures as comparison sigs in OPS checks - for (int i = 0; i < opSignatures.size(); i++) { - int reversedIndex = opSignatures.size() - i - 1; - opSignatures.get(i).setSignature(signatureList.get(reversedIndex)); - } - - for (PGPSignature signature : signatureList) { - if (opSignaturesWithMissingCert.containsKey(signature.getKeyID())) { - OnePassSignatureCheck check = opSignaturesWithMissingCert.remove(signature.getKeyID()); - check.setSignature(signature); - - resultBuilder.addInvalidInbandSignature(new SignatureVerification(signature, null), - new SignatureValidationException( - "Missing verification certificate " + Long.toHexString(signature.getKeyID()))); - } - } - } - - private PGPSignatureList parseSignatures(PGPObjectFactory objectFactory) throws IOException { - PGPSignatureList signatureList = null; - Object pgpObject = objectFactory.nextObject(); - while (pgpObject != null && signatureList == null) { - if (pgpObject instanceof PGPSignatureList) { - signatureList = (PGPSignatureList) pgpObject; - } else { - pgpObject = objectFactory.nextObject(); - } - } - - if (signatureList == null || signatureList.isEmpty()) { - throw new IOException("Verification failed - No Signatures found"); - } - - return signatureList; - } - - - private synchronized void verifyOnePassSignatures() { - Policy policy = PGPainless.getPolicy(); - for (OnePassSignatureCheck opSignature : opSignatures) { - if (opSignature.getSignature() == null) { - LOGGER.warn("Found OnePassSignature without respective signature packet -> skip"); - continue; - } - - try { - signatureWasCreatedInBounds(options.getVerifyNotBefore(), - options.getVerifyNotAfter()).verify(opSignature.getSignature()); - CertificateValidator.validateCertificateAndVerifyOnePassSignature(opSignature, policy); - resultBuilder.addVerifiedInbandSignature( - new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey())); - } catch (SignatureValidationException e) { - LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}", - opSignature.getSigningKey(), e.getMessage(), e); - resultBuilder.addInvalidInbandSignature( - new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey()), e); - } - } - } - - private void verifyDetachedSignatures() { - Policy policy = PGPainless.getPolicy(); - for (SignatureCheck s : detachedSignatures) { - try { - signatureWasCreatedInBounds(options.getVerifyNotBefore(), - options.getVerifyNotAfter()).verify(s.getSignature()); - CertificateValidator.validateCertificateAndVerifyInitializedSignature(s.getSignature(), - (PGPPublicKeyRing) s.getSigningKeyRing(), policy); - resultBuilder.addVerifiedDetachedSignature(new SignatureVerification(s.getSignature(), - s.getSigningKeyIdentifier())); - } catch (SignatureValidationException e) { - LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}", - s.getSigningKeyIdentifier(), e.getMessage(), e); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(s.getSignature(), - s.getSigningKeyIdentifier()), e); - } - } - } - - private void updateOnePassSignatures(byte data) { - for (OnePassSignatureCheck opSignature : opSignatures) { - opSignature.getOnePassSignature().update(data); - } - } - - private void updateOnePassSignatures(byte[] bytes, int offset, int length) { - for (OnePassSignatureCheck opSignature : opSignatures) { - opSignature.getOnePassSignature().update(bytes, offset, length); - } - } - - private void updateDetachedSignatures(byte b) { - for (SignatureCheck detachedSignature : detachedSignatures) { - detachedSignature.getSignature().update(b); - } - } - - private void updateDetachedSignatures(byte[] b, int off, int read) { - for (SignatureCheck detachedSignature : detachedSignatures) { - detachedSignature.getSignature().update(b, off, read); - } - } - - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java deleted file mode 100644 index 6b6f86de..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import java.io.IOException; - -/** - * Wrapper for {@link IOException} indicating that we need to throw this exception up. - */ -public class FinalIOException extends IOException { - - public FinalIOException(IOException e) { - super(e); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java deleted file mode 100644 index e396b1df..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import org.bouncycastle.openpgp.PGPException; - -/** - * Exception that gets thrown if a {@link org.bouncycastle.bcpg.LiteralDataPacket} is expected, but not found. - */ -public class MissingLiteralDataException extends PGPException { - - public MissingLiteralDataException(String message) { - super(message); - } -} From 8cb7d19487d01d4cf0746782068bff500713c2b4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 13:13:27 +0200 Subject: [PATCH 0516/1204] Allow injection of different syntax into PDA --- .../syntax_check/PDA.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index ed9175fd..65210ce0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -8,6 +8,7 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -23,15 +24,24 @@ public class PDA { // right now we implement what rfc4880 specifies. // TODO: Consider implementing what we proposed here: // https://mailarchive.ietf.org/arch/msg/openpgp/uepOF6XpSegMO4c59tt9e5H1i4g/ - private final Syntax syntax = new OpenPgpMessageSyntax(); + private final Syntax syntax; private final Stack stack = new Stack<>(); private final List inputs = new ArrayList<>(); // Track inputs for debugging / error reporting private State state; + /** + * + */ public PDA() { - state = State.OpenPgpMessage; - pushStack(terminus); - pushStack(msg); + this(new OpenPgpMessageSyntax(), State.OpenPgpMessage, terminus, msg); + } + + public PDA(@Nonnull Syntax syntax, @Nonnull State initialState, @Nonnull StackSymbol... initialStack) { + this.syntax = syntax; + this.state = initialState; + for (StackSymbol symbol : initialStack) { + pushStack(symbol); + } } public void next(InputSymbol input) throws MalformedOpenPgpMessageException { From 208612ab56d96c0785d9938b31265644023debdc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 13:53:54 +0200 Subject: [PATCH 0517/1204] Add (commented-out) read(buf, off, len) implementation for DelayedTeeInputStream --- .../TeeBCPGInputStream.java | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index bbcf593e..52ec9001 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -26,20 +26,20 @@ import org.pgpainless.algorithm.OpenPgpPacket; * {@link BCPGInputStream#readPacket()} inconsistently calls a mix of {@link BCPGInputStream#read()} and * {@link InputStream#read()} of the underlying stream. This would cause the second length byte to get swallowed up. * - * Therefore, this class delegates the teeing to an {@link DelayedTeeInputStreamInputStream} which wraps the underlying + * Therefore, this class delegates the teeing to an {@link DelayedTeeInputStream} which wraps the underlying * stream. Since calling {@link BCPGInputStream#nextPacketTag()} reads up to and including the next packets tag, * we need to delay teeing out that byte to signature verifiers. * Hence, the reading methods of the {@link TeeBCPGInputStream} handle pushing this byte to the output stream using - * {@link DelayedTeeInputStreamInputStream#squeeze()}. + * {@link DelayedTeeInputStream#squeeze()}. */ public class TeeBCPGInputStream { - protected final DelayedTeeInputStreamInputStream delayedTee; + protected final DelayedTeeInputStream delayedTee; // InputStream of OpenPGP packets of the current layer protected final BCPGInputStream packetInputStream; public TeeBCPGInputStream(BCPGInputStream inputStream, OutputStream outputStream) { - this.delayedTee = new DelayedTeeInputStreamInputStream(inputStream, outputStream); + this.delayedTee = new DelayedTeeInputStream(inputStream, outputStream); this.packetInputStream = BCPGInputStream.wrap(delayedTee); } @@ -100,13 +100,13 @@ public class TeeBCPGInputStream { this.packetInputStream.close(); } - public static class DelayedTeeInputStreamInputStream extends InputStream { + public static class DelayedTeeInputStream extends InputStream { private int last = -1; private final InputStream inputStream; private final OutputStream outputStream; - public DelayedTeeInputStreamInputStream(InputStream inputStream, OutputStream outputStream) { + public DelayedTeeInputStream(InputStream inputStream, OutputStream outputStream) { this.inputStream = inputStream; this.outputStream = outputStream; } @@ -127,6 +127,26 @@ public class TeeBCPGInputStream { } } + // TODO: Uncomment, once BC-172.1 is available + // see https://github.com/bcgit/bc-java/issues/1257 + /* + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (last != -1) { + outputStream.write(last); + } + + int r = inputStream.read(b, off, len); + if (r > 0) { + outputStream.write(b, off, r - 1); + last = b[off + r - 1]; + } else { + last = -1; + } + return r; + } + */ + /** * Squeeze the last byte out and update the output stream. * From 8fafb6aa562255c7961c7061520c2d5db21eae4b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Oct 2022 13:55:58 +0200 Subject: [PATCH 0518/1204] Add comments --- .../OpenPgpMessageInputStream.java | 14 ++++++++++++-- .../decryption_verification/syntax_check/PDA.java | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index a0f17210..1b11744c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -116,6 +116,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { /** * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of * OpenPGP messages and signatures. + * This factory method takes a custom {@link Policy} instead of using the global policy object. * * @param inputStream underlying input stream containing the OpenPGP message * @param options options for consuming the message @@ -161,7 +162,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { armorIn, options, metadata, policy); } } else { - throw new AssertionError("Huh?"); + throw new AssertionError("Cannot deduce type of data."); } } @@ -212,6 +213,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { switch (type) { + // Binary OpenPGP Message case standard: // tee out packet bytes for signature verification packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), this.signatures); @@ -220,6 +222,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { consumePackets(); break; + // Cleartext Signature Framework (probably signed message) case cleartext_signed: resultBuilder.setCleartextSigned(); MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); @@ -235,6 +238,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { nestedInputStream = new TeeInputStream(multiPassStrategy.getMessageInputStream(), this.signatures); break; + // Non-OpenPGP Data (e.g. detached signature verification) case non_openpgp: packetInputStream = null; nestedInputStream = new TeeInputStream(inputStream, this.signatures); @@ -265,7 +269,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return; } - loop: // we break this when we go deeper. + loop: // we break this when we enter nested packets and later resume while ((nextPacket = packetInputStream.nextPacketTag()) != null) { signatures.nextPacket(nextPacket); switch (nextPacket) { @@ -296,6 +300,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { case SED: case SEIPD: if (processEncryptedData()) { + // Successfully decrypted, enter nested content break loop; } @@ -336,10 +341,12 @@ public class OpenPgpMessageInputStream extends DecryptionStream { LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); syntaxVerifier.next(InputSymbol.LiteralData); PGPLiteralData literalData = packetInputStream.readLiteralData(); + // Extract Metadata this.metadata.setChild(new MessageMetadata.LiteralData( literalData.getFileName(), literalData.getModificationTime(), StreamEncoding.requireFromCode(literalData.getFormat()))); + nestedInputStream = literalData.getDataStream(); } @@ -347,9 +354,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { syntaxVerifier.next(InputSymbol.CompressedData); signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); + // Extract Metadata MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( CompressionAlgorithm.fromId(compressedData.getAlgorithm()), metadata.depth + 1); + LOGGER.debug("Compressed Data Packet (" + compressionLayer.algorithm + ") at depth " + metadata.depth + " encountered"); InputStream decompressed = compressedData.getDataStream(); nestedInputStream = new OpenPgpMessageInputStream(decompressed, options, compressionLayer, policy); @@ -374,6 +383,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { LOGGER.debug("Unsupported Signature at depth " + metadata.depth + " encountered.", e); return; } + long keyId = SignatureUtils.determineIssuerKeyId(signature); if (isSigForOPS) { LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key " + diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 65210ce0..07a5fdcb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -30,7 +30,7 @@ public class PDA { private State state; /** - * + * Default constructor which initializes the PDA to work with the {@link OpenPgpMessageSyntax}. */ public PDA() { this(new OpenPgpMessageSyntax(), State.OpenPgpMessage, terminus, msg); From 705e36080c6030056143e7adb0af917d9a075a3d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 23 Sep 2022 14:51:06 +0200 Subject: [PATCH 0519/1204] Implement caching PublicKeyDataDecryptorFactory --- .../CachingPublicKeyDataDecryptorFactory.java | 75 +++++++++++++++++++ .../java/org/bouncycastle/package-info.java | 8 ++ 2 files changed, 83 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java create mode 100644 pgpainless-core/src/main/java/org/bouncycastle/package-info.java diff --git a/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java new file mode 100644 index 00000000..1498b6f2 --- /dev/null +++ b/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.operator.PGPDataDecryptor; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.util.encoders.Base64; + +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of the {@link PublicKeyDataDecryptorFactory} which caches decrypted session keys. + * That way, if a message needs to be decrypted multiple times, expensive private key operations can be omitted. + * + * This implementation changes the behavior or {@link #recoverSessionData(int, byte[][])} to first return any + * cache hits. + * If no hit is found, the method call is delegated to the underlying {@link PublicKeyDataDecryptorFactory}. + * The result of that is then placed in the cache and returned. + * + * TODO: Do we also cache invalid session keys? + */ +public class CachingPublicKeyDataDecryptorFactory implements PublicKeyDataDecryptorFactory { + + private final Map cachedSessionKeys = new HashMap<>(); + private final PublicKeyDataDecryptorFactory factory; + + public CachingPublicKeyDataDecryptorFactory(PublicKeyDataDecryptorFactory factory) { + this.factory = factory; + } + + @Override + public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { + byte[] sessionKey = lookup(secKeyData); + if (sessionKey == null) { + sessionKey = factory.recoverSessionData(keyAlgorithm, secKeyData); + cache(secKeyData, sessionKey); + } + return sessionKey; + } + + private byte[] lookup(byte[][] secKeyData) { + byte[] sk = secKeyData[0]; + String key = Base64.toBase64String(sk); + byte[] sessionKey = cachedSessionKeys.get(key); + return copy(sessionKey); + } + + private void cache(byte[][] secKeyData, byte[] sessionKey) { + byte[] sk = secKeyData[0]; + String key = Base64.toBase64String(sk); + cachedSessionKeys.put(key, copy(sessionKey)); + } + + private static byte[] copy(byte[] bytes) { + if (bytes == null) { + return null; + } + byte[] copy = new byte[bytes.length]; + System.arraycopy(bytes, 0, copy, 0, copy.length); + return copy; + } + + public void clear() { + cachedSessionKeys.clear(); + } + + @Override + public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) throws PGPException { + return null; + } +} diff --git a/pgpainless-core/src/main/java/org/bouncycastle/package-info.java b/pgpainless-core/src/main/java/org/bouncycastle/package-info.java new file mode 100644 index 00000000..565bb5f4 --- /dev/null +++ b/pgpainless-core/src/main/java/org/bouncycastle/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Classes which could be upstreamed to BC at some point. + */ +package org.bouncycastle; From 8c0d096fc6d34566f937ae26fb49cae5dc38d534 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 14:56:06 +0200 Subject: [PATCH 0520/1204] Fix CachingBcPublicKeyDataDecryptorFactory --- ...chingBcPublicKeyDataDecryptorFactory.java} | 44 ++++++--- ...ngBcPublicKeyDataDecryptorFactoryTest.java | 96 +++++++++++++++++++ 2 files changed, 126 insertions(+), 14 deletions(-) rename pgpainless-core/src/main/java/org/bouncycastle/{CachingPublicKeyDataDecryptorFactory.java => CachingBcPublicKeyDataDecryptorFactory.java} (52%) create mode 100644 pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java diff --git a/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java similarity index 52% rename from pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java rename to pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java index 1498b6f2..3c967224 100644 --- a/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java +++ b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java @@ -5,9 +5,15 @@ package org.bouncycastle; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.operator.PGPDataDecryptor; +import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.bouncycastle.util.encoders.Base64; +import org.bouncycastle.util.encoders.Hex; +import org.pgpainless.decryption_verification.CustomPublicKeyDataDecryptorFactory; +import org.pgpainless.key.SubkeyIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; @@ -20,36 +26,46 @@ import java.util.Map; * cache hits. * If no hit is found, the method call is delegated to the underlying {@link PublicKeyDataDecryptorFactory}. * The result of that is then placed in the cache and returned. - * - * TODO: Do we also cache invalid session keys? */ -public class CachingPublicKeyDataDecryptorFactory implements PublicKeyDataDecryptorFactory { +public class CachingBcPublicKeyDataDecryptorFactory + extends BcPublicKeyDataDecryptorFactory + implements CustomPublicKeyDataDecryptorFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(CachingBcPublicKeyDataDecryptorFactory.class); private final Map cachedSessionKeys = new HashMap<>(); - private final PublicKeyDataDecryptorFactory factory; + private final SubkeyIdentifier decryptionKey; - public CachingPublicKeyDataDecryptorFactory(PublicKeyDataDecryptorFactory factory) { - this.factory = factory; + public CachingBcPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey, SubkeyIdentifier decryptionKey) { + super(privateKey); + this.decryptionKey = decryptionKey; } @Override public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { - byte[] sessionKey = lookup(secKeyData); + byte[] sessionKey = lookupSessionKeyData(secKeyData); if (sessionKey == null) { - sessionKey = factory.recoverSessionData(keyAlgorithm, secKeyData); - cache(secKeyData, sessionKey); + LOGGER.debug("Cache miss for encrypted session key " + Hex.toHexString(secKeyData[0])); + sessionKey = costlyRecoverSessionData(keyAlgorithm, secKeyData); + cacheSessionKeyData(secKeyData, sessionKey); + } else { + LOGGER.debug("Cache hit for encrypted session key " + Hex.toHexString(secKeyData[0])); } return sessionKey; } - private byte[] lookup(byte[][] secKeyData) { + public byte[] costlyRecoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { + return super.recoverSessionData(keyAlgorithm, secKeyData); + } + + private byte[] lookupSessionKeyData(byte[][] secKeyData) { byte[] sk = secKeyData[0]; String key = Base64.toBase64String(sk); byte[] sessionKey = cachedSessionKeys.get(key); return copy(sessionKey); } - private void cache(byte[][] secKeyData, byte[] sessionKey) { + private void cacheSessionKeyData(byte[][] secKeyData, byte[] sessionKey) { byte[] sk = secKeyData[0]; String key = Base64.toBase64String(sk); cachedSessionKeys.put(key, copy(sessionKey)); @@ -69,7 +85,7 @@ public class CachingPublicKeyDataDecryptorFactory implements PublicKeyDataDecryp } @Override - public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) throws PGPException { - return null; + public SubkeyIdentifier getSubkeyIdentifier() { + return decryptionKey; } } diff --git a/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java new file mode 100644 index 00000000..186bcbc2 --- /dev/null +++ b/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package bouncycastle; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.CachingBcPublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; + +public class CachingBcPublicKeyDataDecryptorFactoryTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: C8AE 4279 5958 5F46 86A9 8B5F EC69 7C29 2BE4 44E0\n" + + "Comment: Alice\n" + + "\n" + + "lFgEY1vEcxYJKwYBBAHaRw8BAQdAXOUK1uc1iBeM+mMt2nLCukXWoJd/SodrtN9S\n" + + "U/zzwu0AAP9eePPw91KLuq6PF9jQoTRz/cW4CyiALNJpsOJIZ1rp3xOBtAVBbGlj\n" + + "ZYiPBBMWCgBBBQJjW8RzCRDsaXwpK+RE4BYhBMiuQnlZWF9GhqmLX+xpfCkr5ETg\n" + + "Ap4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAGqWAQC8oz7l8izjUis5ji+sgI+q\n" + + "gML22VNybqmLBpzZwnNU5wEApe9fNTRbK5yAITGBscxH7o74Qe+CLI6Ni5MwzKxr\n" + + "5AucXQRjW8RzEgorBgEEAZdVAQUBAQdAm8xk0QSvpp2ZU1KQ31E7eEZYLKpbW4JE\n" + + "opmtMQx6AlIDAQgHAAD/XTb/qSosfkNvli3BQiUzVRAqKaU4PKAq7at6afxoYSgN\n" + + "4Yh1BBgWCgAdBQJjW8RzAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQ7Gl8KSvk\n" + + "ROB38QEA0MvDt0bjEXwFoM0E34z0MtPcG3VBYcQ+iFRIqFfEl5UA/2yZxFjoZqrs\n" + + "AQE8TaVpXYfbc2p/GEKA9LGd9l/g0QQLnFgEY1vEcxYJKwYBBAHaRw8BAQdAyCOv\n" + + "6hGUvHcCBSDKP3fRz+scyJ9zwMt7nFXK5A/k2YgAAQCn3Es+IhvePn3eBlcYMMr0\n" + + "xcktrY1NJAIZPfjlUJ0J1g6LiNUEGBYKAH0FAmNbxHMCngECmwIFFgIDAQAECwkI\n" + + "BwUVCgkIC18gBBkWCgAGBQJjW8RzAAoJECxLf7KoUc8wD18BANNpIr4E+RRVVztR\n" + + "OVwdxSe0SRWGjkW8nHrRyghHKTuMAP9p4ZKicOYA1uZbiNNjyuJuS8xBH6Hihurb\n" + + "gDypVgxdBQAKCRDsaXwpK+RE4EQjAP9ARZEPxKNLFkrvjoZ8nrts3qhv3VtMrU+9\n" + + "huZnYLe1FQEAtgO6V7wutHvVARHXqPJ6lcv+SueIu+BjLFYEKuBwggs=\n" + + "=ShJd\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DJmQMTBqw3G8SAQdALkHpO0UkS/CqkwxUz74MJU3PV72ZrIL8ZcrO8ofhblkw\n" + + "iDIhSwwGTG3tj+sG+ZVWKsmONKi7Om5seJDHQtQ8MfdCELAgwYHSt6MrgDBhuDIH\n" + + "0kABZhq2/8qk3EGXPpc+xxs4r4g8SgHOiiHSim5NGtounXXIaF6T/hUmlorkeYf/\n" + + "a9pCC0QXRUAr8NOcdsfbvb5V\n" + + "=dQa8\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void test() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + SubkeyIdentifier decryptionKey = new SubkeyIdentifier(secretKeys, + info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID()); + + PGPSecretKey secretKey = secretKeys.getSecretKey(decryptionKey.getSubkeyId()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); + CachingBcPublicKeyDataDecryptorFactory cachingFactory = new CachingBcPublicKeyDataDecryptorFactory( + privateKey, decryptionKey); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(MSG.getBytes()); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory(cachingFactory)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory(cachingFactory)); + out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + } +} From 07320ed3cfc4ab3cf6158a9e2ea549b5166f0372 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 14:56:41 +0200 Subject: [PATCH 0521/1204] Fix HardwareSecurity.getIdsOfHardwareBackedKeys() --- .../decryption_verification/HardwareSecurity.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index f234ff00..6d9719dd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -47,8 +47,8 @@ public class HardwareSecurity { * @param secretKeys secret keys * @return set of keys with S2K type DIVERT_TO_CARD or GNU_DUMMY_S2K */ - public static Set getIdsOfHardwareBackedKeys(PGPSecretKeyRing secretKeys) { - Set hardwareBackedKeys = new HashSet<>(); + public static Set getIdsOfHardwareBackedKeys(PGPSecretKeyRing secretKeys) { + Set hardwareBackedKeys = new HashSet<>(); for (PGPSecretKey secretKey : secretKeys) { S2K s2K = secretKey.getS2K(); if (s2K == null) { @@ -56,9 +56,11 @@ public class HardwareSecurity { } int type = s2K.getType(); + int mode = s2K.getProtectionMode(); // TODO: Is GNU_DUMMY_S2K appropriate? - if (type == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD || type == S2K.GNU_DUMMY_S2K) { - hardwareBackedKeys.add(secretKey.getKeyID()); + if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { + SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); + hardwareBackedKeys.add(hardwareBackedKey); } } return hardwareBackedKeys; @@ -75,7 +77,7 @@ public class HardwareSecurity { // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. private final PublicKeyDataDecryptorFactory factory = new BcPublicKeyDataDecryptorFactory(null); - private SubkeyIdentifier subkey; + private final SubkeyIdentifier subkey; /** * Create a new {@link HardwareDataDecryptorFactory}. From 7467170bccef1ec52b16fe389281f042a9c531b7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 16:20:36 +0200 Subject: [PATCH 0522/1204] Move CachingBcPublicKeyDataDecryptorFactoryTest to correct package --- .../CachingBcPublicKeyDataDecryptorFactoryTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename pgpainless-core/src/test/java/{ => org}/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java (98%) diff --git a/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java similarity index 98% rename from pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java rename to pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java index 186bcbc2..6a43772d 100644 --- a/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package bouncycastle; +package org.bouncycastle; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -10,7 +10,6 @@ import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import org.bouncycastle.CachingBcPublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPSecretKey; From 2487e3300a58f1ed7e615f54f8c9dd0549a76e00 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 16:36:08 +0200 Subject: [PATCH 0523/1204] Add and test GnuDummyKeyUtil --- .../key/gnu_dummy_s2k/GNUExtension.java | 24 +++ .../key/gnu_dummy_s2k/GnuDummyKeyUtil.java | 114 ++++++++++ .../key/gnu_dummy_s2k/package-info.java | 8 + .../HardwareSecurityTest.java | 82 ++++++++ .../gnu_dummy_s2k/GnuDummyKeyUtilTest.java | 199 ++++++++++++++++++ 5 files changed, 427 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java new file mode 100644 index 00000000..ff42ef6e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.gnu_dummy_s2k; + +import org.bouncycastle.bcpg.S2K; + +public enum GNUExtension { + + NO_PRIVATE_KEY(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY), + DIVERT_TO_CARD(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD), + ; + + private final int id; + + GNUExtension(int id) { + this.id = id; + } + + public int getId() { + return id; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java new file mode 100644 index 00000000..a8de5aa3 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.gnu_dummy_s2k; + +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SecretKeyPacket; +import org.bouncycastle.bcpg.SecretSubkeyPacket; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public final class GnuDummyKeyUtil { + + private GnuDummyKeyUtil() { + + } + + public static Builder modify(PGPSecretKeyRing secretKeys) { + return new Builder(secretKeys); + } + + public static final class Builder { + + private final PGPSecretKeyRing keys; + + private Builder(PGPSecretKeyRing keys) { + this.keys = keys; + } + + public PGPSecretKeyRing removePrivateKeys(KeyFilter filter) { + return doIt(GNUExtension.NO_PRIVATE_KEY, null, filter); + } + + public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter) { + return divertPrivateKeysToCard(filter, new byte[16]); + } + + public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter, byte[] cardSerialNumber) { + return doIt(GNUExtension.DIVERT_TO_CARD, cardSerialNumber, filter); + } + + private PGPSecretKeyRing doIt(GNUExtension extension, byte[] serial, KeyFilter filter) { + byte[] encodedSerial = serial != null ? encodeSerial(serial) : null; + S2K s2k = extensionToS2K(extension); + + List secretKeyList = new ArrayList<>(); + for (PGPSecretKey secretKey : keys) { + if (!filter.filter(secretKey.getKeyID())) { + // No conversion, do not modify subkey + secretKeyList.add(secretKey); + continue; + } + + PublicKeyPacket publicKeyPacket = secretKey.getPublicKey().getPublicKeyPacket(); + if (secretKey.isMasterKey()) { + SecretKeyPacket keyPacket = new SecretKeyPacket(publicKeyPacket, + 0, 255, s2k, null, encodedSerial); + PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); + secretKeyList.add(onCard); + } else { + SecretSubkeyPacket keyPacket = new SecretSubkeyPacket(publicKeyPacket, + 0, 255, s2k, null, encodedSerial); + PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); + secretKeyList.add(onCard); + } + } + + PGPSecretKeyRing gnuDummyKey = new PGPSecretKeyRing(secretKeyList); + return gnuDummyKey; + } + + private byte[] encodeSerial(byte[] serial) { + byte[] encoded = new byte[serial.length + 1]; + encoded[0] = 0x10; + System.arraycopy(serial, 0, encoded, 1, serial.length); + return encoded; + } + + private S2K extensionToS2K(GNUExtension extension) { + S2K s2k = S2K.gnuDummyS2K(extension == GNUExtension.DIVERT_TO_CARD ? + S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey()); + return s2k; + } + } + + public interface KeyFilter { + + /** + * Return true, if the given key should be selected, false otherwise. + * + * @param keyId id of the key + * @return select + */ + boolean filter(long keyId); + + static KeyFilter any() { + return keyId -> true; + } + + static KeyFilter only(long onlyKeyId) { + return keyId -> keyId == onlyKeyId; + } + + static KeyFilter selected(Collection ids) { + return keyId -> ids.contains(keyId); + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java new file mode 100644 index 00000000..5c2a727e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Utility classes related to creating keys with GNU DUMMY S2K values. + */ +package org.pgpainless.key.gnu_dummy_s2k; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java new file mode 100644 index 00000000..f1606cec --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.gnu_dummy_s2k.GnuDummyKeyUtil; +import org.pgpainless.key.util.KeyIdUtil; + +public class HardwareSecurityTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: DE2E 9AB2 6650 8191 53E7 D599 C176 507F 2B5D 43B3\n" + + "Comment: Alice \n" + + "\n" + + "lFgEY1vjgRYJKwYBBAHaRw8BAQdAXjLoPTOIOdvlFT2Nt3rcvLTVx5ujPBGghZ5S\n" + + "D5tEnyoAAP0fAUJTiPrxZYdzs6MP0KFo+Nmr/wb1PJHTkzmYpt4wkRKBtBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmNb44EJEMF2UH8rXUOz\n" + + "FiEE3i6asmZQgZFT59WZwXZQfytdQ7MCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAAHLYA/AgW+YrpU+UqrwX2dhY6RAfgHTTMU89RHjaTHJx8pLrBAP4gthGof00a\n" + + "XEjwTWteDOO049SIp2AUfj9deJqtrQcHD5xdBGNb44ESCisGAQQBl1UBBQEBB0DN\n" + + "vUT3awa3YLmwf41LRpPrm7B87AOHfYIP8S9QJ4GDJgMBCAcAAP9bwlSaF+lti8JY\n" + + "qKFO3qt3ZYQMu1l/LRBle89ZB4zD+BDOiHUEGBYKAB0FAmNb44ECngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRDBdlB/K11Ds/TsAP9kvpUrCWnrWGq+a9n1CqEfCMX5\n" + + "cT+qzrwNf+J0L22KowD+M9SVO0qssiAqutLE9h9dGYLbEiFvsHzK3WSnjKYbIgac\n" + + "WARjW+OBFgkrBgEEAdpHDwEBB0BCPh8M5TnXSmG6Ygwp4j5RR4u3hmxl8CYjX4h/\n" + + "XtvvNwAA/RP04coSrLHVI6vUfbJk4MhWYeyhJBRYY0vGp7yq+wVtEpKI1QQYFgoA\n" + + "fQUCY1vjgQKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNb44EACgkQ\n" + + "mlozJSF7rXQW+AD/TA3YBxTd+YbBSwfgqzWNbfT9BBcFrdn3uPCsbvfmqXoA/3oj\n" + + "oupkgoaXesrGxn2k9hW9/GBXSvNcgY2txZ6/oYoIAAoJEMF2UH8rXUOziZ4A/0Xl\n" + + "xSZJWmkRpBh5AO8Cnqosz6j947IYAxS16ay+sIOHAP9aN9CUNJIIdHnHdFHO4GZz\n" + + "ejjknn4wt8NVJP97JxlnBQ==\n" + + "=qSQb\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @Test + public void testGetSingleIdOfHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); + long encryptionKeyId = KeyIdUtil.fromLongKeyId("0AAD8F5891262F50"); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(encryptionKeyId)); + + Set hardwareBackedKeys = HardwareSecurity + .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); + assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys); + } + + + @Test + public void testGetIdsOfFullyHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any()); + Set expected = new HashSet<>(); + for (PGPSecretKey key : secretKeys) { + expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID())); + } + + Set hardwareBackedKeys = HardwareSecurity + .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); + + assertEquals(expected, hardwareBackedKeys); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java new file mode 100644 index 00000000..0b19c551 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.gnu_dummy_s2k; + +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.util.KeyIdUtil; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GnuDummyKeyUtilTest { + // normal, non-hw-backed key + private static final String FULL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeH\n" + + "VtBaHi6efXvnc4rdVj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsM\n" + + "BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" + + "Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" + + "vP0BnFgEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" + + "6nfwJtbDT0YAAQCgnCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUE\n" + + "GBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KI\n" + + "AAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5f\n" + + "AQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6\n" + + "AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwG\n" + + "jF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=+vXp\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final long primaryKeyId = KeyIdUtil.fromLongKeyId("C312C97DA9F76A4F"); + private static final long encryptionKeyId = KeyIdUtil.fromLongKeyId("6924D066714CE8C6"); + private static final long signatureKeyId = KeyIdUtil.fromLongKeyId("94022FA56DC05B49"); + private static final byte[] cardSerial = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; + + public static final String ALL_KEYS_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + + "Me3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" + + "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/wBlAEdOVQIQAAECAwQFBgcICQoL\n" + + "DA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQwxLJ\n" + + "fan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+83te770A/jG0DeJy\n" + + "+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdA\n" + + "vSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0b/AGUAR05VAhAAAQIDBAUG\n" + + "BwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18g\n" + + "BBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9f\n" + + "MM0yUxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAK\n" + + "CRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEA\n" + + "hE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=wsFa\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String PRIMARY_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + + "Me3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" + + "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeHVtBaHi6efXvnc4rd\n" + + "Vj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" + + "83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" + + "KwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0YAAQCg\n" + + "nCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=s+B1\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String ENCRYPTION_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/wBlAEdOVQIQ\n" + + "AAECAwQFBgcICQoLDA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" + + "83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" + + "KwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0YAAQCg\n" + + "nCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=TPAl\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String SIGNATURE_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeH\n" + + "VtBaHi6efXvnc4rdVj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsM\n" + + "BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" + + "Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" + + "vP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" + + "6nfwJtbDT0b/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=p8I9\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @Test + public void testMoveAllKeysToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_ON_CARD); + + PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any(), cardSerial); + + for (PGPSecretKey key : onCard) { + assertEquals(255, key.getS2KUsage()); + S2K s2K = key.getS2K(); + assertEquals(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD, s2K.getProtectionMode()); + } + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMovePrimaryKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(PRIMARY_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(primaryKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMoveEncryptionKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(encryptionKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMoveSigningKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(SIGNATURE_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(signatureKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } +} From a8d2319d6374111fb7287be8f27dbc8490e52dcf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 16:48:49 +0200 Subject: [PATCH 0524/1204] Add documentation to GnuDummyKeyUtil --- .../key/gnu_dummy_s2k/GnuDummyKeyUtil.java | 76 ++++++++++++++++--- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java index a8de5aa3..f074fad2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java @@ -11,16 +11,26 @@ import org.bouncycastle.bcpg.SecretSubkeyPacket; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Collection; import java.util.List; +/** + * This class can be used to remove private keys from secret keys. + */ public final class GnuDummyKeyUtil { private GnuDummyKeyUtil() { } + /** + * Modify the given {@link PGPSecretKeyRing}. + * + * @param secretKeys secret keys + * @return builder + */ public static Builder modify(PGPSecretKeyRing secretKeys) { return new Builder(secretKeys); } @@ -33,19 +43,50 @@ public final class GnuDummyKeyUtil { this.keys = keys; } + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#NO_PRIVATE_KEY}. + * + * @param filter filter to select keys for removal + * @return modified key ring + */ public PGPSecretKeyRing removePrivateKeys(KeyFilter filter) { - return doIt(GNUExtension.NO_PRIVATE_KEY, null, filter); + return replacePrivateKeys(GNUExtension.NO_PRIVATE_KEY, null, filter); } + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#DIVERT_TO_CARD}. + * This method will set the serial number of the card to 0x00000000000000000000000000000000. + * + * NOTE: This method does not actually move any keys to a card. + * + * @param filter filter to select keys for removal + * @return modified key ring + */ public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter) { return divertPrivateKeysToCard(filter, new byte[16]); } + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#DIVERT_TO_CARD}. + * This method will include the card serial number into the encoded dummy key. + * + * NOTE: This method does not actually move any keys to a card. + * + * @param filter filter to select keys for removal + * @param cardSerialNumber serial number of the card (at most 16 bytes long) + * @return modified key ring + */ public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter, byte[] cardSerialNumber) { - return doIt(GNUExtension.DIVERT_TO_CARD, cardSerialNumber, filter); + if (cardSerialNumber != null && cardSerialNumber.length > 16) { + throw new IllegalArgumentException("Card serial number length cannot exceed 16 bytes."); + } + return replacePrivateKeys(GNUExtension.DIVERT_TO_CARD, cardSerialNumber, filter); } - private PGPSecretKeyRing doIt(GNUExtension extension, byte[] serial, KeyFilter filter) { + private PGPSecretKeyRing replacePrivateKeys(GNUExtension extension, byte[] serial, KeyFilter filter) { byte[] encodedSerial = serial != null ? encodeSerial(serial) : null; S2K s2k = extensionToS2K(extension); @@ -71,21 +112,19 @@ public final class GnuDummyKeyUtil { } } - PGPSecretKeyRing gnuDummyKey = new PGPSecretKeyRing(secretKeyList); - return gnuDummyKey; + return new PGPSecretKeyRing(secretKeyList); } - private byte[] encodeSerial(byte[] serial) { + private byte[] encodeSerial(@Nonnull byte[] serial) { byte[] encoded = new byte[serial.length + 1]; - encoded[0] = 0x10; + encoded[0] = (byte) (serial.length & 0xff); System.arraycopy(serial, 0, encoded, 1, serial.length); return encoded; } - private S2K extensionToS2K(GNUExtension extension) { - S2K s2k = S2K.gnuDummyS2K(extension == GNUExtension.DIVERT_TO_CARD ? + private S2K extensionToS2K(@Nonnull GNUExtension extension) { + return S2K.gnuDummyS2K(extension == GNUExtension.DIVERT_TO_CARD ? S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey()); - return s2k; } } @@ -99,15 +138,32 @@ public final class GnuDummyKeyUtil { */ boolean filter(long keyId); + /** + * Select any key. + * @return filter + */ static KeyFilter any() { return keyId -> true; } + /** + * Select only the given keyId. + * + * @param onlyKeyId only acceptable key id + * @return filter + */ static KeyFilter only(long onlyKeyId) { return keyId -> keyId == onlyKeyId; } + /** + * Select all keyIds which are contained in the given set of ids. + * + * @param ids set of acceptable keyIds + * @return filter + */ static KeyFilter selected(Collection ids) { + // noinspection Convert2MethodRef return keyId -> ids.contains(keyId); } } From 033beaa8f2ae1c16e552cd8d2004fff27307a86b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 17:05:56 +0200 Subject: [PATCH 0525/1204] Use S2K usage SHA1 in GnuDummyKeyUtil --- .../key/gnu_dummy_s2k/GNUExtension.java | 7 ++++++ .../key/gnu_dummy_s2k/GnuDummyKeyUtil.java | 6 ++--- .../gnu_dummy_s2k/GnuDummyKeyUtilTest.java | 23 ++++++++++--------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java index ff42ef6e..e829bd7b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java @@ -8,7 +8,14 @@ import org.bouncycastle.bcpg.S2K; public enum GNUExtension { + /** + * Do not store the secret part at all. + */ NO_PRIVATE_KEY(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY), + + /** + * A stub to access smartcards. + */ DIVERT_TO_CARD(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD), ; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java index f074fad2..7817a676 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java @@ -74,7 +74,7 @@ public final class GnuDummyKeyUtil { * This method will include the card serial number into the encoded dummy key. * * NOTE: This method does not actually move any keys to a card. - * + * * @param filter filter to select keys for removal * @param cardSerialNumber serial number of the card (at most 16 bytes long) * @return modified key ring @@ -101,12 +101,12 @@ public final class GnuDummyKeyUtil { PublicKeyPacket publicKeyPacket = secretKey.getPublicKey().getPublicKeyPacket(); if (secretKey.isMasterKey()) { SecretKeyPacket keyPacket = new SecretKeyPacket(publicKeyPacket, - 0, 255, s2k, null, encodedSerial); + 0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial); PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); secretKeyList.add(onCard); } else { SecretSubkeyPacket keyPacket = new SecretSubkeyPacket(publicKeyPacket, - 0, 255, s2k, null, encodedSerial); + 0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial); PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); secretKeyList.add(onCard); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java index 0b19c551..99966903 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java @@ -5,6 +5,7 @@ package org.pgpainless.key.gnu_dummy_s2k; import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SecretKeyPacket; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; @@ -54,22 +55,22 @@ public class GnuDummyKeyUtilTest { "Comment: Hardy Hardware \n" + "\n" + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + - "eBwCeTT/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "eBwCeTT+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + "Me3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" + - "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/wBlAEdOVQIQAAECAwQFBgcICQoL\n" + + "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/gBlAEdOVQIQAAECAwQFBgcICQoL\n" + "DA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQwxLJ\n" + "fan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+83te770A/jG0DeJy\n" + "+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdA\n" + - "vSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0b/AGUAR05VAhAAAQIDBAUG\n" + + "vSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0b+AGUAR05VAhAAAQIDBAUG\n" + "BwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18g\n" + "BBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9f\n" + "MM0yUxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAK\n" + "CRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEA\n" + "hE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + - "=wsFa\n" + + "=rYoa\n" + "-----END PGP PRIVATE KEY BLOCK-----"; public static final String PRIMARY_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -78,7 +79,7 @@ public class GnuDummyKeyUtilTest { "Comment: Hardy Hardware \n" + "\n" + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + - "eBwCeTT/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "eBwCeTT+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + @@ -94,7 +95,7 @@ public class GnuDummyKeyUtilTest { "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + "SlYD3FeJ3Qo=\n" + - "=s+B1\n" + + "=zQLi\n" + "-----END PGP PRIVATE KEY BLOCK-----"; public static final String ENCRYPTION_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -108,7 +109,7 @@ public class GnuDummyKeyUtilTest { "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUB\n" + - "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/wBlAEdOVQIQ\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/gBlAEdOVQIQ\n" + "AAECAwQFBgcICQoLDA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" + "FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" + "83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" + @@ -119,7 +120,7 @@ public class GnuDummyKeyUtilTest { "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + "SlYD3FeJ3Qo=\n" + - "=TPAl\n" + + "=7OZu\n" + "-----END PGP PRIVATE KEY BLOCK-----"; public static final String SIGNATURE_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -138,13 +139,13 @@ public class GnuDummyKeyUtilTest { "BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" + "Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" + "vP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" + - "6nfwJtbDT0b/AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogC\n" + + "6nfwJtbDT0b+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogC\n" + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + "SlYD3FeJ3Qo=\n" + - "=p8I9\n" + + "=GpEw\n" + "-----END PGP PRIVATE KEY BLOCK-----"; @Test @@ -156,7 +157,7 @@ public class GnuDummyKeyUtilTest { .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any(), cardSerial); for (PGPSecretKey key : onCard) { - assertEquals(255, key.getS2KUsage()); + assertEquals(SecretKeyPacket.USAGE_SHA1, key.getS2KUsage()); S2K s2K = key.getS2K(); assertEquals(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD, s2K.getProtectionMode()); } From 3af6ab1b85b7f8cd554fcb6e848e205b3038d3a2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 14:09:41 +0200 Subject: [PATCH 0526/1204] Rename GnuPGDummyExtension + GnuPGDummyKeyUtil --- ...xtension.java => GnuPGDummyExtension.java} | 4 +-- ...mmyKeyUtil.java => GnuPGDummyKeyUtil.java} | 31 ++++++++++++------- .../HardwareSecurityTest.java | 10 +++--- ...ilTest.java => GnuPGDummyKeyUtilTest.java} | 18 +++++------ 4 files changed, 36 insertions(+), 27 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/{GNUExtension.java => GnuPGDummyExtension.java} (88%) rename pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/{GnuDummyKeyUtil.java => GnuPGDummyKeyUtil.java} (81%) rename pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/{GnuDummyKeyUtilTest.java => GnuPGDummyKeyUtilTest.java} (93%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java similarity index 88% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java rename to pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java index e829bd7b..9f75bf7e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GNUExtension.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java @@ -6,7 +6,7 @@ package org.pgpainless.key.gnu_dummy_s2k; import org.bouncycastle.bcpg.S2K; -public enum GNUExtension { +public enum GnuPGDummyExtension { /** * Do not store the secret part at all. @@ -21,7 +21,7 @@ public enum GNUExtension { private final int id; - GNUExtension(int id) { + GnuPGDummyExtension(int id) { this.id = id; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java similarity index 81% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java rename to pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java index 7817a676..3a913894 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java @@ -17,11 +17,15 @@ import java.util.Collection; import java.util.List; /** - * This class can be used to remove private keys from secret keys. + * This class can be used to remove private keys from secret software-keys by replacing them with + * stub secret keys in the style of GnuPGs proprietary extensions. + * + * @see + * GnuPGs doc/DETAILS - GNU extensions to the S2K algorithm */ -public final class GnuDummyKeyUtil { +public final class GnuPGDummyKeyUtil { - private GnuDummyKeyUtil() { + private GnuPGDummyKeyUtil() { } @@ -45,18 +49,18 @@ public final class GnuDummyKeyUtil { /** * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with - * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#NO_PRIVATE_KEY}. + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#NO_PRIVATE_KEY}. * * @param filter filter to select keys for removal * @return modified key ring */ public PGPSecretKeyRing removePrivateKeys(KeyFilter filter) { - return replacePrivateKeys(GNUExtension.NO_PRIVATE_KEY, null, filter); + return replacePrivateKeys(GnuPGDummyExtension.NO_PRIVATE_KEY, null, filter); } /** * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with - * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#DIVERT_TO_CARD}. + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. * This method will set the serial number of the card to 0x00000000000000000000000000000000. * * NOTE: This method does not actually move any keys to a card. @@ -70,7 +74,7 @@ public final class GnuDummyKeyUtil { /** * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with - * GNU_DUMMY keys with S2K protection mode {@link GNUExtension#DIVERT_TO_CARD}. + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. * This method will include the card serial number into the encoded dummy key. * * NOTE: This method does not actually move any keys to a card. @@ -83,10 +87,10 @@ public final class GnuDummyKeyUtil { if (cardSerialNumber != null && cardSerialNumber.length > 16) { throw new IllegalArgumentException("Card serial number length cannot exceed 16 bytes."); } - return replacePrivateKeys(GNUExtension.DIVERT_TO_CARD, cardSerialNumber, filter); + return replacePrivateKeys(GnuPGDummyExtension.DIVERT_TO_CARD, cardSerialNumber, filter); } - private PGPSecretKeyRing replacePrivateKeys(GNUExtension extension, byte[] serial, KeyFilter filter) { + private PGPSecretKeyRing replacePrivateKeys(GnuPGDummyExtension extension, byte[] serial, KeyFilter filter) { byte[] encodedSerial = serial != null ? encodeSerial(serial) : null; S2K s2k = extensionToS2K(extension); @@ -122,12 +126,16 @@ public final class GnuDummyKeyUtil { return encoded; } - private S2K extensionToS2K(@Nonnull GNUExtension extension) { - return S2K.gnuDummyS2K(extension == GNUExtension.DIVERT_TO_CARD ? + private S2K extensionToS2K(@Nonnull GnuPGDummyExtension extension) { + return S2K.gnuDummyS2K(extension == GnuPGDummyExtension.DIVERT_TO_CARD ? S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey()); } } + /** + * Filter for selecting keys. + */ + @FunctionalInterface public interface KeyFilter { /** @@ -140,6 +148,7 @@ public final class GnuDummyKeyUtil { /** * Select any key. + * * @return filter */ static KeyFilter any() { diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java index f1606cec..a2160edf 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java @@ -17,7 +17,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.gnu_dummy_s2k.GnuDummyKeyUtil; +import org.pgpainless.key.gnu_dummy_s2k.GnuPGDummyKeyUtil; import org.pgpainless.key.util.KeyIdUtil; public class HardwareSecurityTest { @@ -53,8 +53,8 @@ public class HardwareSecurityTest { assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); long encryptionKeyId = KeyIdUtil.fromLongKeyId("0AAD8F5891262F50"); - PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(encryptionKeyId)); + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId)); Set hardwareBackedKeys = HardwareSecurity .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); @@ -67,8 +67,8 @@ public class HardwareSecurityTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); - PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any()); + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any()); Set expected = new HashSet<>(); for (PGPSecretKey key : secretKeys) { expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID())); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java similarity index 93% rename from pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java rename to pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java index 99966903..1fc3b892 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java @@ -17,7 +17,7 @@ import java.io.IOException; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; -public class GnuDummyKeyUtilTest { +public class GnuPGDummyKeyUtilTest { // normal, non-hw-backed key private static final String FULL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + @@ -153,8 +153,8 @@ public class GnuDummyKeyUtilTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_ON_CARD); - PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.any(), cardSerial); + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any(), cardSerial); for (PGPSecretKey key : onCard) { assertEquals(SecretKeyPacket.USAGE_SHA1, key.getS2KUsage()); @@ -170,8 +170,8 @@ public class GnuDummyKeyUtilTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(PRIMARY_KEY_ON_CARD); - PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(primaryKeyId), cardSerial); + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(primaryKeyId), cardSerial); assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); } @@ -181,8 +181,8 @@ public class GnuDummyKeyUtilTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_KEY_ON_CARD); - PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(encryptionKeyId), cardSerial); + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId), cardSerial); assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); } @@ -192,8 +192,8 @@ public class GnuDummyKeyUtilTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(SIGNATURE_KEY_ON_CARD); - PGPSecretKeyRing onCard = GnuDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuDummyKeyUtil.KeyFilter.only(signatureKeyId), cardSerial); + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(signatureKeyId), cardSerial); assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); } From df4fc94ce715ea63278d2379df0e75c07f75c627 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 14:51:39 +0200 Subject: [PATCH 0527/1204] Add test for decryption with removed private key --- .../HardwareSecurity.java | 31 ------- .../key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java | 30 +++++++ .../HardwareSecurityTest.java | 82 ------------------- ...DecryptWithUnavailableGnuDummyKeyTest.java | 52 ++++++++++++ .../gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java | 77 +++++++++++++++++ 5 files changed, 159 insertions(+), 113 deletions(-) delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index 6d9719dd..daf902d4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -4,18 +4,12 @@ package org.pgpainless.decryption_verification; -import org.bouncycastle.bcpg.S2K; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.operator.PGPDataDecryptor; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.pgpainless.key.SubkeyIdentifier; -import java.util.HashSet; -import java.util.Set; - /** * Enable integration of hardware-backed OpenPGP keys. */ @@ -41,31 +35,6 @@ public class HardwareSecurity { } - /** - * Return the key-ids of all keys which appear to be stored on a hardware token / smartcard. - * - * @param secretKeys secret keys - * @return set of keys with S2K type DIVERT_TO_CARD or GNU_DUMMY_S2K - */ - public static Set getIdsOfHardwareBackedKeys(PGPSecretKeyRing secretKeys) { - Set hardwareBackedKeys = new HashSet<>(); - for (PGPSecretKey secretKey : secretKeys) { - S2K s2K = secretKey.getS2K(); - if (s2K == null) { - continue; - } - - int type = s2K.getType(); - int mode = s2K.getProtectionMode(); - // TODO: Is GNU_DUMMY_S2K appropriate? - if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { - SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); - hardwareBackedKeys.add(hardwareBackedKey); - } - } - return hardwareBackedKeys; - } - /** * Implementation of {@link PublicKeyDataDecryptorFactory} which delegates decryption of encrypted session keys * to a {@link DecryptionCallback}. diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java index 3a913894..64c7ed26 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java @@ -10,11 +10,14 @@ import org.bouncycastle.bcpg.SecretKeyPacket; import org.bouncycastle.bcpg.SecretSubkeyPacket; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.pgpainless.key.SubkeyIdentifier; import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * This class can be used to remove private keys from secret software-keys by replacing them with @@ -29,6 +32,33 @@ public final class GnuPGDummyKeyUtil { } + /** + * Return the key-ids of all keys which appear to be stored on a hardware token / smartcard by GnuPG. + * Note, that this functionality is based on GnuPGs proprietary S2K extensions, which are not strictly required + * for dealing with hardware-backed keys. + * + * @param secretKeys secret keys + * @return set of keys with S2K type GNU_DUMMY_S2K and protection mode DIVERT_TO_CARD + */ + public static Set getIdsOfKeysWithGnuPGS2KDivertedToCard(PGPSecretKeyRing secretKeys) { + Set hardwareBackedKeys = new HashSet<>(); + for (PGPSecretKey secretKey : secretKeys) { + S2K s2K = secretKey.getS2K(); + if (s2K == null) { + continue; + } + + int type = s2K.getType(); + int mode = s2K.getProtectionMode(); + // TODO: Is GNU_DUMMY_S2K appropriate? + if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { + SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); + hardwareBackedKeys.add(hardwareBackedKey); + } + } + return hardwareBackedKeys; + } + /** * Modify the given {@link PGPSecretKeyRing}. * diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java deleted file mode 100644 index a2160edf..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/HardwareSecurityTest.java +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.gnu_dummy_s2k.GnuPGDummyKeyUtil; -import org.pgpainless.key.util.KeyIdUtil; - -public class HardwareSecurityTest { - - private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: PGPainless\n" + - "Comment: DE2E 9AB2 6650 8191 53E7 D599 C176 507F 2B5D 43B3\n" + - "Comment: Alice \n" + - "\n" + - "lFgEY1vjgRYJKwYBBAHaRw8BAQdAXjLoPTOIOdvlFT2Nt3rcvLTVx5ujPBGghZ5S\n" + - "D5tEnyoAAP0fAUJTiPrxZYdzs6MP0KFo+Nmr/wb1PJHTkzmYpt4wkRKBtBxBbGlj\n" + - "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmNb44EJEMF2UH8rXUOz\n" + - "FiEE3i6asmZQgZFT59WZwXZQfytdQ7MCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + - "AQAAHLYA/AgW+YrpU+UqrwX2dhY6RAfgHTTMU89RHjaTHJx8pLrBAP4gthGof00a\n" + - "XEjwTWteDOO049SIp2AUfj9deJqtrQcHD5xdBGNb44ESCisGAQQBl1UBBQEBB0DN\n" + - "vUT3awa3YLmwf41LRpPrm7B87AOHfYIP8S9QJ4GDJgMBCAcAAP9bwlSaF+lti8JY\n" + - "qKFO3qt3ZYQMu1l/LRBle89ZB4zD+BDOiHUEGBYKAB0FAmNb44ECngECmwwFFgID\n" + - "AQAECwkIBwUVCgkICwAKCRDBdlB/K11Ds/TsAP9kvpUrCWnrWGq+a9n1CqEfCMX5\n" + - "cT+qzrwNf+J0L22KowD+M9SVO0qssiAqutLE9h9dGYLbEiFvsHzK3WSnjKYbIgac\n" + - "WARjW+OBFgkrBgEEAdpHDwEBB0BCPh8M5TnXSmG6Ygwp4j5RR4u3hmxl8CYjX4h/\n" + - "XtvvNwAA/RP04coSrLHVI6vUfbJk4MhWYeyhJBRYY0vGp7yq+wVtEpKI1QQYFgoA\n" + - "fQUCY1vjgQKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNb44EACgkQ\n" + - "mlozJSF7rXQW+AD/TA3YBxTd+YbBSwfgqzWNbfT9BBcFrdn3uPCsbvfmqXoA/3oj\n" + - "oupkgoaXesrGxn2k9hW9/GBXSvNcgY2txZ6/oYoIAAoJEMF2UH8rXUOziZ4A/0Xl\n" + - "xSZJWmkRpBh5AO8Cnqosz6j947IYAxS16ay+sIOHAP9aN9CUNJIIdHnHdFHO4GZz\n" + - "ejjknn4wt8NVJP97JxlnBQ==\n" + - "=qSQb\n" + - "-----END PGP PRIVATE KEY BLOCK-----"; - - @Test - public void testGetSingleIdOfHardwareBackedKey() throws IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); - long encryptionKeyId = KeyIdUtil.fromLongKeyId("0AAD8F5891262F50"); - - PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId)); - - Set hardwareBackedKeys = HardwareSecurity - .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); - assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys); - } - - - @Test - public void testGetIdsOfFullyHardwareBackedKey() throws IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - assertTrue(HardwareSecurity.getIdsOfHardwareBackedKeys(secretKeys).isEmpty()); - - PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) - .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any()); - Set expected = new HashSet<>(); - for (PGPSecretKey key : secretKeys) { - expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID())); - } - - Set hardwareBackedKeys = HardwareSecurity - .getIdsOfHardwareBackedKeys(withHardwareBackedEncryptionKey); - - assertEquals(expected, hardwareBackedKeys); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java new file mode 100644 index 00000000..79fa7a8e --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.key.gnu_dummy_s2k.GnuPGDummyKeyUtil; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class TryDecryptWithUnavailableGnuDummyKeyTest { + + @Test + public void testAttemptToDecryptWithRemovedPrivateKeysThrows() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Hardy Hardware "); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions( + ProducerOptions.encrypt(EncryptionOptions.get().addRecipient(certificate))); + ByteArrayInputStream plaintextIn = new ByteArrayInputStream("Hello, World!\n".getBytes()); + Streams.pipeAll(plaintextIn, encryptionStream); + encryptionStream.close(); + + PGPSecretKeyRing removedKeys = GnuPGDummyKeyUtil.modify(secretKeys) + .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any()); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray()); + assertThrows(PGPException.class, () -> PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get().addDecryptionKey(removedKeys))); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java index 1fc3b892..e175e27f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java @@ -10,12 +10,17 @@ import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.util.KeyIdUtil; import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class GnuPGDummyKeyUtilTest { // normal, non-hw-backed key @@ -73,6 +78,29 @@ public class GnuPGDummyKeyUtilTest { "=rYoa\n" + "-----END PGP PRIVATE KEY BLOCK-----"; + public static final String ALL_KEYS_REMOVED = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lDsEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT+AGUAR05VAbQgSGFyZHkgSGFyZHdhcmUgPGhhcmR5QGhhcmQud2FyZT6I\n" + + "jwQTFgoAQQUCY1vSiAkQwxLJfan3ak8WIQQB/ats4EpQeHn+ShjDEsl9qfdqTwKe\n" + + "AQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAD5NgD/dtk+U0O4bpBZacV904TIYniZ\n" + + "xAhmORKreVNP7xGNV3YA/3hNTJfaqsekBnGjSnvHXjHtxIU2p7epkvbajB6dv94J\n" + + "nEAEY1vSiBIKKwYBBAGXVQEFAQEHQFVwSzbzZhYfSl+oi5nTSTNvGXPTxp8xKAA/\n" + + "fk+KdJQ8AwEIB/4AZQBHTlUBiHUEGBYKAB0FAmNb0ogCngECmwwFFgIDAQAECwkI\n" + + "BwUVCgkICwAKCRDDEsl9qfdqT8nJAP0YGPS+O1hkB/kWLR4Qp2ICCzTJmtA+Qyzp\n" + + "4v7ze17vvQD+MbQN4nL7zx859ZOP6aLE73w9k+dDQzJtYL/VBRO8/QGcOwRjW9KI\n" + + "FgkrBgEEAdpHDwEBB0C9JhMPrS3y/HXR1IQEAJSgh9UKl44HfQPqd/Am1sNPRv4A\n" + + "ZQBHTlUBiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkW\n" + + "CgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0y\n" + + "Uxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDD\n" + + "Esl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r\n" + + "0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=GEN/\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + public static final String PRIMARY_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + @@ -197,4 +225,53 @@ public class GnuPGDummyKeyUtilTest { assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); } + + @Test + public void testRemoveAllKeys() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_REMOVED); + + PGPSecretKeyRing removedSecretKeys = GnuPGDummyKeyUtil.modify(secretKeys) + .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any()); + + for (PGPSecretKey key : removedSecretKeys) { + assertEquals(key.getS2KUsage(), SecretKeyPacket.USAGE_SHA1); + S2K s2k = key.getS2K(); + assertEquals(GnuPGDummyExtension.NO_PRIVATE_KEY.getId(), s2k.getProtectionMode()); + } + + assertArrayEquals(expected.getEncoded(), removedSecretKeys.getEncoded()); + } + + @Test + public void testGetSingleIdOfHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty()); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId)); + + Set hardwareBackedKeys = GnuPGDummyKeyUtil + .getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey); + assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys); + } + + + @Test + public void testGetIdsOfFullyHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty()); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any()); + Set expected = new HashSet<>(); + for (PGPSecretKey key : secretKeys) { + expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID())); + } + + Set hardwareBackedKeys = GnuPGDummyKeyUtil + .getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey); + + assertEquals(expected, hardwareBackedKeys); + } } From 58aa9f571201e9df55d532e830287a26203899f6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 14:58:18 +0200 Subject: [PATCH 0528/1204] Move classes related to GNU dummy keys to gnupg package --- .../key/gnu_dummy_s2k => gnupg}/GnuPGDummyExtension.java | 2 +- .../key/gnu_dummy_s2k => gnupg}/GnuPGDummyKeyUtil.java | 2 +- .../{pgpainless/key/gnu_dummy_s2k => gnupg}/package-info.java | 2 +- .../key/gnu_dummy_s2k => gnupg}/GnuPGDummyKeyUtilTest.java | 4 +++- .../TryDecryptWithUnavailableGnuDummyKeyTest.java | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) rename pgpainless-core/src/main/java/org/{pgpainless/key/gnu_dummy_s2k => gnupg}/GnuPGDummyExtension.java (93%) rename pgpainless-core/src/main/java/org/{pgpainless/key/gnu_dummy_s2k => gnupg}/GnuPGDummyKeyUtil.java (99%) rename pgpainless-core/src/main/java/org/{pgpainless/key/gnu_dummy_s2k => gnupg}/package-info.java (81%) rename pgpainless-core/src/test/java/org/{pgpainless/key/gnu_dummy_s2k => gnupg}/GnuPGDummyKeyUtilTest.java (99%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java similarity index 93% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java rename to pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java index 9f75bf7e..d744e222 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyExtension.java +++ b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.gnu_dummy_s2k; +package org.gnupg; import org.bouncycastle.bcpg.S2K; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java similarity index 99% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java rename to pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java index 64c7ed26..983d8e68 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.gnu_dummy_s2k; +package org.gnupg; import org.bouncycastle.bcpg.PublicKeyPacket; import org.bouncycastle.bcpg.S2K; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java b/pgpainless-core/src/main/java/org/gnupg/package-info.java similarity index 81% rename from pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java rename to pgpainless-core/src/main/java/org/gnupg/package-info.java index 5c2a727e..03268619 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/gnu_dummy_s2k/package-info.java +++ b/pgpainless-core/src/main/java/org/gnupg/package-info.java @@ -5,4 +5,4 @@ /** * Utility classes related to creating keys with GNU DUMMY S2K values. */ -package org.pgpainless.key.gnu_dummy_s2k; +package org.gnupg; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java similarity index 99% rename from pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java rename to pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java index e175e27f..b9562f60 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/gnu_dummy_s2k/GnuPGDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java @@ -2,12 +2,14 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.gnu_dummy_s2k; +package org.gnupg; import org.bouncycastle.bcpg.S2K; import org.bouncycastle.bcpg.SecretKeyPacket; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.gnupg.GnuPGDummyExtension; +import org.gnupg.GnuPGDummyKeyUtil; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.SubkeyIdentifier; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java index 79fa7a8e..419c529d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java @@ -13,7 +13,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.key.gnu_dummy_s2k.GnuPGDummyKeyUtil; +import org.gnupg.GnuPGDummyKeyUtil; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; From 58195c19b139f3fcd34693b0e7053529e8f59d17 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 15:12:12 +0200 Subject: [PATCH 0529/1204] Properly handle failed decryption caused by removed private keys --- .../OpenPgpMessageInputStream.java | 25 +++++++++++++++++++ .../java/org/gnupg/GnuPGDummyKeyUtilTest.java | 24 ++++++++---------- ...DecryptWithUnavailableGnuDummyKeyTest.java | 3 ++- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 1b11744c..a509cde2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -18,6 +18,7 @@ import javax.annotation.Nonnull; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.S2K; import org.bouncycastle.bcpg.UnsupportedPacketVersionException; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; @@ -507,6 +508,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); + S2K s2K = secretKey.getS2K(); + if (s2K != null) { + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); + continue; + } + } LOGGER.debug("Attempt decryption using secret key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); @@ -532,6 +541,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSecretKeyRing decryptionKeys = decryptionKeyCandidate.getA(); PGPSecretKey secretKey = decryptionKeyCandidate.getB(); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); + S2K s2K = secretKey.getS2K(); + if (s2K != null) { + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); + continue; + } + } LOGGER.debug("Attempt decryption of anonymous PKESK with key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); if (!protector.hasPassphraseFor(secretKey.getKeyID())) { @@ -567,6 +584,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { long keyId = secretKey.getKeyID(); PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); + S2K s2K = secretKey.getS2K(); + if (s2K != null) { + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); + continue; + } + } LOGGER.debug("Attempt decryption with key " + decryptionKeyId + " while interactively requesting its passphrase"); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); diff --git a/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java index b9562f60..87c5b02e 100644 --- a/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java @@ -4,25 +4,23 @@ package org.gnupg; -import org.bouncycastle.bcpg.S2K; -import org.bouncycastle.bcpg.SecretKeyPacket; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.gnupg.GnuPGDummyExtension; -import org.gnupg.GnuPGDummyKeyUtil; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.util.KeyIdUtil; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.Set; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SecretKeyPacket; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.util.KeyIdUtil; public class GnuPGDummyKeyUtilTest { // normal, non-hw-backed key diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java index 419c529d..640025b1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java @@ -14,6 +14,7 @@ import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.gnupg.GnuPGDummyKeyUtil; +import org.pgpainless.exception.MissingDecryptionMethodException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -45,7 +46,7 @@ public class TryDecryptWithUnavailableGnuDummyKeyTest { .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any()); ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray()); - assertThrows(PGPException.class, () -> PGPainless.decryptAndOrVerify() + assertThrows(MissingDecryptionMethodException.class, () -> PGPainless.decryptAndOrVerify() .onInputStream(ciphertextIn) .withOptions(ConsumerOptions.get().addDecryptionKey(removedKeys))); } From ca49ed087b4f1bb7e416d0d636caaeb37d35e225 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 29 Oct 2022 15:50:34 +0200 Subject: [PATCH 0530/1204] Small clean-ups in OpenPgpMessageInputStream --- .../OpenPgpMessageInputStream.java | 108 +++++++----------- 1 file changed, 43 insertions(+), 65 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index a509cde2..f2c30ced 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -109,7 +109,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { * @throws PGPException in case of an OpenPGP error */ public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) + @Nonnull ConsumerOptions options) throws IOException, PGPException { return create(inputStream, options, PGPainless.getPolicy()); } @@ -127,8 +127,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { * @throws IOException in case of an IO error */ public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options, - @Nonnull Policy policy) + @Nonnull ConsumerOptions options, + @Nonnull Policy policy) throws PGPException, IOException { return create(inputStream, options, new MessageMetadata.Message(), policy); } @@ -479,9 +479,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (Passphrase passphrase : options.getDecryptionPassphrases()) { for (PGPPBEEncryptedData skesk : esks.skesks) { LOGGER.debug("Attempt decryption with provided passphrase"); - SymmetricKeyAlgorithm kekAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); + SymmetricKeyAlgorithm encapsulationAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); try { - throwIfUnacceptable(kekAlgorithm); + throwIfUnacceptable(encapsulationAlgorithm); } catch (UnacceptableAlgorithmException e) { LOGGER.debug("Skipping SKESK with unacceptable encapsulation algorithm", e); continue; @@ -489,7 +489,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPBEDataDecryptorFactory(passphrase); - if (decryptSKESKAndStream(skesk, decryptorFactory)) { return true; } @@ -497,10 +496,11 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } List> postponedDueToMissingPassphrase = new ArrayList<>(); + // Try (known) secret keys for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { long keyId = pkesk.getKeyID(); - LOGGER.debug("Encountered PKESK with recipient " + KeyIdUtil.formatKeyId(keyId)); + LOGGER.debug("Encountered PKESK for recipient " + KeyIdUtil.formatKeyId(keyId)); PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); if (decryptionKeys == null) { LOGGER.debug("Skipping PKESK because no matching key " + KeyIdUtil.formatKeyId(keyId) + " was provided"); @@ -508,13 +508,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); - S2K s2K = secretKey.getS2K(); - if (s2K != null) { - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); - continue; - } + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue; } LOGGER.debug("Attempt decryption using secret key " + decryptionKeyId); @@ -527,10 +522,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); - - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { + if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -541,13 +533,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSecretKeyRing decryptionKeys = decryptionKeyCandidate.getA(); PGPSecretKey secretKey = decryptionKeyCandidate.getB(); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); - S2K s2K = secretKey.getS2K(); - if (s2K != null) { - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); - continue; - } + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue; } LOGGER.debug("Attempt decryption of anonymous PKESK with key " + decryptionKeyId); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); @@ -556,11 +543,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); continue; } - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKeyCandidate.getB(), protector); - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); + if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -571,7 +556,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { Set keyIds = new HashSet<>(); for (Tuple k : postponedDueToMissingPassphrase) { PGPSecretKey key = k.getA(); - keyIds.add(new SubkeyIdentifier(getDecryptionKey(key.getKeyID()), key.getKeyID())); + PGPSecretKeyRing keys = getDecryptionKey(key.getKeyID()); + keyIds.add(new SubkeyIdentifier(keys, key.getKeyID())); } if (!keyIds.isEmpty()) { throw new MissingPassphraseException(keyIds); @@ -584,21 +570,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { long keyId = secretKey.getKeyID(); PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); - S2K s2K = secretKey.getS2K(); - if (s2K != null) { - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); - continue; - } + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue; } + LOGGER.debug("Attempt decryption with key " + decryptionKeyId + " while interactively requesting its passphrase"); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector.getDecryptor(keyId)); - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - - if (decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk)) { + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); + if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -613,6 +592,27 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } + private boolean decryptWithPrivateKey(PGPPrivateKey privateKey, + SubkeyIdentifier decryptionKeyId, + PGPPublicKeyEncryptedData pkesk) + throws PGPException, IOException { + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + return decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk); + } + + private static boolean hasUnsupportedS2KSpecifier(PGPSecretKey secretKey, SubkeyIdentifier decryptionKeyId) { + S2K s2K = secretKey.getS2K(); + if (s2K != null) { + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); + return true; + } + } + return false; + } + private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) throws IOException, UnacceptableAlgorithmException { try { @@ -661,14 +661,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } - private PGPSecretKey getDecryptionKey(PGPSecretKeyRing decryptionKeys, long keyId) { - KeyRingInfo info = PGPainless.inspectKeyRing(decryptionKeys); - if (info.getEncryptionSubkeys(EncryptionPurpose.ANY).contains(info.getPublicKey(keyId))) { - return info.getSecretKey(keyId); - } - return null; - } - private void throwIfUnacceptable(SymmetricKeyAlgorithm algorithm) throws UnacceptableAlgorithmException { if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { @@ -883,20 +875,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return resultBuilder.build(); } - static void log(String message) { - LOGGER.debug(message); - // CHECKSTYLE:OFF - System.out.println(message); - // CHECKSTYLE:ON - } - - static void log(String message, Throwable e) { - log(message); - // CHECKSTYLE:OFF - e.printStackTrace(); - // CHECKSTYLE:ON - } - // In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. @@ -1004,7 +982,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { try { SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(signature); + .verify(signature); CertificateValidator.validateCertificateAndVerifyOnePassSignature(onePassSignature, policy); LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); layer.addVerifiedOnePassSignature(verification); From f86aae499726e715ef1dd793314014c1bbdb2050 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 2 Nov 2022 10:37:24 +0100 Subject: [PATCH 0531/1204] Implement efficient read(buf,off,len) for DelayedInputStream --- .../decryption_verification/TeeBCPGInputStream.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index 52ec9001..28b415e0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -127,9 +127,6 @@ public class TeeBCPGInputStream { } } - // TODO: Uncomment, once BC-172.1 is available - // see https://github.com/bcgit/bc-java/issues/1257 - /* @Override public int read(byte[] b, int off, int len) throws IOException { if (last != -1) { @@ -145,7 +142,6 @@ public class TeeBCPGInputStream { } return r; } - */ /** * Squeeze the last byte out and update the output stream. From b1f9a1398a879ef0d8ed2a1b4301618cade9df5e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 12:07:26 +0100 Subject: [PATCH 0532/1204] Add comment for ArmorUtils method --- .../src/main/java/org/pgpainless/util/ArmorUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index 8a51ff3d..b3e60023 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -178,6 +178,7 @@ public final class ArmorUtils { * If it is
false
, the signature will be encoded as-is. * * @param signature signature + * @param export whether to exclude non-exportable subpackets or trust-packets. * @return ascii armored string * * @throws IOException in case of an error in the {@link ArmoredOutputStream} From 313dbcfaa88d1bacc6f38f2af676f58d1157e9c7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 12:29:50 +0100 Subject: [PATCH 0533/1204] Update changelog --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 551728aa..af1029ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.0-SNAPSHOT +- Reimplement message consumption via new `OpenPgpMessageInputStream` + - Fix validation of prepended signatures (#314) + - Fix validation of nested signatures (#319) + - Reject malformed messages (#237) + - Utilize new `PDA` syntax verifier class + - Allow for custom message syntax via `Syntax` class + - Gracefully handle `UnsupportedPacketVersionException` for signatures + - Allow plugin decryption code (e.g. to add support for hardware-backed keys (see #318)) + - Add `HardwareSecurity` utility class + - Add `GnuPGDummyKeyUtil` which can be used to mimic GnuPGs proprietary S2K extensions + for keys which were placed on hardware tokens + - Add `OpenPgpPacket` enum class to enumerate available packet tags + - Remove old decryption classes in favor of new implementation + - Removed `DecryptionStream` class and replaced with new abstract class + - Removed `DecryptionStreamFactory` + - Removed `FinalIOException` + - Removed `MissingLiteralDataException` (replaced by `MalformedOpenPgpMessageException`) + - Introduce `MessageMetadata` class as potential future replacement for `OpenPgpMetadata`. + - can be obtained via `((OpenPgpMessageInputStream) decryptionStream).getMetadata();` +- Add `CachingBcPublicKeyDataDecryptorFactory` which can be extended to prevent costly decryption + of session keys +- Fix: Only verify message integrity once +- Remove unnecessary `@throws` declarations on `KeyRingReader` methods +- Remove unnecessary `@throws` declarations on `KeyRingUtils` methods +- Add `KeyIdUtil.formatKeyId(long id)` to format hexadecimal key-ids. +- Add `KeyRingUtils.publicKeys(PGPKeyRing keys)` +- Remove `BCUtil` class + ## 1.3.13 - Bump `sop-java` to `4.0.7` From 256920bfae35da2009ba328d57d04862f567b949 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 12:34:52 +0100 Subject: [PATCH 0534/1204] PGPainless 1.4.0-rc1 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af1029ce..54b9b8a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.4.0-SNAPSHOT +## 1.4.0-rc1 - Reimplement message consumption via new `OpenPgpMessageInputStream` - Fix validation of prepended signatures (#314) - Fix validation of nested signatures (#319) diff --git a/README.md b/README.md index 90f6be81..1855f742 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.13' + implementation 'org.pgpainless:pgpainless-core:1.4.0-rc1' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 4a18136d..893804dd 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.13" + implementation "org.pgpainless:pgpainless-sop:1.4.0-rc1" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.13 + 1.4.0-rc1 ... diff --git a/version.gradle b/version.gradle index 7d25632e..d99e2f6b 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.0' - isSnapshot = true + shortVersion = '1.4.0-rc1' + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 6243d690614eb10cb22fa378d59b647cf5a67f09 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 12:38:38 +0100 Subject: [PATCH 0535/1204] PGPainless 1.4.0-rc2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index d99e2f6b..d6634b93 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.0-rc1' - isSnapshot = false + shortVersion = '1.4.0-rc2' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From f80b3e0cdb4d2ef6b5cb34229a9af06d44532960 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 12:52:20 +0100 Subject: [PATCH 0536/1204] Use BCs PGPEncryptedDataList.isIntegrityProtected() --- .../decryption_verification/OpenPgpMessageInputStream.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index f2c30ced..83c0a3f2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -405,9 +405,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { syntaxVerifier.next(InputSymbol.EncryptedData); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); - // TODO: Replace with !encDataList.isIntegrityProtected() - // once BC ships it - if (!encDataList.get(0).isIntegrityProtected()) { + if (!encDataList.isIntegrityProtected()) { LOGGER.debug("Symmetrically Encrypted Data Packet is not integrity-protected and is therefore rejected."); throw new MessageNotIntegrityProtectedException(); } From 59e81dc514018b4e1705decdc0e375e34b43bb33 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 13:04:13 +0100 Subject: [PATCH 0537/1204] Use BCs PGPEncryptedDataList.extractSessionKeyEncryptedData() for decryption with session key --- .../OpenPgpMessageInputStream.java | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 83c0a3f2..28440257 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -33,6 +33,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSessionKeyEncryptedData; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; @@ -444,29 +445,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( sessionKey.getAlgorithm(), metadata.depth + 1); + PGPSessionKeyEncryptedData sessionKeyEncryptedData = encDataList.extractSessionKeyEncryptedData(); try { - // TODO: Use BCs new API once shipped - // see https://github.com/bcgit/bc-java/pull/1228 (discussion) - PGPEncryptedData esk = esks.all().get(0); - if (esk instanceof PGPPBEEncryptedData) { - PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; - InputStream decrypted = skesk.getDataStream(decryptorFactory); - encryptedData.sessionKey = sessionKey; - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); - nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); - LOGGER.debug("Successfully decrypted data with provided session key"); - return true; - } else if (esk instanceof PGPPublicKeyEncryptedData) { - PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - encryptedData.sessionKey = sessionKey; - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); - nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); - LOGGER.debug("Successfully decrypted data with provided session key"); - return true; - } else { - throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); - } + InputStream decrypted = sessionKeyEncryptedData.getDataStream(decryptorFactory); + encryptedData.sessionKey = sessionKey; + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, sessionKeyEncryptedData, options); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); + LOGGER.debug("Successfully decrypted data with provided session key"); + return true; } catch (PGPException e) { // Session key mismatch? LOGGER.debug("Decryption using provided session key failed. Mismatched session key and message?", e); From 963b678a9e5ec5b98e50e1a68bf2a391e6e8f8c4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 13:04:36 +0100 Subject: [PATCH 0538/1204] Enable test for decryption of messages without ESKs --- .../TestDecryptionOfMessageWithoutESKUsingSessionKey.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java index c9778426..b28af822 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TestDecryptionOfMessageWithoutESKUsingSessionKey.java @@ -7,7 +7,6 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSessionKey; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.util.SessionKey; @@ -55,8 +54,6 @@ public class TestDecryptionOfMessageWithoutESKUsingSessionKey { assertEquals("Hello, World!\n", out.toString()); } - // TODO: Enable when BC 173 gets released with our fix - @Disabled("Bug in BC 172. See https://github.com/bcgit/bc-java/pull/1228") @Test public void decryptMessageWithoutSKESK() throws PGPException, IOException { ByteArrayInputStream in = new ByteArrayInputStream(encryptedMessageWithoutESK.getBytes(StandardCharsets.UTF_8)); From 6dcc1e68cd2ea4969caf0a7a94bb1fdb925f5ff0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 13:04:53 +0100 Subject: [PATCH 0539/1204] Fix expected exception in roundtrip test --- .../java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java index 515cf71d..22af5b70 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -512,7 +512,7 @@ public class EncryptDecryptRoundTripTest { "-----END PGP MESSAGE-----").getBytes(StandardCharsets.UTF_8); SessionKey wrongSessionKey = SessionKey.fromString("9:63F741E7FB60247BE59C64158573308F727236482DB7653908C95839E4166AAE"); - assertThrows(SOPGPException.BadData.class, () -> + assertThrows(SOPGPException.CannotDecrypt.class, () -> sop.decrypt().withSessionKey(wrongSessionKey).ciphertext(ciphertext)); } From 2dc72d76908f8f00200b187d2b95751722731326 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Nov 2022 13:14:58 +0100 Subject: [PATCH 0540/1204] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54b9b8a4..cff696f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.0-rc2-SNAPSHOT +- Use BCs `PGPEncryptedDataList.extractSessionKeyEncryptedData()` method + to do decryption using session keys. This enables decryption of messages + without encrypted session key packets. +- Use BCs `PGPEncryptedDataList.isIntegrityProtected()` to check for integrity protection + ## 1.4.0-rc1 - Reimplement message consumption via new `OpenPgpMessageInputStream` - Fix validation of prepended signatures (#314) From 552459608234e4c4edc9b9b50f7ab5fd91d82947 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Nov 2022 18:53:40 +0100 Subject: [PATCH 0541/1204] Add more tests for sop code --- .../cli/commands/DearmorCmdTest.java | 13 +++ .../RoundTripInlineSignVerifyCmdTest.java | 107 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java index 4ebaf21d..b8e5532f 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java @@ -6,6 +6,7 @@ package org.pgpainless.cli.commands; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -87,4 +88,16 @@ public class DearmorCmdTest extends CLITest { assertEquals("Hello, World\n", out.toString()); } + + @Test + public void dearmorGarbageEmitsEmpty() { + String noArmoredData = "This is not armored."; + System.setIn(new ByteArrayInputStream(noArmoredData.getBytes(StandardCharsets.UTF_8))); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + System.setOut(new PrintStream(out)); + PGPainlessCLI.execute("dearmor"); + + assertTrue(out.toString().isEmpty()); + } + } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java new file mode 100644 index 00000000..82fda430 --- /dev/null +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.cli.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +import com.ginsberg.junit.exit.FailOnSystemExit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.pgpainless.cli.PGPainlessCLI; +import org.pgpainless.cli.TestUtils; + +public class RoundTripInlineSignVerifyCmdTest { + private static File tempDir; + private static PrintStream originalSout; + + @BeforeAll + public static void prepare() throws IOException { + tempDir = TestUtils.createTempDirectory(); + } + + @Test + @FailOnSystemExit + public void encryptAndDecryptAMessage() throws IOException { + originalSout = System.out; + File sigmundKeyFile = new File(tempDir, "sigmund.key"); + assertTrue(sigmundKeyFile.createNewFile()); + + File sigmundCertFile = new File(tempDir, "sigmund.cert"); + assertTrue(sigmundCertFile.createNewFile()); + + File msgFile = new File(tempDir, "signed.asc"); + assertTrue(msgFile.createNewFile()); + + File passwordFile = new File(tempDir, "password"); + assertTrue(passwordFile.createNewFile()); + + // write password file + FileOutputStream passwordOut = new FileOutputStream(passwordFile); + passwordOut.write("sw0rdf1sh".getBytes(StandardCharsets.UTF_8)); + passwordOut.close(); + + // generate key + OutputStream sigmundKeyOut = new FileOutputStream(sigmundKeyFile); + System.setOut(new PrintStream(sigmundKeyOut)); + PGPainlessCLI.execute("generate-key", + "--with-key-password=" + passwordFile.getAbsolutePath(), + "Sigmund Freud "); + sigmundKeyOut.close(); + + // extract cert + FileInputStream sigmundKeyIn = new FileInputStream(sigmundKeyFile); + System.setIn(sigmundKeyIn); + OutputStream sigmundCertOut = new FileOutputStream(sigmundCertFile); + System.setOut(new PrintStream(sigmundCertOut)); + PGPainlessCLI.execute("extract-cert"); + sigmundKeyIn.close(); + sigmundCertOut.close(); + + // sign message + String msg = "Hello World!\n"; + ByteArrayInputStream msgIn = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); + System.setIn(msgIn); + OutputStream msgAscOut = new FileOutputStream(msgFile); + System.setOut(new PrintStream(msgAscOut)); + PGPainlessCLI.execute("inline-sign", + "--with-key-password=" + passwordFile.getAbsolutePath(), + sigmundKeyFile.getAbsolutePath()); + msgAscOut.close(); + + File verifyFile = new File(tempDir, "verify.txt"); + + FileInputStream msgAscIn = new FileInputStream(msgFile); + System.setIn(msgAscIn); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrintStream pOut = new PrintStream(out); + System.setOut(pOut); + PGPainlessCLI.execute("inline-verify", + "--verifications-out", verifyFile.getAbsolutePath(), + sigmundCertFile.getAbsolutePath()); + msgAscIn.close(); + + assertEquals(msg, out.toString()); + } + + @AfterAll + public static void after() { + System.setOut(originalSout); + // CHECKSTYLE:OFF + System.out.println(tempDir.getAbsolutePath()); + // CHECKSTYLE:ON + } +} From 86c72291727e8f202a1101d48047e90648480494 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Nov 2022 22:23:24 +0100 Subject: [PATCH 0542/1204] Do not reject bnacksig signatures when they predate subkey binding date Fixes #334 SOP verify: force data to be non-openpgp data Update changelog SOP: Unify key/certificate reading code Fix key/password matching in SOPs detached sign command Rework CLI tests update changelog PGPainless 1.3.11 PGPainless 1.3.12-SNAPSHOT Merge branch 'release/1.3' --- .../org/pgpainless/cli/commands/DearmorCmdTest.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java index b8e5532f..8fcf093d 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java @@ -90,13 +90,11 @@ public class DearmorCmdTest extends CLITest { } @Test - public void dearmorGarbageEmitsEmpty() { + public void dearmorGarbageEmitsEmpty() throws IOException { String noArmoredData = "This is not armored."; - System.setIn(new ByteArrayInputStream(noArmoredData.getBytes(StandardCharsets.UTF_8))); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("dearmor"); - + pipeStringToStdin(noArmoredData); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("dearmor")); assertTrue(out.toString().isEmpty()); } From b287d28a28bf07cfc4aaa6c51fb3fdca4029d4a1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 9 Aug 2022 15:10:59 +0200 Subject: [PATCH 0543/1204] Depend on pgp-certificate-store --- pgpainless-core/build.gradle | 3 +++ version.gradle | 1 + 2 files changed, 4 insertions(+) diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 3c73121f..544fb8a4 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -24,6 +24,9 @@ dependencies { api "org.bouncycastle:bcpg-jdk15to18:$bouncyPgVersion" // api(files("../libs/bcpg-jdk18on-1.70.jar")) + // certificate store + api "org.pgpainless:pgp-certificate-store:$pgpCertificateStoreVersion" + // @Nullable, @Nonnull annotations implementation "com.google.code.findbugs:jsr305:3.0.2" } diff --git a/version.gradle b/version.gradle index d6634b93..302f54e0 100644 --- a/version.gradle +++ b/version.gradle @@ -17,6 +17,7 @@ allprojects { junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' + pgpCertificateStoreVersion = '0.1.1' slf4jVersion = '1.7.36' sopJavaVersion = '4.0.7' } From d486a17cf1b864d9d37073bd9e7cb891654daf14 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 9 Aug 2022 15:11:18 +0200 Subject: [PATCH 0544/1204] Implement EncryptionOptions.addRecipient(store, fingerprint) --- .../encryption_signing/EncryptionOptions.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 6d2ca642..4bf21434 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -4,6 +4,7 @@ package org.pgpainless.encryption_signing; +import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -21,6 +22,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; +import org.pgpainless.PGPainless; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.exception.KeyException; @@ -30,6 +32,10 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyAccessor; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.Passphrase; +import pgp.certificate_store.Certificate; +import pgp.certificate_store.CertificateStore; +import pgp.certificate_store.exception.BadDataException; +import pgp.certificate_store.exception.BadNameException; /** * Options for the encryption process. @@ -235,6 +241,30 @@ public class EncryptionOptions { return this; } + /** + * Add a recipient by providing a {@link CertificateStore} and the {@link OpenPgpFingerprint} of the recipients key. + * If no such certificate is found in the store, a {@link NoSuchElementException is thrown}. + * + * @param certificateStore certificate store + * @param certificateFingerprint fingerprint of the recipient certificate + * @return builder + * @throws BadDataException if the certificate contains bad data + * @throws BadNameException if the fingerprint is not in a recognizable form for the store + * @throws IOException in case of an IO error + * @throws NoSuchElementException if the store does not contain a certificate for the given fingerprint + */ + public EncryptionOptions addRecipient(@Nonnull CertificateStore certificateStore, + @Nonnull OpenPgpFingerprint certificateFingerprint) + throws BadDataException, BadNameException, IOException { + String fingerprint = certificateFingerprint.toString().toLowerCase(); + Certificate certificateRecord = certificateStore.getCertificate(fingerprint); + if (certificateRecord == null) { + throw new NoSuchElementException("Cannot find certificate '" + certificateFingerprint + "'"); + } + PGPPublicKeyRing recipientCertificate = PGPainless.readKeyRing().publicKeyRing(certificateRecord.getInputStream()); + return addRecipient(recipientCertificate); + } + private void addRecipientKey(PGPPublicKeyRing keyRing, PGPPublicKey key) { encryptionKeys.add(new SubkeyIdentifier(keyRing, key.getKeyID())); PGPKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory From 6dc5b84d668388646085018ada1945e5933f346c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 12 Aug 2022 15:05:42 +0200 Subject: [PATCH 0545/1204] Depend on pgp-certificate-store again --- .../encryption_signing/EncryptionOptions.java | 11 ++++++----- version.gradle | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 4bf21434..13131f08 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -32,8 +32,8 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyAccessor; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.Passphrase; -import pgp.certificate_store.Certificate; -import pgp.certificate_store.CertificateStore; +import pgp.certificate_store.PGPCertificateStore; +import pgp.certificate_store.certificate.Certificate; import pgp.certificate_store.exception.BadDataException; import pgp.certificate_store.exception.BadNameException; @@ -242,7 +242,7 @@ public class EncryptionOptions { } /** - * Add a recipient by providing a {@link CertificateStore} and the {@link OpenPgpFingerprint} of the recipients key. + * Add a recipient by providing a {@link PGPCertificateStore} and the {@link OpenPgpFingerprint} of the recipients key. * If no such certificate is found in the store, a {@link NoSuchElementException is thrown}. * * @param certificateStore certificate store @@ -253,7 +253,7 @@ public class EncryptionOptions { * @throws IOException in case of an IO error * @throws NoSuchElementException if the store does not contain a certificate for the given fingerprint */ - public EncryptionOptions addRecipient(@Nonnull CertificateStore certificateStore, + public EncryptionOptions addRecipient(@Nonnull PGPCertificateStore certificateStore, @Nonnull OpenPgpFingerprint certificateFingerprint) throws BadDataException, BadNameException, IOException { String fingerprint = certificateFingerprint.toString().toLowerCase(); @@ -261,7 +261,8 @@ public class EncryptionOptions { if (certificateRecord == null) { throw new NoSuchElementException("Cannot find certificate '" + certificateFingerprint + "'"); } - PGPPublicKeyRing recipientCertificate = PGPainless.readKeyRing().publicKeyRing(certificateRecord.getInputStream()); + PGPPublicKeyRing recipientCertificate = PGPainless.readKeyRing() + .publicKeyRing(certificateRecord.getInputStream()); return addRecipient(recipientCertificate); } diff --git a/version.gradle b/version.gradle index 302f54e0..260ef548 100644 --- a/version.gradle +++ b/version.gradle @@ -17,7 +17,7 @@ allprojects { junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' - pgpCertificateStoreVersion = '0.1.1' + pgpCertificateStoreVersion = '0.1.2-SNAPSHOT' slf4jVersion = '1.7.36' sopJavaVersion = '4.0.7' } From d0277fbbecfeed576e1d0dab9960fe43deec368e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 25 Aug 2022 17:20:45 +0200 Subject: [PATCH 0546/1204] Bump cert-d-java to 0.2.0 --- pgpainless-core/build.gradle | 3 ++- version.gradle | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 544fb8a4..84b4273f 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -25,7 +25,8 @@ dependencies { // api(files("../libs/bcpg-jdk18on-1.70.jar")) // certificate store - api "org.pgpainless:pgp-certificate-store:$pgpCertificateStoreVersion" + api "org.pgpainless:pgp-certificate-store:$pgpCertDJavaVersion" + testImplementation "org.pgpainless:pgpainless-cert-d:$pgpainlessCertDVersion" // @Nullable, @Nonnull annotations implementation "com.google.code.findbugs:jsr305:3.0.2" diff --git a/version.gradle b/version.gradle index 260ef548..109e592e 100644 --- a/version.gradle +++ b/version.gradle @@ -17,7 +17,8 @@ allprojects { junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' - pgpCertificateStoreVersion = '0.1.2-SNAPSHOT' + pgpainlessCertDVersion = '0.1.3-SNAPSHOT' + pgpCertDJavaVersion = '0.2.0' slf4jVersion = '1.7.36' sopJavaVersion = '4.0.7' } From 22abb624435ec3fc5bd6ad974b6e0f5b2df75941 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 25 Aug 2022 18:56:37 +0200 Subject: [PATCH 0547/1204] Add test for encryption to cert from certificate store --- .../EncryptWithKeyFromKeyStoreTest.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java new file mode 100644 index 00000000..9291d703 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.certificate_store.MergeCallbacks; +import org.pgpainless.certificate_store.PGPainlessCertD; +import org.pgpainless.key.OpenPgpFingerprint; +import pgp.cert_d.PGPCertificateStoreAdapter; +import pgp.certificate_store.certificate.Certificate; +import pgp.certificate_store.exception.BadDataException; +import pgp.certificate_store.exception.BadNameException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class EncryptWithKeyFromKeyStoreTest { + + // Collection of 3 certificates (fingerprints below) + private static final String CERT_COLLECTION = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "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\n" + + "Pr2snaTp6/NukIbg3D/YMXm+mM6119v7HJkO\n" + + "=KVYs\n" + + "-----END PGP PUBLIC KEY BLOCK-----"; + private static final OpenPgpFingerprint cert1fp = OpenPgpFingerprint.parse("76324A41CC19C0AA33B3C1F6070730D6CC6AAE26"); + private static final OpenPgpFingerprint cert2fp = OpenPgpFingerprint.parse("C7CD4DE07F1C972E04D338600E64F63484F50F82"); + private static final OpenPgpFingerprint cert3fp = OpenPgpFingerprint.parse("9165E83E17F979BD2631FEFD545DC02306859824"); + + @Test + public void encryptWithKeyFromStore() throws PGPException, IOException, BadDataException, InterruptedException, BadNameException { + // In-Memory certificate store + PGPainlessCertD certificateDirectory = PGPainlessCertD.inMemory(); + PGPCertificateStoreAdapter adapter = new PGPCertificateStoreAdapter(certificateDirectory); + + // Populate store + PGPPublicKeyRingCollection certificates = PGPainless.readKeyRing().publicKeyRingCollection(CERT_COLLECTION); + for (PGPPublicKeyRing cert : certificates) { + certificateDirectory.insert(new ByteArrayInputStream(cert.getEncoded()), MergeCallbacks.mergeWithExisting()); + } + + // Encrypt message + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions(ProducerOptions.encrypt( + EncryptionOptions.encryptCommunications() + .addRecipient(adapter, cert2fp))); + ByteArrayInputStream plaintext = new ByteArrayInputStream("Hello, World! This message is encrypted using a cert from a store!".getBytes()); + Streams.pipeAll(plaintext, encryptionStream); + encryptionStream.close(); + + // Get cert from store + Certificate cert = adapter.getCertificate(cert2fp.toString()); + PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(cert.getInputStream()); + + // check if message was encrypted for cert + assertTrue(encryptionStream.getResult().isEncryptedFor(publicKeys)); + } +} From 4594b494a96089534f597fb2a6b14ca292ef8959 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 25 Aug 2022 19:46:28 +0200 Subject: [PATCH 0548/1204] Implement signature verification with certificate stores as cert source --- .../ConsumerOptions.java | 93 +++++++- .../EncryptWithKeyFromKeyStoreTest.java | 213 ++++++++++++++---- .../pgpainless/sop/DetachedVerifyImpl.java | 2 +- .../org/pgpainless/sop/InlineVerifyImpl.java | 2 +- 4 files changed, 257 insertions(+), 53 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index f8f59d36..f7b3c020 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -6,10 +6,12 @@ package org.pgpainless.decryption_verification; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -23,6 +25,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.key.SubkeyIdentifier; @@ -30,6 +33,9 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; +import pgp.certificate_store.PGPCertificateStore; +import pgp.certificate_store.certificate.Certificate; +import pgp.certificate_store.exception.BadDataException; /** * Options for decryption and signature verification. @@ -43,8 +49,7 @@ public class ConsumerOptions { private Date verifyNotBefore = null; private Date verifyNotAfter = new Date(); - // Set of verification keys - private final Set certificates = new HashSet<>(); + private final CertificateSource certificates = new CertificateSource(); private final Set detachedSignatures = new HashSet<>(); private MissingPublicKeyCallback missingCertificateCallback = null; @@ -113,7 +118,7 @@ public class ConsumerOptions { * @return options */ public ConsumerOptions addVerificationCert(PGPPublicKeyRing verificationCert) { - this.certificates.add(verificationCert); + this.certificates.addCertificate(verificationCert); return this; } @@ -130,6 +135,11 @@ public class ConsumerOptions { return this; } + public ConsumerOptions addVerificationCerts(PGPCertificateStore certificateStore) { + this.certificates.addStore(certificateStore); + return this; + } + public ConsumerOptions addVerificationOfDetachedSignatures(InputStream signatureInputStream) throws IOException, PGPException { List signatures = SignatureUtils.readSignatures(signatureInputStream); return addVerificationOfDetachedSignatures(signatures); @@ -266,8 +276,19 @@ public class ConsumerOptions { return Collections.unmodifiableSet(decryptionPassphrases); } + /** + * Return the explicitly set verification certificates. + * + * @deprecated use {@link #getCertificateSource()} instead. + * @return verification certs + */ + @Deprecated public @Nonnull Set getCertificates() { - return Collections.unmodifiableSet(certificates); + return certificates.getExplicitCertificates(); + } + + public @Nonnull CertificateSource getCertificateSource() { + return certificates; } public @Nullable MissingPublicKeyCallback getMissingCertificateCallback() { @@ -385,4 +406,68 @@ public class ConsumerOptions { public MultiPassStrategy getMultiPassStrategy() { return multiPassStrategy; } + + public static class CertificateSource { + + private List stores = new ArrayList<>(); + private Set explicitCertificates = new HashSet<>(); + + /** + * Add a certificate store as source for verification certificates. + * + * @param certificateStore cert store + */ + public void addStore(PGPCertificateStore certificateStore) { + this.stores.add(certificateStore); + } + + /** + * Add a certificate as verification cert explicitly. + * + * @param certificate certificate + */ + public void addCertificate(PGPPublicKeyRing certificate) { + this.explicitCertificates.add(certificate); + } + + /** + * Return the set of explicitly set verification certificates. + * @return explicitly set verification certs + */ + public Set getExplicitCertificates() { + return Collections.unmodifiableSet(explicitCertificates); + } + + /** + * Return a certificate which contains a subkey with the given keyId. + * This method first checks all explicitly set verification certs and if no cert is found it consults + * the certificate stores. + * + * @param keyId key id + * @return certificate + */ + public PGPPublicKeyRing getCertificate(long keyId) { + + for (PGPPublicKeyRing cert : explicitCertificates) { + if (cert.getPublicKey(keyId) != null) { + return cert; + } + } + + for (PGPCertificateStore store : stores) { + try { + Iterator certs = store.getCertificatesBySubkeyId(keyId); + if (!certs.hasNext()) { + continue; + } + Certificate cert = certs.next(); + PGPPublicKeyRing publicKey = PGPainless.readKeyRing().publicKeyRing(cert.getInputStream()); + return publicKey; + } catch (IOException | BadDataException e) { + continue; + } + } + return null; + } + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java index 9291d703..48f1bbd7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java @@ -7,12 +7,18 @@ package org.pgpainless.encryption_signing; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.certificate_store.MergeCallbacks; import org.pgpainless.certificate_store.PGPainlessCertD; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.key.protection.SecretKeyRingProtector; import pgp.cert_d.PGPCertificateStoreAdapter; import pgp.certificate_store.certificate.Certificate; import pgp.certificate_store.exception.BadDataException; @@ -26,60 +32,116 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class EncryptWithKeyFromKeyStoreTest { + // Collection of 3 keys (fingerprints below) + private static final String KEY_COLLECTION = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "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\n" + + "jzJc4ViKmwiNJAPttDQCpYjzJT7LUKAA\n" + + "=EAvh\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + // Collection of 3 certificates (fingerprints below) private static final String CERT_COLLECTION = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "Version: BCPG v1.71\n" + "\n" + - "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\n" + - "Pr2snaTp6/NukIbg3D/YMXm+mM6119v7HJkO\n" + - "=KVYs\n" + + "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\n" + + "Mwj4jzJc4ViKmwiNJAPttDQCpYjzJT7LUKAA\n" + + "=WaRm\n" + "-----END PGP PUBLIC KEY BLOCK-----"; - private static final OpenPgpFingerprint cert1fp = OpenPgpFingerprint.parse("76324A41CC19C0AA33B3C1F6070730D6CC6AAE26"); - private static final OpenPgpFingerprint cert2fp = OpenPgpFingerprint.parse("C7CD4DE07F1C972E04D338600E64F63484F50F82"); - private static final OpenPgpFingerprint cert3fp = OpenPgpFingerprint.parse("9165E83E17F979BD2631FEFD545DC02306859824"); + private static final OpenPgpFingerprint cert1fp = OpenPgpFingerprint.parse("DA8AF595FC41F71BC4F26608688F656CA3A169B3"); + private static final OpenPgpFingerprint cert2fp = OpenPgpFingerprint.parse("DBF2F91C16DAE881431EEF1C09EC13A1534C7365"); + private static final OpenPgpFingerprint cert3fp = OpenPgpFingerprint.parse("29F8CAF0F3A115D66C948142A269971D8C7597DE"); @Test - public void encryptWithKeyFromStore() throws PGPException, IOException, BadDataException, InterruptedException, BadNameException { + public void encryptWithCertFromCertificateStore() throws PGPException, IOException, BadDataException, InterruptedException, BadNameException { // In-Memory certificate store PGPainlessCertD certificateDirectory = PGPainlessCertD.inMemory(); PGPCertificateStoreAdapter adapter = new PGPCertificateStoreAdapter(certificateDirectory); @@ -108,4 +170,61 @@ public class EncryptWithKeyFromKeyStoreTest { // check if message was encrypted for cert assertTrue(encryptionStream.getResult().isEncryptedFor(publicKeys)); } + + @Test + public void verifyWithCertFromCertificateStore() + throws PGPException, IOException, BadDataException, InterruptedException, BadNameException { + // In-Memory certificate store + PGPainlessCertD certificateDirectory = PGPainlessCertD.inMemory(); + PGPCertificateStoreAdapter adapter = new PGPCertificateStoreAdapter(certificateDirectory); + + // Populate store + PGPPublicKeyRingCollection certificates = PGPainless.readKeyRing().publicKeyRingCollection(CERT_COLLECTION); + for (PGPPublicKeyRing cert : certificates) { + certificateDirectory.insert(new ByteArrayInputStream(cert.getEncoded()), MergeCallbacks.mergeWithExisting()); + } + + // Prepare keys + OpenPgpFingerprint cryptFp = cert3fp; + OpenPgpFingerprint signFp = cert1fp; + PGPSecretKeyRingCollection secretKeys = PGPainless.readKeyRing().secretKeyRingCollection(KEY_COLLECTION); + PGPSecretKeyRing signingKey = secretKeys.getSecretKeyRing(signFp.getKeyId()); + PGPSecretKeyRing decryptionKey = secretKeys.getSecretKeyRing(cryptFp.getKeyId()); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + // Encrypt and sign message + ByteArrayInputStream plaintextIn = new ByteArrayInputStream( + "This message was encrypted with a cert from a store and gets verified with a cert from a store as well".getBytes()); + ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertext) + .withOptions( + ProducerOptions.signAndEncrypt( + EncryptionOptions.encryptCommunications() + .addRecipient(adapter, cryptFp), + SigningOptions.get() + .addSignature(protector, signingKey) + )); + Streams.pipeAll(plaintextIn, encryptionStream); + encryptionStream.close(); + + // Prepare ciphertext for decryption + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertext.toByteArray()); + ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); + // Decrypt and verify + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions( + new ConsumerOptions() + .addDecryptionKey(decryptionKey, protector) + .addVerificationCerts(adapter)); + Streams.pipeAll(decryptionStream, plaintextOut); + decryptionStream.close(); + + // Check that message can be decrypted and is verified + OpenPgpMetadata result = decryptionStream.getResult(); + assertTrue(result.isEncrypted()); + assertTrue(result.isVerified()); + assertTrue(result.containsVerifiedSignatureFrom(signFp)); + } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index b563e704..3a5b3b6a 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -76,7 +76,7 @@ public class DetachedVerifyImpl implements DetachedVerify { verificationList.add(map(signatureVerification)); } - if (!options.getCertificates().isEmpty()) { + if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { if (verificationList.isEmpty()) { throw new SOPGPException.NoSignature(); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 4948712c..82ed4282 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -74,7 +74,7 @@ public class InlineVerifyImpl implements InlineVerify { verificationList.add(map(signatureVerification)); } - if (!options.getCertificates().isEmpty()) { + if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { if (verificationList.isEmpty()) { throw new SOPGPException.NoSignature(); } From c19b8297a3fd09df528d03942f1396aa14de9738 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 29 Aug 2022 10:36:39 +0200 Subject: [PATCH 0549/1204] Add TODO for when bumping cert-d-java --- .../org/pgpainless/encryption_signing/EncryptionOptions.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 13131f08..8d7098d3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -258,6 +258,8 @@ public class EncryptionOptions { throws BadDataException, BadNameException, IOException { String fingerprint = certificateFingerprint.toString().toLowerCase(); Certificate certificateRecord = certificateStore.getCertificate(fingerprint); + // TODO: getCertificate throws NSEE automatically in 0.2.2+ + // Remove if statement below when bumping if (certificateRecord == null) { throw new NoSuchElementException("Cannot find certificate '" + certificateFingerprint + "'"); } From 43e0f43bd9ff96744adcaa1239a7281b45cc3237 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 1 Sep 2022 11:44:27 +0200 Subject: [PATCH 0550/1204] Bump cert-d-java to 0.2.1 and cert-d-pgpainless to 0.2.0 --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 109e592e..eb20dc66 100644 --- a/version.gradle +++ b/version.gradle @@ -17,8 +17,8 @@ allprojects { junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' - pgpainlessCertDVersion = '0.1.3-SNAPSHOT' - pgpCertDJavaVersion = '0.2.0' + pgpainlessCertDVersion = '0.2.0' + pgpCertDJavaVersion = '0.2.1' slf4jVersion = '1.7.36' sopJavaVersion = '4.0.7' } From a7929528455c5587b4e3064167970ba6aabeac10 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 1 Sep 2022 13:29:53 +0200 Subject: [PATCH 0551/1204] Remove code to manually throw NSEE for missing certs This is now done further down in the store itself --- .../org/pgpainless/encryption_signing/EncryptionOptions.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 8d7098d3..cf3b426a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -258,11 +258,6 @@ public class EncryptionOptions { throws BadDataException, BadNameException, IOException { String fingerprint = certificateFingerprint.toString().toLowerCase(); Certificate certificateRecord = certificateStore.getCertificate(fingerprint); - // TODO: getCertificate throws NSEE automatically in 0.2.2+ - // Remove if statement below when bumping - if (certificateRecord == null) { - throw new NoSuchElementException("Cannot find certificate '" + certificateFingerprint + "'"); - } PGPPublicKeyRing recipientCertificate = PGPainless.readKeyRing() .publicKeyRing(certificateRecord.getInputStream()); return addRecipient(recipientCertificate); From d7e4fcaec6293881e2de3cb75fde9e8c7b5b38d5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 14:20:17 +0100 Subject: [PATCH 0552/1204] OpenPgpMessageInputStream: Source verification certs from ConsumerOptions.getCertificateSource() --- .../OpenPgpMessageInputStream.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 28440257..65c8dad5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -1018,11 +1018,9 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private PGPPublicKeyRing findCertificate(long keyId) { - for (PGPPublicKeyRing cert : options.getCertificates()) { - PGPPublicKey verificationKey = cert.getPublicKey(keyId); - if (verificationKey != null) { - return cert; - } + PGPPublicKeyRing cert = options.getCertificateSource().getCertificate(keyId); + if (cert != null) { + return cert; } if (options.getMissingCertificateCallback() != null) { From 3877410a65b0bb7c838417cb97403a4a056f535a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 11 Nov 2022 14:28:33 +0100 Subject: [PATCH 0553/1204] Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cff696f1..6f19bc95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ SPDX-License-Identifier: CC0-1.0 to do decryption using session keys. This enables decryption of messages without encrypted session key packets. - Use BCs `PGPEncryptedDataList.isIntegrityProtected()` to check for integrity protection +- Depend on `pgp-certificate-store` +- Add `ConsumerOptions.addVerificationCerts(PGPCertificateStore)` to allow sourcing certificates from + e.g. a [certificate store implementation](https://github.com/pgpainless/cert-d-java). ## 1.4.0-rc1 - Reimplement message consumption via new `OpenPgpMessageInputStream` From 90c3a015770cc9f2b1aa4bafd4fe8be873c5f79b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Nov 2022 12:58:31 +0100 Subject: [PATCH 0554/1204] Add test to verify proper functionality of MatchMakingSecretKeyRingProtector --- ...oundTripInlineSignInlineVerifyCmdTest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index 3d7980b0..5d635beb 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -233,4 +233,32 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { String verificationString = verificationsOut.toString(); assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); } + + @Test + public void testUnlockKeyWithOneOfMultiplePasswords() throws IOException { + File key = writeFile("key.asc", KEY_1); + File wrong1 = writeFile("wrong_1", "BuzzAldr1n"); + File correct = writeFile("correct", KEY_1_PASSWORD); + File wrong2 = writeFile("wrong_2", "NeilArmstr0ng"); + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + key.getAbsolutePath(), + "--with-key-password", wrong1.getAbsolutePath(), + "--with-key-password", correct.getAbsolutePath(), + "--with-key-password", wrong2.getAbsolutePath())); + + File cert = writeFile("cert.asc", CERT_1); + pipeStringToStdin(ciphertextOut.toString()); + ByteArrayOutputStream msgOut = pipeStdoutToStream(); + File verificationsFile = nonExistentFile("verifications"); + assertSuccess(executeCommand("inline-verify", + "--verifications-out", verificationsFile.getAbsolutePath(), + cert.getAbsolutePath())); + + assertEquals(MESSAGE, msgOut.toString()); + String verificationString = readStringFromFile(verificationsFile); + assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); + } } From 093d786329bdd1ae38177dbf49b26677622fecc1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 21:27:22 +0100 Subject: [PATCH 0555/1204] Doc: Add section about indirect data types --- docs/source/pgpainless-cli/usage.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/source/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md index 967bfc11..2045de6b 100644 --- a/docs/source/pgpainless-cli/usage.md +++ b/docs/source/pgpainless-cli/usage.md @@ -84,8 +84,24 @@ Exit Codes: 61 Input file does not exist 67 Cannot unlock password protected secret key 69 Unsupported subcommand - 71 Unsupported special prefix (e.g. "@env/@fd") of indirect parameter + 71 Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter 73 Ambiguous input (a filename matching the designator already exists) 79 Key is not signing capable Powered by picocli -``` \ No newline at end of file +``` + +## Indirect Data Types + +Some commands take options whose arguments are indirect data types. Those are arguments which are not used directly, +but instead they point to a place where the argument value can be sourced from, such as a file, an environment variable +or a file descriptor. + +It is important to keep in mind, that options like `--with-password` or `--with-key-password` are examples for such +indirect data types. If you want to unlock a key whose password is `sw0rdf1sh`, you *cannot* provide the password +like `--with-key-password sw0rdf1sh`, but instead you have to either write out the password into a file and provide +the file's path (e.g. `--with-key-password /path/to/file`), store the password in an environment variable and pass that +(e.g. `--with-key-password @ENV:myvar`), or provide a numbered file descriptor from which the password can be read +(e.g. `--with-key-password @FD:4`). + +Note, that environment variables and file descriptors can only be used to pass input data to the program. +For output parameters (e.g. `--verifications-out`) only file paths are allowed. From b7478729b13b8792b34aa64cd144c64ceeab78d1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 21:47:46 +0100 Subject: [PATCH 0556/1204] Update man pages Since commit 0e777de14f927ad4b265adfc5ba201be29b1bde1 in pgpainless/sop-java, man page generation is reproducible. This very commit adopts reproducible man pages for the first time --- pgpainless-cli/packaging/man/pgpainless-cli-armor.1 | 4 +--- pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 | 4 +--- pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 | 10 ++-------- pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 | 10 ++-------- .../packaging/man/pgpainless-cli-extract-cert.1 | 4 +--- .../packaging/man/pgpainless-cli-generate-completion.1 | 7 +++---- .../packaging/man/pgpainless-cli-generate-key.1 | 10 ++-------- pgpainless-cli/packaging/man/pgpainless-cli-help.1 | 7 +++---- .../packaging/man/pgpainless-cli-inline-detach.1 | 4 +--- .../packaging/man/pgpainless-cli-inline-sign.1 | 10 ++-------- .../packaging/man/pgpainless-cli-inline-verify.1 | 10 ++-------- pgpainless-cli/packaging/man/pgpainless-cli-sign.1 | 10 ++-------- pgpainless-cli/packaging/man/pgpainless-cli-verify.1 | 4 +--- pgpainless-cli/packaging/man/pgpainless-cli-version.1 | 4 +--- pgpainless-cli/packaging/man/pgpainless-cli.1 | 7 +++---- 15 files changed, 27 insertions(+), 78 deletions(-) diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 index d85b6bf9..c34cf923 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-armor .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-ARMOR" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-ARMOR" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -43,5 +42,4 @@ Label to be used in the header and tail of the armoring .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 b/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 index 98fae5ea..19ef25f1 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-dearmor.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-dearmor .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-DEARMOR" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-DEARMOR" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -38,5 +37,4 @@ pgpainless\-cli\-dearmor \- Remove ASCII Armor from standard input .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 index 400adcff..17d59134 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-decrypt .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-DECRYPT" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-DECRYPT" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -65,12 +64,7 @@ Defaults to beginning of time (\(aq\-\(aq). Can be used to learn the session key on successful decryption .RE .sp -\fB\-\-stacktrace\fP -.RS 4 -Print Stacktrace -.RE -.sp -\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP +\fB\-\-stacktrace\fP, \fB\-\-verify\-out, \-\-verifications\-out\fP=\fIVERIFICATIONS\fP .RS 4 Emits signature verification status to the designated output .RE diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 index 5cb9495b..f1d804d0 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-encrypt .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-ENCRYPT" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-ENCRYPT" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -53,12 +52,7 @@ ASCII armor the output Sign the output with a private key .RE .sp -\fB\-\-stacktrace\fP -.RS 4 -Print Stacktrace -.RE -.sp -\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP .RS 4 Passphrase to unlock the secret key(s). .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 b/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 index 0482c183..dcaf6e71 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-extract-cert.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-extract-cert .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-EXTRACT\-CERT" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-EXTRACT\-CERT" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -43,5 +42,4 @@ ASCII armor the output .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 index 637b1231..5ab3d673 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-generate-completion .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: generate-completion 4.6.3 .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-GENERATE\-COMPLETION" "1" "2022-11-06" "generate\-completion 4.6.3" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-GENERATE\-COMPLETION" "1" "" "generate\-completion 4.6.3" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -51,7 +50,7 @@ Show this help message and exit. .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace +Print stacktrace .RE .sp \fB\-V\fP, \fB\-\-version\fP @@ -142,7 +141,7 @@ Unsupported subcommand .sp \fB71\fP .RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter .RE .sp \fB73\fP diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 index ef950df9..96b069f3 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-generate-key .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-GENERATE\-KEY" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-GENERATE\-KEY" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -42,12 +41,7 @@ pgpainless\-cli\-generate\-key \- Generate a secret key ASCII armor the output .RE .sp -\fB\-\-stacktrace\fP -.RS 4 -Print Stacktrace -.RE -.sp -\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP .RS 4 Password to protect the private key with .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-help.1 b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 index 67ef1ce6..6152fc87 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-help.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-help .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-HELP" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-HELP" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -45,7 +44,7 @@ Show usage help for the help command and exit. .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace +Print stacktrace .RE .SH "ARGUMENTS" .sp @@ -137,7 +136,7 @@ Unsupported subcommand .sp \fB71\fP .RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter .RE .sp \fB73\fP diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 index 3d356293..c5d9d983 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-detach.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-inline-detach .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-INLINE\-DETACH" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-INLINE\-DETACH" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -48,5 +47,4 @@ Destination to which a detached signatures block will be written .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 index 3f653b40..7deb568c 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-inline-sign .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-INLINE\-SIGN" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-INLINE\-SIGN" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -56,12 +55,7 @@ If \(aq\-\-as=text\(aq and the input data is not valid UTF\-8, inline\-sign fail ASCII armor the output .RE .sp -\fB\-\-stacktrace\fP -.RS 4 -Print Stacktrace -.RE -.sp -\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP .RS 4 Passphrase to unlock the secret key(s). .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 index 73f8316d..d97f274d 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-verify.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-inline-verify .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-INLINE\-VERIFY" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-INLINE\-VERIFY" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -57,12 +56,7 @@ Reject signatures with a creation date not in range. Defaults to beginning of time ("\-"). .RE .sp -\fB\-\-stacktrace\fP -.RS 4 -Print Stacktrace -.RE -.sp -\fB\-\-verifications\-out\fP=\fI\fP +\fB\-\-stacktrace\fP, \fB\-\-verifications\-out\fP=\fI\fP .RS 4 File to write details over successful verifications to .RE diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 index 3c63dccf..6519e0ec 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-sign .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-SIGN" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-SIGN" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -56,12 +55,7 @@ Emits the digest algorithm used to the specified file in a way that can be used ASCII armor the output .RE .sp -\fB\-\-stacktrace\fP -.RS 4 -Print Stacktrace -.RE -.sp -\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP +\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP .RS 4 Passphrase to unlock the secret key(s). .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 index a07ef2d7..5cf0020c 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-verify .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-VERIFY" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-VERIFY" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -48,7 +47,6 @@ Defaults to beginning of time ("\-"). .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace .RE .SH "ARGUMENTS" .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 index 9e80f4b3..003e549f 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli-version .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI\-VERSION" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI\-VERSION" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -48,5 +47,4 @@ Print an extended version string .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli.1 b/pgpainless-cli/packaging/man/pgpainless-cli.1 index 14bd9fb9..686f728f 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli.1 @@ -2,12 +2,11 @@ .\" Title: pgpainless-cli .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.10 -.\" Date: 2022-11-06 .\" Manual: PGPainless-CLI Manual .\" Source: .\" Language: English .\" -.TH "PGPAINLESS\-CLI" "1" "2022-11-06" "" "PGPainless\-CLI Manual" +.TH "PGPAINLESS\-CLI" "1" "" "" "PGPainless\-CLI Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -38,7 +37,7 @@ pgpainless\-cli \- Stateless OpenPGP Protocol .sp \fB\-\-stacktrace\fP .RS 4 -Print Stacktrace +Print stacktrace .RE .SH "COMMANDS" .sp @@ -195,7 +194,7 @@ Unsupported subcommand .sp \fB71\fP .RS 4 -Unsupported special prefix (e.g. "@env/@fd") of indirect parameter +Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter .RE .sp \fB73\fP From bfeed54ede095bfa70b394340edf21b277bce7ee Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 21:52:02 +0100 Subject: [PATCH 0557/1204] Docs: Update output of pgpainless-cli help command --- docs/source/pgpainless-cli/usage.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/source/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md index 2045de6b..9d3f6d93 100644 --- a/docs/source/pgpainless-cli/usage.md +++ b/docs/source/pgpainless-cli/usage.md @@ -49,7 +49,10 @@ Hereafter, the program will be referred to as `pgpainless-cli`. ``` $ pgpainless-cli help Stateless OpenPGP Protocol -Usage: pgpainless-cli [COMMAND] +Usage: pgpainless-cli [--stacktrace] [COMMAND] + +Options: + --stacktrace Print Stacktrace Commands: help Display usage information for the specified subcommand @@ -68,7 +71,7 @@ Commands: version Display version information about the tool Exit Codes: - 0 Successful program execution. + 0 Successful program execution 1 Generic program error 3 Verification requested but no verifiable signature found 13 Unsupported asymmetric algorithm @@ -87,7 +90,6 @@ Exit Codes: 71 Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter 73 Ambiguous input (a filename matching the designator already exists) 79 Key is not signing capable -Powered by picocli ``` ## Indirect Data Types From 38a2162d6a2db64fe21f81ea9155f4e55e1ead96 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 22:07:20 +0100 Subject: [PATCH 0558/1204] Docs: Document generation/updating of man pages --- docs/source/pgpainless-cli/usage.md | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/source/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md index 9d3f6d93..dd56c8b1 100644 --- a/docs/source/pgpainless-cli/usage.md +++ b/docs/source/pgpainless-cli/usage.md @@ -42,6 +42,36 @@ $ gradle installDist Afterwards, an uncompressed distributable is installed in `build/install/`. To execute the application, you can call `build/install/bin/pgpainless-cli{.bat}` +Building / updating man pages is a two-step process. +The contents of the man pages is largely defined by the `sop-java-picocli` source code. + +In order to generate a fresh set of man pages from the `sop-java-picocli` source, you need to clone that repository +next to the `pgpainless` repository: +```shell +$ ls +pgpainless +$ git clone https://github.com/pgpainless/sop-java.git +$ ls +pgpainless sop-java +``` + +Next, you need to execute the `asciiDoctor` gradle task inside the sop-java repository: +```shell +$ cd sop-java +$ gradle asciiDoctor +``` + +This will generate generic sop manpages in `sop-java-picocli/build/docs/manpage/`. + +Next, you need to execute a script for converting the `sop` manpages to fit the `pgpainless-cli` command with the help +of a script in the `pgpainless` repository: +```shell +$ cd ../pgpainless/pgpainless-cli +$ ./rewriteManPages.sh +``` + +The resulting updated man pages are placed in `packaging/man/`. + ## Usage Hereafter, the program will be referred to as `pgpainless-cli`. From 936a3f654fa13886e3f625cd538d2e5dd2fd9d80 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 22:28:18 +0100 Subject: [PATCH 0559/1204] Docs: Add usage examples for pgpainless-cli --- docs/source/pgpainless-cli/usage.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/source/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md index dd56c8b1..4bc1d166 100644 --- a/docs/source/pgpainless-cli/usage.md +++ b/docs/source/pgpainless-cli/usage.md @@ -122,6 +122,24 @@ Exit Codes: 79 Key is not signing capable ``` +To get help on a subcommand, e.g. `encrypt`, just call the help subcommand followed by the subcommand you +are interested in (e.g. `pgpainless-cli help encrypt`). + +## Examples +```shell +$ # Generate a key +$ pgpainless-cli generate-key "Alice " > key.asc +$ # Extract a certificate from a key +$ cat key.asc | pgpainless-cli extract-cert > cert.asc +$ # Create an encrypted signed message +$ echo "Hello, World!" | pgpainless-cli encrypt cert.asc --sign-with key.asc > msg.asc +$ # Decrypt an encrypted message and verify the signature +$ cat msg.asc | pgpainless-cli decrypt key.asc --verify-with cert.asc --verifications-out verifications.txt +Hello, World! +$ cat verifications.txt +2022-11-15T21:25:48Z 4FF67C69150209ED8139DE22578CB2FABD5D7897 9000235358B8CEA6A368EC86DE56DC2D942ACAA4 +``` + ## Indirect Data Types Some commands take options whose arguments are indirect data types. Those are arguments which are not used directly, From 847d4b5e3327772bd67de33e9776f0c128591453 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 16:05:29 +0100 Subject: [PATCH 0560/1204] Update SECURITY.md --- SECURITY.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 59e9adbf..fad439b5 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,11 +12,12 @@ SPDX-License-Identifier: Apache-2.0 Use this section to tell people about which versions of your project are currently being supported with security updates. -| Version | Supported | -|---------| ------------------ | -| 1.1.X | :white_check_mark: | -| 1.0.X | :white_check_mark: | -| < 1.0.0 | :x: | +| Version | Supported | +|----------|--------------------| +| 1.4.X-rc | :white_check_mark: | +| 1.3.X | :white_check_mark: | +| 1.2.X | :white_check_mark: | +| < 1.2.0 | :x: | ## Reporting a Vulnerability From 03d04fb324672c1532488a7faba62c1a17007dc6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 16:29:24 +0100 Subject: [PATCH 0561/1204] Tests: Replace usages of default algorithm policies with specific policies --- .../encryption_signing/EncryptDecryptTest.java | 4 ++-- .../org/pgpainless/example/ManagePolicy.java | 17 +++++++++-------- .../generation/GeneratingWeakKeyThrowsTest.java | 10 ++++++---- .../modification/RefuseToAddWeakSubkeyTest.java | 4 ++-- .../java/org/pgpainless/policy/PolicyTest.java | 2 +- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java index 88a2c38b..216d0c65 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java @@ -66,9 +66,9 @@ public class EncryptDecryptTest { @BeforeEach public void setDefaultPolicy() { PGPainless.getPolicy().setSymmetricKeyEncryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyEncryptionAlgorithmPolicy()); + Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022()); PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyDecryptionAlgorithmPolicy()); + Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022()); } @TestTemplate diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java index fa2759dc..711de225 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java @@ -44,22 +44,22 @@ public class ManagePolicy { public void resetPolicy() { // Policy for hash algorithms in non-revocation signatures PGPainless.getPolicy().setSignatureHashAlgorithmPolicy( - Policy.HashAlgorithmPolicy.defaultSignatureAlgorithmPolicy()); + Policy.HashAlgorithmPolicy.static2022SignatureHashAlgorithmPolicy()); // Policy for hash algorithms in revocation signatures PGPainless.getPolicy().setRevocationSignatureHashAlgorithmPolicy( - Policy.HashAlgorithmPolicy.defaultRevocationSignatureHashAlgorithmPolicy()); + Policy.HashAlgorithmPolicy.static2022RevocationSignatureHashAlgorithmPolicy()); // Policy for public key algorithms and bit lengths PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( - Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); // Policy for acceptable symmetric encryption algorithms when decrypting messages PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyDecryptionAlgorithmPolicy()); + Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022()); // Policy for acceptable symmetric encryption algorithms when encrypting messages PGPainless.getPolicy().setSymmetricKeyEncryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyEncryptionAlgorithmPolicy()); + Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022()); // Policy for acceptable compression algorithms PGPainless.getPolicy().setCompressionAlgorithmPolicy( - Policy.CompressionAlgorithmPolicy.defaultCompressionAlgorithmPolicy()); + Policy.CompressionAlgorithmPolicy.anyCompressionAlgorithmPolicy()); // Known notations PGPainless.getPolicy().getNotationRegistry().clear(); } @@ -73,7 +73,7 @@ public class ManagePolicy { * * Per default, PGPainless will reject non-revocation signatures that use SHA-1 as hash algorithm. * To inspect PGPainless' default signature hash algorithm policy, see - * {@link Policy.HashAlgorithmPolicy#defaultSignatureAlgorithmPolicy()}. + * {@link Policy.HashAlgorithmPolicy#static2022SignatureHashAlgorithmPolicy()}. * * Since it may be a valid use-case to accept signatures made using SHA-1 as part of a less strict policy, * this example demonstrates how to set a custom signature hash algorithm policy. @@ -108,7 +108,8 @@ public class ManagePolicy { /** * Similar to hash algorithms, {@link PublicKeyAlgorithm PublicKeyAlgorithms} tend to get outdated eventually. * Per default, PGPainless will reject signatures made by keys of unacceptable algorithm or length. - * See {@link Policy.PublicKeyAlgorithmPolicy#defaultPublicKeyAlgorithmPolicy()} to inspect PGPainless' defaults. + * See {@link Policy.PublicKeyAlgorithmPolicy#bsi2021PublicKeyAlgorithmPolicy()} + * to inspect PGPainless' defaults. * * This example demonstrates how to set a custom public key algorithm policy. */ diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GeneratingWeakKeyThrowsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GeneratingWeakKeyThrowsTest.java index aa79add5..6fbf0572 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GeneratingWeakKeyThrowsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GeneratingWeakKeyThrowsTest.java @@ -26,7 +26,7 @@ public class GeneratingWeakKeyThrowsTest { public void refuseToGenerateWeakPrimaryKeyTest() { // ensure we have default public key algorithm policy set PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( - Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); assertThrows(IllegalArgumentException.class, () -> PGPainless.buildKeyRing() @@ -38,7 +38,7 @@ public class GeneratingWeakKeyThrowsTest { public void refuseToAddWeakSubkeyDuringGenerationTest() { // ensure we have default public key algorithm policy set PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( - Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); KeyRingBuilder kb = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), @@ -50,7 +50,8 @@ public class GeneratingWeakKeyThrowsTest { } @Test - public void allowToAddWeakKeysWithWeakPolicy() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void allowToAddWeakKeysWithWeakPolicy() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { // set a weak algorithm policy Map bitStrengths = new HashMap<>(); bitStrengths.put(PublicKeyAlgorithm.RSA_GENERAL, 512); @@ -67,6 +68,7 @@ public class GeneratingWeakKeyThrowsTest { .build(); // reset public key algorithm policy - PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( + Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java index 2570837f..73c5953c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java @@ -34,7 +34,7 @@ public class RefuseToAddWeakSubkeyTest { public void testEditorRefusesToAddWeakSubkey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { // ensure default policy is set - PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .modernKeyRing("Alice"); @@ -84,6 +84,6 @@ public class RefuseToAddWeakSubkeyTest { assertEquals(2, PGPainless.inspectKeyRing(secretKeys).getEncryptionSubkeys(EncryptionPurpose.ANY).size()); // reset default policy - PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java index 4e3cb51e..aa7078e4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java @@ -56,7 +56,7 @@ public class PolicyTest { policy.setRevocationSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, revHashAlgoMap)); - policy.setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); + policy.setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); } @Test From e976cc6dd244ce61b165984a46f24f1f061e55bc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Nov 2022 16:46:17 +0100 Subject: [PATCH 0562/1204] Move getResult() method around --- .../OpenPgpMessageInputStream.java | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 65c8dad5..a73b5154 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -799,6 +799,33 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return new MessageMetadata((MessageMetadata.Message) metadata); } + @Override + public OpenPgpMetadata getResult() { + MessageMetadata m = getMetadata(); + resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); + resultBuilder.setModificationDate(m.getModificationDate()); + resultBuilder.setFileName(m.getFilename()); + resultBuilder.setFileEncoding(m.getFormat()); + resultBuilder.setSessionKey(m.getSessionKey()); + resultBuilder.setDecryptionKey(m.getDecryptionKey()); + + for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { + resultBuilder.addVerifiedDetachedSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { + resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { + resultBuilder.addVerifiedInbandSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { + resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + return resultBuilder.build(); + } + private static class SortedESKs { private final List skesks = new ArrayList<>(); @@ -832,33 +859,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } } - @Override - public OpenPgpMetadata getResult() { - MessageMetadata m = getMetadata(); - resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); - resultBuilder.setModificationDate(m.getModificationDate()); - resultBuilder.setFileName(m.getFilename()); - resultBuilder.setFileEncoding(m.getFormat()); - resultBuilder.setSessionKey(m.getSessionKey()); - resultBuilder.setDecryptionKey(m.getDecryptionKey()); - - for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { - resultBuilder.addVerifiedDetachedSignature(accepted); - } - for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { - resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { - resultBuilder.addVerifiedInbandSignature(accepted); - } - for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { - resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - return resultBuilder.build(); - } - // In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. From 3023d532e3a36e793538380915b7e0b81716fa5d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 15:32:15 +0100 Subject: [PATCH 0563/1204] Make DecryptionStream.getMetadata() first-class, deprecate getResult() --- .../CloseForResultInputStream.java | 39 ------------------- .../DecryptionStream.java | 12 ++++-- .../MessageMetadata.java | 35 +++++++++++++++++ .../OpenPgpMessageInputStream.java | 33 ++-------------- 4 files changed, 46 insertions(+), 73 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CloseForResultInputStream.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CloseForResultInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CloseForResultInputStream.java deleted file mode 100644 index ba895a08..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CloseForResultInputStream.java +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nonnull; - -public abstract class CloseForResultInputStream extends InputStream { - - protected final OpenPgpMetadata.Builder resultBuilder; - private boolean isClosed = false; - - public CloseForResultInputStream(@Nonnull OpenPgpMetadata.Builder resultBuilder) { - this.resultBuilder = resultBuilder; - } - - @Override - public void close() throws IOException { - this.isClosed = true; - } - - /** - * Return the result of the decryption. - * The result contains metadata about the decryption, such as signatures, used keys and algorithms, as well as information - * about the decrypted file/stream. - * - * Can only be obtained once the stream got successfully closed ({@link #close()}). - * @return metadata - */ - public OpenPgpMetadata getResult() { - if (!isClosed) { - throw new IllegalStateException("Stream MUST be closed before the result can be accessed."); - } - return resultBuilder.build(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index 0f221ad7..e3a51bb9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -4,10 +4,14 @@ package org.pgpainless.decryption_verification; -import javax.annotation.Nonnull; +import java.io.InputStream; -public abstract class DecryptionStream extends CloseForResultInputStream { - public DecryptionStream(@Nonnull OpenPgpMetadata.Builder resultBuilder) { - super(resultBuilder); +public abstract class DecryptionStream extends InputStream { + + public abstract MessageMetadata getMetadata(); + + @Deprecated + public OpenPgpMetadata getResult() { + return getMetadata().toLegacyMetadata(); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 7811578e..26439b40 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -300,9 +300,16 @@ public class MessageMetadata { public static class Message extends Layer { + protected boolean cleartextSigned; + public Message() { super(0); } + + public boolean isCleartextSigned() { + return cleartextSigned; + } + } public static class LiteralData implements Nested { @@ -453,4 +460,32 @@ public class MessageMetadata { abstract O getProperty(Layer last); } + public OpenPgpMetadata toLegacyMetadata() { + OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); + resultBuilder.setCompressionAlgorithm(getCompressionAlgorithm()); + resultBuilder.setModificationDate(getModificationDate()); + resultBuilder.setFileName(getFilename()); + resultBuilder.setFileEncoding(getFormat()); + resultBuilder.setSessionKey(getSessionKey()); + resultBuilder.setDecryptionKey(getDecryptionKey()); + + for (SignatureVerification accepted : getVerifiedDetachedSignatures()) { + resultBuilder.addVerifiedDetachedSignature(accepted); + } + for (SignatureVerification.Failure rejected : getRejectedDetachedSignatures()) { + resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + for (SignatureVerification accepted : getVerifiedInlineSignatures()) { + resultBuilder.addVerifiedInbandSignature(accepted); + } + for (SignatureVerification.Failure rejected : getRejectedInlineSignatures()) { + resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + if (message.isCleartextSigned()) { + resultBuilder.setCleartextSigned(); + } + + return resultBuilder.build(); + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index a73b5154..a820dca5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -156,6 +156,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { if (openPgpIn.isAsciiArmored()) { ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); if (armorIn.isClearText()) { + ((MessageMetadata.Message) metadata).cleartextSigned = true; return new OpenPgpMessageInputStream(Type.cleartext_signed, armorIn, options, metadata, policy); } else { @@ -173,7 +174,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Nonnull MessageMetadata.Layer metadata, @Nonnull Policy policy) throws PGPException, IOException { - super(OpenPgpMetadata.getBuilder()); + super(); this.policy = policy; this.options = options; @@ -203,7 +204,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { @Nonnull ConsumerOptions options, @Nonnull MessageMetadata.Layer metadata, @Nonnull Policy policy) throws PGPException, IOException { - super(OpenPgpMetadata.getBuilder()); + super(); this.policy = policy; this.options = options; this.metadata = metadata; @@ -226,7 +227,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { // Cleartext Signature Framework (probably signed message) case cleartext_signed: - resultBuilder.setCleartextSigned(); MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); PGPSignatureList detachedSignatures = ClearsignedMessageUtil .detachSignaturesFromInbandClearsignedMessage( @@ -799,33 +799,6 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return new MessageMetadata((MessageMetadata.Message) metadata); } - @Override - public OpenPgpMetadata getResult() { - MessageMetadata m = getMetadata(); - resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); - resultBuilder.setModificationDate(m.getModificationDate()); - resultBuilder.setFileName(m.getFilename()); - resultBuilder.setFileEncoding(m.getFormat()); - resultBuilder.setSessionKey(m.getSessionKey()); - resultBuilder.setDecryptionKey(m.getDecryptionKey()); - - for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { - resultBuilder.addVerifiedDetachedSignature(accepted); - } - for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { - resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { - resultBuilder.addVerifiedInbandSignature(accepted); - } - for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { - resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - return resultBuilder.build(); - } - private static class SortedESKs { private final List skesks = new ArrayList<>(); From 33d9a784bb34da7b68200cfc4d3714fcf13aed03 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 17:20:43 +0100 Subject: [PATCH 0564/1204] Add javadoc to MEssageMetadata class --- .../MessageMetadata.java | 368 +++++++++++++++--- 1 file changed, 312 insertions(+), 56 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 26439b40..1bf22b47 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -19,6 +19,9 @@ import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; +/** + * View for extracting metadata about a {@link Message}. + */ public class MessageMetadata { protected Message message; @@ -27,6 +30,57 @@ public class MessageMetadata { this.message = message; } + /** + * Convert this {@link MessageMetadata} object into a legacy {@link OpenPgpMetadata} object. + * This method is intended to be used for a transition period between the 1.3 / 1.4+ branches. + * TODO: Remove in 1.5.X + * + * @return converted {@link OpenPgpMetadata} object + */ + public @Nonnull OpenPgpMetadata toLegacyMetadata() { + OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); + resultBuilder.setCompressionAlgorithm(getCompressionAlgorithm()); + Date modDate = getModificationDate(); + if (modDate != null) { + resultBuilder.setModificationDate(modDate); + } + String fileName = getFilename(); + if (fileName != null) { + resultBuilder.setFileName(fileName); + } + StreamEncoding encoding = getFormat(); + if (encoding != null) { + resultBuilder.setFileEncoding(encoding); + } + resultBuilder.setSessionKey(getSessionKey()); + resultBuilder.setDecryptionKey(getDecryptionKey()); + + for (SignatureVerification accepted : getVerifiedDetachedSignatures()) { + resultBuilder.addVerifiedDetachedSignature(accepted); + } + for (SignatureVerification.Failure rejected : getRejectedDetachedSignatures()) { + resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + for (SignatureVerification accepted : getVerifiedInlineSignatures()) { + resultBuilder.addVerifiedInbandSignature(accepted); + } + for (SignatureVerification.Failure rejected : getRejectedInlineSignatures()) { + resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + if (message.isCleartextSigned()) { + resultBuilder.setCleartextSigned(); + } + + return resultBuilder.build(); + } + + /** + * Return the {@link SymmetricKeyAlgorithm} of the outermost encrypted data packet, or null if message is + * unencrypted. + * + * @return encryption algorithm + */ public @Nullable SymmetricKeyAlgorithm getEncryptionAlgorithm() { Iterator algorithms = getEncryptionAlgorithms(); if (algorithms.hasNext()) { @@ -35,6 +89,14 @@ public class MessageMetadata { return null; } + /** + * Return an {@link Iterator} of {@link SymmetricKeyAlgorithm SymmetricKeyAlgorithms} encountered in the message. + * The first item returned by the iterator is the algorithm of the outermost encrypted data packet, the next item + * that of the next nested encrypted data packet and so on. + * The iterator might also be empty, in case of an unencrypted message. + * + * @return iterator of symmetric encryption algorithms + */ public @Nonnull Iterator getEncryptionAlgorithms() { return new LayerIterator(message) { @Override @@ -49,6 +111,12 @@ public class MessageMetadata { }; } + /** + * Return the {@link CompressionAlgorithm} of the outermost compressed data packet, or null, if the message + * does not contain any compressed data packets. + * + * @return compression algorithm + */ public @Nullable CompressionAlgorithm getCompressionAlgorithm() { Iterator algorithms = getCompressionAlgorithms(); if (algorithms.hasNext()) { @@ -57,6 +125,14 @@ public class MessageMetadata { return null; } + /** + * Return an {@link Iterator} of {@link CompressionAlgorithm CompressionAlgorithms} encountered in the message. + * The first item returned by the iterator is the algorithm of the outermost compressed data packet, the next + * item that of the next nested compressed data packet and so on. + * The iterator might also be empty, in case of a message without any compressed data packets. + * + * @return iterator of compression algorithms + */ public @Nonnull Iterator getCompressionAlgorithms() { return new LayerIterator(message) { @Override @@ -71,6 +147,12 @@ public class MessageMetadata { }; } + /** + * Return the {@link SessionKey} of the outermost encrypted data packet. + * If the message was unencrypted, this method returns
null
. + * + * @return session key of the message + */ public @Nullable SessionKey getSessionKey() { Iterator sessionKeys = getSessionKeys(); if (sessionKeys.hasNext()) { @@ -79,6 +161,14 @@ public class MessageMetadata { return null; } + /** + * Return an {@link Iterator} of {@link SessionKey SessionKeys} for all encrypted data packets in the message. + * The first item returned by the iterator is the session key of the outermost encrypted data packet, + * the next item that of the next nested encrypted data packet and so on. + * The iterator might also be empty, in case of an unencrypted message. + * + * @return iterator of session keys + */ public @Nonnull Iterator getSessionKeys() { return new LayerIterator(message) { @Override @@ -93,14 +183,31 @@ public class MessageMetadata { }; } - public List getVerifiedDetachedSignatures() { - return new ArrayList<>(message.verifiedDetachedSignatures); + /** + * Return a list of all verified detached signatures. + * This list contains all acceptable, correct detached signatures. + * + * @return verified detached signatures + */ + public @Nonnull List getVerifiedDetachedSignatures() { + return message.getVerifiedDetachedSignatures(); } - public List getRejectedDetachedSignatures() { - return new ArrayList<>(message.rejectedDetachedSignatures); + /** + * Return a list of all rejected detached signatures. + * + * @return rejected detached signatures + */ + public @Nonnull List getRejectedDetachedSignatures() { + return message.getRejectedDetachedSignatures(); } + /** + * Return a list of all verified inline-signatures. + * This list contains all acceptable, correct signatures that were part of the message itself. + * + * @return verified inline signatures + */ public @Nonnull List getVerifiedInlineSignatures() { List verifications = new ArrayList<>(); Iterator> verificationsByLayer = getVerifiedInlineSignaturesByLayer(); @@ -110,6 +217,16 @@ public class MessageMetadata { return verifications; } + /** + * Return an {@link Iterator} of {@link List Lists} of verified inline-signatures of the message. + * Since signatures might occur in different layers within a message, this method can be used to gain more detailed + * insights into what signatures were encountered at what layers of the message structure. + * Each item of the {@link Iterator} represents a layer of the message and contains only signatures from + * this layer. + * An empty list means no (or no acceptable) signatures were encountered in that layer. + * + * @return iterator of lists of signatures by-layer. + */ public @Nonnull Iterator> getVerifiedInlineSignaturesByLayer() { return new LayerIterator>(message) { @Override @@ -132,6 +249,11 @@ public class MessageMetadata { }; } + /** + * Return a list of all rejected inline-signatures of the message. + * + * @return list of rejected inline-signatures + */ public @Nonnull List getRejectedInlineSignatures() { List rejected = new ArrayList<>(); Iterator> rejectedByLayer = getRejectedInlineSignaturesByLayer(); @@ -141,6 +263,12 @@ public class MessageMetadata { return rejected; } + /** + * Similar to {@link #getVerifiedInlineSignaturesByLayer()}, this method returns all rejected inline-signatures + * of the message, but organized by layer. + * + * @return rejected inline-signatures by-layer + */ public @Nonnull Iterator> getRejectedInlineSignaturesByLayer() { return new LayerIterator>(message) { @Override @@ -163,7 +291,16 @@ public class MessageMetadata { }; } - public String getFilename() { + /** + * Return the value of the literal data packet's filename field. + * This value can be used to store a decrypted file under its original filename, + * but since this field is not necessarily part of the signed data of a message, usage of this field is + * discouraged. + * + * @return filename + * @see RFC4880 §5.9. Literal Data Packet + */ + public @Nullable String getFilename() { LiteralData literalData = findLiteralData(); if (literalData == null) { return null; @@ -171,7 +308,15 @@ public class MessageMetadata { return literalData.getFileName(); } - public Date getModificationDate() { + /** + * Return the value of the literal data packets modification date field. + * This value can be used to restore the modification date of a decrypted file, + * but since this field is not necessarily part of the signed data, its use is discouraged. + * + * @return modification date + * @see RFC4880 §5.9. Literal Data Packet + */ + public @Nullable Date getModificationDate() { LiteralData literalData = findLiteralData(); if (literalData == null) { return null; @@ -179,7 +324,15 @@ public class MessageMetadata { return literalData.getModificationDate(); } - public StreamEncoding getFormat() { + /** + * Return the value of the format field of the literal data packet. + * This value indicates what format (text, binary data, ...) the data has. + * Since this field is not necessarily part of the signed data of a message, its usage is discouraged. + * + * @return format + * @see RFC4880 §5.9. Literal Data Packet + */ + public @Nullable StreamEncoding getFormat() { LiteralData literalData = findLiteralData(); if (literalData == null) { return null; @@ -187,19 +340,33 @@ public class MessageMetadata { return literalData.getFormat(); } - private LiteralData findLiteralData() { - Nested nested = message.child; + /** + * Find the {@link LiteralData} layer of an OpenPGP message. + * Usually, every message has a literal data packet, but for malformed messages this method might still + * return
null
. + * + * @return literal data + */ + private @Nullable LiteralData findLiteralData() { + Nested nested = message.getChild(); if (nested == null) { return null; } - while (nested.hasNestedChild()) { + while (nested != null && nested.hasNestedChild()) { Layer layer = (Layer) nested; - nested = layer.child; + nested = layer.getChild(); } return (LiteralData) nested; } + /** + * Return the {@link SubkeyIdentifier} of the decryption key that was used to decrypt the outermost encryption + * layer. + * If the message was unencrypted, this might return
null
. + * + * @return decryption key + */ public SubkeyIdentifier getDecryptionKey() { Iterator iterator = new LayerIterator(message) { @Override @@ -236,58 +403,130 @@ public class MessageMetadata { } } - public Nested getChild() { + /** + * Return the nested child element of this layer. + * Might return
null
, if this layer does not have a child element + * (e.g. if this is a {@link LiteralData} packet). + * + * @return child element + */ + public @Nullable Nested getChild() { return child; } - public void setChild(Nested child) { + /** + * Set the nested child element for this layer. + * + * @param child child element + */ + void setChild(Nested child) { this.child = child; } + /** + * Return a list of all verified detached signatures of this layer. + * + * @return all verified detached signatures of this layer + */ public List getVerifiedDetachedSignatures() { return new ArrayList<>(verifiedDetachedSignatures); } + /** + * Return a list of all rejected detached signatures of this layer. + * + * @return all rejected detached signatures of this layer + */ public List getRejectedDetachedSignatures() { return new ArrayList<>(rejectedDetachedSignatures); } + /** + * Add a verified detached signature for this layer. + * + * @param signatureVerification verified detached signature + */ void addVerifiedDetachedSignature(SignatureVerification signatureVerification) { verifiedDetachedSignatures.add(signatureVerification); } + /** + * Add a rejected detached signature for this layer. + * + * @param failure rejected detached signature + */ void addRejectedDetachedSignature(SignatureVerification.Failure failure) { rejectedDetachedSignatures.add(failure); } + /** + * Return a list of all verified one-pass-signatures of this layer. + * + * @return all verified one-pass-signatures of this layer + */ public List getVerifiedOnePassSignatures() { return new ArrayList<>(verifiedOnePassSignatures); } + /** + * Return a list of all rejected one-pass-signatures of this layer. + * + * @return all rejected one-pass-signatures of this layer + */ public List getRejectedOnePassSignatures() { return new ArrayList<>(rejectedOnePassSignatures); } + /** + * Add a verified one-pass-signature for this layer. + * + * @param verifiedOnePassSignature verified one-pass-signature for this layer + */ void addVerifiedOnePassSignature(SignatureVerification verifiedOnePassSignature) { this.verifiedOnePassSignatures.add(verifiedOnePassSignature); } + /** + * Add a rejected one-pass-signature for this layer. + * + * @param rejected rejected one-pass-signature for this layer + */ void addRejectedOnePassSignature(SignatureVerification.Failure rejected) { this.rejectedOnePassSignatures.add(rejected); } + /** + * Return a list of all verified prepended signatures of this layer. + * + * @return all verified prepended signatures of this layer + */ public List getVerifiedPrependedSignatures() { return new ArrayList<>(verifiedPrependedSignatures); } + /** + * Return a list of all rejected prepended signatures of this layer. + * + * @return all rejected prepended signatures of this layer + */ public List getRejectedPrependedSignatures() { return new ArrayList<>(rejectedPrependedSignatures); } + /** + * Add a verified prepended signature for this layer. + * + * @param verified verified prepended signature + */ void addVerifiedPrependedSignature(SignatureVerification verified) { this.verifiedPrependedSignatures.add(verified); } + /** + * Add a rejected prepended signature for this layer. + * + * @param rejected rejected prepended signature + */ void addRejectedPrependedSignature(SignatureVerification.Failure rejected) { this.rejectedPrependedSignatures.add(rejected); } @@ -306,6 +545,12 @@ public class MessageMetadata { super(0); } + /** + * Returns true, is the message is a signed message using the cleartext signature framework. + * + * @return
true
if message is cleartext-signed,
false
otherwise + * @see RFC4880 §7. Cleartext Signature Framework + */ public boolean isCleartextSigned() { return cleartextSigned; } @@ -321,26 +566,46 @@ public class MessageMetadata { this("", new Date(0L), StreamEncoding.BINARY); } - public LiteralData(String fileName, Date modificationDate, StreamEncoding format) { + public LiteralData(@Nonnull String fileName, + @Nonnull Date modificationDate, + @Nonnull StreamEncoding format) { this.fileName = fileName; this.modificationDate = modificationDate; this.format = format; } - public String getFileName() { + /** + * Return the value of the filename field. + * An empty String
""
indicates no filename. + * + * @return filename + */ + public @Nonnull String getFileName() { return fileName; } - public Date getModificationDate() { + /** + * Return the value of the modification date field. + * A special date
{@code new Date(0L)}
indicates no modification date. + * + * @return modification date + */ + public @Nonnull Date getModificationDate() { return modificationDate; } - public StreamEncoding getFormat() { + /** + * Return the value of the format field. + * + * @return format + */ + public @Nonnull StreamEncoding getFormat() { return format; } @Override public boolean hasNestedChild() { + // A literal data packet MUST NOT have a child element, as its content is the plaintext return false; } } @@ -348,17 +613,22 @@ public class MessageMetadata { public static class CompressedData extends Layer implements Nested { protected final CompressionAlgorithm algorithm; - public CompressedData(CompressionAlgorithm zip, int depth) { + public CompressedData(@Nonnull CompressionAlgorithm zip, int depth) { super(depth); this.algorithm = zip; } - public CompressionAlgorithm getAlgorithm() { + /** + * Return the {@link CompressionAlgorithm} used to compress the packet. + * @return compression algorithm + */ + public @Nonnull CompressionAlgorithm getAlgorithm() { return algorithm; } @Override public boolean hasNestedChild() { + // A compressed data packet MUST have a child element return true; } } @@ -369,25 +639,40 @@ public class MessageMetadata { protected SessionKey sessionKey; protected List recipients; - public EncryptedData(SymmetricKeyAlgorithm algorithm, int depth) { + public EncryptedData(@Nonnull SymmetricKeyAlgorithm algorithm, int depth) { super(depth); this.algorithm = algorithm; } - public SymmetricKeyAlgorithm getAlgorithm() { + /** + * Return the {@link SymmetricKeyAlgorithm} used to encrypt the packet. + * @return symmetric encryption algorithm + */ + public @Nonnull SymmetricKeyAlgorithm getAlgorithm() { return algorithm; } - public SessionKey getSessionKey() { + /** + * Return the {@link SessionKey} used to decrypt the packet. + * + * @return session key + */ + public @Nonnull SessionKey getSessionKey() { return sessionKey; } - public List getRecipients() { + /** + * Return a list of all recipient key ids to which the packet was encrypted for. + * + * @return recipients + */ + public @Nonnull List getRecipients() { return new ArrayList<>(recipients); } @Override public boolean hasNestedChild() { + // An encrypted data packet MUST have a child element return true; } } @@ -398,10 +683,10 @@ public class MessageMetadata { Layer last = null; Message parent; - LayerIterator(Message message) { + LayerIterator(@Nonnull Message message) { super(); this.parent = message; - this.current = message.child; + this.current = message.getChild(); if (matches(current)) { last = (Layer) current; } @@ -437,8 +722,8 @@ public class MessageMetadata { } private void findNext() { - while (current instanceof Layer) { - current = ((Layer) current).child; + while (current != null && current instanceof Layer) { + current = ((Layer) current).getChild(); if (matches(current)) { last = (Layer) current; break; @@ -459,33 +744,4 @@ public class MessageMetadata { abstract O getProperty(Layer last); } - - public OpenPgpMetadata toLegacyMetadata() { - OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - resultBuilder.setCompressionAlgorithm(getCompressionAlgorithm()); - resultBuilder.setModificationDate(getModificationDate()); - resultBuilder.setFileName(getFilename()); - resultBuilder.setFileEncoding(getFormat()); - resultBuilder.setSessionKey(getSessionKey()); - resultBuilder.setDecryptionKey(getDecryptionKey()); - - for (SignatureVerification accepted : getVerifiedDetachedSignatures()) { - resultBuilder.addVerifiedDetachedSignature(accepted); - } - for (SignatureVerification.Failure rejected : getRejectedDetachedSignatures()) { - resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - for (SignatureVerification accepted : getVerifiedInlineSignatures()) { - resultBuilder.addVerifiedInbandSignature(accepted); - } - for (SignatureVerification.Failure rejected : getRejectedInlineSignatures()) { - resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - if (message.isCleartextSigned()) { - resultBuilder.setCleartextSigned(); - } - - return resultBuilder.build(); - } } From 70cca563d7dd0ab87e61c0a7b0f41bf83e5bc220 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 17:24:26 +0100 Subject: [PATCH 0565/1204] Add javadoc to getMetadata() and getResult() --- .../decryption_verification/DecryptionStream.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index e3a51bb9..c967ba10 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -8,8 +8,21 @@ import java.io.InputStream; public abstract class DecryptionStream extends InputStream { + /** + * Return {@link MessageMetadata metadata} about the decrypted / verified message. + * The {@link DecryptionStream} MUST be closed via {@link #close()} before the metadata object can be accessed. + * + * @return message metadata + */ public abstract MessageMetadata getMetadata(); + /** + * Return a {@link OpenPgpMetadata} object containing information about the decrypted / verified message. + * The {@link DecryptionStream} MUST be closed via {@link #close()} before the metadata object can be accessed. + * + * @return message metadata + * @deprecated use {@link #getMetadata()} instead. + */ @Deprecated public OpenPgpMetadata getResult() { return getMetadata().toLegacyMetadata(); From 14376048360331d8204979a8e3dc83f74458a604 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 17:36:25 +0100 Subject: [PATCH 0566/1204] Add documentation to DecryptionStream --- .../pgpainless/decryption_verification/DecryptionStream.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index c967ba10..28642bbf 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -6,6 +6,9 @@ package org.pgpainless.decryption_verification; import java.io.InputStream; +/** + * Abstract definition of an {@link InputStream} which can be used to decrypt / verify OpenPGP messages. + */ public abstract class DecryptionStream extends InputStream { /** From fd2f6523ecb8caea3f830fbb63384e11e79808f8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 17:36:51 +0100 Subject: [PATCH 0567/1204] More specific exception message for when nesting depth is exceeded --- .../org/pgpainless/decryption_verification/MessageMetadata.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 1bf22b47..3af2a4d3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -399,7 +399,7 @@ public class MessageMetadata { public Layer(int depth) { this.depth = depth; if (depth > MAX_LAYER_DEPTH) { - throw new MalformedOpenPgpMessageException("Maximum nesting depth exceeded."); + throw new MalformedOpenPgpMessageException("Maximum packet nesting depth (" + MAX_LAYER_DEPTH + ") exceeded."); } } From 8faec25ecf1b39c888f0da138c0366b4e4307f30 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 17:38:38 +0100 Subject: [PATCH 0568/1204] Enable previously disabled test for marker+seipd packet processing --- .../java/org/pgpainless/signature/IgnoreMarkerPackets.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java index 87766dfe..c86efd05 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java @@ -20,7 +20,6 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; @@ -160,8 +159,6 @@ public class IgnoreMarkerPackets { } @Test - @Disabled - // TODO: Enable and fix public void markerPlusEncryptedMessage() throws IOException, PGPException { String msg = "-----BEGIN PGP MESSAGE-----\n" + "\n" + From b95568f30a82e38f028b7c3dcb8d1c4de96cf3f5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 17:39:48 +0100 Subject: [PATCH 0569/1204] Rename IgnoreMarkerPacketsTest --- .../{IgnoreMarkerPackets.java => IgnoreMarkerPacketsTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename pgpainless-core/src/test/java/org/pgpainless/signature/{IgnoreMarkerPackets.java => IgnoreMarkerPacketsTest.java} (99%) diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java similarity index 99% rename from pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java rename to pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java index c86efd05..6ece093b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPackets.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java @@ -33,7 +33,7 @@ import org.pgpainless.key.util.KeyRingUtils; * * @see Sequoia Test-Suite */ -public class IgnoreMarkerPackets { +public class IgnoreMarkerPacketsTest { private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Comment: Bob's OpenPGP Transferable Secret Key\n" + From 4e4c095d8dfe4f55dd7c0ffbf85e5e3cb24dc49c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 17:40:51 +0100 Subject: [PATCH 0570/1204] Rename tests to end in Test --- ...pientMessage.java => DecryptHiddenRecipientMessageTest.java} | 2 +- ...va => RejectWeakSymmetricAlgorithmDuringDecryptionTest.java} | 2 +- ... SignedMessageVerificationWithoutCertIsStillSignedTest.java} | 2 +- ...allback.java => VerifyWithMissingPublicKeyCallbackTest.java} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename pgpainless-core/src/test/java/org/pgpainless/decryption_verification/{DecryptHiddenRecipientMessage.java => DecryptHiddenRecipientMessageTest.java} (99%) rename pgpainless-core/src/test/java/org/pgpainless/decryption_verification/{RejectWeakSymmetricAlgorithmDuringDecryption.java => RejectWeakSymmetricAlgorithmDuringDecryptionTest.java} (99%) rename pgpainless-core/src/test/java/org/pgpainless/decryption_verification/{SignedMessageVerificationWithoutCertIsStillSigned.java => SignedMessageVerificationWithoutCertIsStillSignedTest.java} (96%) rename pgpainless-core/src/test/java/org/pgpainless/decryption_verification/{VerifyWithMissingPublicKeyCallback.java => VerifyWithMissingPublicKeyCallbackTest.java} (98%) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessage.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java similarity index 99% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessage.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java index 5c63a996..5400d17c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessage.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java @@ -24,7 +24,7 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.TestAllImplementations; -public class DecryptHiddenRecipientMessage { +public class DecryptHiddenRecipientMessageTest { @TestTemplate @ExtendWith(TestAllImplementations.class) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryption.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryptionTest.java similarity index 99% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryption.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryptionTest.java index 53c690ed..1201ef69 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryption.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryptionTest.java @@ -22,7 +22,7 @@ import org.pgpainless.exception.UnacceptableAlgorithmException; * Test PGPainless' default symmetric key algorithm policy for decryption of messages. * The default decryption policy rejects messages encrypted with IDEA and TripleDES, as well as unencrypted messages. */ -public class RejectWeakSymmetricAlgorithmDuringDecryption { +public class RejectWeakSymmetricAlgorithmDuringDecryptionTest { private static final String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + " Comment: Bob's OpenPGP Transferable Secret Key\n" + diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java similarity index 96% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java index 0b2f5d7d..27bc9954 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSigned.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java @@ -17,7 +17,7 @@ import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; -public class SignedMessageVerificationWithoutCertIsStillSigned { +public class SignedMessageVerificationWithoutCertIsStillSignedTest { private static final String message = "-----BEGIN PGP MESSAGE-----\n" + "\n" + diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java similarity index 98% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java index bcbad822..14136650 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallback.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java @@ -38,7 +38,7 @@ import org.pgpainless.key.util.KeyRingUtils; * a signature is encountered which was made by a key that was not provided in * {@link ConsumerOptions#addVerificationCert(PGPPublicKeyRing)}. */ -public class VerifyWithMissingPublicKeyCallback { +public class VerifyWithMissingPublicKeyCallbackTest { @Test public void testMissingPublicKeyCallback() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { From 6ba7e91f2a57eecdd510ebcae3b178a1116ca432 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 16 Nov 2022 18:00:44 +0100 Subject: [PATCH 0571/1204] Add documentation and removal-TODO to old OpenPgpMetadata class --- .../pgpainless/decryption_verification/OpenPgpMetadata.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java index 130ea554..e26fb804 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java @@ -27,6 +27,12 @@ import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.util.SessionKey; +/** + * Legacy class containing metadata about an OpenPGP message. + * It is advised to use {@link MessageMetadata} instead. + * + * TODO: Remove in 1.5.X + */ public class OpenPgpMetadata { private final Set recipientKeyIds; From b9152d5cdea2b31a4dcc66c2126d8540d3b52365 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 17 Nov 2022 14:28:29 +0100 Subject: [PATCH 0572/1204] SOP: Add test to ensure that armoring already-armored data is idempotent --- .../java/org/pgpainless/cli/commands/ArmorCmdTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java index c1fb810f..afd5ded4 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java @@ -97,4 +97,14 @@ public class ArmorCmdTest extends CLITest { assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, exitCode); assertEquals(0, out.size()); } + + @Test + public void armorAlreadyArmoredDataIsIdempotent() throws IOException { + pipeStringToStdin(key); + ByteArrayOutputStream armorOut = pipeStdoutToStream(); + assertSuccess(executeCommand("armor")); + + String armored = armorOut.toString(); + assertEquals(key, armored); + } } From de76f4b3a9f79a3a3bf7b52548925ac7a005348d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 18 Nov 2022 14:17:47 +0100 Subject: [PATCH 0573/1204] Fix indentation for CLI tests --- .../java/org/pgpainless/cli/commands/CLITest.java | 3 ++- .../cli/commands/ExtractCertCmdTest.java | 3 ++- .../cli/commands/GenerateKeyCmdTest.java | 6 ++++-- .../commands/RoundTripEncryptDecryptCmdTest.java | 15 ++++++++++----- .../cli/commands/RoundTripSignVerifyCmdTest.java | 3 ++- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java index d5d076cc..9e2fd895 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/CLITest.java @@ -161,6 +161,7 @@ public abstract class CLITest { } public void assertSuccess(int exitCode) { - assertEquals(0, exitCode, "Expected successful program execution"); + assertEquals(0, exitCode, + "Expected successful program execution"); } } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java index 3eebcb47..f1f69912 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ExtractCertCmdTest.java @@ -30,7 +30,8 @@ public class ExtractCertCmdTest extends CLITest { } @Test - public void testExtractCert() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { + public void testExtractCert() + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .simpleEcKeyRing("Juliet Capulet "); diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java index 2e4e6e7f..302e4b8f 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/GenerateKeyCmdTest.java @@ -83,7 +83,8 @@ public class GenerateKeyCmdTest extends CLITest { KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); assertTrue(info.isFullyEncrypted()); - assertNotNull(UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), Passphrase.fromPassword("sw0rdf1sh"))); + assertNotNull(UnlockSecretKey + .unlockSecretKey(secretKeys.getSecretKey(), Passphrase.fromPassword("sw0rdf1sh"))); } @Test @@ -91,6 +92,7 @@ public class GenerateKeyCmdTest extends CLITest { int exit = executeCommand("generate-key", "--with-key-password", "nonexistent", "Alice "); - assertEquals(SOPGPException.MissingInput.EXIT_CODE, exit, "Expected MISSING_INPUT (" + SOPGPException.MissingInput.EXIT_CODE + ")"); + assertEquals(SOPGPException.MissingInput.EXIT_CODE, exit, + "Expected MISSING_INPUT (" + SOPGPException.MissingInput.EXIT_CODE + ")"); } } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java index 5d183ea6..9122829a 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java @@ -168,7 +168,8 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { File verifications = nonExistentFile("verifications"); ByteArrayOutputStream out = pipeStdoutToStream(); - int exitCode = executeCommand("decrypt", "--verifications-out", verifications.getAbsolutePath(), key.getAbsolutePath()); + int exitCode = executeCommand("decrypt", "--verifications-out", + verifications.getAbsolutePath(), key.getAbsolutePath()); assertEquals(SOPGPException.IncompleteVerification.EXIT_CODE, exitCode); assertEquals(0, out.size()); @@ -211,7 +212,8 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { pipeStringToStdin(ciphertext); ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); - assertSuccess(executeCommand("decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(), key.getAbsolutePath())); + assertSuccess(executeCommand("decrypt", "--session-key-out", + sessionKeyFile.getAbsolutePath(), key.getAbsolutePath())); assertEquals(plaintext, plaintextOut.toString()); String resultSessionKey = readStringFromFile(sessionKeyFile); @@ -300,7 +302,8 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("No Crypt ") - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), + KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .build(); PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); File certFile = writeFile("cert.pgp", cert.getEncoded()); @@ -314,11 +317,13 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { } @Test - public void testSignWithIncapableKey() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void testSignWithIncapableKey() + throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("Cannot Sign ") .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .addSubkey(KeySpec.getBuilder( + KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .build(); File keyFile = writeFile("key.pgp", secretKeys.getEncoded()); File certFile = writeFile("cert.pgp", PGPainless.extractCertificate(secretKeys).getEncoded()); diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java index 7f420054..6196a847 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java @@ -189,7 +189,8 @@ public class RoundTripSignVerifyCmdTest extends CLITest { } @Test - public void testSignWithIncapableKey() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void testSignWithIncapableKey() + throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("Cannot Sign ") .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) From b9f985a84c0fa5e64977173ceb2240b0901b51bd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 18 Nov 2022 14:55:14 +0100 Subject: [PATCH 0574/1204] Add tests for SOP decrypt --- .../RoundTripEncryptDecryptCmdTest.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java index 9122829a..d314de20 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java @@ -563,4 +563,85 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { keyFile.getAbsolutePath())); assertEquals(msg, out.toString()); } + + @Test + public void decryptMalformedMessageYieldsBadData() throws IOException { + // Message contains encrypted data packet which contains the plaintext directly - no literal data packet. + // It is therefore malformed. + String malformed = "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.72b04\n" + + "\n" + + "hF4D831k4umlLu4SAQdApKA6VDKSLQvwS2kbWqlhcXD8XHdFkSccqv5tBptZnBgw\n" + + "nZNXVhwUpap0ymb4jPTD+EVPKOfPyy04ouIGZAJKkfYDeSL/8sKcbnPPuQJYYjGQ\n" + + "ySDNmidrtTonwcSuwAfRyn74BBqOVhrr8GXkVIfevIlZFQ==\n" + + "=wIgl\n" + + "-----END PGP MESSAGE-----"; + File key = writeFile("key.asc", KEY); + pipeStringToStdin(malformed); + int exitCode = executeCommand("decrypt", key.getAbsolutePath()); + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + } + + @Test + public void decryptWithPasswordWithPendingWhitespaceWorks() throws IOException { + assertEncryptWithPasswordADecryptWithPasswordBWorks("sw0rdf1sh", "sw0rdf1sh \n"); + } + + @Test + public void encryptWithTrailingWhitespaceDecryptWithoutWorks() throws IOException { + assertEncryptWithPasswordADecryptWithPasswordBWorks("sw0rdf1sh \n", "sw0rdf1sh"); + } + + @Test + public void decryptWithWhitespacePasswordWorks() throws IOException { + // is encrypted for "sw0rdf1sh \n" + String encryptedToPasswordWithTrailingWhitespace = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "jA0ECQMC32tEJug0BCpg0kABfT3dKgA4K8XGpk2ul67BXLZD//fCCSmIQIWnNhE1\n" + + "6q97xFQ628K8f/58+XoBzLqLDT+LEz9Bz+Yg9QfzkEFy\n" + + "=2Y+K\n" + + "-----END PGP MESSAGE-----"; + pipeStringToStdin(encryptedToPasswordWithTrailingWhitespace); + File password = writeFile("password", "sw0rdf1sh \n"); + ByteArrayOutputStream plaintext = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--with-password", password.getAbsolutePath())); + + assertEquals("Hello, World!\n", plaintext.toString()); + } + + private void assertEncryptWithPasswordADecryptWithPasswordBWorks(String passwordA, String passwordB) + throws IOException { + File passwordAFile = writeFile("password", passwordA); + File passwordBFile = writeFile("passwordWithWS", passwordB); + + String msg = "Hello, World!\n"; + pipeStringToStdin(msg); + ByteArrayOutputStream ciphertext = pipeStdoutToStream(); + assertSuccess(executeCommand("encrypt", "--with-password", passwordAFile.getAbsolutePath())); + + pipeStringToStdin(ciphertext.toString()); + ByteArrayOutputStream plaintext = pipeStdoutToStream(); + assertSuccess(executeCommand("decrypt", "--with-password", passwordBFile.getAbsolutePath())); + + assertEquals(msg, plaintext.toString()); + } + + @Test + public void testDecryptWithoutDecryptionOptionFails() throws IOException { + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4D831k4umlLu4SAQdAYisjZTDRm217LHQbqjB766tm62CKTkRj3Gd0wYxVRCgw\n" + + "48SnOJINCJoPgDsxk2NiJmLCImoiET7IElqxN9htdDXQJwcRK+71r/ZyO4YJpWuX\n" + + "0sAAAcEFc3nT+un31sOi8KoBJlc5n+MemntQvcWDs8B87BEW/Ncjrs0s4pJpZKBQ\n" + + "/AWc4wLCI3ylfMQJB2pICqaOO3KP3WepgTIw5fuZm6YfriKQi7uZvVx1N+uaCIoa\n" + + "K2IVVf/7O9KZJ9GbsGYdpBj9IdaIZiVS3Xi8rwgQl3haI/EeHC3nnCsWyj23Fjt3\n" + + "LjbMqpHbSnp8U1cQ8rXavrREaKv69PFeJio6/hRg32TzJqn05dPALRxHMEkxxa4h\n" + + "FpVU\n" + + "=edS5\n" + + "-----END PGP MESSAGE-----"; + pipeStringToStdin(ciphertext); + int exitCode = executeCommand("decrypt"); + assertEquals(SOPGPException.MissingArg.EXIT_CODE, exitCode); + } } From a9014f1985c63045f58b5d5f27e25ae8825004ce Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 18 Nov 2022 15:27:58 +0100 Subject: [PATCH 0575/1204] Add disabled test for broken data during dearmor --- .../pgpainless/cli/commands/DearmorCmdTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java index 8fcf093d..64576bfe 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java @@ -13,9 +13,11 @@ import java.io.IOException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; public class DearmorCmdTest extends CLITest { @@ -59,6 +61,19 @@ public class DearmorCmdTest extends CLITest { assertArrayEquals(secretKey.getEncoded(), dearmored.toByteArray()); } + @Test + @Disabled("Enable with SOP-Java 4.0.7") + // TODO: Enable + public void dearmorBrokenArmoredKeyFails() throws IOException { + // contains a "-" + String invalidBase64 = "lFgEY2vOkhYJKwYBBAHaRw8BAQdAqGOtLd1tKnuwaYYcdr2/7C0cPiCCggRMKG+Wt32QQdEAAP9VaBzjk/AaAqyykZnQHmS1HByEvRLv5/4yJMSr22451BFjtBRhbGljZUBwZ3BhaW5sZXNzLm9yZ4iOBBMWCgBBBQJja86SCRCLB1F3AflTTBYhBGLp3aTyD4NB0rxLT-IsHUXcB+VNMAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAACZhAP4s8hn/RBDvyLvGROOd15EYATnWlgyi+b5WXP6cELalJwD1FZy3RROhfNtZWcJPS43fG03pYNyb0NXoitIMAaXEB5xdBGNrzpISCisGAQQBl1UBBQEBB0CqCcYethOynfni8uRO+r/cZWp9hCLy8pRIExKqzcyEFAMBCAcAAP9sRRLoZkLpDaTNNrtIBovXu2ANhL8keUMWtVcuEHnkQA6iiHUEGBYKAB0FAmNrzpICngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRCLB1F3AflTTBVpAP491etrjqCMWx2bBaw3K1vP0Mix6U0vF3J4kP9UeZm6owEA4kX9VAGESvLgIc7CEiswmxdWjxnLQyCRtWXfjgFmYQucWARja86SFgkrBgEEAdpHDwEBB0DBslhDpWC6CV3xJUSo071NSO5Cf4fgOwOj+QHs8mpFbwABAPkQioSydYiMi04LyfPohyrhhcdJDHallQg+jYHHUb2pEJCI1QQYFgoAfQUCY2vOkgKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNrzpIACgkQiHlkvEXh+f1eywEA9A2GLU9LxCJxZf2X4qcZY//YJDChIZHPnY0Vaek1DsMBAN1YILrH2rxQeCXjm4bUKfJIRrGt6ZJscwORgNI1dFQFAAoJEIsHUXcB+VNMK3gA/3vvPm57JsHA860wlB4D1II71oFNL8TFnJqTAvpSKe1AAP49S4mKB4PE0ElcDo7n+nEYt6ba8IMRDlMorsH85mUgCw=="; + pipeStringToStdin(invalidBase64); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("dearmor"); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } @Test public void dearmorCertificate() throws IOException { From ce929fd05554ca15b9ff89140bb29cbd1a4a9c80 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 18 Nov 2022 15:35:30 +0100 Subject: [PATCH 0576/1204] Add inline-verify test for message without verifiable signatures --- ...oundTripInlineSignInlineVerifyCmdTest.java | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index 5d635beb..ce0a7674 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -4,16 +4,17 @@ package org.pgpainless.cli.commands; -import org.junit.jupiter.api.Test; -import org.slf4j.LoggerFactory; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; +import sop.exception.SOPGPException; public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { @@ -171,6 +172,30 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); } + @Test + public void createSignedMessageWithKeyAAndVerifyWithKeyBFails() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password", KEY_1_PASSWORD); + File cert = writeFile("cert.asc", CERT_2); // mismatch + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath())); + + File verifications = nonExistentFile("verifications"); + pipeStringToStdin(ciphertextOut.toString()); + ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); + int exitCode = executeCommand("inline-verify", + "--verifications-out", verifications.getAbsolutePath(), + cert.getAbsolutePath()); + + assertEquals(SOPGPException.NoSignature.EXIT_CODE, exitCode); + assertEquals(MESSAGE, plaintextOut.toString()); // message is emitted nonetheless + assertFalse(verifications.exists(), "Verifications file MUST NOT be written."); + } + @Test public void createAndVerifyMultiKeyBinarySignedMessage() throws IOException { File key1Pass = writeFile("password", KEY_1_PASSWORD); From 508864c4ff21d7db8ccf2e2c7fe7f915fe433a06 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 18 Nov 2022 15:36:58 +0100 Subject: [PATCH 0577/1204] Add test for inline-sign --as=text --- ...oundTripInlineSignInlineVerifyCmdTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index ce0a7674..1ff3a14b 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -172,6 +172,31 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); } + @Test + public void createAndVerifyTextSignedMessage() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password", KEY_1_PASSWORD); + + pipeStringToStdin(MESSAGE); + ByteArrayOutputStream ciphertextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-sign", + "--as", "text", + key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath())); + + File cert = writeFile("cert.asc", CERT_1); + File verifications = nonExistentFile("verifications"); + pipeStringToStdin(ciphertextOut.toString()); + ByteArrayOutputStream plaintextOut = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-verify", + "--verifications-out", verifications.getAbsolutePath(), + cert.getAbsolutePath())); + + assertEquals(MESSAGE, plaintextOut.toString()); + String verificationString = readStringFromFile(verifications); + assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); + } + @Test public void createSignedMessageWithKeyAAndVerifyWithKeyBFails() throws IOException { File key = writeFile("key.asc", KEY_1); From 75c39c2fde55fd0744c9c0d070e9cb7d36ff0bdf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 18 Nov 2022 16:03:20 +0100 Subject: [PATCH 0578/1204] Add tests for inline-verify --- ...oundTripInlineSignInlineVerifyCmdTest.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index 1ff3a14b..d5a94f3a 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -11,8 +11,19 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.key.protection.SecretKeyRingProtector; import org.slf4j.LoggerFactory; import sop.exception.SOPGPException; @@ -311,4 +322,93 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { String verificationString = readStringFromFile(verificationsFile); assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); } + + @Test + public void cannotVerifyEncryptedMessage() throws IOException { + File key = writeFile("key.asc", KEY_2); + File cert = writeFile("cert.asc", CERT_2); + + String msg = "Hello, World!\n"; + pipeStringToStdin(msg); + ByteArrayOutputStream ciphertext = pipeStdoutToStream(); + assertSuccess(executeCommand("encrypt", cert.getAbsolutePath(), + "--sign-with", key.getAbsolutePath())); + + File verifications = nonExistentFile("verifications"); + pipeBytesToStdin(ciphertext.toByteArray()); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("inline-verify", cert.getAbsolutePath(), + "--verifications-out", verifications.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void createMalformedMessage() throws IOException, PGPException { + String msg = "Hello, World!\n"; + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY_2); + ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertext) + .withOptions(ProducerOptions.sign(SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key) + ).overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) + .setAsciiArmor(false)); + encryptionStream.write(msg.getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + PGPSignature sig = encryptionStream.getResult().getDetachedSignatures().entrySet() + .iterator().next().getValue().iterator().next(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + armorOut.write(ciphertext.toByteArray()); + armorOut.write(sig.getEncoded()); + armorOut.close(); + } + + @Test + public void cannotVerifyMalformedMessage() throws IOException { + // appended signature -> malformed + String malformedSignedMessage = "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.72b04\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCoh1BAAWCgAnBQJjd52aCRCPvdNtAYMWcxYh\n" + + "BHoHPt8nPJAnltJZUo+9020BgxZzAACThwD/Vr7CMitMOul40VK12XXjOv5f8vgi\n" + + "ksqhrI2ysItID9oA/0Csgf3Sv2YenYVzqnd0hhiPe5IVPl8w4sTZKpriYMIG\n" + + "=DPPU\n" + + "-----END PGP MESSAGE-----"; + File cert = writeFile("cert.asc", CERT_2); + File verifications = nonExistentFile("verifications"); + + pipeStringToStdin(malformedSignedMessage); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("inline-verify", cert.getAbsolutePath(), + "--verifications-out", verifications.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + assertEquals("Hello, World!\n", out.toString()); + } + + @Test + public void verifyPrependedSignedMessage() throws IOException { + // message with prepended signature + String malformedSignedMessage = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: BCPG v1.72b04\n" + + "\n" + + "iHUEABYKACcFAmN3nOUJEI+9020BgxZzFiEEegc+3yc8kCeW0llSj73TbQGDFnMA\n" + + "ANPKAPkBxLVHvgeCkX/tTHdBH3CDeuUQF2wmtUmGXqhZA1IFtwD/dK0XQBHO3RO+\n" + + "GHpzA7fDAroqF0zM72tu2W4PPw04FgKjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" + + "=xtik\n" + + "-----END PGP SIGNATURE-----"; + File cert = writeFile("cert.asc", CERT_2); + File verifications = nonExistentFile("verifications"); + + pipeStringToStdin(malformedSignedMessage); + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("inline-verify", cert.getAbsolutePath(), + "--verifications-out", verifications.getAbsolutePath())); + assertEquals("Hello, World!\n", out.toString()); + String ver = readStringFromFile(verifications); + assertEquals( + "2022-11-18T14:55:33Z 7A073EDF273C902796D259528FBDD36D01831673 AEA0FD2C899D3FC077815F0026560D2AE53DB86F\n", ver); + } } From a34f46b6c6b64949ffa2525bcd40760ae513ef96 Mon Sep 17 00:00:00 2001 From: GregorGott <85785843+GregorGott@users.noreply.github.com> Date: Sun, 20 Nov 2022 10:54:11 +0000 Subject: [PATCH 0579/1204] Correct method name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correct `verifyWith()` to `verifyWithCert()´ --- docs/source/pgpainless-sop/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/pgpainless-sop/quickstart.md b/docs/source/pgpainless-sop/quickstart.md index ecd7d81d..10ef0a72 100644 --- a/docs/source/pgpainless-sop/quickstart.md +++ b/docs/source/pgpainless-sop/quickstart.md @@ -209,7 +209,7 @@ byte[] ciphertext = ...; // the encrypted message ReadyWithResult readyWithResult = sop.decrypt() .withKey(bobKey) - .verifyWith(aliceCert) + .verifyWithCert(aliceCert) .withKeyPassword("password123") // if decryption key is protected .ciphertext(ciphertext); ``` From ab82a638ccbe4de4ce0dca2c7049ef4944ef6c0e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 21 Nov 2022 14:11:12 +0100 Subject: [PATCH 0580/1204] Add tests for inline-sign --- ...oundTripInlineSignInlineVerifyCmdTest.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index d5a94f3a..1e5310f5 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -411,4 +411,52 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { assertEquals( "2022-11-18T14:55:33Z 7A073EDF273C902796D259528FBDD36D01831673 AEA0FD2C899D3FC077815F0026560D2AE53DB86F\n", ver); } + + @Test + public void testInlineSignWithMissingSecretKeysFails() throws IOException { + String missingSecretKeys = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: 8677 37CA 1979 28FA 325A DE56 B455 9329 9882 36BE\n" + + "Comment: Mrs. Secret Key \n" + + "\n" + + "lEwEY3t3pRYJKwYBBAHaRw8BAQdA7lifUc85s7omw7eYNIaIj2mZrGeZ9KkG0WX2\n" + + "hAx5qXT+AGUAR05VAhAAAAAAAAAAAAAAAAAAAAAAtCFNcnMuIFNlY3JldCBLZXkg\n" + + "PG1pc3NAc2VjcmV0LmtleT6IjwQTFgoAQQUCY3t3pQkQtFWTKZiCNr4WIQSGdzfK\n" + + "GXko+jJa3la0VZMpmII2vgKeAQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAABNTQEA\n" + + "uU5L9hJ1QKWxL5wetJwR08rXJTzsuX1LRfy8dlnlJl0BAKPSqydLoTEVlJQ/2sjO\n" + + "xQmc6aedoOoXKKVNDW5ibrsEnFEEY3t3pRIKKwYBBAGXVQEFAQEHQA/WdwR+NFaY\n" + + "7NeZnRwI3X9sI5fMq0vtEauMLfZjqTc/AwEIB/4AZQBHTlUCEAAAAAAAAAAAAAAA\n" + + "AAAAAACIdQQYFgoAHQUCY3t3pQKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJELRV\n" + + "kymYgja+8XMA/1quBVvaSf4QxbB2S7rKt93rAynDLqGQD8hC6wiZc+ihAQC87n2r\n" + + "meZ9kiYLYiQuBTGvXyzDBtw5m7wQtMWTfXisBpxMBGN7d6UWCSsGAQQB2kcPAQEH\n" + + "QMguDhFon0ZI//CIpC2ZndmtvKdJhcEAeVNkdcsIZajl/gBlAEdOVQIQAAAAAAAA\n" + + "AAAAAAAAAAAAAIjVBBgWCgB9BQJje3elAp4BApsCBRYCAwEABAsJCAcFFQoJCAtf\n" + + "IAQZFgoABgUCY3t3pQAKCRC14KclsvqqOstPAQDYiL7+4HucWKmd7dcd9XJZpdB6\n" + + "lneoK0qku0wvTVjX7gEAtUt2eXMlBE4ox+ZmY964PCc2gEHuC7PBtsAzuF7GSQwA\n" + + "CgkQtFWTKZiCNr7JKwEA3aLsOWAYzqvKgiboYSzle+SVBUb3chKlzf3YmckjmwgA\n" + + "/3YN1W8CiQFvE9NvetZkr2wXB+QVkuL6cxM0ogEo4lAG\n" + + "=9ZMl\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + File key = writeFile("key.asc", missingSecretKeys); + + pipeStringToStdin("Hello, World!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("inline-sign", key.getAbsolutePath()); + + assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } + + @Test + public void signWithProtectedKeyWithWrongPassphraseFails() throws IOException { + File key = writeFile("key.asc", KEY_1); + File password = writeFile("password.asc", "not_correct!"); + + pipeStringToStdin("Hello, World!\n"); + ByteArrayOutputStream out = pipeStdoutToStream(); + int exitCode = executeCommand("inline-sign", key.getAbsolutePath(), + "--with-key-password", password.getAbsolutePath()); + + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, exitCode); + assertEquals(0, out.size()); + } } From a19fc9ebda01cd694c9e1a50a1d4327dc1d0e556 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 21 Nov 2022 15:04:13 +0100 Subject: [PATCH 0581/1204] Add tests for inline-detach --- .../cli/commands/InlineDetachCmdTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java index eabdd6ad..8854d837 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java @@ -129,4 +129,27 @@ public class InlineDetachCmdTest extends CLITest { assertEquals(0, msgOut.size()); } + @Test + public void detachNonOpenPgpDataFails() throws IOException { + File sig = nonExistentFile("sig.asc"); + pipeStringToStdin("This is non-OpenPGP data and therefore we cannot detach any signatures from it."); + int exitCode = executeCommand("inline-detach", "--signatures-out", sig.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + } + + @Test + public void detachMissingSignaturesFromCleartextSignedMessageFails() throws IOException { + String cleartextSignedNoSigs = "-----BEGIN PGP SIGNED MESSAGE-----\n" + + "\n" + + "Hello, World!\n" + + "What's Up!??\n" + + "\n" + + "\n"; + pipeStringToStdin(cleartextSignedNoSigs); + File sig = nonExistentFile("sig.asc"); + int exitCode = executeCommand("inline-detach", "--signatures-out", sig.getAbsolutePath()); + + assertEquals(SOPGPException.BadData.EXIT_CODE, exitCode); + } } From e4560ac5b502334d0bd6f50122517c59eccb8850 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 21 Nov 2022 15:04:52 +0100 Subject: [PATCH 0582/1204] Cleartext Signaure Framework: Support for multiple Hash: headers --- .../encryption_signing/EncryptionStream.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 27d35ea5..2e370b94 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -8,7 +8,10 @@ import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Set; import javax.annotation.Nonnull; import org.bouncycastle.bcpg.ArmoredOutputStream; @@ -22,6 +25,7 @@ import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.implementation.ImplementationFactory; @@ -165,9 +169,8 @@ public final class EncryptionStream extends OutputStream { private void prepareLiteralDataProcessing() throws IOException { if (options.isCleartextSigned()) { - // Begin cleartext with hash algorithm of first signing method - SigningOptions.SigningMethod firstMethod = options.getSigningOptions().getSigningMethods().values().iterator().next(); - armorOutputStream.beginClearText(firstMethod.getHashAlgorithm().getAlgorithmId()); + int[] algorithmIds = collectHashAlgorithmsForCleartextSigning(); + armorOutputStream.beginClearText(algorithmIds); return; } @@ -195,6 +198,24 @@ public final class EncryptionStream extends OutputStream { outermostStream = crlfGeneratorStream; } + private int[] collectHashAlgorithmsForCleartextSigning() { + SigningOptions signOpts = options.getSigningOptions(); + Set hashAlgorithms = new HashSet<>(); + if (signOpts != null) { + for (SigningOptions.SigningMethod method : signOpts.getSigningMethods().values()) { + hashAlgorithms.add(method.getHashAlgorithm()); + } + } + + int[] algorithmIds = new int[hashAlgorithms.size()]; + Iterator iterator = hashAlgorithms.iterator(); + for (int i = 0; i < algorithmIds.length; i++) { + algorithmIds[i] = iterator.next().getAlgorithmId(); + } + + return algorithmIds; + } + @Override public void write(int data) throws IOException { outermostStream.write(data); From 24ec665f76a66d7e310c9c9e74d403f2c09a8ef6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 14:39:32 +0100 Subject: [PATCH 0583/1204] Bump bcpg to 1.72.3 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index eb20dc66..86fc8f54 100644 --- a/version.gradle +++ b/version.gradle @@ -13,7 +13,7 @@ allprojects { // unfortunately we rely on 1.72.1 or 1.72.3 for a patch for https://github.com/bcgit/bc-java/issues/1257 // which is a bug we introduced with a PR against BC :/ oops // When bouncyCastleVersion is 1.71, bouncyPgVersion can simply be set to 1.71 as well. - bouncyPgVersion = '1.72.1' + bouncyPgVersion = '1.72.3' junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' From 616e14d04354d072413feef1dbcd8238aa69038d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 14:40:31 +0100 Subject: [PATCH 0584/1204] Enable tests for unsupported s2k identifiers --- .../decryption_verification/UnsupportedPacketVersionsTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java index 68916348..f8ce9e29 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java @@ -15,7 +15,6 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -361,13 +360,11 @@ public class UnsupportedPacketVersionsTest { } @Test - @Disabled("Enable once https://github.com/bcgit/bc-java/pull/1268 is available") public void pkesk3_skesk4Ws2k23_seip() throws PGPException, IOException { decryptAndCompare(PKESK3_SKESK4wS2K23_SEIP, "Encrypted using SEIP + MDC."); } @Test - @Disabled("Enable once https://github.com/bcgit/bc-java/pull/1268 is available") public void skesk4Ws2k23_pkesk3_seip() throws PGPException, IOException { decryptAndCompare(SKESK4wS2K23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); } From 3ae2afcfa0155ba6439a1ebd74b4a160d2df0323 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 14:46:08 +0100 Subject: [PATCH 0585/1204] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f19bc95..9fc7e655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog ## 1.4.0-rc2-SNAPSHOT +- Bump `bcpg-jdk15to18` to `1.72.3` - Use BCs `PGPEncryptedDataList.extractSessionKeyEncryptedData()` method to do decryption using session keys. This enables decryption of messages without encrypted session key packets. @@ -13,6 +14,8 @@ SPDX-License-Identifier: CC0-1.0 - Depend on `pgp-certificate-store` - Add `ConsumerOptions.addVerificationCerts(PGPCertificateStore)` to allow sourcing certificates from e.g. a [certificate store implementation](https://github.com/pgpainless/cert-d-java). +- Make `DecryptionStream.getMetadata()` first class + - Deprecate `DecryptionStream.getResult()` ## 1.4.0-rc1 - Reimplement message consumption via new `OpenPgpMessageInputStream` From 39f8f89fe0bb117f91655ef21440042b7f36a8f1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 15:19:39 +0100 Subject: [PATCH 0586/1204] Add convenience methods to MessageMetadata --- .../MessageMetadata.java | 221 +++++++++++------- 1 file changed, 139 insertions(+), 82 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 3af2a4d3..5318fcb1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -4,6 +4,18 @@ package org.pgpainless.decryption_verification; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPPublicKey; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; @@ -11,14 +23,6 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.util.SessionKey; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; - /** * View for extracting metadata about a {@link Message}. */ @@ -40,18 +44,9 @@ public class MessageMetadata { public @Nonnull OpenPgpMetadata toLegacyMetadata() { OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); resultBuilder.setCompressionAlgorithm(getCompressionAlgorithm()); - Date modDate = getModificationDate(); - if (modDate != null) { - resultBuilder.setModificationDate(modDate); - } - String fileName = getFilename(); - if (fileName != null) { - resultBuilder.setFileName(fileName); - } - StreamEncoding encoding = getFormat(); - if (encoding != null) { - resultBuilder.setFileEncoding(encoding); - } + resultBuilder.setModificationDate(getModificationDate()); + resultBuilder.setFileName(getFilename()); + resultBuilder.setFileEncoding(getLiteralDataEncoding()); resultBuilder.setSessionKey(getSessionKey()); resultBuilder.setDecryptionKey(getDecryptionKey()); @@ -75,6 +70,39 @@ public class MessageMetadata { return resultBuilder.build(); } + public boolean isEncrypted() { + SymmetricKeyAlgorithm algorithm = getEncryptionAlgorithm(); + return algorithm != null && algorithm != SymmetricKeyAlgorithm.NULL; + } + + public boolean isEncryptedFor(@Nonnull PGPKeyRing keys) { + Iterator encryptionLayers = getEncryptionLayers(); + while (encryptionLayers.hasNext()) { + EncryptedData encryptedData = encryptionLayers.next(); + for (long recipient : encryptedData.getRecipients()) { + PGPPublicKey key = keys.getPublicKey(recipient); + if (key != null) { + return true; + } + } + } + return false; + } + + public @Nonnull Iterator getEncryptionLayers() { + return new LayerIterator(message) { + @Override + public boolean matches(Nested layer) { + return layer instanceof EncryptedData; + } + + @Override + public EncryptedData getProperty(Layer last) { + return (EncryptedData) last; + } + }; + } + /** * Return the {@link SymmetricKeyAlgorithm} of the outermost encrypted data packet, or null if message is * unencrypted. @@ -82,11 +110,7 @@ public class MessageMetadata { * @return encryption algorithm */ public @Nullable SymmetricKeyAlgorithm getEncryptionAlgorithm() { - Iterator algorithms = getEncryptionAlgorithms(); - if (algorithms.hasNext()) { - return algorithms.next(); - } - return null; + return firstOrNull(getEncryptionAlgorithms()); } /** @@ -98,15 +122,19 @@ public class MessageMetadata { * @return iterator of symmetric encryption algorithms */ public @Nonnull Iterator getEncryptionAlgorithms() { - return new LayerIterator(message) { + return map(getEncryptionLayers(), encryptedData -> encryptedData.algorithm); + } + + public @Nonnull Iterator getCompressionLayers() { + return new LayerIterator(message) { @Override - public boolean matches(Nested layer) { - return layer instanceof EncryptedData; + boolean matches(Layer layer) { + return layer instanceof CompressedData; } @Override - public SymmetricKeyAlgorithm getProperty(Layer last) { - return ((EncryptedData) last).algorithm; + CompressedData getProperty(Layer last) { + return (CompressedData) last; } }; } @@ -118,11 +146,7 @@ public class MessageMetadata { * @return compression algorithm */ public @Nullable CompressionAlgorithm getCompressionAlgorithm() { - Iterator algorithms = getCompressionAlgorithms(); - if (algorithms.hasNext()) { - return algorithms.next(); - } - return null; + return firstOrNull(getCompressionAlgorithms()); } /** @@ -134,17 +158,7 @@ public class MessageMetadata { * @return iterator of compression algorithms */ public @Nonnull Iterator getCompressionAlgorithms() { - return new LayerIterator(message) { - @Override - public boolean matches(Nested layer) { - return layer instanceof CompressedData; - } - - @Override - public CompressionAlgorithm getProperty(Layer last) { - return ((CompressedData) last).algorithm; - } - }; + return map(getCompressionLayers(), compressionLayer -> compressionLayer.algorithm); } /** @@ -154,11 +168,7 @@ public class MessageMetadata { * @return session key of the message */ public @Nullable SessionKey getSessionKey() { - Iterator sessionKeys = getSessionKeys(); - if (sessionKeys.hasNext()) { - return sessionKeys.next(); - } - return null; + return firstOrNull(getSessionKeys()); } /** @@ -170,17 +180,15 @@ public class MessageMetadata { * @return iterator of session keys */ public @Nonnull Iterator getSessionKeys() { - return new LayerIterator(message) { - @Override - boolean matches(Nested layer) { - return layer instanceof EncryptedData; - } + return map(getEncryptionLayers(), encryptedData -> encryptedData.sessionKey); + } - @Override - SessionKey getProperty(Layer last) { - return ((EncryptedData) last).getSessionKey(); - } - }; + public boolean isVerifiedSignedBy(@Nonnull PGPKeyRing keys) { + return isVerifiedInlineSignedBy(keys) || isVerifiedDetachedSignedBy(keys); + } + + public boolean isVerifiedDetachedSignedBy(@Nonnull PGPKeyRing keys) { + return containsSignatureBy(getVerifiedDetachedSignatures(), keys); } /** @@ -202,6 +210,10 @@ public class MessageMetadata { return message.getRejectedDetachedSignatures(); } + public boolean isVerifiedInlineSignedBy(@Nonnull PGPKeyRing keys) { + return containsSignatureBy(getVerifiedInlineSignatures(), keys); + } + /** * Return a list of all verified inline-signatures. * This list contains all acceptable, correct signatures that were part of the message itself. @@ -291,6 +303,28 @@ public class MessageMetadata { }; } + private static boolean containsSignatureBy(@Nonnull List verifications, + @Nonnull PGPKeyRing keys) { + for (SignatureVerification verification : verifications) { + SubkeyIdentifier issuer = verification.getSigningKey(); + if (issuer == null) { + // No issuer, shouldn't happen, but better be safe and skip... + continue; + } + + if (keys.getPublicKey().getKeyID() != issuer.getPrimaryKeyId()) { + // Wrong cert + continue; + } + + if (keys.getPublicKey(issuer.getSubkeyId()) != null) { + // Matching cert and signing key + return true; + } + } + return false; + } + /** * Return the value of the literal data packet's filename field. * This value can be used to store a decrypted file under its original filename, @@ -300,14 +334,23 @@ public class MessageMetadata { * @return filename * @see RFC4880 §5.9. Literal Data Packet */ - public @Nullable String getFilename() { + public @Nonnull String getFilename() { LiteralData literalData = findLiteralData(); if (literalData == null) { - return null; + throw new NoSuchElementException("No Literal Data Packet found."); } return literalData.getFileName(); } + /** + * Returns true, if the filename of the literal data packet indicates that the data is intended for your eyes only. + * + * @return isForYourEyesOnly + */ + public boolean isForYourEyesOnly() { + return PGPLiteralData.CONSOLE.equals(getFilename()); + } + /** * Return the value of the literal data packets modification date field. * This value can be used to restore the modification date of a decrypted file, @@ -316,10 +359,10 @@ public class MessageMetadata { * @return modification date * @see RFC4880 §5.9. Literal Data Packet */ - public @Nullable Date getModificationDate() { + public @Nonnull Date getModificationDate() { LiteralData literalData = findLiteralData(); if (literalData == null) { - return null; + throw new NoSuchElementException("No Literal Data Packet found."); } return literalData.getModificationDate(); } @@ -332,10 +375,10 @@ public class MessageMetadata { * @return format * @see RFC4880 §5.9. Literal Data Packet */ - public @Nullable StreamEncoding getFormat() { + public @Nonnull StreamEncoding getLiteralDataEncoding() { LiteralData literalData = findLiteralData(); if (literalData == null) { - return null; + throw new NoSuchElementException("No Literal Data Packet found."); } return literalData.getFormat(); } @@ -368,21 +411,7 @@ public class MessageMetadata { * @return decryption key */ public SubkeyIdentifier getDecryptionKey() { - Iterator iterator = new LayerIterator(message) { - @Override - public boolean matches(Nested layer) { - return layer instanceof EncryptedData; - } - - @Override - public SubkeyIdentifier getProperty(Layer last) { - return ((EncryptedData) last).decryptionKey; - } - }; - if (iterator.hasNext()) { - return iterator.next(); - } - return null; + return firstOrNull(map(getEncryptionLayers(), encryptedData -> encryptedData.decryptionKey)); } public abstract static class Layer { @@ -744,4 +773,32 @@ public class MessageMetadata { abstract O getProperty(Layer last); } + + private static Iterator map(Iterator from, Function mapping) { + return new Iterator() { + @Override + public boolean hasNext() { + return from.hasNext(); + } + + @Override + public B next() { + return mapping.apply(from.next()); + } + }; + } + + private static @Nullable A firstOrNull(Iterator iterator) { + if (iterator.hasNext()) { + return iterator.next(); + } + return null; + } + + private static @Nonnull A firstOr(Iterator iterator, A item) { + if (iterator.hasNext()) { + return iterator.next(); + } + return item; + } } From 8f6227c14b359792fe9d03ef3b3b938d9e1e9718 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 15:20:06 +0100 Subject: [PATCH 0587/1204] Rework some tests to use MessageMetadata --- .../MessageMetadataTest.java | 4 ++-- .../OpenPgpMessageInputStreamTest.java | 2 +- .../FileInformationTest.java | 20 +++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java index d87fc6bf..771cb8f1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java @@ -62,7 +62,7 @@ public class MessageMetadataTest { assertEquals("", metadata.getFilename()); JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); - assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + assertEquals(StreamEncoding.BINARY, metadata.getLiteralDataEncoding()); } @Test @@ -79,6 +79,6 @@ public class MessageMetadataTest { assertNull(metadata.getEncryptionAlgorithm()); assertEquals("collateral_murder.zip", metadata.getFilename()); assertEquals(DateUtil.parseUTCDate("2010-04-05 10:12:03 UTC"), metadata.getModificationDate()); - assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + assertEquals(StreamEncoding.BINARY, metadata.getLiteralDataEncoding()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index d2c62fb2..01966bbe 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -335,7 +335,7 @@ public class OpenPgpMessageInputStreamTest { assertNull(metadata.getEncryptionAlgorithm()); assertEquals("", metadata.getFilename()); JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); - assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + assertEquals(StreamEncoding.BINARY, metadata.getLiteralDataEncoding()); assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java index 662971e4..2b0a7106 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInformationTest.java @@ -28,7 +28,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; public class FileInformationTest { @@ -80,11 +80,11 @@ public class FileInformationTest { decryptionStream.close(); - OpenPgpMetadata decResult = decryptionStream.getResult(); + MessageMetadata decResult = decryptionStream.getMetadata(); - assertEquals(fileName, decResult.getFileName()); + assertEquals(fileName, decResult.getFilename()); JUtils.assertDateEquals(modificationDate, decResult.getModificationDate()); - assertEquals(encoding, decResult.getFileEncoding()); + assertEquals(encoding, decResult.getLiteralDataEncoding()); } @Test @@ -119,11 +119,11 @@ public class FileInformationTest { decryptionStream.close(); - OpenPgpMetadata decResult = decryptionStream.getResult(); + MessageMetadata decResult = decryptionStream.getMetadata(); - assertEquals("", decResult.getFileName()); + assertEquals("", decResult.getFilename()); JUtils.assertDateEquals(PGPLiteralData.NOW, decResult.getModificationDate()); - assertEquals(PGPLiteralData.BINARY, decResult.getFileEncoding().getCode()); + assertEquals(PGPLiteralData.BINARY, decResult.getLiteralDataEncoding().getCode()); assertFalse(decResult.isForYourEyesOnly()); } @@ -160,11 +160,11 @@ public class FileInformationTest { decryptionStream.close(); - OpenPgpMetadata decResult = decryptionStream.getResult(); + MessageMetadata decResult = decryptionStream.getMetadata(); - assertEquals(PGPLiteralData.CONSOLE, decResult.getFileName()); + assertEquals(PGPLiteralData.CONSOLE, decResult.getFilename()); JUtils.assertDateEquals(PGPLiteralData.NOW, decResult.getModificationDate()); - assertEquals(PGPLiteralData.BINARY, decResult.getFileEncoding().getCode()); + assertEquals(PGPLiteralData.BINARY, decResult.getLiteralDataEncoding().getCode()); assertTrue(decResult.isForYourEyesOnly()); } } From 6926cedf615dc0f079f7b96394328f64a6f92fe9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 15:41:58 +0100 Subject: [PATCH 0588/1204] Fix compilation errors and simplify LayerIterator by introducing Packet interface --- ...oundTripInlineSignInlineVerifyCmdTest.java | 2 +- .../MessageMetadata.java | 53 +++++++------------ .../OpenPgpMetadata.java | 2 +- .../pgpainless/sop/DetachedVerifyImpl.java | 4 +- 4 files changed, 24 insertions(+), 37 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index 1e5310f5..d36ee58f 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -287,7 +287,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { File cert = writeFile("cert.asc", CERT_1); pipeStringToStdin(msgOut.toString()); ByteArrayOutputStream verificationsOut = pipeStdoutToStream(); - assertSuccess(executeCommand("verify", + assertSuccess(executeCommand("verify", "--stacktrace", sigFile.getAbsolutePath(), cert.getAbsolutePath())); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 5318fcb1..da6902c5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -9,7 +9,6 @@ import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; -import java.util.function.Function; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -92,7 +91,7 @@ public class MessageMetadata { public @Nonnull Iterator getEncryptionLayers() { return new LayerIterator(message) { @Override - public boolean matches(Nested layer) { + public boolean matches(Packet layer) { return layer instanceof EncryptedData; } @@ -128,7 +127,7 @@ public class MessageMetadata { public @Nonnull Iterator getCompressionLayers() { return new LayerIterator(message) { @Override - boolean matches(Layer layer) { + boolean matches(Packet layer) { return layer instanceof CompressedData; } @@ -242,15 +241,10 @@ public class MessageMetadata { public @Nonnull Iterator> getVerifiedInlineSignaturesByLayer() { return new LayerIterator>(message) { @Override - boolean matches(Nested layer) { + boolean matches(Packet layer) { return layer instanceof Layer; } - @Override - boolean matches(Layer layer) { - return true; - } - @Override List getProperty(Layer last) { List list = new ArrayList<>(); @@ -284,15 +278,10 @@ public class MessageMetadata { public @Nonnull Iterator> getRejectedInlineSignaturesByLayer() { return new LayerIterator>(message) { @Override - boolean matches(Nested layer) { + boolean matches(Packet layer) { return layer instanceof Layer; } - @Override - boolean matches(Layer layer) { - return true; - } - @Override List getProperty(Layer last) { List list = new ArrayList<>(); @@ -334,10 +323,10 @@ public class MessageMetadata { * @return filename * @see RFC4880 §5.9. Literal Data Packet */ - public @Nonnull String getFilename() { + public @Nullable String getFilename() { LiteralData literalData = findLiteralData(); if (literalData == null) { - throw new NoSuchElementException("No Literal Data Packet found."); + return null; } return literalData.getFileName(); } @@ -359,10 +348,10 @@ public class MessageMetadata { * @return modification date * @see RFC4880 §5.9. Literal Data Packet */ - public @Nonnull Date getModificationDate() { + public @Nullable Date getModificationDate() { LiteralData literalData = findLiteralData(); if (literalData == null) { - throw new NoSuchElementException("No Literal Data Packet found."); + return null; } return literalData.getModificationDate(); } @@ -375,10 +364,10 @@ public class MessageMetadata { * @return format * @see RFC4880 §5.9. Literal Data Packet */ - public @Nonnull StreamEncoding getLiteralDataEncoding() { + public @Nullable StreamEncoding getLiteralDataEncoding() { LiteralData literalData = findLiteralData(); if (literalData == null) { - throw new NoSuchElementException("No Literal Data Packet found."); + return null; } return literalData.getFormat(); } @@ -414,7 +403,10 @@ public class MessageMetadata { return firstOrNull(map(getEncryptionLayers(), encryptedData -> encryptedData.decryptionKey)); } - public abstract static class Layer { + public interface Packet { + + } + public abstract static class Layer implements Packet { public static final int MAX_LAYER_DEPTH = 16; protected final int depth; protected final List verifiedDetachedSignatures = new ArrayList<>(); @@ -562,7 +554,7 @@ public class MessageMetadata { } - public interface Nested { + public interface Nested extends Packet { boolean hasNestedChild(); } @@ -760,16 +752,7 @@ public class MessageMetadata { } } - boolean matches(Nested layer) { - return false; - } - - boolean matches(Layer layer) { - if (layer instanceof Nested) { - return matches((Nested) layer); - } - return false; - } + abstract boolean matches(Packet layer); abstract O getProperty(Layer last); } @@ -788,6 +771,10 @@ public class MessageMetadata { }; } + public interface Function { + B apply(A item); + } + private static @Nullable A firstOrNull(Iterator iterator) { if (iterator.hasNext()) { return iterator.next(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java index e26fb804..be9cf2b1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java @@ -328,7 +328,7 @@ public class OpenPgpMetadata { return this; } - public Builder setFileName(@Nonnull String fileName) { + public Builder setFileName(@Nullable String fileName) { this.fileName = fileName; return this; } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index 3a5b3b6a..1ed43941 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -16,7 +16,7 @@ import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.exception.MalformedOpenPgpMessageException; import sop.Verification; @@ -69,7 +69,7 @@ public class DetachedVerifyImpl implements DetachedVerify { Streams.drain(decryptionStream); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); List verificationList = new ArrayList<>(); for (SignatureVerification signatureVerification : metadata.getVerifiedDetachedSignatures()) { From c031ea92856360fea41a6717bd99ef6ea52289ef Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 15:51:31 +0100 Subject: [PATCH 0589/1204] Remove empty newlines --- .../test/java/org/pgpainless/example/ManagePolicy.java | 6 ------ .../src/test/java/org/pgpainless/example/ModifyKeys.java | 8 -------- .../src/test/java/org/pgpainless/example/ReadKeys.java | 2 -- .../java/org/pgpainless/example/UnlockSecretKeys.java | 2 -- 4 files changed, 18 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java index 711de225..858c99a9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java @@ -88,7 +88,6 @@ public class ManagePolicy { // Per default, non-revocation signatures using SHA-1 are rejected assertFalse(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA1)); - // Create a new custom policy which contains SHA-1 Policy.HashAlgorithmPolicy customPolicy = new Policy.HashAlgorithmPolicy( // The default hash algorithm will be used when hash algorithm negotiation fails when creating a sig @@ -98,7 +97,6 @@ public class ManagePolicy { // Set the hash algo policy as policy for non-revocation signatures policy.setSignatureHashAlgorithmPolicy(customPolicy); - sigHashAlgoPolicy = policy.getSignatureHashAlgorithmPolicy(); assertTrue(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA512)); // SHA-1 is now acceptable as well @@ -122,7 +120,6 @@ public class ManagePolicy { assertFalse(pkAlgorithmPolicy.isAcceptable(PublicKeyAlgorithm.RSA_GENERAL, 1024)); assertTrue(pkAlgorithmPolicy.isAcceptable(PublicKeyAlgorithm.ECDSA, 256)); - Policy.PublicKeyAlgorithmPolicy customPolicy = new Policy.PublicKeyAlgorithmPolicy( new HashMap(){{ // Put minimum bit strengths for acceptable algorithms. @@ -133,7 +130,6 @@ public class ManagePolicy { ); policy.setPublicKeyAlgorithmPolicy(customPolicy); - pkAlgorithmPolicy = policy.getPublicKeyAlgorithmPolicy(); assertTrue(pkAlgorithmPolicy.isAcceptable(PublicKeyAlgorithm.RSA_GENERAL, 4096)); // RSA 2048 is no longer acceptable @@ -156,10 +152,8 @@ public class ManagePolicy { NotationRegistry notationRegistry = policy.getNotationRegistry(); assertFalse(notationRegistry.isKnownNotation("unknown@pgpainless.org")); - notationRegistry.addKnownNotation("unknown@pgpainless.org"); - assertTrue(notationRegistry.isKnownNotation("unknown@pgpainless.org")); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java index a0785993..768064e7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java @@ -67,7 +67,6 @@ public class ModifyKeys { // the certificate consists of only the public keys PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); - KeyRingInfo info = PGPainless.inspectKeyRing(certificate); assertFalse(info.isSecretKey()); } @@ -79,11 +78,9 @@ public class ModifyKeys { public void toAsciiArmoredString() throws IOException { PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); - String asciiArmoredSecretKey = PGPainless.asciiArmor(secretKey); String asciiArmoredCertificate = PGPainless.asciiArmor(certificate); - assertTrue(asciiArmoredSecretKey.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----")); assertTrue(asciiArmoredCertificate.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")); } @@ -99,7 +96,6 @@ public class ModifyKeys { .toNewPassphrase(Passphrase.fromPassword("n3wP4ssW0rD")) .done(); - // Old passphrase no longer works assertThrows(WrongPassphraseException.class, () -> UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(), Passphrase.fromPassword(originalPassphrase))); @@ -120,7 +116,6 @@ public class ModifyKeys { .toNewPassphrase(Passphrase.fromPassword("cryptP4ssphr4s3")) .done(); - // encryption key can now only be unlocked using the new passphrase assertThrows(WrongPassphraseException.class, () -> UnlockSecretKey.unlockSecretKey( @@ -143,7 +138,6 @@ public class ModifyKeys { .addUserId("additional@user.id", protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); assertTrue(info.isUserIdValid("additional@user.id")); assertFalse(info.isUserIdValid("another@user.id")); @@ -176,7 +170,6 @@ public class ModifyKeys { protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); assertEquals(4, info.getSecretKeys().size()); assertEquals(4, info.getPublicKeys().size()); @@ -199,7 +192,6 @@ public class ModifyKeys { .setExpirationDate(expirationDate, protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); assertEquals(DateUtil.formatUTCDate(expirationDate), DateUtil.formatUTCDate(info.getPrimaryKeyExpirationDate())); diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ReadKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/ReadKeys.java index 60256c06..54e00c26 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ReadKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ReadKeys.java @@ -44,7 +44,6 @@ public class ReadKeys { PGPPublicKeyRing publicKey = PGPainless.readKeyRing() .publicKeyRing(certificate); - KeyRingInfo keyInfo = new KeyRingInfo(publicKey); OpenPgpFingerprint fingerprint = new OpenPgpV4Fingerprint("EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E"); assertEquals(fingerprint, keyInfo.getFingerprint()); @@ -77,7 +76,6 @@ public class ReadKeys { PGPSecretKeyRing secretKey = PGPainless.readKeyRing() .secretKeyRing(key); - KeyRingInfo keyInfo = new KeyRingInfo(secretKey); OpenPgpFingerprint fingerprint = new OpenPgpV4Fingerprint("EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E"); assertEquals(fingerprint, keyInfo.getFingerprint()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/UnlockSecretKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/UnlockSecretKeys.java index c6f227f3..92387978 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/UnlockSecretKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/UnlockSecretKeys.java @@ -40,7 +40,6 @@ public class UnlockSecretKeys { // This protector will only unlock unprotected keys SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - assertProtectorUnlocksAllSecretKeys(unprotectedKey, protector); } @@ -105,7 +104,6 @@ public class UnlockSecretKeys { protector.addPassphrase(new OpenPgpV4Fingerprint("DD8E1195E4B1720E7FB10EF7F60402708E75D941"), Passphrase.fromPassword("s3c0ndsubk3y")); - assertProtectorUnlocksAllSecretKeys(secretKey, protector); } From f005885318321b397b83371de3e6efc874c09ccd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 15:52:04 +0100 Subject: [PATCH 0590/1204] Add MessageMetadata.isVerifiedSigned() and .getVerifiedSignatures() --- .../decryption_verification/MessageMetadata.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index da6902c5..7743d32e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -186,6 +186,12 @@ public class MessageMetadata { return isVerifiedInlineSignedBy(keys) || isVerifiedDetachedSignedBy(keys); } + public List getVerifiedSignatures() { + List allVerifiedSignatures = getVerifiedInlineSignatures(); + allVerifiedSignatures.addAll(getVerifiedDetachedSignatures()); + return allVerifiedSignatures; + } + public boolean isVerifiedDetachedSignedBy(@Nonnull PGPKeyRing keys) { return containsSignatureBy(getVerifiedDetachedSignatures(), keys); } @@ -403,6 +409,10 @@ public class MessageMetadata { return firstOrNull(map(getEncryptionLayers(), encryptedData -> encryptedData.decryptionKey)); } + public boolean isVerifiedSigned() { + return !getVerifiedSignatures().isEmpty(); + } + public interface Packet { } From 27fd15a01262949b77f9854aa8f53d907980335d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 15:52:15 +0100 Subject: [PATCH 0591/1204] Update examples with new MessageMetadata class --- .../pgpainless/example/DecryptOrVerify.java | 21 +++++----- .../java/org/pgpainless/example/Encrypt.java | 42 +++++++++---------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java index 074fc206..2e6c982f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java @@ -24,7 +24,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; @@ -168,9 +168,9 @@ public class DecryptOrVerify { decryptionStream.close(); // remember to close the stream! // The metadata object contains information about the message - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); assertTrue(metadata.isEncrypted()); // message was encrypted - assertFalse(metadata.isVerified()); // We did not do any signature verification + assertFalse(metadata.isVerifiedSigned()); // We did not do any signature verification // The output stream now contains the decrypted message assertEquals(PLAINTEXT, plaintextOut.toString()); @@ -200,11 +200,10 @@ public class DecryptOrVerify { decryptionStream.close(); // remember to close the stream to finish signature verification // metadata with information on the message, like signatures - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); assertTrue(metadata.isEncrypted()); // messages was in fact encrypted - assertTrue(metadata.isSigned()); // message contained some signatures - assertTrue(metadata.isVerified()); // the signatures were actually correct - assertTrue(metadata.containsVerifiedSignatureFrom(certificate)); // the signatures could be verified using the certificate + assertTrue(metadata.isVerifiedSigned()); // the signatures were actually correct + assertTrue(metadata.isVerifiedSignedBy(certificate)); // the signatures could be verified using the certificate assertEquals(PLAINTEXT, plaintextOut.toString()); } @@ -232,8 +231,8 @@ public class DecryptOrVerify { verificationStream.close(); // remember to close the stream to finish sig verification // Get the metadata object for information about the message - OpenPgpMetadata metadata = verificationStream.getResult(); - assertTrue(metadata.isVerified()); // signatures were verified successfully + MessageMetadata metadata = verificationStream.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); // signatures were verified successfully // The output stream we piped to now contains the message assertEquals(PLAINTEXT, out.toString()); } @@ -286,8 +285,8 @@ public class DecryptOrVerify { verificationStream.close(); // as always, remember to close the stream // Metadata will confirm that the message was in fact signed - OpenPgpMetadata metadata = verificationStream.getResult(); - assertTrue(metadata.isVerified()); + MessageMetadata metadata = verificationStream.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); // compare the plaintext to what we originally signed assertArrayEquals(msg.getBytes(StandardCharsets.UTF_8), plain.toByteArray()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java index ed57b90b..ba832516 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java @@ -21,7 +21,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; @@ -148,12 +148,12 @@ public class Encrypt { EncryptionStream encryptor = PGPainless.encryptAndOrSign() .onOutputStream(ciphertext) .withOptions(ProducerOptions.signAndEncrypt( - // we want to encrypt communication (affects key selection based on key flags) - EncryptionOptions.encryptCommunications() - .addRecipient(certificateBob) - .addRecipient(certificateAlice), - new SigningOptions() - .addInlineSignature(protectorAlice, keyAlice, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) + // we want to encrypt communication (affects key selection based on key flags) + EncryptionOptions.encryptCommunications() + .addRecipient(certificateBob) + .addRecipient(certificateAlice), + new SigningOptions() + .addInlineSignature(protectorAlice, keyAlice, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) ).setAsciiArmor(true) ); @@ -176,9 +176,9 @@ public class Encrypt { decryptor.close(); // Check the metadata to see how the message was encrypted/signed - OpenPgpMetadata metadata = decryptor.getResult(); + MessageMetadata metadata = decryptor.getMetadata(); assertTrue(metadata.isEncrypted()); - assertTrue(metadata.containsVerifiedSignatureFrom(certificateAlice)); + assertTrue(metadata.isVerifiedSignedBy(certificateAlice)); assertEquals(message, plaintext.toString()); } @@ -236,10 +236,10 @@ public class Encrypt { // plaintext message to encrypt String message = "Hello, World!\n"; String[] comments = { - "This comment was added using options.", - "And it has three lines.", - " ", - "Empty lines are skipped." + "This comment was added using options.", + "And it has three lines.", + " ", + "Empty lines are skipped." }; String comment = comments[0] + "\n" + comments[1] + "\n" + comments[2] + "\n" + comments[3]; ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); @@ -247,12 +247,12 @@ public class Encrypt { EncryptionStream encryptor = PGPainless.encryptAndOrSign() .onOutputStream(ciphertext) .withOptions(ProducerOptions.encrypt( - // we want to encrypt communication (affects key selection based on key flags) - EncryptionOptions.encryptCommunications() - .addRecipient(certificateBob) - .addRecipient(certificateAlice) - ).setAsciiArmor(true) - .setComment(comment) + // we want to encrypt communication (affects key selection based on key flags) + EncryptionOptions.encryptCommunications() + .addRecipient(certificateBob) + .addRecipient(certificateAlice) + ).setAsciiArmor(true) + .setComment(comment) ); // Pipe data trough and CLOSE the stream (important) @@ -281,10 +281,8 @@ public class Encrypt { decryptor.close(); // Check the metadata to see how the message was encrypted/signed - OpenPgpMetadata metadata = decryptor.getResult(); + MessageMetadata metadata = decryptor.getMetadata(); assertTrue(metadata.isEncrypted()); assertEquals(message, plaintext.toString()); } - - } From 2c7801b7590e4080bb5964997b239f44c782304c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 16:09:37 +0100 Subject: [PATCH 0592/1204] Add MatchMakingSecretKeyRingProtectorTest --- ...MatchMakingSecretKeyRingProtectorTest.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java new file mode 100644 index 00000000..9dc2b6c6 --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.util.Passphrase; + +public class MatchMakingSecretKeyRingProtectorTest { + + private static final String PROTECTED_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 0221 626A 3B5A 4705 7A41 7EAB 2B9F C90E 44FA 1947\n" + + "Comment: Alice\n" + + "\n" + + "lIYEY3zkcRYJKwYBBAHaRw8BAQdAzww+ctlV7imTD/LSQlVn3onybSvQa54CIUaN\n" + + "xN9FDFH+CQMCqDw0ZfofkfxgK7+uSfi7btqa6+o+zGkKfKQCvYCuU5gorD7vyOFL\n" + + "2ezeQOjb17HHaKbJqLrx+p+LS2uU2f3cwa73PFHwNcBoDLRTrUXjzrQFQWxpY2WI\n" + + "jwQTFgoAQQUCY3zkcQkQK5/JDkT6GUcWIQQCIWJqO1pHBXpBfqsrn8kORPoZRwKe\n" + + "AQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAABTCQD9HCDmb8LlO+n/5jJv7n6gAHCA\n" + + "UUNAe7xU4WcYSxLpTPIBAOxBmbZiDai0QwDOqNihpwrInu82fRi8OEpSjE/9OrEC\n" + + "nIsEY3zkcRIKKwYBBAGXVQEFAQEHQBsnJtVYXMaGB4BDcUEKB1v/lsXJ1z+favfn\n" + + "e73/crYEAwEIB/4JAwKoPDRl+h+R/GBdZY7QJt8TPaXckyOR1eZvUejD+Vw/slB1\n" + + "3KUwGI/3MG2iJYp924wP67DewZI89eYHu24wN75XxVKAGnUX5n7Dr2JIB79liHUE\n" + + "GBYKAB0FAmN85HECngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRArn8kORPoZR5bF\n" + + "APsHLmhDRDV2Ra0BsfQRNI2yMXxVRFD/ZzryWFT/BPNGUAEA9cnhItp9ucqBbeWE\n" + + "PDzf1vdx5BCNYhRpOqGjGtFgMQGchgRjfORxFgkrBgEEAdpHDwEBB0AAvsniUT76\n" + + "OeLyq9e1bgdsDLiGrtroSt6wR/B94Dm5uP4JAwKoPDRl+h+R/GC8mQnynSzBJXdy\n" + + "DFDnxOieEOh7390vs3P4NwULTqV12sAQ6i5MbsIHnFMtYCCA9aOPlpofQ0Sm3m6q\n" + + "T/uyx9RE1LRiceW5iNUEGBYKAH0FAmN85HECngECmwIFFgIDAQAECwkIBwUVCgkI\n" + + "C18gBBkWCgAGBQJjfORxAAoJEGiOcMMZDPmyw7QBAJuNTLiNWgieuGOVCAmkaN6g\n" + + "L6JlYYwqFS88zzDLJJq5AQDwKt+jvKco6Mya3b1NEXogBLhWHTle9deL07NrCwp4\n" + + "AwAKCRArn8kORPoZR7r0AP0TDUKaooNfW2MWqLWHbbIhdWFIQEYIGnGSFj28y6t1\n" + + "zQD/UFtpzBP5ZlTUtZCdjNqo9SEPktbiOxTS8m4SW7xeNwE=\n" + + "=91/N\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + private static final String PASSWORD = "sw0rdf1sh"; + private static final String UNPROTECTED_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9A0A F461 3E00 E1D4 4C29 601E AAF1 12F4 64BB 1E8E\n" + + "Comment: Bob\n" + + "\n" + + "lFgEY3zlDBYJKwYBBAHaRw8BAQdAvHofWedfSBvyW2+gCADX9CptwFzqVea4A2tL\n" + + "zr3wnwsAAP9ICAoMGkgdNLy3LiVP0q4+OljXcQTIAJbJ2wCpIF9Y7g05tANCb2KI\n" + + "jwQTFgoAQQUCY3zlDAkQqvES9GS7Ho4WIQSaCvRhPgDh1EwpYB6q8RL0ZLsejgKe\n" + + "AQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAAZ8QD/fEW105B77KBt/OmA0QLTq3GG\n" + + "5PI6kITM8+2cd60VOzEA/ivzkhmtdvHzmOARBl81Y3LfeRWWm45z/dYDnffk/DcI\n" + + "nF0EY3zlDBIKKwYBBAGXVQEFAQEHQC1sYpvzEsjCoTOKEllFkWA3U51FXsHbbALq\n" + + "QfprOrYKAwEIBwAA/3H4zdk83/0A55hJxBIgh3v/+EV1RKPDCjMHjI5ULc7AEa6I\n" + + "dQQYFgoAHQUCY3zlDAKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEKrxEvRkux6O\n" + + "pEsA/imXUEpj6mKkT4ZBioT7Gn2mUR4iMGS/pt7QBscDX2/PAP9FFRzsaDII1K+i\n" + + "zW5sHEif9EjgX6ThIpg8z4/5/7yQBZxYBGN85QwWCSsGAQQB2kcPAQEHQDTAYxP0\n" + + "rH0tjpOKOxdoHKq87n4tYXd1t/A9Nzjbl36AAAD+PMBIpNmN+k3THARd9UGQtLo4\n" + + "nieLnqbuPVtMps0kQjgQxojVBBgWCgB9BQJjfOUMAp4BApsCBRYCAwEABAsJCAcF\n" + + "FQoJCAtfIAQZFgoABgUCY3zlDAAKCRB0YfQ676jh4zoMAP98SwGcoy8Vzk8QnQ0X\n" + + "gziC+4HtmTLuiDVAvrMLpPz5cwD8C40DDHEjrOJs9bgyOeTELXtjq40Wrt2Fld0G\n" + + "3JJpFAwACgkQqvES9GS7Ho7EXwD7BwICVWrg458XKpy2EXGSI3mGA47EbyyFc9X3\n" + + "lBzjnCgA/jUBlZE2LhpAyMTbjDC9eAD1iXeTALdRKBeqnZrQTL0N\n" + + "=OypZ\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + @Test + public void addSamePasswordTwice() throws IOException { + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(PROTECTED_KEY); + MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + protector.addPassphrase(Passphrase.fromPassword(PASSWORD)); + protector.addPassphrase(Passphrase.fromPassword(PASSWORD)); + protector.addSecretKey(key); + + assertTrue(protector.hasPassphraseFor(key.getPublicKey().getKeyID())); + } + + @Test + public void addKeyTwiceAndEmptyPasswordTest() throws IOException { + PGPSecretKeyRing unprotectedKey = PGPainless.readKeyRing().secretKeyRing(UNPROTECTED_KEY); + MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + protector.addSecretKey(unprotectedKey); + protector.addPassphrase(Passphrase.emptyPassphrase()); + protector.addSecretKey(unprotectedKey); + assertTrue(protector.hasPassphraseFor(unprotectedKey.getPublicKey().getKeyID())); + } + + @Test + public void getEncryptorTest() throws IOException, PGPException { + PGPSecretKeyRing unprotectedKey = PGPainless.readKeyRing().secretKeyRing(UNPROTECTED_KEY); + MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + protector.addSecretKey(unprotectedKey); + assertTrue(protector.hasPassphraseFor(unprotectedKey.getPublicKey().getKeyID())); + assertNull(protector.getEncryptor(unprotectedKey.getPublicKey().getKeyID())); + assertNull(protector.getDecryptor(unprotectedKey.getPublicKey().getKeyID())); + + PGPSecretKeyRing protectedKey = PGPainless.readKeyRing().secretKeyRing(PROTECTED_KEY); + protector.addSecretKey(protectedKey); + protector.addPassphrase(Passphrase.fromPassword(PASSWORD)); + assertNotNull(protector.getEncryptor(protectedKey.getPublicKey().getKeyID())); + assertNotNull(protector.getDecryptor(protectedKey.getPublicKey().getKeyID())); + } +} From b36b5413e2a93bf0d7a7caba0deffa7bc34a8746 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 16:22:01 +0100 Subject: [PATCH 0593/1204] Fix isEncryptedFor() --- .../MessageMetadata.java | 3 ++ .../OpenPgpMessageInputStream.java | 46 ++++++++++++------- .../pgpainless/example/DecryptOrVerify.java | 3 ++ 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 7743d32e..8c18eadb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -698,6 +698,9 @@ public class MessageMetadata { * @return recipients */ public @Nonnull List getRecipients() { + if (recipients == null) { + return new ArrayList<>(); + } return new ArrayList<>(recipients); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index a820dca5..263871de 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -428,7 +428,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } // attempt decryption - if (decryptPKESKAndStream(subkeyIdentifier, decryptorFactory, pkesk)) { + if (decryptPKESKAndStream(esks, subkeyIdentifier, decryptorFactory, pkesk)) { return true; } } @@ -473,7 +473,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPBEDataDecryptorFactory(passphrase); - if (decryptSKESKAndStream(skesk, decryptorFactory)) { + if (decryptSKESKAndStream(esks, skesk, decryptorFactory)) { return true; } } @@ -506,7 +506,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); - if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { + if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -529,7 +529,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); - if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { + if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -561,7 +561,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { LOGGER.debug("Attempt decryption with key " + decryptionKeyId + " while interactively requesting its passphrase"); SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); - if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { + if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { return true; } } @@ -576,13 +576,14 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } - private boolean decryptWithPrivateKey(PGPPrivateKey privateKey, + private boolean decryptWithPrivateKey(SortedESKs esks, + PGPPrivateKey privateKey, SubkeyIdentifier decryptionKeyId, PGPPublicKeyEncryptedData pkesk) throws PGPException, IOException { PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() .getPublicKeyDataDecryptorFactory(privateKey); - return decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk); + return decryptPKESKAndStream(esks, decryptionKeyId, decryptorFactory, pkesk); } private static boolean hasUnsupportedS2KSpecifier(PGPSecretKey secretKey, SubkeyIdentifier decryptionKeyId) { @@ -597,17 +598,23 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } - private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) + private boolean decryptSKESKAndStream(SortedESKs esks, + PGPPBEEncryptedData symEsk, + PBEDataDecryptorFactory decryptorFactory) throws IOException, UnacceptableAlgorithmException { try { - InputStream decrypted = skesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); + InputStream decrypted = symEsk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(symEsk.getSessionKey(decryptorFactory)); throwIfUnacceptable(sessionKey.getAlgorithm()); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( sessionKey.getAlgorithm(), metadata.depth + 1); encryptedData.sessionKey = sessionKey; + encryptedData.recipients = new ArrayList<>(); + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + encryptedData.recipients.add(pkesk.getKeyID()); + } LOGGER.debug("Successfully decrypted data with passphrase"); - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, symEsk, options); nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { @@ -618,23 +625,28 @@ public class OpenPgpMessageInputStream extends DecryptionStream { return false; } - private boolean decryptPKESKAndStream(SubkeyIdentifier decryptionKeyId, + private boolean decryptPKESKAndStream(SortedESKs esks, + SubkeyIdentifier decryptionKeyId, PublicKeyDataDecryptorFactory decryptorFactory, - PGPPublicKeyEncryptedData pkesk) + PGPPublicKeyEncryptedData asymEsk) throws IOException, UnacceptableAlgorithmException { try { - InputStream decrypted = pkesk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + InputStream decrypted = asymEsk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(asymEsk.getSessionKey(decryptorFactory)); throwIfUnacceptable(sessionKey.getAlgorithm()); MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + SymmetricKeyAlgorithm.requireFromId(asymEsk.getSymmetricAlgorithm(decryptorFactory)), metadata.depth + 1); encryptedData.decryptionKey = decryptionKeyId; encryptedData.sessionKey = sessionKey; + encryptedData.recipients = new ArrayList<>(); + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + encryptedData.recipients.add(pkesk.getKeyID()); + } LOGGER.debug("Successfully decrypted data with key " + decryptionKeyId); - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, asymEsk, options); nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); return true; } catch (UnacceptableAlgorithmException e) { diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java index 2e6c982f..c35b3572 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java @@ -170,6 +170,7 @@ public class DecryptOrVerify { // The metadata object contains information about the message MessageMetadata metadata = decryptionStream.getMetadata(); assertTrue(metadata.isEncrypted()); // message was encrypted + assertTrue(metadata.isEncryptedFor(secretKey)); assertFalse(metadata.isVerifiedSigned()); // We did not do any signature verification // The output stream now contains the decrypted message @@ -202,6 +203,7 @@ public class DecryptOrVerify { // metadata with information on the message, like signatures MessageMetadata metadata = decryptionStream.getMetadata(); assertTrue(metadata.isEncrypted()); // messages was in fact encrypted + assertTrue(metadata.isEncryptedFor(certificate)); assertTrue(metadata.isVerifiedSigned()); // the signatures were actually correct assertTrue(metadata.isVerifiedSignedBy(certificate)); // the signatures could be verified using the certificate @@ -233,6 +235,7 @@ public class DecryptOrVerify { // Get the metadata object for information about the message MessageMetadata metadata = verificationStream.getMetadata(); assertTrue(metadata.isVerifiedSigned()); // signatures were verified successfully + assertTrue(metadata.isVerifiedSignedBy(certificate)); // The output stream we piped to now contains the message assertEquals(PLAINTEXT, out.toString()); } From 25190fc5df41ad3a6c2acb245d6abf63ce7d197a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 16:27:34 +0100 Subject: [PATCH 0594/1204] SOP: Use new MessageMetadata class --- .../decryption_verification/MessageMetadata.java | 4 ++++ .../src/main/java/org/pgpainless/sop/DecryptImpl.java | 6 +++--- .../main/java/org/pgpainless/sop/InlineVerifyImpl.java | 8 ++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 8c18eadb..648b99ab 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -69,6 +69,10 @@ public class MessageMetadata { return resultBuilder.build(); } + public boolean isUsingCleartextSignatureFramework() { + return message.isCleartextSigned(); + } + public boolean isEncrypted() { SymmetricKeyAlgorithm algorithm = getEncryptionAlgorithm(); return algorithm != null && algorithm != SymmetricKeyAlgorithm.NULL; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index ee86f5e8..867024ef 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -21,7 +21,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MissingDecryptionMethodException; @@ -136,14 +136,14 @@ public class DecryptImpl implements Decrypt { public DecryptionResult writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { Streams.pipeAll(decryptionStream, outputStream); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); if (!metadata.isEncrypted()) { throw new SOPGPException.BadData("Data is not encrypted."); } List verificationList = new ArrayList<>(); - for (SignatureVerification signatureVerification : metadata.getVerifiedInbandSignatures()) { + for (SignatureVerification signatureVerification : metadata.getVerifiedInlineSignatures()) { verificationList.add(map(signatureVerification)); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 82ed4282..f33f718b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -17,7 +17,7 @@ import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MissingDecryptionMethodException; @@ -63,12 +63,12 @@ public class InlineVerifyImpl implements InlineVerify { Streams.pipeAll(decryptionStream, outputStream); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); List verificationList = new ArrayList<>(); - List verifications = metadata.isCleartextSigned() ? + List verifications = metadata.isUsingCleartextSignatureFramework() ? metadata.getVerifiedDetachedSignatures() : - metadata.getVerifiedInbandSignatures(); + metadata.getVerifiedInlineSignatures(); for (SignatureVerification signatureVerification : verifications) { verificationList.add(map(signatureVerification)); From b495e602e58b606472730670750f2185c83b57f2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 22 Nov 2022 16:30:06 +0100 Subject: [PATCH 0595/1204] More precise error message for malformed message --- .../pgpainless/decryption_verification/syntax_check/PDA.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index 07a5fdcb..adc5bb39 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -55,7 +55,7 @@ public class PDA { inputs.add(input); } catch (MalformedOpenPgpMessageException e) { MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException( - "Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + + "Malformed message: After reading packet sequence " + Arrays.toString(inputs.toArray()) + ", token '" + input + "' is not allowed." + "\nNo transition from state '" + state + "' with stack " + Arrays.toString(stack.toArray()) + (stackSymbol != null ? "||'" + stackSymbol + "'." : "."), e); From be7349f0b52976f93e4e287b0fd4653a3fbc135d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Nov 2022 20:07:03 +0100 Subject: [PATCH 0596/1204] Clean up CachingBcPublicKeyDataDecryptorFactory --- .../CachingBcPublicKeyDataDecryptorFactory.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java index 3c967224..510b0938 100644 --- a/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java +++ b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java @@ -59,16 +59,20 @@ public class CachingBcPublicKeyDataDecryptorFactory } private byte[] lookupSessionKeyData(byte[][] secKeyData) { - byte[] sk = secKeyData[0]; - String key = Base64.toBase64String(sk); + String key = toKey(secKeyData); byte[] sessionKey = cachedSessionKeys.get(key); return copy(sessionKey); } private void cacheSessionKeyData(byte[][] secKeyData, byte[] sessionKey) { + String key = toKey(secKeyData); + cachedSessionKeys.put(key, copy(sessionKey)); + } + + private static String toKey(byte[][] secKeyData) { byte[] sk = secKeyData[0]; String key = Base64.toBase64String(sk); - cachedSessionKeys.put(key, copy(sessionKey)); + return key; } private static byte[] copy(byte[] bytes) { From c72b3a4b8e251a3fa99df0c010a5785b5ce6a7ff Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Nov 2022 20:07:42 +0100 Subject: [PATCH 0597/1204] Improve CachingBcPublicKeyDataDecryptorFactoryTest --- .../CachingBcPublicKeyDataDecryptorFactoryTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java index 6a43772d..e57df5d9 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -25,6 +25,8 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class CachingBcPublicKeyDataDecryptorFactoryTest { private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -83,7 +85,9 @@ public class CachingBcPublicKeyDataDecryptorFactoryTest { ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decryptionStream, out); decryptionStream.close(); + assertEquals("Hello, World!\n", out.toString()); + ciphertextIn = new ByteArrayInputStream(MSG.getBytes()); decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(ciphertextIn) .withOptions(ConsumerOptions.get() @@ -91,5 +95,8 @@ public class CachingBcPublicKeyDataDecryptorFactoryTest { out = new ByteArrayOutputStream(); Streams.pipeAll(decryptionStream, out); decryptionStream.close(); + assertEquals("Hello, World!\n", out.toString()); + + cachingFactory.clear(); } } From a495f2275c233c6ed7989697da67823e122d5e26 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 21:34:25 +0100 Subject: [PATCH 0598/1204] Precise error message for IntegrityProtectedInputStream --- .../decryption_verification/IntegrityProtectedInputStream.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java index 286160e8..37dcfca4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java @@ -53,7 +53,7 @@ public class IntegrityProtectedInputStream extends InputStream { } LOGGER.debug("Integrity Protection check passed"); } catch (PGPException e) { - throw new IOException("Failed to verify integrity protection", e); + throw new IOException("Data appears to not be integrity protected.", e); } } } From 5bdd4f6ad04d05a97011ea55639fd381136d4fb7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 22:09:22 +0100 Subject: [PATCH 0599/1204] Test rejection of messages with unacceptable skesk kek algorithm --- .../DecryptAndVerifyMessageTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java index f7402238..e939de0a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java @@ -7,6 +7,7 @@ package org.pgpainless.decryption_verification; 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.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; @@ -18,14 +19,17 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.TestKeys; import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.util.Passphrase; import org.pgpainless.util.TestAllImplementations; public class DecryptAndVerifyMessageTest { @@ -118,4 +122,21 @@ public class DecryptAndVerifyMessageTest { assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.JULIET_FINGERPRINT)); assertEquals(new SubkeyIdentifier(TestKeys.JULIET_FINGERPRINT), metadata.getDecryptionKey()); } + + @Test + public void testDecryptMessageWithUnacceptableSymmetricAlgorithm() { + String ciphertext = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0EAQMCZv8glrLeXPhg0jgBpMN+E8dCuEDxJnSi8/e+HOKcdYQbgQh/MG4Kn7NK\n" + + "wRM5wNOFKn8jbsoC+JalzjwzMJSV+ZM1aQ==\n" + + "=9aCQ\n" + + "-----END PGP MESSAGE-----"; + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertext.getBytes()); + assertThrows(MissingDecryptionMethodException.class, + () -> PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addDecryptionPassphrase(Passphrase.fromPassword("sw0rdf1sh")))); + } } From 68886613a6c0c28a79b411d2e9b230c794a317e4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 22:14:06 +0100 Subject: [PATCH 0600/1204] SOP KeyReader: wrap IOException in BadData --- .../src/main/java/org/pgpainless/sop/KeyReader.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java index 064a5e39..5e6f3a7d 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java @@ -31,7 +31,7 @@ class KeyReader { } throw e; } catch (PGPException e) { - throw new IOException("Cannot read keys.", e); + throw new SOPGPException.BadData("Cannot read keys.", e); } if (requireContent && (keys == null || keys.size() == 0)) { @@ -41,7 +41,8 @@ class KeyReader { return keys; } - static PGPPublicKeyRingCollection readPublicKeys(InputStream certIn, boolean requireContent) throws IOException { + static PGPPublicKeyRingCollection readPublicKeys(InputStream certIn, boolean requireContent) + throws IOException { PGPPublicKeyRingCollection certs; try { certs = PGPainless.readKeyRing().publicKeyRingCollection(certIn); From 9919bbf0136b2a835ef564aba94589115962b7bc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 22:20:02 +0100 Subject: [PATCH 0601/1204] Enable test for reading broken keys in SOP --- .../test/java/org/pgpainless/cli/commands/DearmorCmdTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java index 64576bfe..7ebb7308 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/DearmorCmdTest.java @@ -13,7 +13,6 @@ import java.io.IOException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.slf4j.LoggerFactory; @@ -62,8 +61,6 @@ public class DearmorCmdTest extends CLITest { } @Test - @Disabled("Enable with SOP-Java 4.0.7") - // TODO: Enable public void dearmorBrokenArmoredKeyFails() throws IOException { // contains a "-" String invalidBase64 = "lFgEY2vOkhYJKwYBBAHaRw8BAQdAqGOtLd1tKnuwaYYcdr2/7C0cPiCCggRMKG+Wt32QQdEAAP9VaBzjk/AaAqyykZnQHmS1HByEvRLv5/4yJMSr22451BFjtBRhbGljZUBwZ3BhaW5sZXNzLm9yZ4iOBBMWCgBBBQJja86SCRCLB1F3AflTTBYhBGLp3aTyD4NB0rxLT-IsHUXcB+VNMAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAACZhAP4s8hn/RBDvyLvGROOd15EYATnWlgyi+b5WXP6cELalJwD1FZy3RROhfNtZWcJPS43fG03pYNyb0NXoitIMAaXEB5xdBGNrzpISCisGAQQBl1UBBQEBB0CqCcYethOynfni8uRO+r/cZWp9hCLy8pRIExKqzcyEFAMBCAcAAP9sRRLoZkLpDaTNNrtIBovXu2ANhL8keUMWtVcuEHnkQA6iiHUEGBYKAB0FAmNrzpICngECmwwFFgIDAQAECwkIBwUVCgkICwAKCRCLB1F3AflTTBVpAP491etrjqCMWx2bBaw3K1vP0Mix6U0vF3J4kP9UeZm6owEA4kX9VAGESvLgIc7CEiswmxdWjxnLQyCRtWXfjgFmYQucWARja86SFgkrBgEEAdpHDwEBB0DBslhDpWC6CV3xJUSo071NSO5Cf4fgOwOj+QHs8mpFbwABAPkQioSydYiMi04LyfPohyrhhcdJDHallQg+jYHHUb2pEJCI1QQYFgoAfQUCY2vOkgKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmNrzpIACgkQiHlkvEXh+f1eywEA9A2GLU9LxCJxZf2X4qcZY//YJDChIZHPnY0Vaek1DsMBAN1YILrH2rxQeCXjm4bUKfJIRrGt6ZJscwORgNI1dFQFAAoJEIsHUXcB+VNMK3gA/3vvPm57JsHA860wlB4D1II71oFNL8TFnJqTAvpSKe1AAP49S4mKB4PE0ElcDo7n+nEYt6ba8IMRDlMorsH85mUgCw=="; From 39d656d2dd86ccf758bf32b1e2d8a8ad5b9a979d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 22:22:21 +0100 Subject: [PATCH 0602/1204] Add javadoc for HardwareDataDecryptorFactory constructor argument --- .../org/pgpainless/decryption_verification/HardwareSecurity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java index daf902d4..cdadf2c4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -51,6 +51,7 @@ public class HardwareSecurity { /** * Create a new {@link HardwareDataDecryptorFactory}. * + * @param subkeyIdentifier identifier of the decryption subkey * @param callback decryption callback */ public HardwareDataDecryptorFactory(SubkeyIdentifier subkeyIdentifier, DecryptionCallback callback) { From e88a88a447e78c3ee7bc5bea703eed1fffbde6d2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 22:24:12 +0100 Subject: [PATCH 0603/1204] Add javadoc for OpenPgpMessageInputStream factory method return value --- .../decryption_verification/OpenPgpMessageInputStream.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 263871de..c26c949b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -105,6 +105,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { * * @param inputStream underlying input stream * @param options options for consuming the stream + * @return input stream that consumes OpenPGP messages * * @throws IOException in case of an IO error * @throws PGPException in case of an OpenPGP error @@ -123,6 +124,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { * @param inputStream underlying input stream containing the OpenPGP message * @param options options for consuming the message * @param policy policy for acceptable algorithms etc. + * @return input stream that consumes OpenPGP messages * * @throws PGPException in case of an OpenPGP error * @throws IOException in case of an IO error From ce049bf9a42b8a70cc6c1e9acba2effefab989de Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 22:25:27 +0100 Subject: [PATCH 0604/1204] PGPainless 1.4.0-rc2 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fc7e655..0a335092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.4.0-rc2-SNAPSHOT +## 1.4.0-rc2 - Bump `bcpg-jdk15to18` to `1.72.3` - Use BCs `PGPEncryptedDataList.extractSessionKeyEncryptedData()` method to do decryption using session keys. This enables decryption of messages diff --git a/README.md b/README.md index 1855f742..30af42f8 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.0-rc1' + implementation 'org.pgpainless:pgpainless-core:1.4.0-rc2' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 893804dd..2e4927a6 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.0-rc1" + implementation "org.pgpainless:pgpainless-sop:1.4.0-rc2" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.0-rc1 + 1.4.0-rc2 ... diff --git a/version.gradle b/version.gradle index 86fc8f54..aee46cd8 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.4.0-rc2' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From c8c93594853a881e527624e8d8044cc5e94c96da Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Nov 2022 22:28:15 +0100 Subject: [PATCH 0605/1204] PGPainless 1.4.0-rc3-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index aee46cd8..2d9745dd 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.0-rc2' - isSnapshot = false + shortVersion = '1.4.0-rc3' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 3f70936ff158c7d0c0b884751cb2d0ad38541b55 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Nov 2022 14:26:55 +0100 Subject: [PATCH 0606/1204] Add documetation to PDA class --- .../syntax_check/PDA.java | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java index adc5bb39..7d7cf973 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -9,6 +9,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -17,6 +18,12 @@ import java.util.Stack; import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.msg; import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.terminus; +/** + * Pushdown Automaton for validating context-free languages. + * In PGPainless, this class is used to validate OpenPGP message packet sequences against the allowed syntax. + * + * @see OpenPGP Message Syntax + */ public class PDA { private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); @@ -36,6 +43,13 @@ public class PDA { this(new OpenPgpMessageSyntax(), State.OpenPgpMessage, terminus, msg); } + /** + * Construct a PDA with a custom {@link Syntax}, initial {@link State} and initial {@link StackSymbol StackSymbols}. + * + * @param syntax syntax + * @param initialState initial state + * @param initialStack zero or more initial stack items (get pushed onto the stack in order of appearance) + */ public PDA(@Nonnull Syntax syntax, @Nonnull State initialState, @Nonnull StackSymbol... initialStack) { this.syntax = syntax; this.state = initialState; @@ -44,7 +58,16 @@ public class PDA { } } - public void next(InputSymbol input) throws MalformedOpenPgpMessageException { + /** + * Process the next {@link InputSymbol}. + * This will either leave the PDA in the next state, or throw a {@link MalformedOpenPgpMessageException} if the + * input symbol is rejected. + * + * @param input input symbol + * @throws MalformedOpenPgpMessageException if the input symbol is rejected + */ + public void next(@Nonnull InputSymbol input) + throws MalformedOpenPgpMessageException { StackSymbol stackSymbol = popStack(); try { Transition transition = syntax.transition(state, input, stackSymbol); @@ -69,11 +92,16 @@ public class PDA { * * @return state */ - public State getState() { + public @Nonnull State getState() { return state; } - public StackSymbol peekStack() { + /** + * Peek at the stack, returning the topmost stack item without changing the stack. + * + * @return topmost stack item, or null if stack is empty + */ + public @Nullable StackSymbol peekStack() { if (stack.isEmpty()) { return null; } @@ -89,6 +117,11 @@ public class PDA { return getState() == State.Valid && stack.isEmpty(); } + /** + * Throw a {@link MalformedOpenPgpMessageException} if the pda is not in a valid state right now. + * + * @throws MalformedOpenPgpMessageException if the pda is not in an acceptable state + */ public void assertValid() throws MalformedOpenPgpMessageException { if (!isValid()) { throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString()); From 7cc27515272f192fab678bbd32c9fc79bd0bbe27 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Nov 2022 14:38:45 +0100 Subject: [PATCH 0607/1204] Add @Nonnull annotations to OpenPgpMessageSyntax --- .../syntax_check/OpenPgpMessageSyntax.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java index c6de8765..6abb507a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -40,6 +40,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(from, input, stackItem); } + @Nonnull Transition fromOpenPgpMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { if (stackItem != StackSymbol.msg) { @@ -68,6 +69,7 @@ public class OpenPgpMessageSyntax implements Syntax { } } + @Nonnull Transition fromLiteralMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { @@ -87,6 +89,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem); } + @Nonnull Transition fromCompressedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { @@ -106,6 +109,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem); } + @Nonnull Transition fromEncryptedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { switch (input) { @@ -125,6 +129,7 @@ public class OpenPgpMessageSyntax implements Syntax { throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem); } + @Nonnull Transition fromValid(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { // There is no applicable transition rule out of Valid From e1ab128c2e01db597d4f584dc3555a89b3c2d810 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Nov 2022 14:40:57 +0100 Subject: [PATCH 0608/1204] Add annotations to GnuPGDummyKeyUtil --- .../java/org/gnupg/GnuPGDummyKeyUtil.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java index 983d8e68..42af92d8 100644 --- a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java +++ b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java @@ -13,6 +13,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.pgpainless.key.SubkeyIdentifier; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -40,7 +41,7 @@ public final class GnuPGDummyKeyUtil { * @param secretKeys secret keys * @return set of keys with S2K type GNU_DUMMY_S2K and protection mode DIVERT_TO_CARD */ - public static Set getIdsOfKeysWithGnuPGS2KDivertedToCard(PGPSecretKeyRing secretKeys) { + public static Set getIdsOfKeysWithGnuPGS2KDivertedToCard(@Nonnull PGPSecretKeyRing secretKeys) { Set hardwareBackedKeys = new HashSet<>(); for (PGPSecretKey secretKey : secretKeys) { S2K s2K = secretKey.getS2K(); @@ -65,7 +66,7 @@ public final class GnuPGDummyKeyUtil { * @param secretKeys secret keys * @return builder */ - public static Builder modify(PGPSecretKeyRing secretKeys) { + public static Builder modify(@Nonnull PGPSecretKeyRing secretKeys) { return new Builder(secretKeys); } @@ -73,7 +74,7 @@ public final class GnuPGDummyKeyUtil { private final PGPSecretKeyRing keys; - private Builder(PGPSecretKeyRing keys) { + private Builder(@Nonnull PGPSecretKeyRing keys) { this.keys = keys; } @@ -84,7 +85,7 @@ public final class GnuPGDummyKeyUtil { * @param filter filter to select keys for removal * @return modified key ring */ - public PGPSecretKeyRing removePrivateKeys(KeyFilter filter) { + public PGPSecretKeyRing removePrivateKeys(@Nonnull KeyFilter filter) { return replacePrivateKeys(GnuPGDummyExtension.NO_PRIVATE_KEY, null, filter); } @@ -92,13 +93,12 @@ public final class GnuPGDummyKeyUtil { * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. * This method will set the serial number of the card to 0x00000000000000000000000000000000. - * * NOTE: This method does not actually move any keys to a card. * * @param filter filter to select keys for removal * @return modified key ring */ - public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter) { + public PGPSecretKeyRing divertPrivateKeysToCard(@Nonnull KeyFilter filter) { return divertPrivateKeysToCard(filter, new byte[16]); } @@ -106,21 +106,22 @@ public final class GnuPGDummyKeyUtil { * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. * This method will include the card serial number into the encoded dummy key. - * * NOTE: This method does not actually move any keys to a card. * * @param filter filter to select keys for removal * @param cardSerialNumber serial number of the card (at most 16 bytes long) * @return modified key ring */ - public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter, byte[] cardSerialNumber) { + public PGPSecretKeyRing divertPrivateKeysToCard(@Nonnull KeyFilter filter, @Nullable byte[] cardSerialNumber) { if (cardSerialNumber != null && cardSerialNumber.length > 16) { throw new IllegalArgumentException("Card serial number length cannot exceed 16 bytes."); } return replacePrivateKeys(GnuPGDummyExtension.DIVERT_TO_CARD, cardSerialNumber, filter); } - private PGPSecretKeyRing replacePrivateKeys(GnuPGDummyExtension extension, byte[] serial, KeyFilter filter) { + private PGPSecretKeyRing replacePrivateKeys(@Nonnull GnuPGDummyExtension extension, + @Nullable byte[] serial, + @Nonnull KeyFilter filter) { byte[] encodedSerial = serial != null ? encodeSerial(serial) : null; S2K s2k = extensionToS2K(extension); From 4426895814952b6ec10e4e7a9a49b2dd358dd200 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Nov 2022 14:55:46 +0100 Subject: [PATCH 0609/1204] Add tests for CollectionUtils --- .../pgpainless/util/CollectionUtilsTest.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtilsTest.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtilsTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtilsTest.java new file mode 100644 index 00000000..2bfe6cb1 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtilsTest.java @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util; + +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.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.junit.jupiter.api.Test; + +public class CollectionUtilsTest { + + @Test + public void testConcat() { + String a = "A"; + String[] bc = new String[] {"B", "C"}; + + String[] abc = CollectionUtils.concat(a, bc); + assertArrayEquals(new String[] {"A", "B", "C"}, abc); + } + + @Test + public void testConcatWithEmptyArray() { + String a = "A"; + String[] empty = new String[0]; + + String[] concat = CollectionUtils.concat(a, empty); + assertArrayEquals(new String[] {"A"}, concat); + } + + @Test + public void iteratorToListTest() { + List list = Arrays.asList("A", "B", "C"); + Iterator iterator = list.iterator(); + + List listFromIterator = CollectionUtils.iteratorToList(iterator); + assertEquals(list, listFromIterator); + } + + @Test + public void iteratorToList_emptyIteratorTest() { + Iterator iterator = Collections.emptyIterator(); + + List listFromIterator = CollectionUtils.iteratorToList(iterator); + assertTrue(listFromIterator.isEmpty()); + } + + @Test + public void containsTest() { + String[] abc = new String[] {"A", "B", "C"}; + + assertTrue(CollectionUtils.contains(abc, "A")); + assertTrue(CollectionUtils.contains(abc, "B")); + assertTrue(CollectionUtils.contains(abc, "C")); + assertFalse(CollectionUtils.contains(abc, "D")); + } + + @Test + public void contains_emptyTest() { + String[] empty = new String[0]; + + assertFalse(CollectionUtils.contains(empty, "A")); + } + + @Test + public void addAllTest() { + List list = new ArrayList<>(); + list.add("A"); + list.add("B"); + + List other = new ArrayList<>(); + other.add("C"); + other.add("D"); + Iterator iterator = other.iterator(); + + CollectionUtils.addAll(iterator, list); + + assertEquals(Arrays.asList("A", "B", "C", "D"), list); + } + + @Test + public void addAllEmptyListTest() { + List empty = new ArrayList<>(); + + List other = Arrays.asList("A", "B", "C"); + Iterator iterator = other.iterator(); + + CollectionUtils.addAll(iterator, empty); + assertEquals(Arrays.asList("A", "B", "C"), empty); + } + + @Test + public void addAllEmptyIterator() { + List list = new ArrayList<>(); + list.add("A"); + list.add("B"); + + Iterator iterator = Collections.emptyIterator(); + + CollectionUtils.addAll(iterator, list); + assertEquals(Arrays.asList("A", "B"), list); + } +} From ae6a427d90a29aa030ee4cc8a96b9316c0004192 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Nov 2022 15:34:54 +0100 Subject: [PATCH 0610/1204] Add test for UniversalSignatureBuilder --- .../builder/UniversalSignatureBuilder.java | 3 +- .../subpackets/SignatureSubpackets.java | 4 + .../UniversalSignatureBuilderTest.java | 90 +++++++++++++++++++ 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/signature/builder/UniversalSignatureBuilderTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java index b3ed5bf0..7ca512bf 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java @@ -12,7 +12,6 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.subpackets.BaseSignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpackets; /** @@ -43,7 +42,7 @@ public class UniversalSignatureBuilder extends AbstractSignatureBuilder { + + } + public static SignatureSubpackets refreshHashedSubpackets(PGPPublicKey issuer, PGPSignature oldSignature) { return createHashedSubpacketsFrom(issuer, oldSignature.getHashedSubPackets()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/UniversalSignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/UniversalSignatureBuilderTest.java new file mode 100644 index 00000000..37bc6fd3 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/UniversalSignatureBuilderTest.java @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.IOException; + +import org.bouncycastle.bcpg.sig.PrimaryUserID; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.subpackets.SignatureSubpackets; + +public class UniversalSignatureBuilderTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9611 510F 313E DBC2 BBBC DC24 3BAD F1F8 3E70 DC34\n" + + "Comment: Signora Universa \n" + + "\n" + + "lFgEY4DKKRYJKwYBBAHaRw8BAQdA65vJxvvLASI/gczDP8ZKH4C+16MLU7F5iP91\n" + + "8WWUqM0AAQCRSTHLLQWT9tuNRgkG3xaIiBGkEGD7Ou/R3oga6tc1MA8UtClTaWdu\n" + + "b3JhIFVuaXZlcnNhIDxzaWdub3JhQHBncGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJj\n" + + "gMopCRA7rfH4PnDcNBYhBJYRUQ8xPtvCu7zcJDut8fg+cNw0Ap4BApsBBRYCAwEA\n" + + "BAsJCAcFFQoJCAsCmQEAAOgMAPwIOXWt3EBBusK5Ps3m7p/5HsecZv3IXtscEQBx\n" + + "vKlULwD/YuLP1XJSqcE2cQJRNt6OLi9Nt02MKBYkhWrRCYZAcQicXQRjgMopEgor\n" + + "BgEEAZdVAQUBAQdAWTstuhvHwmSXaQ4Vh8yxl0DZcvjrWkZI+n9/uFBxEmoDAQgH\n" + + "AAD/eRt6kgOMzWsTuM00am4UhSygxmDt7h6JkBTnpyyhK0gPiYh1BBgWCgAdBQJj\n" + + "gMopAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQO63x+D5w3DRnZAEA6GlS9Tw8\n" + + "9SJlUvh5aciYSlQUplnEdng+Pvzbj74zcXIA/2OkyMN428ddNhkHWWkZCMOxApum\n" + + "/zNDSYMwvByQ2KcFnFgEY4DKKRYJKwYBBAHaRw8BAQdAfhPrtVuG3g/zXF51VrPv\n" + + "kpQQk9aqjrkBMI0qlztBpu0AAP9Mw7NCsAVwg9CgmSzG2ATIDp3yf/4BGVYDs7qu\n" + + "+sbn7xKIiNUEGBYKAH0FAmOAyikCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkW\n" + + "CgAGBQJjgMopAAoJENmzwZA/hq5ZCqIBAMYeOnASBd+WWta7Teh3g7Bl7sFY42Qy\n" + + "0OnaSGk/pLm9AP4yC62Xpb9DhWeiQIOY7k5n4lhNn173IfzDK6KXzBKkBgAKCRA7\n" + + "rfH4PnDcNMInAP4oanG9tbuczBNLN3JY4Hg4AaB+w5kfdOJxKwnAw7U0cgEAtasg\n" + + "67qSjHvsEvjNKeXzUm+db7NWP3fpIHxAmjWVjwM=\n" + + "=Dqbd\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private PGPSecretKeyRing secretKeys; + private final SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + + @BeforeEach + public void parseKey() throws IOException { + secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + } + + @Test + public void createPetNameSignature() throws PGPException { + PGPSecretKey signingKey = secretKeys.getSecretKey(); + PGPSignature archetype = signingKey.getPublicKey().getSignatures().next(); + UniversalSignatureBuilder builder = new UniversalSignatureBuilder( + signingKey, protector, archetype); + + builder.applyCallback(new SignatureSubpackets.Callback() { + @Override + public void modifyHashedSubpackets(SignatureSubpackets hashedSubpackets) { + hashedSubpackets.setExportable(true, false); + hashedSubpackets.setPrimaryUserId(new PrimaryUserID(false, false)); + } + }); + + PGPSignatureGenerator generator = builder.getSignatureGenerator(); + + String petName = "mykey"; + PGPSignature petNameSig = generator.generateCertification(petName, secretKeys.getPublicKey()); + + assertEquals(SignatureType.POSITIVE_CERTIFICATION.getCode(), petNameSig.getSignatureType()); + assertEquals(4, petNameSig.getVersion()); + assertEquals(signingKey.getKeyID(), petNameSig.getKeyID()); + assertEquals(HashAlgorithm.SHA512.getAlgorithmId(), petNameSig.getHashAlgorithm()); + assertEquals(KeyFlag.toBitmask(KeyFlag.CERTIFY_OTHER), petNameSig.getHashedSubPackets().getKeyFlags()); + assertFalse(petNameSig.getHashedSubPackets().isExportable()); + assertFalse(petNameSig.getHashedSubPackets().isPrimaryUserID()); + } +} From 6913aa3d6dce6566715c12ffdca4f7863691ba7d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Nov 2022 15:41:56 +0100 Subject: [PATCH 0611/1204] Add more tests for RevocationState --- .../pgpainless/algorithm/RevocationStateTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java index c1e45413..e23d92d3 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java @@ -72,6 +72,8 @@ public class RevocationStateTest { Date now = new Date(); assertEquals(RevocationState.softRevoked(now), RevocationState.softRevoked(now)); + assertEquals(1, RevocationState.softRevoked(now).compareTo(RevocationState.notRevoked())); + assertEquals(0, RevocationState.notRevoked().compareTo(RevocationState.notRevoked())); assertEquals(0, RevocationState.hardRevoked().compareTo(RevocationState.hardRevoked())); assertTrue(RevocationState.hardRevoked().compareTo(RevocationState.notRevoked()) > 0); @@ -93,4 +95,15 @@ public class RevocationStateTest { assertEquals(states, Arrays.asList(not, not2, laterSoft, earlySoft, hard)); } + + @SuppressWarnings({"SimplifiableAssertion", "ConstantConditions", "EqualsWithItself", "EqualsBetweenInconvertibleTypes"}) + @Test + public void equalsTest() { + RevocationState rev = RevocationState.hardRevoked(); + assertFalse(rev.equals(null)); + assertTrue(rev.equals(rev)); + assertFalse(rev.equals("not a revocation")); + RevocationState other = RevocationState.notRevoked(); + assertFalse(rev.equals(other)); + } } From b0c283e1435f859a5d23e1b656bc512a90f19638 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Nov 2022 15:34:04 +0100 Subject: [PATCH 0612/1204] Clean up UserId.toString() behavior --- .../java/org/pgpainless/key/util/UserId.java | 37 ++++++++++--------- .../java/org/pgpainless/key/UserIdTest.java | 10 ++--- .../GenerateKeyWithAdditionalUserIdTest.java | 6 +-- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index ca233759..25db7e39 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -5,6 +5,7 @@ package org.pgpainless.key.util; import javax.annotation.Nonnull; +import javax.annotation.Nullable; public final class UserId implements CharSequence { public static final class Builder { @@ -64,18 +65,18 @@ public final class UserId implements CharSequence { private final String email; private long hash = Long.MAX_VALUE; - private UserId(String name, String comment, String email) { - this.name = name; - this.comment = comment; - this.email = email; + private UserId(@Nullable String name, @Nullable String comment, @Nullable String email) { + this.name = name == null ? null : name.trim(); + this.comment = comment == null ? null : comment.trim(); + this.email = email == null ? null : email.trim(); } - public static UserId onlyEmail(String email) { + public static UserId onlyEmail(@Nonnull String email) { checkNotNull("email", email); return new UserId(null, null, email); } - public static UserId nameAndEmail(String name, String email) { + public static UserId nameAndEmail(@Nonnull String name, @Nonnull String email) { checkNotNull("name", name); checkNotNull("email", email); return new UserId(name, null, email); @@ -118,27 +119,29 @@ public final class UserId implements CharSequence { @Override public @Nonnull String toString() { - return asString(false); + return asString(); } /** * Returns a string representation of the object. - * @param ignoreEmptyValues Flag which indicates that empty string values should not be outputted. * @return a string representation of the object. */ - public String asString(boolean ignoreEmptyValues) { + public String asString() { StringBuilder sb = new StringBuilder(); - if (name != null && (!ignoreEmptyValues || !name.isEmpty())) { + if (name != null && !name.isEmpty()) { sb.append(name); } - if (comment != null && (!ignoreEmptyValues || !comment.isEmpty())) { - sb.append(" (").append(comment).append(')'); + if (comment != null && !comment.isEmpty()) { + if (sb.length() > 0) { + sb.append(' '); + } + sb.append('(').append(comment).append(')'); } - if (email != null && (!ignoreEmptyValues || !email.isEmpty())) { - final boolean moreThanJustEmail = sb.length() > 0; - if (moreThanJustEmail) sb.append(" <"); - sb.append(email); - if (moreThanJustEmail) sb.append('>'); + if (email != null && !email.isEmpty()) { + if (sb.length() > 0) { + sb.append(' '); + } + sb.append('<').append(email).append('>'); } return sb.toString(); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index 4e596b3f..32d56eff 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -151,15 +151,13 @@ public class UserIdTest { @Test void testEmailOnlyFormatting() { final UserId userId = UserId.onlyEmail("john.smith@example.com"); - assertEquals("john.smith@example.com", userId.toString()); + assertEquals("", userId.toString()); } @Test void testEmptyNameAndValidEmailFormatting() { final UserId userId = UserId.nameAndEmail("", "john.smith@example.com"); - assertEquals("john.smith@example.com", userId.toString()); - assertEquals("john.smith@example.com", userId.asString(false)); - assertEquals("john.smith@example.com", userId.asString(true)); + assertEquals("", userId.toString()); } @Test @@ -169,9 +167,7 @@ public class UserIdTest { .withName("") .withEmail("john.smith@example.com") .build(); - assertEquals(" () ", userId.toString()); - assertEquals(" () ", userId.asString(false)); - assertEquals("john.smith@example.com", userId.asString(true)); + assertEquals("", userId.toString()); } @Test diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java index e68d8e9b..cf12ab57 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java @@ -51,9 +51,9 @@ public class GenerateKeyWithAdditionalUserIdTest { JUtils.assertDateEquals(expiration, PGPainless.inspectKeyRing(publicKeys).getPrimaryKeyExpirationDate()); Iterator userIds = publicKeys.getPublicKey().getUserIDs(); - assertEquals("primary@user.id", userIds.next()); - assertEquals("additional@user.id", userIds.next()); - assertEquals("additional2@user.id", userIds.next()); + assertEquals("", userIds.next()); + assertEquals("", userIds.next()); + assertEquals("", userIds.next()); assertEquals("trimThis@user.id", userIds.next()); assertFalse(userIds.hasNext()); } From 4c1d359971106124f2c0c68eb61836a8886d3651 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Nov 2022 15:35:31 +0100 Subject: [PATCH 0613/1204] Deprecate UserId.asString() --- .../java/org/pgpainless/key/util/UserId.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 25db7e39..3ac3d77a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -119,14 +119,6 @@ public final class UserId implements CharSequence { @Override public @Nonnull String toString() { - return asString(); - } - - /** - * Returns a string representation of the object. - * @return a string representation of the object. - */ - public String asString() { StringBuilder sb = new StringBuilder(); if (name != null && !name.isEmpty()) { sb.append(name); @@ -146,6 +138,16 @@ public final class UserId implements CharSequence { return sb.toString(); } + /** + * Returns a string representation of the object. + * @return a string representation of the object. + * @deprecated use {@link #toString()} instead. + */ + @Deprecated + public String asString() { + return toString(); + } + @Override public boolean equals(Object o) { if (o == null) return false; From 837fbd36355008fa81c263c510e9201b9752c02e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Nov 2022 15:41:53 +0100 Subject: [PATCH 0614/1204] Simplify UserIdTests --- .../java/org/pgpainless/key/util/UserId.java | 18 +++------------- .../java/org/pgpainless/key/UserIdTest.java | 21 ------------------- 2 files changed, 3 insertions(+), 36 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 3ac3d77a..71ee8121 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -22,20 +22,17 @@ public final class UserId implements CharSequence { this.email = email; } - public Builder withName(String name) { - checkNotNull("name", name); + public Builder withName(@Nonnull String name) { this.name = name; return this; } - public Builder withComment(String comment) { - checkNotNull("comment", comment); + public Builder withComment(@Nonnull String comment) { this.comment = comment; return this; } - public Builder withEmail(String email) { - checkNotNull("email", email); + public Builder withEmail(@Nonnull String email) { this.email = email; return this; } @@ -72,13 +69,10 @@ public final class UserId implements CharSequence { } public static UserId onlyEmail(@Nonnull String email) { - checkNotNull("email", email); return new UserId(null, null, email); } public static UserId nameAndEmail(@Nonnull String name, @Nonnull String email) { - checkNotNull("name", name); - checkNotNull("email", email); return new UserId(name, null, email); } @@ -180,10 +174,4 @@ public final class UserId implements CharSequence { || (!valueIsNull && !otherValueIsNull && (ignoreCase ? value.equalsIgnoreCase(otherValue) : value.equals(otherValue))); } - - private static void checkNotNull(String paramName, String value) { - if (value == null) { - throw new IllegalArgumentException(paramName + " must be not null"); - } - } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index 32d56eff..5fe4d9c9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -10,25 +10,9 @@ import org.pgpainless.key.util.UserId; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; public class UserIdTest { - @Test - public void throwForNullName() { - assertThrows(IllegalArgumentException.class, () -> UserId.newBuilder().withName(null)); - } - - @Test - public void throwForNullComment() { - assertThrows(IllegalArgumentException.class, () -> UserId.newBuilder().withComment(null)); - } - - @Test - public void throwForNullEmail() { - assertThrows(IllegalArgumentException.class, () -> UserId.newBuilder().withEmail(null)); - } - @Test public void testFormatOnlyName() { assertEquals( @@ -66,11 +50,6 @@ public class UserIdTest { .toString()); } - @Test - public void throwIfOnlyEmailEmailNull() { - assertThrows(IllegalArgumentException.class, () -> UserId.onlyEmail(null)); - } - @Test public void testNameAndEmail() { UserId userId = UserId.nameAndEmail("Maurice Moss", "moss.m@reynholm.co.uk"); From e69c4a8cf709ee55d5f8e6e0ba65bc9526f15308 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Nov 2022 16:06:01 +0100 Subject: [PATCH 0615/1204] More UserId tests --- .../java/org/pgpainless/key/UserIdTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index 5fe4d9c9..5a553513 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -181,4 +181,35 @@ public class UserIdTest { final UserId userId2 = UserId.newBuilder().withComment(comment2).withName(name).withEmail(email).build(); assertNotEquals(userId1, userId2); } + + @Test + public void testLength() { + UserId id = UserId.nameAndEmail("Alice", "alice@pgpainless.org"); + assertEquals(28, id.length()); + } + + @Test + public void testSubSequence() { + UserId id = UserId.onlyEmail("alice@pgpainless.org"); + assertEquals("alice@pgpainless.org", id.subSequence(1, id.length() - 1)); + } + + @Test + public void asStringTest() { + UserId id = UserId.newBuilder() + .withName("Alice") + .withComment("Work Email") + .withEmail("alice@pgpainless.org") + .build(); + + // noinspection deprecation + assertEquals(id.toString(), id.asString()); + } + + @Test + public void charAtTest() { + UserId id = UserId.onlyEmail("alice@pgpainless.org"); + assertEquals('<', id.charAt(0)); + assertEquals('>', id.charAt(id.length() - 1)); + } } From b07e0c2be5952e06c41f79b3271e36b3304c56ba Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 1 Dec 2022 14:48:33 +0100 Subject: [PATCH 0616/1204] Programmatically confirm that we do not yet support OpenPGP V5 keys :/ --- .../org/bouncycastle/V5OpenPgpKeyTest.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/bouncycastle/V5OpenPgpKeyTest.java diff --git a/pgpainless-core/src/test/java/org/bouncycastle/V5OpenPgpKeyTest.java b/pgpainless-core/src/test/java/org/bouncycastle/V5OpenPgpKeyTest.java new file mode 100644 index 00000000..b1337bcb --- /dev/null +++ b/pgpainless-core/src/test/java/org/bouncycastle/V5OpenPgpKeyTest.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle; + +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +import java.io.IOException; + +public class V5OpenPgpKeyTest { + + // Both key and cert are provided by Daniel on + // https://mailarchive.ietf.org/arch/msg/openpgp/Z2Mkq9TfvgY5jUJzlNRwgDsDSUk/ + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "xVwFY4d/4xYAAAAtCSsGAQQB2kcPAQEHQPlNp7tI1gph5WdwamWH0DMZmbud\n" + + "iRoIJC6thFQ9+JWjAAD9GXKBexK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditl\n" + + "sLcOpMKkBR8WCgAAAB8FAmOHf+MDCwkHBRUKDggMAhYAAhsDAh4JBScJAgcC\n" + + "AAAAIyIhBRe8+DZtlDb3rzfq2hVsZJRlblqXac8tXLNF+Lg0NvZSecUms7MC\n" + + "rI0Ofp1iKV6QwGFEAQDnd37qxR3r/ezwXEfWUd64NKsHy88o3UG3QasrgR9e\n" + + "SwEAmCPJHs0LvoU81IFsYhEYaZok9uC0DhdnO2lwYUbCTAXHYQVjh3/jEgAA\n" + + "ADIKKwYBBAGXVQEFAQEHQPz3/CmqzgFI9D6tvzoPlpHQoyKiQ2JWJ4Dtkl2o\n" + + "TnFbAwEIBwAA/01gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24Eb3C\n" + + "jgUYFgoAAAAJBQJjh3/jAhsMAAAAIyIhBRe8+DZtlDb3rzfq2hVsZJRlblqX\n" + + "ac8tXLNF+Lg0NvZS78S6dZamUg5K+sXfU/N1umwTAP9JjPVrtnHjtvYTazZm\n" + + "dZhAn8aRLUtGG1owtmLGwCSh6wD/bNrWG4nHfVk/aEHGZ4cjaFlapFr5t1QS\n" + + "psL7nEy94gs=\n" + + "=5xrR\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + private static final String CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "xjcFY4d/4xYAAAAtCSsGAQQB2kcPAQEHQPlNp7tI1gph5WdwamWH0DMZmbud\n" + + "iRoIJC6thFQ9+JWjwqQFHxYKAAAAHwUCY4d/4wMLCQcFFQoOCAwCFgACGwMC\n" + + "HgkFJwkCBwIAAAAjIiEFF7z4Nm2UNvevN+raFWxklGVuWpdpzy1cs0X4uDQ2\n" + + "9lJ5xSazswKsjQ5+nWIpXpDAYUQBAOd3furFHev97PBcR9ZR3rg0qwfLzyjd\n" + + "QbdBqyuBH15LAQCYI8kezQu+hTzUgWxiERhpmiT24LQOF2c7aXBhRsJMBc48\n" + + "BWOHf+MSAAAAMgorBgEEAZdVAQUBAQdA/Pf8KarOAUj0Pq2/Og+WkdCjIqJD\n" + + "YlYngO2SXahOcVsDAQgHwo4FGBYKAAAACQUCY4d/4wIbDAAAACMiIQUXvPg2\n" + + "bZQ296836toVbGSUZW5al2nPLVyzRfi4NDb2Uu/EunWWplIOSvrF31Pzdbps\n" + + "EwD/SYz1a7Zx47b2E2s2ZnWYQJ/GkS1LRhtaMLZixsAkoesA/2za1huJx31Z\n" + + "P2hBxmeHI2hZWqRa+bdUEqbC+5xMveIL\n" + + "=sVUI\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + @Test + @Disabled("BC 1.72 does not yet support V5 keys") + public void testParseCert() throws IOException { + PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(CERT); + } + + @Test + @Disabled("BC 1.72 does not yet support V5 keys") + public void testParseKey() throws IOException { + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY); + } +} From bfcfaa04c4c0d6ee98ee31d19a1033162fd484e1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 1 Dec 2022 16:02:45 +0100 Subject: [PATCH 0617/1204] Add UserId.compare(uid1, uid2, comparator) along with some default comparators --- .../java/org/pgpainless/key/util/UserId.java | 85 +++++++++++++++++++ .../java/org/pgpainless/key/UserIdTest.java | 65 +++++++++++++- 2 files changed, 147 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 71ee8121..a1b251dc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -4,6 +4,7 @@ package org.pgpainless.key.util; +import java.util.Comparator; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -174,4 +175,88 @@ public final class UserId implements CharSequence { || (!valueIsNull && !otherValueIsNull && (ignoreCase ? value.equalsIgnoreCase(otherValue) : value.equals(otherValue))); } + + public static int compare(@Nullable UserId o1, @Nullable UserId o2, @Nonnull Comparator comparator) { + return comparator.compare(o1, o2); + } + + public static class DefaultComparator implements Comparator { + + @Override + public int compare(UserId o1, UserId o2) { + if (o1 == o2) { + return 0; + } + if (o1 == null) { + return -1; + } + if (o2 == null) { + return 1; + } + + NullSafeStringComparator c = new NullSafeStringComparator(); + int cName = c.compare(o1.getName(), o2.getName()); + if (cName != 0) { + return cName; + } + + int cComment = c.compare(o1.getComment(), o2.getComment()); + if (cComment != 0) { + return cComment; + } + + return c.compare(o1.getEmail(), o2.getEmail()); + } + } + + public static class DefaultIgnoreCaseComparator implements Comparator { + + @Override + public int compare(UserId o1, UserId o2) { + if (o1 == o2) { + return 0; + } + if (o1 == null) { + return -1; + } + if (o2 == null) { + return 1; + } + + NullSafeStringComparator c = new NullSafeStringComparator(); + int cName = c.compare(lower(o1.getName()), lower(o2.getName())); + if (cName != 0) { + return cName; + } + + int cComment = c.compare(lower(o1.getComment()), lower(o2.getComment())); + if (cComment != 0) { + return cComment; + } + + return c.compare(lower(o1.getEmail()), lower(o2.getEmail())); + } + + private static String lower(String string) { + return string == null ? null : string.toLowerCase(); + } + } + + private static class NullSafeStringComparator implements Comparator { + + @Override + public int compare(String o1, String o2) { + // noinspection StringEquality + if (o1 == o2) { + return 0; + } + if (o1 == null) { + return -1; + } + if (o2 == null) { + return 1; + } + return o1.compareTo(o2); + } + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index 5a553513..acc90918 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -4,13 +4,15 @@ package org.pgpainless.key; -import org.junit.jupiter.api.Test; -import org.pgpainless.key.util.UserId; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import java.util.Comparator; + +import org.junit.jupiter.api.Test; +import org.pgpainless.key.util.UserId; + public class UserIdTest { @Test @@ -212,4 +214,61 @@ public class UserIdTest { assertEquals('<', id.charAt(0)); assertEquals('>', id.charAt(id.length() - 1)); } + + @Test + public void defaultCompareTest() { + UserId id1 = UserId.onlyEmail("alice@pgpainless.org"); + UserId id2 = UserId.onlyEmail("alice@gnupg.org"); + UserId id3 = UserId.nameAndEmail("Alice", "alice@pgpainless.org"); + UserId id3_ = UserId.nameAndEmail("Alice", "alice@pgpainless.org"); + UserId id4 = UserId.newBuilder().withName("Alice").build(); + UserId id5 = UserId.newBuilder().withName("Alice").withComment("Work Mail").withEmail("alice@pgpainless.org").build(); + + assertEquals(id3.hashCode(), id3_.hashCode()); + assertNotEquals(id2.hashCode(), id3.hashCode()); + + Comparator c = new UserId.DefaultComparator(); + assertEquals(0, UserId.compare(null, null, c)); + assertEquals(0, UserId.compare(id1, id1, c)); + assertNotEquals(0, UserId.compare(id1, null, c)); + assertNotEquals(0, UserId.compare(null, id1, c)); + assertNotEquals(0, UserId.compare(id1, id2, c)); + assertNotEquals(0, UserId.compare(id2, id1, c)); + assertNotEquals(0, UserId.compare(id1, id3, c)); + assertNotEquals(0, UserId.compare(id1, id4, c)); + assertNotEquals(0, UserId.compare(id4, id1, c)); + assertNotEquals(0, UserId.compare(id2, id3, c)); + assertNotEquals(0, UserId.compare(id1, id5, c)); + assertNotEquals(0, UserId.compare(id5, id1, c)); + assertNotEquals(0, UserId.compare(id3, id5, c)); + assertNotEquals(0, UserId.compare(id5, id3, c)); + assertEquals(0, UserId.compare(id3, id3, c)); + assertEquals(0, UserId.compare(id3, id3_, c)); + } + + @Test + public void defaultIgnoreCaseCompareTest() { + UserId id1 = UserId.nameAndEmail("Alice", "alice@pgpainless.org"); + UserId id2 = UserId.nameAndEmail("alice", "alice@pgpainless.org"); + UserId id3 = UserId.nameAndEmail("Alice", "Alice@Pgpainless.Org"); + UserId id4 = UserId.newBuilder().withName("Alice").withComment("Work Email").withEmail("Alice@Pgpainless.Org").build(); + UserId id5 = UserId.newBuilder().withName("alice").withComment("work email").withEmail("alice@pgpainless.org").build(); + UserId id6 = UserId.nameAndEmail("Bob", "bob@pgpainless.org"); + + Comparator c = new UserId.DefaultIgnoreCaseComparator(); + assertEquals(0, UserId.compare(id1, id2, c)); + assertEquals(0, UserId.compare(id1, id3, c)); + assertEquals(0, UserId.compare(id2, id3, c)); + assertEquals(0, UserId.compare(null, null, c)); + assertEquals(0, UserId.compare(id1, id1, c)); + assertEquals(0, UserId.compare(id4, id4, c)); + assertEquals(0, UserId.compare(id4, id5, c)); + assertEquals(0, UserId.compare(id5, id4, c)); + assertNotEquals(0, UserId.compare(null, id1, c)); + assertNotEquals(0, UserId.compare(id1, null, c)); + assertNotEquals(0, UserId.compare(id1, id4, c)); + assertNotEquals(0, UserId.compare(id4, id1, c)); + assertNotEquals(0, UserId.compare(id1, id6, c)); + assertNotEquals(0, UserId.compare(id6, id1, c)); + } } From 907d1c4d1c2eda73ee662f35ea19cf5586d6c301 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 1 Dec 2022 16:04:45 +0100 Subject: [PATCH 0618/1204] move V5OpenPgpKeyTest to org.pgpainless.key --- .../org/{bouncycastle => pgpainless/key}/V5OpenPgpKeyTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename pgpainless-core/src/test/java/org/{bouncycastle => pgpainless/key}/V5OpenPgpKeyTest.java (99%) diff --git a/pgpainless-core/src/test/java/org/bouncycastle/V5OpenPgpKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/V5OpenPgpKeyTest.java similarity index 99% rename from pgpainless-core/src/test/java/org/bouncycastle/V5OpenPgpKeyTest.java rename to pgpainless-core/src/test/java/org/pgpainless/key/V5OpenPgpKeyTest.java index b1337bcb..d1b941fd 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/V5OpenPgpKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/V5OpenPgpKeyTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.bouncycastle; +package org.pgpainless.key; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; From 218da50da30f6751e0b498fcfcc433b1a2643adb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Dec 2022 13:47:54 +0100 Subject: [PATCH 0619/1204] Create gradle mavenCentralChecksums task to quickly fetch checksums of published artifacts gradle mavenCentralChecksums will fetch the checksums of the currently checked out release, while gradle -Prelease=1.3.13 for example will fetch those of the 1.3.13 release --- build.gradle | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/build.gradle b/build.gradle index 7802bdb1..6e086754 100644 --- a/build.gradle +++ b/build.gradle @@ -257,3 +257,44 @@ task javadocAll(type: Javadoc) { "https://docs.oracle.com/javase/${sourceCompatibility.getMajorVersion()}/docs/api/", ] as String[] } + +/** + * Fetch sha256 checksums of artifacts published to maven central. + * + * Example: gradle -Prelease=1.3.13 mavenCentralChecksums + */ +task mavenCentralChecksums() { + description 'Fetch and display checksums for artifacts published to Maven Central' + String ver = project.hasProperty('release') ? release : shortVersion + doLast { + Process p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-core/${ver}/pgpainless-core-${ver}.jar.sha256".execute() + if (p.waitFor() == 0) { + print p.text.trim() + println " pgpainless-core/build/libs/pgpainless-core-${ver}.jar" + } + + p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-sop/${ver}/pgpainless-sop-${ver}.jar.sha256".execute() + if (p.waitFor() == 0) { + print p.text.trim() + println " pgpainless-sop/build/libs/pgpainless-sop-${ver}.jar" + } + + p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-cli/${ver}/pgpainless-cli-${ver}-all.jar.sha256".execute() + if (p.waitFor() == 0) { + print p.text.trim() + println " pgpainless-cli/build/libs/pgpainless-cli-${ver}-all.jar" + } + + p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-cli/${ver}/pgpainless-cli-${ver}.jar.sha256".execute() + if (p.waitFor() == 0) { + print p.text.trim() + println " pgpainless-cli/build/libs/pgpainless-cli-${ver}.jar" + } + + p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/hsregex/${ver}/hsregex-${ver}.jar.sha256".execute() + if (p.waitFor() == 0) { + print p.text.trim() + println " hsregex/build/libs/hsregex-${ver}.jar" + } + } +} From e168ac6f557f96be478d7d979575dfeb3892c700 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Dec 2022 14:00:34 +0100 Subject: [PATCH 0620/1204] Update documentation to use new MessageMetadata class --- docs/source/pgpainless-core/quickstart.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/source/pgpainless-core/quickstart.md b/docs/source/pgpainless-core/quickstart.md index 371a5f6f..d61a881c 100644 --- a/docs/source/pgpainless-core/quickstart.md +++ b/docs/source/pgpainless-core/quickstart.md @@ -242,7 +242,7 @@ or the passphrase. ### Decrypt and/or Verify a Message Decryption and verification of a message is both done using the same API. Whether a message was actually signed / encrypted can be determined after the message has been processed by checking -the `OpenPgpMetadata` object which can be obtained from the `DecryptionStream`. +the `MessageMetadata` object which can be obtained from the `DecryptionStream`. To configure the decryption / verification process, the `ConsumerOptions` object is used: @@ -283,16 +283,16 @@ Streams.pipeAll(consumerStream, plaintext); consumerStream.close(); // important! // The result will contain metadata of the message -OpenPgpMetadata result = consumerStream.getResult(); +MessageMetadata result = consumerStream.getMetadata(); ``` -After the message has been processed, you can consult the `OpenPgpMetadata` object to determine the nature of the message: +After the message has been processed, you can consult the `MessageMetadata` object to determine the nature of the message: ```java boolean wasEncrypted = result.isEncrypted(); SubkeyIdentifier decryptionKey = result.getDecryptionKey(); -Map validSignatures = result.getVerifiedSignatures(); -boolean wasSignedByCert = result.containsVerifiedSignatureFrom(certificate); +List validSignatures = result.getVerifiedSignatures(); +boolean wasSignedByCert = result.isVerifiedSignedBy(certificate); // For files: String fileName = result.getFileName(); @@ -323,6 +323,6 @@ DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() Streams.drain(verificationStream); // push all the data through the stream verificationStream.close(); // finish verification -OpenPgpMetadata result = verificationStream.getResult(); // get metadata of signed message -assertTrue(result.containsVerifiedSignatureFrom(certificate)); // check if message was in fact signed +MessageMetadata result = verificationStream.getMetadata(); // get metadata of signed message +assertTrue(result.isVerifiedSignedBy(certificate)); // check if message was in fact signed ``` \ No newline at end of file From f5414bcc196c91da22d40be4dc32e10ead488c08 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Dec 2022 16:11:17 +0100 Subject: [PATCH 0621/1204] Use proper method to unlock private key when detached-signing --- .../java/org/pgpainless/encryption_signing/SigningOptions.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java index 0a7c47c4..0af07fc9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -359,8 +359,7 @@ public final class SigningOptions { if (signingSecKey == null) { throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), signingPubKey.getKeyID()); } - PGPPrivateKey signingSubkey = signingSecKey.extractPrivateKey( - secretKeyDecryptor.getDecryptor(signingPubKey.getKeyID())); + PGPPrivateKey signingSubkey = UnlockSecretKey.unlockSecretKey(signingSecKey, secretKeyDecryptor); Set hashAlgorithms = userId != null ? keyRingInfo.getPreferredHashAlgorithms(userId) : keyRingInfo.getPreferredHashAlgorithms(signingPubKey.getKeyID()); HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); From 4f435a0fa0dd9d70383d1c0c9e8eaf832a5a102e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Dec 2022 16:15:32 +0100 Subject: [PATCH 0622/1204] Fix parameter check for DSA keys Fixes #345 --- .../PublicKeyParameterValidationUtil.java | 2 +- .../sop/CarolKeySignEncryptRoundtripTest.java | 295 ++++++++++++++++++ 2 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java index 69940691..88e9897a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java @@ -195,7 +195,7 @@ public class PublicKeyParameterValidationUtil { } // q > 160 bits - boolean qLarge = pQ.getLowestSetBit() > 160; + boolean qLarge = pQ.bitLength() > 160; if (!qLarge) { return false; } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java new file mode 100644 index 00000000..81bf7645 --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java @@ -0,0 +1,295 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import sop.ByteArrayAndResult; +import sop.DecryptionResult; +import sop.Ready; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CarolKeySignEncryptRoundtripTest { + + private static final String CAROL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "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\n" + + "1FkOSekLi8WNMdUx3XMyvP8nJ65P2Q==\n" + + "=Xj8h\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final String CAROL_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "\n" + + "xsPuBF3+CmgRDADZhdKTM3ms3XpXnQke83FgaIBtP1g1qhqpCfg50WiPS0kjiMC0\n" + + "OJz2vh59nusbBLzgI//Y1VMhKfIWYbqMcIY+lWbseHjl52rqW6AaJ0TH4NgVt7vh\n" + + "yVeJt0k/NnxvNhMd0587KXmfpDxrwBqc/l5cVB+p0rL8vs8kxojHXAi5V3koM0Uj\n" + + "REWs5Jpj/XU9LhEoyXZkeJC/pes1u6UKoFYn7dFIP49Kkd1kb+1bNfdPYtA0JpcG\n" + + "zYgeMNOvdWJwn43dNhxoeuXfmAEhA8LdzT0C0O+7akXOKWrfhXJ8MTBqvPgWZYx7\n" + + "MNuQx/ejIMZHl+Iaf7hG976ILH+NCGiKkhidd9GIuA/WteHiQbXLyfiQ4n8P12q9\n" + + "+4dq6ybUM65tnozRyyN+1m3rU2a/+Ly3JCh4TeO27w+cxMWkaeHyTQaJVMbMbDpX\n" + + "duVd32MA33UVNH5/KXMVczVi5asVjuKDSojJDV1QwX8izZNl1t+AI0L3balCabV0\n" + + "SFhlfnBEUj1my1sBAMOSO/I67BvBS3IPHZWXHjgclhs26mPzRlZLryAUWR2DDACH\n" + + "5fx+yUAdZ8Vu/2zWTHxwWJ/X6gGTLqa9CmfDq5UDqYFFzuWwN4HJ+ryOuak1CGwS\n" + + "KJUBSA75HExbv0naWg+suy+pEDvF0VALPU9VUkSQtHyR10YO2FWOe3AEtpbYDRwp\n" + + "dr1ZwEbb3L6IGQ5i/4CNHbJ2u3yUeXsDNAvrpVSEcIjA01RPCOKmf58SDZp4yDdP\n" + + "xGhM8w6a18+fdQr22f2cJ0xgfPlbzFbO+FUsEgKvn6QTLhbaYw4zs7rdQDejWHV8\n" + + "2hP4K+rb9FwknYdV9uo4m77MgGlU+4yvJnGEYaL3jwjI3bH9aooNOl6XbvVAzNzo\n" + + "mYmaTO7mp6xFAu43yuGyd9K+1E4k7CQTROxTZ+RdtQjV95hSsEmMg792nQvDSBW4\n" + + "xwfOQ7pf3kC7r9fm8u9nBlEN12HsbQ8Yvux/ld5q5RaIlD19jzfVR6+hJzbj2ZnU\n" + + "yQs4ksAfIHTzTdLttRxS9lTRTkVx2vbUnoSBy6TYF1mf6nRPpSm1riZxnkR4+BQL\n" + + "/0rUAxwegTNIG/5M612s2a45QvYK1turZ7spI1RGitJUIjBXUuR76jIsyqagIhBl\n" + + "5nEsQ4HLv8OQ3EgJ5T9gldLFpHNczLxBQnnNwfPoD2e0kC/iy0rfiNX8HWpTgQpb\n" + + "zAosLj5/E0iNlildynIhuqBosyRWFqGva0O6qioL90srlzlfKCloe9R9w3HizjCb\n" + + "f59yEspuJt9iHVNOPOW2Wj5ub0KTiJPp9vBmrFaB79/IlgojpQoYvQ77Hx5A9CJq\n" + + "paMCHGOW6Uz9euN1ozzETEkIPtL8XAxcogfpe2JKE1uS7ugxsKEGEDfxOQFKAGV0\n" + + "XFtIx50vFCr2vQro0WB858CGN47dCxChhNUxNtGc11JNEkNv/X7hKtRf/5VCmnaz\n" + + "GWwNK47cqZ7GJfEBnElD7s/tQvTC5Qp7lg9gEt47TUX0bjzUTCxNvLosuKL9+J1W\n" + + "ln1myRpff/5ZOAnZTPHR+AbX4bRB4sK5zijQe4139Dn2oRYK+EIYoBAxFxSOzehP\n" + + "IcKKBB8RCAA8BQJd/gppAwsJCgkQm6eJ3HbWhJoEFQoJCAIWAQIXgAIbAwIeARYh\n" + + "BHH/2gBECeXdsMPo8Zunidx21oSaAABihQD/VWnF1HbBhP+kLwWsqxuYjEslEsM2\n" + + "UQPeKGK9an8HZ78BAJPaiL3OpuOmsIoCfOghhMZOKXjIV+Z57LwaMw7FQfPgzSZD\n" + + "YXJvbCBPbGRzdHlsZSA8Y2Fyb2xAb3BlbnBncC5leGFtcGxlPsKKBBMRCAA8BQJd\n" + + "/gppAwsJCgkQm6eJ3HbWhJoEFQoJCAIWAQIXgAIbAwIeARYhBHH/2gBECeXdsMPo\n" + + "8Zunidx21oSaAABQTAD/ZMXAvSbKaMJJpAfwp1C7KAj6K2k2CAz5jwUXyGf1+jUA\n" + + "/2iAMiX1XcLy3n0L8ytzge8/UAFHafBl4rn4DmUugfhjzsPMBF3+CmgQDADZhdKT\n" + + "M3ms3XpXnQke83FgaIBtP1g1qhqpCfg50WiPS0kjiMC0OJz2vh59nusbBLzgI//Y\n" + + "1VMhKfIWYbqMcIY+lWbseHjl52rqW6AaJ0TH4NgVt7vhyVeJt0k/NnxvNhMd0587\n" + + "KXmfpDxrwBqc/l5cVB+p0rL8vs8kxojHXAi5V3koM0UjREWs5Jpj/XU9LhEoyXZk\n" + + "eJC/pes1u6UKoFYn7dFIP49Kkd1kb+1bNfdPYtA0JpcGzYgeMNOvdWJwn43dNhxo\n" + + "euXfmAEhA8LdzT0C0O+7akXOKWrfhXJ8MTBqvPgWZYx7MNuQx/ejIMZHl+Iaf7hG\n" + + "976ILH+NCGiKkhidd9GIuA/WteHiQbXLyfiQ4n8P12q9+4dq6ybUM65tnozRyyN+\n" + + "1m3rU2a/+Ly3JCh4TeO27w+cxMWkaeHyTQaJVMbMbDpXduVd32MA33UVNH5/KXMV\n" + + "czVi5asVjuKDSojJDV1QwX8izZNl1t+AI0L3balCabV0SFhlfnBEUj1my1sMAIfl\n" + + "/H7JQB1nxW7/bNZMfHBYn9fqAZMupr0KZ8OrlQOpgUXO5bA3gcn6vI65qTUIbBIo\n" + + "lQFIDvkcTFu/SdpaD6y7L6kQO8XRUAs9T1VSRJC0fJHXRg7YVY57cAS2ltgNHCl2\n" + + "vVnARtvcvogZDmL/gI0dsna7fJR5ewM0C+ulVIRwiMDTVE8I4qZ/nxINmnjIN0/E\n" + + "aEzzDprXz591CvbZ/ZwnTGB8+VvMVs74VSwSAq+fpBMuFtpjDjOzut1AN6NYdXza\n" + + "E/gr6tv0XCSdh1X26jibvsyAaVT7jK8mcYRhovePCMjdsf1qig06Xpdu9UDM3OiZ\n" + + "iZpM7uanrEUC7jfK4bJ30r7UTiTsJBNE7FNn5F21CNX3mFKwSYyDv3adC8NIFbjH\n" + + "B85Dul/eQLuv1+by72cGUQ3XYextDxi+7H+V3mrlFoiUPX2PN9VHr6EnNuPZmdTJ\n" + + "CziSwB8gdPNN0u21HFL2VNFORXHa9tSehIHLpNgXWZ/qdE+lKbWuJnGeRHj4FAv+\n" + + "MQaafW0uHF+N8MDm8UWPvf4Vd0UJ0UpIjRWl2hTV+BHkNfvZlBRhhQIphNiKRe/W\n" + + "ap0f/lW2Gm2uS0KgByjjNXEzTiwrte2GX65M6F6Lz8N31kt1Iig1xGOuv+6HmxTN\n" + + "R8gL2K5PdJeJn8PTJWrRS7+BY8Hdkgb+wVpzE5cCvpFiG/P0yqfBdLWxVPlPI7dc\n" + + "hDkmx4iAhHJX9J/gX/hC6L3AzPNJqNPAKy20wYp/ruTbbwBolW/4ikWij460JrvB\n" + + "sm6Sp81A3ebaiN9XkJygLOyhGyhMieGulCYz6AahAFcECtPXGTcordV1mJth8yjF\n" + + "4gZfDQyg0nMW4Yr49yeFXcRMUw1yzN3Q9v2zzqDuFi2lGYTXYmVqLYzM9KbLO2Wx\n" + + "E/21xnBjLsl09l/FdA/bhdZq3t4/apbFOeQQ/j/AphvzWbsJnhG9Q7+d3VoDlz0g\n" + + "FiSduCYIAAq8dUOJNjrUTkZsL1pOIjhYjCMi2uiKS6RQkT6nvuumPF/D/VTnUGeZ\n" + + "wooEGBEIADwFAl3+CmkDCwkKCRCbp4ncdtaEmgQVCgkIAhYBAheAAhsMAh4BFiEE\n" + + "cf/aAEQJ5d2ww+jxm6eJ3HbWhJoAAEEpAP91hFqmcb2ZqVcaRDMSVmhkEcFIRmpH\n" + + "vDoQtVn8sArWqwEAi8HwbMhL+YwRItRZDknpC4vFjTHVMd1zMrz/JyeuT9k=\n" + + "=pa/S\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + private static final String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "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\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final String BOB_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP certificate\n" + + "\n" + + "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\n" + + "NEJd3XZRzaXZE2aAMQ==\n" + + "=NXei\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + @Test + public void regressionTest() throws IOException { + SOPImpl sop = new SOPImpl(); + byte[] msg = "Hello, World!\n".getBytes(); + Ready encryption = sop.encrypt() + .signWith(CAROL_KEY.getBytes()) + .withCert(BOB_CERT.getBytes()) + .plaintext(msg); + byte[] ciphertext = encryption.getBytes(); + + ByteArrayAndResult decryption = sop.decrypt() + .withKey(BOB_KEY.getBytes()) + .verifyWithCert(CAROL_CERT.getBytes()) + .ciphertext(ciphertext) + .toByteArrayAndResult(); + + byte[] plaintext = decryption.getBytes(); + assertArrayEquals(msg, plaintext); + assertEquals(1, decryption.getResult().getVerifications().size()); + } +} From 23130b6c8aec7aa39833d49424332e0a0e4ad941 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Dec 2022 16:25:44 +0100 Subject: [PATCH 0623/1204] PGPainless 1.3.14 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a335092..8e5b5ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,11 @@ SPDX-License-Identifier: CC0-1.0 - Add `KeyRingUtils.publicKeys(PGPKeyRing keys)` - Remove `BCUtil` class +## 1.3.14 +- Bump `bcpg` to `1.72.3` +- Fix DSA key parameter check +- Use proper method to unlock private signing keys when creating detached signatures + ## 1.3.13 - Bump `sop-java` to `4.0.7` From 66abd5f65fb4bfc9ab32f06faa2f988d300e66ad Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Dec 2022 16:42:51 +0100 Subject: [PATCH 0624/1204] Cleartext-signatures MUST use TEXT mode --- .../src/main/java/org/pgpainless/sop/InlineSignImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index 30e1d71e..82c3603b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -75,7 +75,7 @@ public class InlineSignImpl implements InlineSign { for (PGPSecretKeyRing key : signingKeys) { try { if (mode == InlineSignAs.clearsigned) { - signingOptions.addDetachedSignature(protector, key); + signingOptions.addDetachedSignature(protector, key, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT); } else { signingOptions.addInlineSignature(protector, key, modeToSigType(mode)); } From 2d46fb18f775d6298b95d3f2dcd331d573b45e08 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 Dec 2022 17:02:53 +0100 Subject: [PATCH 0625/1204] SOP: Allow generation of keys without user-ids --- .../key/generation/KeyRingTemplates.java | 53 ++++++++++--------- .../org/pgpainless/sop/GenerateKeyImpl.java | 9 ++-- .../org/pgpainless/sop/GenerateKeyTest.java | 7 --- 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java index 444e7d74..42eb7efa 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java @@ -7,6 +7,7 @@ package org.pgpainless.key.generation; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; @@ -38,9 +39,9 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull UserId userId, @Nonnull RsaLength length) + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable UserId userId, @Nonnull RsaLength length) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId.toString(), length); + return simpleRsaKeyRing(userId == null ? null : userId.toString(), length); } /** @@ -56,7 +57,7 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull String userId, @Nonnull RsaLength length) + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { return simpleRsaKeyRing(userId, length, Passphrase.emptyPassphrase()); } @@ -75,9 +76,9 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull UserId userId, @Nonnull RsaLength length, String password) + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable UserId userId, @Nonnull RsaLength length, @Nullable String password) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId.toString(), length, password); + return simpleRsaKeyRing(userId == null ? null : userId.toString(), length, password); } /** @@ -94,7 +95,7 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull String userId, @Nonnull RsaLength length, String password) + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length, @Nullable String password) throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { Passphrase passphrase = Passphrase.emptyPassphrase(); if (!isNullOrEmpty(password)) { @@ -103,12 +104,14 @@ public final class KeyRingTemplates { return simpleRsaKeyRing(userId, length, passphrase); } - public PGPSecretKeyRing simpleRsaKeyRing(@Nonnull String userId, @Nonnull RsaLength length, @Nonnull Passphrase passphrase) + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length, @Nonnull Passphrase passphrase) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { KeyRingBuilder builder = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) - .addUserId(userId) .setPassphrase(passphrase); + if (userId != null) { + builder.addUserId(userId); + } return builder.build(); } @@ -125,9 +128,9 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleEcKeyRing(@Nonnull UserId userId) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable UserId userId) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId.toString()); + return simpleEcKeyRing(userId == null ? null : userId.toString()); } /** @@ -143,7 +146,7 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleEcKeyRing(@Nonnull String userId) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { return simpleEcKeyRing(userId, Passphrase.emptyPassphrase()); } @@ -162,9 +165,9 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleEcKeyRing(@Nonnull UserId userId, String password) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable UserId userId, String password) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId.toString(), password); + return simpleEcKeyRing(userId == null ? null : userId.toString(), password); } /** @@ -181,7 +184,7 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleEcKeyRing(@Nonnull String userId, String password) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId, String password) throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { Passphrase passphrase = Passphrase.emptyPassphrase(); if (!isNullOrEmpty(password)) { @@ -190,13 +193,15 @@ public final class KeyRingTemplates { return simpleEcKeyRing(userId, passphrase); } - public PGPSecretKeyRing simpleEcKeyRing(@Nonnull String userId, @Nonnull Passphrase passphrase) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId, @Nonnull Passphrase passphrase) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { KeyRingBuilder builder = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) - .addUserId(userId) .setPassphrase(passphrase); + if (userId != null) { + builder.addUserId(userId); + } return builder.build(); } @@ -211,8 +216,8 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing modernKeyRing(String userId) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - return modernKeyRing(userId, (Passphrase) null); + public PGPSecretKeyRing modernKeyRing(@Nullable String userId) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + return modernKeyRing(userId, Passphrase.emptyPassphrase()); } /** @@ -227,21 +232,21 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing modernKeyRing(String userId, String password) + public PGPSecretKeyRing modernKeyRing(@Nullable String userId, @Nullable String password) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - Passphrase passphrase = (password != null ? Passphrase.fromPassword(password) : null); + Passphrase passphrase = (password != null ? Passphrase.fromPassword(password) : Passphrase.emptyPassphrase()); return modernKeyRing(userId, passphrase); } - public PGPSecretKeyRing modernKeyRing(String userId, Passphrase passphrase) + public PGPSecretKeyRing modernKeyRing(@Nullable String userId, @Nonnull Passphrase passphrase) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { KeyRingBuilder builder = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) - .addUserId(userId); - if (passphrase != null && !passphrase.isEmpty()) { - builder.setPassphrase(passphrase); + .setPassphrase(passphrase); + if (userId != null) { + builder.addUserId(userId); } return builder.build(); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index 893c8dd8..c75087c8 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -28,7 +28,7 @@ public class GenerateKeyImpl implements GenerateKey { private boolean armor = true; private final Set userIds = new LinkedHashSet<>(); - private Passphrase passphrase; + private Passphrase passphrase = Passphrase.emptyPassphrase(); @Override public GenerateKey noArmor() { @@ -51,14 +51,11 @@ public class GenerateKeyImpl implements GenerateKey { @Override public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { Iterator userIdIterator = userIds.iterator(); - if (!userIdIterator.hasNext()) { - throw new SOPGPException.MissingArg("Missing user-id."); - } - PGPSecretKeyRing key; try { + String primaryUserId = userIdIterator.hasNext() ? userIdIterator.next() : null; key = PGPainless.generateKeyRing() - .modernKeyRing(userIdIterator.next(), passphrase); + .modernKeyRing(primaryUserId, passphrase); if (userIdIterator.hasNext()) { SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(key); diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java index 7f1710fd..a71eda12 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -6,7 +6,6 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; @@ -17,7 +16,6 @@ import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.info.KeyRingInfo; import sop.SOP; -import sop.exception.SOPGPException; public class GenerateKeyTest { @@ -28,11 +26,6 @@ public class GenerateKeyTest { sop = new SOPImpl(); } - @Test - public void testMissingUserId() { - assertThrows(SOPGPException.MissingArg.class, () -> sop.generateKey().generate()); - } - @Test public void generateKey() throws IOException { byte[] bytes = sop.generateKey() From cfba77dea5eb75bf5fdf9ec255e196f338d0fe11 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 14 Dec 2022 00:11:23 +0100 Subject: [PATCH 0626/1204] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e5b5ebe..ac12a0f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.0-rc3-SNAPSHOT +- `sop generate-key`: Add support for keys without user-ids +- `sop inline-sign --as=clearsigned`: Make signature in TEXT mode + ## 1.4.0-rc2 - Bump `bcpg-jdk15to18` to `1.72.3` - Use BCs `PGPEncryptedDataList.extractSessionKeyEncryptedData()` method From bfbaa30e4cb5567078098a080055cf6cf3b376f7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Dec 2022 16:12:30 +0100 Subject: [PATCH 0627/1204] Make KO-countermeasures configurable (off by default) --- .../key/protection/UnlockSecretKey.java | 5 +++- .../java/org/pgpainless/policy/Policy.java | 29 +++++++++++++++++++ .../ModifiedPublicKeysInvestigation.java | 6 ++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java index 42a5b9b7..78c849ab 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java @@ -9,6 +9,7 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.pgpainless.PGPainless; import org.pgpainless.exception.KeyIntegrityException; import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.key.info.KeyInfo; @@ -51,7 +52,9 @@ public final class UnlockSecretKey { throw new PGPException("Cannot decrypt secret key."); } - PublicKeyParameterValidationUtil.verifyPublicKeyParameterIntegrity(privateKey, secretKey.getPublicKey()); + if (PGPainless.getPolicy().isEnableKeyParameterValidation()) { + PublicKeyParameterValidationUtil.verifyPublicKeyParameterIntegrity(privateKey, secretKey.getPublicKey()); + } return privateKey; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java index ad9f0635..3556f104 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java @@ -49,6 +49,8 @@ public final class Policy { // Signers User-ID is soon to be deprecated. private SignerUserIdValidationLevel signerUserIdValidationLevel = SignerUserIdValidationLevel.DISABLED; + private boolean enableKeyParameterValidation = false; + public enum SignerUserIdValidationLevel { /** * PGPainless will verify {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets in signatures strictly. @@ -701,4 +703,31 @@ public final class Policy { this.signerUserIdValidationLevel = signerUserIdValidationLevel; return this; } + + /** + * Enable or disable validation of public key parameters when unlocking private keys. + * Disabled by default. + * When enabled, PGPainless will validate, whether public key parameters have been tampered with. + * This is a countermeasure against possible attacks described in the paper + * "Victory by KO: Attacking OpenPGP Using Key Overwriting" by Lara Bruseghini, Daniel Huigens, and Kenneth G. Paterson. + * Since these attacks are only possible in very special conditions (attacker has access to the encrypted private key), + * and the countermeasures are very costly, they are disabled by default, but can be enabled using this method. + * + * @see KOpenPGP.com + * @param enable boolean + * @return this + */ + public Policy setEnableKeyParameterValidation(boolean enable) { + this.enableKeyParameterValidation = enable; + return this; + } + + /** + * Return true, if countermeasures against the KOpenPGP attacks are enabled, false otherwise. + * + * @return true if countermeasures are enabled, false otherwise. + */ + public boolean isEnableKeyParameterValidation() { + return enableKeyParameterValidation; + } } diff --git a/pgpainless-core/src/test/java/investigations/ModifiedPublicKeysInvestigation.java b/pgpainless-core/src/test/java/investigations/ModifiedPublicKeysInvestigation.java index ba16e2d0..6930f78f 100644 --- a/pgpainless-core/src/test/java/investigations/ModifiedPublicKeysInvestigation.java +++ b/pgpainless-core/src/test/java/investigations/ModifiedPublicKeysInvestigation.java @@ -212,10 +212,12 @@ public class ModifiedPublicKeysInvestigation { SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("12345678")); PGPSecretKeyRing dsa = PGPainless.readKeyRing().secretKeyRing(DSA); + PGPainless.getPolicy().setEnableKeyParameterValidation(true); assertThrows(KeyIntegrityException.class, () -> UnlockSecretKey.unlockSecretKey(dsa.getSecretKey(KeyIdUtil.fromLongKeyId("b1bd1f049ec87f3d")), protector)); assertThrows(KeyIntegrityException.class, () -> UnlockSecretKey.unlockSecretKey(dsa.getSecretKey(KeyIdUtil.fromLongKeyId("f5ffdf6d71dd5789")), protector)); + PGPainless.getPolicy().setEnableKeyParameterValidation(false); } @Test @@ -223,8 +225,10 @@ public class ModifiedPublicKeysInvestigation { SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("12345678")); PGPSecretKeyRing elgamal = PGPainless.readKeyRing().secretKeyRing(ELGAMAL); + PGPainless.getPolicy().setEnableKeyParameterValidation(true); assertThrows(KeyIntegrityException.class, () -> UnlockSecretKey.unlockSecretKey(elgamal.getSecretKey(KeyIdUtil.fromLongKeyId("f5ffdf6d71dd5789")), protector)); + PGPainless.getPolicy().setEnableKeyParameterValidation(false); } @Test @@ -232,8 +236,10 @@ public class ModifiedPublicKeysInvestigation { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(INJECTED_KEY); SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("pass")); + PGPainless.getPolicy().setEnableKeyParameterValidation(true); assertThrows(KeyIntegrityException.class, () -> UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), protector)); + PGPainless.getPolicy().setEnableKeyParameterValidation(false); } @Test From 6a5c6c55096ea8ac99587accc3120b72d9dcfe23 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Dec 2022 16:28:10 +0100 Subject: [PATCH 0628/1204] Improve ElGamal validation by refraining from biginteger for loop variable --- .../key/util/PublicKeyParameterValidationUtil.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java index 88e9897a..344f063b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java @@ -265,14 +265,15 @@ public class PublicKeyParameterValidationUtil { // check g^i mod p != 1 for i < threshold BigInteger res = g; - BigInteger i = BigInteger.valueOf(1); - BigInteger threshold = BigInteger.valueOf(2).shiftLeft(17); - while (i.compareTo(threshold) < 0) { + // 262144 + int threshold = 2 << 17; + int i = 1; + while (i < threshold) { res = res.multiply(g).mod(p); if (res.equals(one)) { return false; } - i = i.add(one); + i++; } // blinded exponentiation to check y = g^(r*(p-1)+x) mod p From 3f10efac7a8f561da5ce5f503020b6ab1fa78d28 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Dec 2022 18:06:50 +0100 Subject: [PATCH 0629/1204] Update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac12a0f1..bb449799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ SPDX-License-Identifier: CC0-1.0 ## 1.4.0-rc3-SNAPSHOT - `sop generate-key`: Add support for keys without user-ids - `sop inline-sign --as=clearsigned`: Make signature in TEXT mode +- Make countermeasures against KOpenPGP attacks configurable + - Countermeasures are now disabled by default since they are costly and have a specific threat model + - Can be enabled by calling `Policy.setEnableKeyParameterValidation(true)` ## 1.4.0-rc2 - Bump `bcpg-jdk15to18` to `1.72.3` @@ -50,6 +53,11 @@ SPDX-License-Identifier: CC0-1.0 - Add `KeyRingUtils.publicKeys(PGPKeyRing keys)` - Remove `BCUtil` class +## 1.3.15 +- Fix crash in `sop generate-key --with-key-password` when more than one user-id is given +- `sop generate-key`: Allow key generation without user-ids +- `sop inline-sign --as=clearsigned`: Make signatures of type 'text' instead of 'binary' + ## 1.3.14 - Bump `bcpg` to `1.72.3` - Fix DSA key parameter check From 7a326ddef0e21360cadf519a68450440dacee72e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Dec 2022 18:19:13 +0100 Subject: [PATCH 0630/1204] PGPainless 1.4.0 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb449799..a8c57019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.4.0-rc3-SNAPSHOT +## 1.4.0 - `sop generate-key`: Add support for keys without user-ids - `sop inline-sign --as=clearsigned`: Make signature in TEXT mode - Make countermeasures against KOpenPGP attacks configurable diff --git a/README.md b/README.md index 30af42f8..39741bb5 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.0-rc2' + implementation 'org.pgpainless:pgpainless-core:1.4.0' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 2e4927a6..e7e612de 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.0-rc2" + implementation "org.pgpainless:pgpainless-sop:1.4.0" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.0-rc2 + 1.4.0 ... diff --git a/version.gradle b/version.gradle index 2d9745dd..8ced1bc0 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.0-rc3' - isSnapshot = true + shortVersion = '1.4.0' + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 01e6ef0013ab0638e3465f2069032bf39cda6309 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Dec 2022 18:22:04 +0100 Subject: [PATCH 0631/1204] PGPainless 1.4.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 8ced1bc0..65038afb 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.0' - isSnapshot = false + shortVersion = '1.4.1' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From c90b56ba13eac5ade70c64caf6808b52e31187a6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 16 Dec 2022 17:20:10 +0100 Subject: [PATCH 0632/1204] Add YourKit endorsement --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 39741bb5..6dbceacd 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,9 @@ Parts of PGPainless development ([project page](https://nlnet.nl/project/PGPainl NGI Assure is made possible with financial support from the [European Commission](https://ec.europa.eu/)'s [Next Generation Internet](https://ngi.eu/) programme, under the aegis of [DG Communications Networks, Content and Technology](https://ec.europa.eu/info/departments/communications-networks-content-and-technology_en). [![NGI Assure Logo](https://blog.jabberhead.tk/wp-content/uploads/2022/05/NGIAssure_tag.svg)](https://nlnet.nl/assure/) +Thanks to [YourKit](https://www.yourkit.com/) for providing a free license of the [YourKit Java Profiler](https://www.yourkit.com/java/profiler/) to support PGPainless Development! +[![YourKit Logo](https://www.yourkit.com/images/yklogo.png)](https://www.yourkit.com/) + Big thank you also to those who decided to support the work by donating! Notably @msfjarvis From dbcca586d1c58d931d053ce323dbe189fa1110da Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 16 Dec 2022 17:34:53 +0100 Subject: [PATCH 0633/1204] Add link to KOpenPGP to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8c57019..4bafa039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ SPDX-License-Identifier: CC0-1.0 ## 1.4.0 - `sop generate-key`: Add support for keys without user-ids - `sop inline-sign --as=clearsigned`: Make signature in TEXT mode -- Make countermeasures against KOpenPGP attacks configurable +- Make countermeasures against [KOpenPGP](https://kopenpgp.com/) attacks configurable - Countermeasures are now disabled by default since they are costly and have a specific threat model - Can be enabled by calling `Policy.setEnableKeyParameterValidation(true)` From 59217d25013df10eb8c22c129d7f30201e77bfc0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 19 Dec 2022 16:25:27 +0100 Subject: [PATCH 0634/1204] Implement UserId.parse(mailbox) --- .../java/org/pgpainless/key/util/UserId.java | 62 +++++++++- .../java/org/pgpainless/key/UserIdTest.java | 110 ++++++++++++++++++ 2 files changed, 171 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index a1b251dc..6dca06b5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -5,10 +5,21 @@ package org.pgpainless.key.util; import java.util.Comparator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.Nullable; public final class UserId implements CharSequence { + + private static final Pattern emailPattern = Pattern.compile("(?:[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+(?:\\.[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-" + + "\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9" + + "-]*[\\p{L}\\u0900-\\u097F0-9])?\\.)+[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[$\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f" + + "\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)])"); + + private static final Pattern nameAddrPattern = Pattern.compile("^((?.+?)\\s)?(\\((?.+?)\\)\\s)?(<(?.+?)>)?$"); + public static final class Builder { private String name; private String comment; @@ -58,6 +69,37 @@ public final class UserId implements CharSequence { } } + public static UserId parse(@Nonnull String string) { + Builder builder = newBuilder(); + string = string.trim(); + Matcher matcher = nameAddrPattern.matcher(string); + if (matcher.find()) { + String name = matcher.group("name"); + String comment = matcher.group("comment"); + String mail = matcher.group("email"); + matcher = emailPattern.matcher(mail); + if (!matcher.matches()) { + throw new IllegalArgumentException("Malformed email address"); + } + + if (name != null) { + builder.withName(name); + } + if (comment != null) { + builder.withComment(comment); + } + builder.withEmail(mail); + } else { + matcher = emailPattern.matcher(string); + if (matcher.matches()) { + builder.withEmail(string); + } else { + throw new IllegalArgumentException("Malformed email address"); + } + } + return builder.build(); + } + private final String name; private final String comment; private final String email; @@ -86,6 +128,24 @@ public final class UserId implements CharSequence { } public String getName() { + return getName(false); + } + + public String getName(boolean preserveQuotes) { + if (name == null || name.isEmpty()) { + return name; + } + + if (name.startsWith("\"")) { + if (preserveQuotes) { + return name; + } + String withoutQuotes = name.substring(1); + if (withoutQuotes.endsWith("\"")) { + withoutQuotes = withoutQuotes.substring(0, withoutQuotes.length() - 1); + } + return withoutQuotes; + } return name; } @@ -116,7 +176,7 @@ public final class UserId implements CharSequence { public @Nonnull String toString() { StringBuilder sb = new StringBuilder(); if (name != null && !name.isEmpty()) { - sb.append(name); + sb.append(getName(true)); } if (comment != null && !comment.isEmpty()) { if (sb.length() > 0) { diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index acc90918..b910e1b4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -271,4 +271,114 @@ public class UserIdTest { assertNotEquals(0, UserId.compare(id1, id6, c)); assertNotEquals(0, UserId.compare(id6, id1, c)); } + + @Test + public void parseNameAndEmail() { + UserId id = UserId.parse("Alice "); + + assertEquals("Alice", id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("Alice ", id.toString()); + } + + @Test + public void parseNameCommentAndEmail() { + UserId id = UserId.parse("Alice (work mail) "); + + assertEquals("Alice", id.getName()); + assertEquals("work mail", id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("Alice (work mail) ", id.toString()); + } + + @Test + public void parseLongNameAndEmail() { + UserId id = UserId.parse("Alice von Painleicester "); + + assertEquals("Alice von Painleicester", id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("Alice von Painleicester ", id.toString()); + } + + @Test + public void parseLongNameCommentAndEmail() { + UserId id = UserId.parse("Alice von Painleicester (work email) "); + + assertEquals("Alice von Painleicester", id.getName()); + assertEquals("work email", id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("Alice von Painleicester (work email) ", id.toString()); + } + + @Test + public void parseQuotedNameAndEmail() { + UserId id = UserId.parse("\"Alice\" "); + + assertEquals("Alice", id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("\"Alice\" ", id.toString()); + } + + @Test + public void parseQuotedNameCommentAndEmail() { + UserId id = UserId.parse("\"Alice\" (work email) "); + + assertEquals("Alice", id.getName()); + assertEquals("work email", id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("\"Alice\" (work email) ", id.toString()); + } + + @Test + public void parseLongQuotedNameAndEmail() { + UserId id = UserId.parse("\"Alice Mac Painlester\" "); + + assertEquals("Alice Mac Painlester", id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("\"Alice Mac Painlester\" ", id.toString()); + } + + @Test + public void parseLongQuotedNameCommentAndEmail() { + UserId id = UserId.parse("\"Alice Mac Painlester\" (work email) "); + + assertEquals("Alice Mac Painlester", id.getName()); + assertEquals("work email", id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("\"Alice Mac Painlester\" (work email) ", id.toString()); + } + + @Test + public void parseEmailOnly() { + UserId id = UserId.parse("alice@pgpainless.org"); + + assertNull(id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("", id.toString()); + } + + @Test + public void parseBracketedEmailOnly() { + UserId id = UserId.parse(""); + + assertNull(id.getName()); + assertNull(id.getComment()); + assertEquals("alice@pgpainless.org", id.getEmail()); + + assertEquals("", id.toString()); + } } From 94851ccb8f5294941f4715a66d5e4a50467d91d1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 20 Dec 2022 15:57:11 +0100 Subject: [PATCH 0635/1204] Add javadoc for UserId.parse() --- .../java/org/pgpainless/key/util/UserId.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 6dca06b5..3c43b92c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -69,6 +69,24 @@ public final class UserId implements CharSequence { } } + /** + * Parse a {@link UserId} from free-form text,
name-addr
or
mailbox
string and split it + * up into its components. + * Example inputs for this method: + *
    + *
  • john@pgpainless.org
  • + *
  • <john@pgpainless.org>
  • + *
  • John Doe
  • + *
  • John Doe <john@pgpainless.org>
  • + *
  • John Doe (work email) <john@pgpainless.org>
  • + *
+ * In these cases, {@link #parse(String)} will detect email addresses, names and comments and expose those + * via the respective getters. + * + * @see RFC5322 §3.4. Address Specification + * @param string user-id + * @return parsed {@link UserId} object + */ public static UserId parse(@Nonnull String string) { Builder builder = newBuilder(); string = string.trim(); From 75f69c0473c1a2807fc39d0d89ce20c474df79c5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 20 Dec 2022 17:27:32 +0100 Subject: [PATCH 0636/1204] Fix Android compatibility by using Matcher.group(int) instead of Matcher.group(String) --- .../src/main/java/org/pgpainless/key/util/UserId.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 3c43b92c..e79d9700 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -92,9 +92,9 @@ public final class UserId implements CharSequence { string = string.trim(); Matcher matcher = nameAddrPattern.matcher(string); if (matcher.find()) { - String name = matcher.group("name"); - String comment = matcher.group("comment"); - String mail = matcher.group("email"); + String name = matcher.group(2); + String comment = matcher.group(4); + String mail = matcher.group(6); matcher = emailPattern.matcher(mail); if (!matcher.matches()) { throw new IllegalArgumentException("Malformed email address"); From a376587680ecd76b8b3302875731f596e4c44805 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Dec 2022 14:43:09 +0100 Subject: [PATCH 0637/1204] Add tests for international user-ids --- .../java/org/pgpainless/key/UserIdTest.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index b910e1b4..aece0efb 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -381,4 +381,64 @@ public class UserIdTest { assertEquals("", id.toString()); } + + @Test + public void parseLatinWithDiacritics() { + UserId pele = UserId.parse("PelÊ@example.com"); + assertEquals("PelÊ@example.com", pele.getEmail()); + + pele = UserId.parse("Marquez PelÊ "); + assertEquals("PelÊ@example.com", pele.getEmail()); + assertEquals("Marquez PelÊ", pele.getName()); + } + + @Test + public void parseGreekAlphabet() { + UserId dokimi = UserId.parse("δÎŋÎēΚÎŧÎŽ@Ī€ÎąĪÎŦδÎĩÎšÎŗÎŧÎą.δÎŋÎēΚÎŧÎŽ"); + assertEquals("δÎŋÎēΚÎŧÎŽ@Ī€ÎąĪÎŦδÎĩÎšÎŗÎŧÎą.δÎŋÎēΚÎŧÎŽ", dokimi.getEmail()); + + dokimi = UserId.parse("δÎŋÎēΚÎŧÎŽ <δÎŋÎēΚÎŧÎŽ@Ī€ÎąĪÎŦδÎĩÎšÎŗÎŧÎą.δÎŋÎēΚÎŧÎŽ>"); + assertEquals("δÎŋÎēΚÎŧÎŽ", dokimi.getName()); + assertEquals("δÎŋÎēΚÎŧÎŽ@Ī€ÎąĪÎŦδÎĩÎšÎŗÎŧÎą.δÎŋÎēΚÎŧÎŽ", dokimi.getEmail()); + } + + @Test + public void parseTraditionalChinese() { + UserId womai = UserId.parse("æˆ‘č˛ˇ@åą‹äŧ.éĻ™æ¸¯"); + assertEquals("æˆ‘č˛ˇ@åą‹äŧ.éĻ™æ¸¯", womai.getEmail()); + + womai = UserId.parse("æˆ‘č˛ˇ <æˆ‘č˛ˇ@åą‹äŧ.éĻ™æ¸¯>"); + assertEquals("æˆ‘č˛ˇ@åą‹äŧ.éĻ™æ¸¯", womai.getEmail()); + assertEquals("æˆ‘č˛ˇ", womai.getName()); + } + + @Test + public void parseJapanese() { + UserId ninomiya = UserId.parse("äēŒãƒŽåŽŽ@éģ’åˇ.æ—ĨæœŦ"); + assertEquals("äēŒãƒŽåŽŽ@éģ’åˇ.æ—ĨæœŦ", ninomiya.getEmail()); + + ninomiya = UserId.parse("äēŒãƒŽåŽŽ <äēŒãƒŽåŽŽ@éģ’åˇ.æ—ĨæœŦ>"); + assertEquals("äēŒãƒŽåŽŽ@éģ’åˇ.æ—ĨæœŦ", ninomiya.getEmail()); + assertEquals("äēŒãƒŽåŽŽ", ninomiya.getName()); + } + + @Test + public void parseCyrillic() { + UserId medved = UserId.parse("ĐŧĐĩдвĐĩĐ´ŅŒ@ҁ-йаĐģаĐģаКĐēОК.Ҁ҄"); + assertEquals("ĐŧĐĩдвĐĩĐ´ŅŒ@ҁ-йаĐģаĐģаКĐēОК.Ҁ҄", medved.getEmail()); + + medved = UserId.parse("ĐŧĐĩдвĐĩĐ´ŅŒ <ĐŧĐĩдвĐĩĐ´ŅŒ@ҁ-йаĐģаĐģаКĐēОК.Ҁ҄>"); + assertEquals("ĐŧĐĩдвĐĩĐ´ŅŒ@ҁ-йаĐģаĐģаКĐēОК.Ҁ҄", medved.getEmail()); + assertEquals("ĐŧĐĩдвĐĩĐ´ŅŒ", medved.getName()); + } + + @Test + public void parseDevanagari() { + UserId samparka = UserId.parse("⤏⤂ā¤Ē⤰āĨā¤•@ā¤Ąā¤žā¤Ÿā¤žā¤ŽāĨ‡ā¤˛.ā¤­ā¤žā¤°ā¤¤"); + assertEquals("⤏⤂ā¤Ē⤰āĨā¤•@ā¤Ąā¤žā¤Ÿā¤žā¤ŽāĨ‡ā¤˛.ā¤­ā¤žā¤°ā¤¤", samparka.getEmail()); + + samparka = UserId.parse("⤏⤂ā¤Ē⤰āĨā¤• <⤏⤂ā¤Ē⤰āĨā¤•@ā¤Ąā¤žā¤Ÿā¤žā¤ŽāĨ‡ā¤˛.ā¤­ā¤žā¤°ā¤¤>"); + assertEquals("⤏⤂ā¤Ē⤰āĨā¤•@ā¤Ąā¤žā¤Ÿā¤žā¤ŽāĨ‡ā¤˛.ā¤­ā¤žā¤°ā¤¤", samparka.getEmail()); + assertEquals("⤏⤂ā¤Ē⤰āĨā¤•", samparka.getName()); + } } From 533b54a6b7c5f1aad340d261fdfcd54fa8cde032 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Dec 2022 15:01:10 +0100 Subject: [PATCH 0638/1204] Add some more tests for valid email address formats --- .../java/org/pgpainless/key/util/UserId.java | 6 ++ .../java/org/pgpainless/key/UserIdTest.java | 91 +++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index e79d9700..1a2ea265 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -82,6 +82,12 @@ public final class UserId implements CharSequence { * * In these cases, {@link #parse(String)} will detect email addresses, names and comments and expose those * via the respective getters. + * This method does not support parsing mail addresses of the following formats: + *
    + *
  • Local domains without TLDs (
    user@localdomain1
    )
  • + *
  • " "@example.org
    (spaces between the quotes)
  • + *
  • "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
  • + *
* * @see RFC5322 §3.4. Address Specification * @param string user-id diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index aece0efb..93cb6922 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -441,4 +441,95 @@ public class UserIdTest { assertEquals("⤏⤂ā¤Ē⤰āĨā¤•@ā¤Ąā¤žā¤Ÿā¤žā¤ŽāĨ‡ā¤˛.ā¤­ā¤žā¤°ā¤¤", samparka.getEmail()); assertEquals("⤏⤂ā¤Ē⤰āĨā¤•", samparka.getName()); } + + @Test + public void parseMailWithPlus() { + UserId id = UserId.parse("disposable.style.email.with+symbol@example.com"); + assertEquals("disposable.style.email.with+symbol@example.com", id.getEmail()); + + id = UserId.parse("Disposable Mail "); + assertEquals("disposable.style.email.with+symbol@example.com", id.getEmail()); + assertEquals("Disposable Mail", id.getName()); + } + + @Test + public void parseMailWithHyphen() { + UserId id = UserId.parse("other.email-with-hyphen@example.com"); + assertEquals("other.email-with-hyphen@example.com", id.getEmail()); + + id = UserId.parse("Other Email "); + assertEquals("other.email-with-hyphen@example.com", id.getEmail()); + assertEquals("Other Email", id.getName()); + } + + @Test + public void parseMailWithTagAndSorting() { + UserId id = UserId.parse("user.name+tag+sorting@example.com"); + assertEquals("user.name+tag+sorting@example.com", id.getEmail()); + + id = UserId.parse("User Name "); + assertEquals("user.name+tag+sorting@example.com", id.getEmail()); + assertEquals("User Name", id.getName()); + } + + @Test + public void parseMailWithSlash() { + UserId id = UserId.parse("test/test@test.com"); + assertEquals("test/test@test.com", id.getEmail()); + + id = UserId.parse("Who uses Slashes "); + assertEquals("test/test@test.com", id.getEmail()); + assertEquals("Who uses Slashes", id.getName()); + } + + @Test + public void parseDoubleDots() { + UserId id = UserId.parse("\"john..doe\"@example.org"); + assertEquals("\"john..doe\"@example.org", id.getEmail()); + + id = UserId.parse("John Doe <\"john..doe\"@example.org>"); + assertEquals("\"john..doe\"@example.org", id.getEmail()); + assertEquals("John Doe", id.getName()); + } + + @Test + public void parseBangifiedHostRoute() { + UserId id = UserId.parse("mailhost!username@example.org"); + assertEquals("mailhost!username@example.org", id.getEmail()); + + id = UserId.parse("Bangified Host Route "); + assertEquals("mailhost!username@example.org", id.getEmail()); + assertEquals("Bangified Host Route", id.getName()); + } + + @Test + public void parsePercentRouted() { + UserId id = UserId.parse("user%example.com@example.org"); + assertEquals("user%example.com@example.org", id.getEmail()); + + id = UserId.parse("User "); + assertEquals("user%example.com@example.org", id.getEmail()); + assertEquals("User", id.getName()); + } + + @Test + public void parseLocalPartEndingWithNonAlphanumericCharacter() { + UserId id = UserId.parse("user-@example.org"); + assertEquals("user-@example.org", id.getEmail()); + + id = UserId.parse("User "); + assertEquals("user-@example.org", id.getEmail()); + assertEquals("User", id.getName()); + } + + @Test + public void parseDomainIsIpAddress() { + UserId id = UserId.parse("postmaster@[123.123.123.123]"); + assertEquals("postmaster@[123.123.123.123]", id.getEmail()); + + id = UserId.parse("Alice (work email) "); + assertEquals("postmaster@[123.123.123.123]", id.getEmail()); + assertEquals("Alice", id.getName()); + assertEquals("work email", id.getComment()); + } } From 44738766e5fa5da50ba883ecd5c347061fc4387c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Dec 2022 15:19:42 +0100 Subject: [PATCH 0639/1204] Add comments to regexes --- .../src/main/java/org/pgpainless/key/util/UserId.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 1a2ea265..f59cd36f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -12,12 +12,20 @@ import javax.annotation.Nullable; public final class UserId implements CharSequence { + // Email regex: https://emailregex.com/ + // switched "a-z0-9" to "\p{L}\u0900-\u097F0-9" for better support for international characters + // \\p{L} = Unicode Letters + // \u0900-\u097F = Hindi Letters private static final Pattern emailPattern = Pattern.compile("(?:[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+(?:\\.[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-" + "\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9" + "-]*[\\p{L}\\u0900-\\u097F0-9])?\\.)+[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + "\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[$\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f" + "\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)])"); + // User-ID Regex + // "Firstname Lastname (Comment) " + // All groups are optional + // https://www.rfc-editor.org/rfc/rfc5322#page-16 private static final Pattern nameAddrPattern = Pattern.compile("^((?.+?)\\s)?(\\((?.+?)\\)\\s)?(<(?.+?)>)?$"); public static final class Builder { From b5128be6fb65e7fb791e8e8e3debae49ec644c90 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Dec 2022 15:23:00 +0100 Subject: [PATCH 0640/1204] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bafa039..4493fe1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.1 +- Add `UserId.parse()` method to parse user-ids into their components + ## 1.4.0 - `sop generate-key`: Add support for keys without user-ids - `sop inline-sign --as=clearsigned`: Make signature in TEXT mode From 35c62663e95c0f444531cc2bf20854d6079792ba Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Dec 2022 15:30:11 +0100 Subject: [PATCH 0641/1204] Fix javadoc --- .../src/main/java/org/pgpainless/key/util/UserId.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index f59cd36f..03a50321 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -94,7 +94,7 @@ public final class UserId implements CharSequence { *
    *
  • Local domains without TLDs (
    user@localdomain1
    )
  • *
  • " "@example.org
    (spaces between the quotes)
  • - *
  • "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
  • + *
  • "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
  • *
* * @see RFC5322 §3.4. Address Specification From 77c476dff2c0015cdac8be63d708e7081e136941 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Dec 2022 15:31:32 +0100 Subject: [PATCH 0642/1204] PGPainless 1.4.1 --- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6dbceacd..49320f94 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.0' + implementation 'org.pgpainless:pgpainless-core:1.4.1' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index e7e612de..c1b2a988 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.0" + implementation "org.pgpainless:pgpainless-sop:1.4.1" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.0 + 1.4.1 ... diff --git a/version.gradle b/version.gradle index 65038afb..76939ca7 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.4.1' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 7be12b0aaaca1bb5fcdecf55ef7e947cdc385986 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Dec 2022 15:33:24 +0100 Subject: [PATCH 0643/1204] PGPainless 1.4.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 76939ca7..c6f7104e 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.1' - isSnapshot = false + shortVersion = '1.4.2' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 94d9efa1e7c123445b24754fec13631672f6d4fa Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jan 2023 13:12:14 +0100 Subject: [PATCH 0644/1204] OpenPgpMessageInputStream: Ignore non-integrity-protected data if configured --- .../decryption_verification/OpenPgpMessageInputStream.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index c26c949b..bb5d67b3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -409,8 +409,10 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); if (!encDataList.isIntegrityProtected()) { - LOGGER.debug("Symmetrically Encrypted Data Packet is not integrity-protected and is therefore rejected."); - throw new MessageNotIntegrityProtectedException(); + LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected."); + if (!options.isIgnoreMDCErrors()) { + throw new MessageNotIntegrityProtectedException(); + } } SortedESKs esks = new SortedESKs(encDataList); From 00b593823a2bf7fce7b897c530f60032c2d16d7c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jan 2023 13:18:18 +0100 Subject: [PATCH 0645/1204] Modify SED test to test successful decryption of SED packet --- .../ModificationDetectionTests.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java index 9ecaa38a..59021f95 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java @@ -4,6 +4,7 @@ package org.pgpainless.decryption_verification; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; @@ -374,7 +375,7 @@ public class ModificationDetectionTests { @TestTemplate @ExtendWith(TestAllImplementations.class) - public void decryptMessageWithSEDPacket() throws IOException { + public void decryptMessageWithSEDPacket() throws IOException, PGPException { Passphrase passphrase = Passphrase.fromPassword("flowcrypt compatibility tests"); String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n" + "Version: FlowCrypt 6.9.1 Gmail Encryption\r\n" + @@ -536,6 +537,21 @@ public class ModificationDetectionTests { .withOptions(new ConsumerOptions().addDecryptionKey(secretKeyRing, SecretKeyRingProtector.unlockAnyKeyWith(passphrase))) ); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ciphertext.getBytes(StandardCharsets.UTF_8))) + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeyRing, + SecretKeyRingProtector.unlockAnyKeyWith(passphrase)) + .setIgnoreMDCErrors(true)); + ByteArrayOutputStream plaintext = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, plaintext); + decryptionStream.close(); + + assertEquals("As stated in subject\r\n" + + "\r\n" + + "Shall not decrypt automatically\r\n" + + "\r\n" + + "Has to show a warning\r\n", plaintext.toString()); } private PGPSecretKeyRingCollection getDecryptionKey() throws IOException { From b36494ecd439546fbf4b4c34c4a63649b396ff7a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jan 2023 13:53:12 +0100 Subject: [PATCH 0646/1204] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4493fe1a..9a35d4d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.2-SNAPSHOT +- Properly decrypt messages without MDC packets when `ConsumerOptions.setIgnoreMDCErrors(true)` is set + ## 1.4.1 - Add `UserId.parse()` method to parse user-ids into their components From 507b36468b299173da06f4a21583763f2697541d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jan 2023 14:06:19 +0100 Subject: [PATCH 0647/1204] Update docs on UserId parsing --- docs/source/pgpainless-core/userids.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/source/pgpainless-core/userids.md b/docs/source/pgpainless-core/userids.md index 78126828..d2b5730e 100644 --- a/docs/source/pgpainless-core/userids.md +++ b/docs/source/pgpainless-core/userids.md @@ -29,4 +29,19 @@ UserId full = UserId.newBuilder() .withComment("Work Address") .build(); assertEquals("Peter Pattern (Work Address) ", full.toString()); -``` \ No newline at end of file +``` + +If you have a User-ID in form of a string (e.g. because a user provided it via a text field), +you can parse it into its components like this: + +```java +String string = "John Doe "; +UserId userId = UserId.parse(string); + +// Now you can access the different components +assertEquals("John Doe", userId.getName()); +assertEquals("john@doe.corp", userId.getEmail()); +assertNull(userId.getComment()); +``` + +The method `UserId.parse(String string)` will throw an `IllegalArgumentException` if the User-ID is malformed. From 1452af71fd885b65f781b4104212c96d5f0e7cea Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jan 2023 15:23:29 +0100 Subject: [PATCH 0648/1204] Add more docs for Policy related configuration --- docs/source/pgpainless-core/quickstart.md | 162 ++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/docs/source/pgpainless-core/quickstart.md b/docs/source/pgpainless-core/quickstart.md index d61a881c..5382923f 100644 --- a/docs/source/pgpainless-core/quickstart.md +++ b/docs/source/pgpainless-core/quickstart.md @@ -325,4 +325,166 @@ verificationStream.close(); // finish verification MessageMetadata result = verificationStream.getMetadata(); // get metadata of signed message assertTrue(result.isVerifiedSignedBy(certificate)); // check if message was in fact signed +``` + +### Legacy Compatibility +Out of the box, PGPainless is configured to use secure defaults and perform checks for recommended +security features. This means that for example messages generated using older OpenPGP +implementations which do not follow those best practices might fail to decrypt/verify. + +It is however possible to circumvent certain security checks to allow processing of such messages. + +:::{note} +It is not recommended to disable security checks, as that might enable certain attacks on the OpenPGP protocol. +::: + +#### Missing / broken MDC (modification detection code) +RFC4880 has two different types of encrypted data packets. The *Symmetrically Encrypted Data* packet (SED) and the *Symmetrically Encrypted Integrity-Protected Data* packet. +The latter has an added MDC packet which prevents modifications to the ciphertext. + +While implementations are highly encouraged to only use the latter package type, some older implementations still generate +encrypted data packets which are not integrity protected. + +To allow PGPainless to decrypt such messages, you need to set a flag in the `ConsumerOptions` object: +```java +ConsumerOptions options = ConsumerOptions.get() + .setIgnoreMDCErrors(true) // <- + .setDecryptionKey(secretKey) + ... + +DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(options); +... +``` + +:::{note} +It is highly advised to only set this flag if you know what you are doing. +It might also be a good idea to try decrypting a message without the flag set first and only re-try +decryption with the flag set in case of a `MessageNotIntegrityProtectedException` (don't forget to rewind the ciphertextInputStream). +::: + +#### Weak keys and broken algorithms +Some users might cling on to older keys using weak algorithms / small key sizes. +PGPainless refuses to encrypt to weak certificates and sign with weak keys. +By default, PGPainless follows the recommendations for acceptable key sizes of [the German BSI in 2021](https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-1.pdf). +It can however be configured to accept older key material / algorithms too. + +Minimal key lengths can be configured by changing PGPainless' policy: +```java +Map algorithms = new HashMap<>(); +// put all acceptable algorithms and their minimal key length +algorithms.put(PublicKeyAlgorithm.RSA_GENERAL, 1024); +algorithms.put(PublicKeyAlgorithm.ECDSA, 100); +... +Policy.PublicKeyAlgorithmPolicy pkPolicy = + new Policy.PublicKeyAlgorithmPolicy(algorithms); +// set the custom algorithm policy +PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(); +``` + +Since OpenPGP uses a hybrid encryption scheme of asymmetric and symmetric encryption algorithms, +it also comes with a policy for symmetric encryption algorithms. +This list can be modified to allow for weaker algorithms like follows: +```java +// default fallback algorithm for message encryption +SymmetricKeyAlgorithm fallbackAlgorithm = SymmetricKeyAlgorithm.AES_256; +// acceptable algorithms +List algorithms = new ArrayList<>(); +algorithms.add(SymmetricKeyAlgorithm.AES_256); +algorithms.add(SymmetricKeyAlgorithm.AES_192); +algorithms.add(SymmetricKeyAlgorithm.AES_128); +algorithms.add(SymmetricKeyAlgorithm.TWOFISH); +algorithms.add(SymmetricKeyAlgorithm.BLOWFISH); +... +Policy.SymmetricKeyAlgorithmPolicy skPolicy = + new SymmtricKeyAlgorithmPolicy(fallbackAlgorithm, algorithms); +// set the custom algorithm policy +// algorithm policy applicable when decrypting messages created by legacy senders: +PGPainless.getPolicy() + .setSymmetricKeyDecryptionAlgorithmPolicy(skPolicy); +// algorithm policy applicable when generating messages for legacy recipients: +PGPainless.getPolicy() + .setSymmetricKeyEncryptionAlgorithmPolicy(skPolicy); +``` + +Hash algorithms are used in OpenPGP to create signatures. +Since signature verification is an integral part of the OpenPGP protocol, PGPainless comes +with multiple policies for acceptable hash algorithms, depending on the use-case. +Revocation signatures are critical, so you might want to handle revocation signatures differently from normal signatures. + +By default, PGPainless uses a smart hash algorithm policy for both use-cases, which takes into consideration +not only the hash algorithm itself, but also the creation date of the signature. +That way, signatures using SHA-1 are acceptable if they were created before February 2013, but are rejected if their +creation date is after that point in time. + +A custom hash algorithm policy can be set like this: +```java +HashAlgorithm fallbackAlgorithm = HashAlgorithm.SHA512; +Map algorithms = new HashMap<>(); +// Accept MD5 on signatures made before 1997-02-01 +algorithms.put(HashAlgorithm.MD5, + DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")); +// Accept SHA-1, regardless of signature creation time +algorithms.put(HashAlgorithm.SHA1, null); +... +Policy.HashAlgorithmPolicy hPolicy = + new Policy.HashAlgorithmPolicy(fallbackAlgorithm, algorithms); +// set policy for revocation signatures +PGPainless.getPolicy() + .setRevocationSignatureHashAlgorithmPolicy(hPolicy); +// set policy for normal signatures (certifications and document signatures) +PGPainless.getPolicy() + .setSignatureHashAlgorithmPolicy(hPolicy); +``` + +Lastly, PGPainless comes with a policy on acceptable compression algorithms, which currently accepts any +compression algorithm. +A custom compression algorithm policy can be set in a similar way: +```java +CompressionAlgorithm fallback = CompressionAlgorithm.ZIP; +List algorithms = new ArrayList<>(); +algorithms.add(CompressionAlgorith.ZIP); +algorithms.add(CompressionAlgorithm.BZIP2); +... +Policy.CompressionAlgorithmPolicy cPolicy = + new Policy.CompressionAlgorithmPolicy(fallback, algorithms); +PGPainless.getPolicy() + .setCompressionAlgorithmPolicy(cPolicy); +``` + +To prevent a class of attacks described in the [paper](https://www.kopenpgp.com/#paper) +"Victory by KO: Attacking OpenPGP Using Key Overwriting", +PGPainless offers the option to validate private key material each time before using it, +to make sure that an attacker didn't tamper with the corresponding public key parameters. + +These checks are disabled by default, but they can be enabled as follows: +```java +PGPainless.getPolicy() + .setEnableKeyParameterValidation(true); +``` + +:::{note} +Validation checks against KOpenPGP attacks are disabled by default, since they are very costly +and only make sense in certain scenarios. +Please read and understand the paper to decide, if enabling the checks makes sense for your use-case. +::: + + +### Known Notations +In OpenPGP, signatures can contain [notation subpackets](https://www.rfc-editor.org/rfc/rfc4880#section-5.2.3.16). +A notation can give meaning to a signature, or add additional contextual information. +Signature subpackets can be marked as critical, meaning an implementation that does not know about +a certain subpacket MUST reject the signature. +The same is true for critical notations. + +For that reason, PGPainless comes with a `NotationRegistry` class which can be used to register known notations, +such that a signature containing a critical notation of a certain value is not rejected. +To register a known notation, you can do the following: + +```java +NotationRegistry registry = PGPainless.getPolicy() + .getNotationRegistry(); + +registry.addKnownNotation("sample@example.com"); ``` \ No newline at end of file From 51c7bc932b73a7c55571ef3907cd7b644d588ddd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jan 2023 15:35:55 +0100 Subject: [PATCH 0649/1204] Bump gradlew to 7.5 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a488f210..8049c684 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-rc-1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From abf723cc6c94a492993835a2a01d78afe789cb06 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 4 Jan 2023 18:27:14 +0100 Subject: [PATCH 0650/1204] Add note about UserId.parse().toString() not guaranteing identity --- .../src/main/java/org/pgpainless/key/util/UserId.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 03a50321..427f9060 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -96,6 +96,8 @@ public final class UserId implements CharSequence { *
  • " "@example.org
    (spaces between the quotes)
  • *
  • "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
  • * + * Note: This method does not guarantee that
    string.equals(UserId.parse(string).toString())
    is true. + * For example,
    UserId.parse("alice@pgpainless.org").toString()
    wraps the mail address in angled brackets. * * @see RFC5322 §3.4. Address Specification * @param string user-id From 41cc71c274a3560019010de300a56a035aabb6d4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 4 Jan 2023 18:50:10 +0100 Subject: [PATCH 0651/1204] Add missing javadoc to ConsumerOptions --- .../ConsumerOptions.java | 99 ++++++++++++++++--- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index f7b3c020..33404c48 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -42,7 +42,6 @@ import pgp.certificate_store.exception.BadDataException; */ public class ConsumerOptions { - private boolean ignoreMDCErrors = false; private boolean forceNonOpenPgpData = false; @@ -55,7 +54,8 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; - private final Map customPublicKeyDataDecryptorFactories = new HashMap<>(); + private final Map customPublicKeyDataDecryptorFactories = + new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -135,16 +135,38 @@ public class ConsumerOptions { return this; } + /** + * Pass in a {@link PGPCertificateStore} from which certificates can be sourced for signature verification. + * + * @param certificateStore certificate store + * @return options + */ public ConsumerOptions addVerificationCerts(PGPCertificateStore certificateStore) { this.certificates.addStore(certificateStore); return this; } - public ConsumerOptions addVerificationOfDetachedSignatures(InputStream signatureInputStream) throws IOException, PGPException { + /** + * Add some detached signatures from the given {@link InputStream} for verification. + * + * @param signatureInputStream input stream of detached signatures + * @return options + * + * @throws IOException in case of an IO error + * @throws PGPException in case of an OpenPGP error + */ + public ConsumerOptions addVerificationOfDetachedSignatures(InputStream signatureInputStream) + throws IOException, PGPException { List signatures = SignatureUtils.readSignatures(signatureInputStream); return addVerificationOfDetachedSignatures(signatures); } + /** + * Add some detached signatures for verification. + * + * @param detachedSignatures detached signatures + * @return options + */ public ConsumerOptions addVerificationOfDetachedSignatures(List detachedSignatures) { for (PGPSignature signature : detachedSignatures) { addVerificationOfDetachedSignature(signature); @@ -211,14 +233,15 @@ public class ConsumerOptions { } /** - * Add a key for message decryption. If the key is encrypted, the {@link SecretKeyRingProtector} is used to decrypt it - * when needed. + * Add a key for message decryption. If the key is encrypted, the {@link SecretKeyRingProtector} + * is used to decrypt it when needed. * * @param key key * @param keyRingProtector protector for the secret key * @return options */ - public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key, @Nonnull SecretKeyRingProtector keyRingProtector) { + public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key, + @Nonnull SecretKeyRingProtector keyRingProtector) { decryptionKeys.put(key, keyRingProtector); return this; } @@ -230,7 +253,8 @@ public class ConsumerOptions { * @param keyRingProtector protector for encrypted secret keys * @return options */ - public ConsumerOptions addDecryptionKeys(@Nonnull PGPSecretKeyRingCollection keys, @Nonnull SecretKeyRingProtector keyRingProtector) { + public ConsumerOptions addDecryptionKeys(@Nonnull PGPSecretKeyRingCollection keys, + @Nonnull SecretKeyRingProtector keyRingProtector) { for (PGPSecretKeyRing key : keys) { addDecryptionKey(key, keyRingProtector); } @@ -264,14 +288,31 @@ public class ConsumerOptions { return this; } + /** + * Return the custom {@link PublicKeyDataDecryptorFactory PublicKeyDataDecryptorFactories} that were + * set by the user. + * These factories can be used to decrypt session keys using a custom logic. + * + * @return custom decryptor factories + */ Map getCustomDecryptorFactories() { return new HashMap<>(customPublicKeyDataDecryptorFactories); } + /** + * Return the set of available decryption keys. + * + * @return decryption keys + */ public @Nonnull Set getDecryptionKeys() { return Collections.unmodifiableSet(decryptionKeys.keySet()); } + /** + * Return the set of available message decryption passphrases. + * + * @return decryption passphrases + */ public @Nonnull Set getDecryptionPassphrases() { return Collections.unmodifiableSet(decryptionPassphrases); } @@ -287,18 +328,40 @@ public class ConsumerOptions { return certificates.getExplicitCertificates(); } + /** + * Return an object holding available certificates for signature verification. + * + * @return certificate source + */ public @Nonnull CertificateSource getCertificateSource() { return certificates; } + /** + * Return the callback that gets called when a certificate for signature verification is missing. + * This method might return
    null
    if the users hasn't set a callback. + * + * @return missing public key callback + */ public @Nullable MissingPublicKeyCallback getMissingCertificateCallback() { return missingCertificateCallback; } + /** + * Return the {@link SecretKeyRingProtector} for the given {@link PGPSecretKeyRing}. + * + * @param decryptionKeyRing secret key + * @return protector for that particular secret key + */ public @Nonnull SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) { return decryptionKeys.get(decryptionKeyRing); } + /** + * Return the set of detached signatures the user provided. + * + * @return detached signatures + */ public @Nonnull Set getDetachedSignatures() { return Collections.unmodifiableSet(detachedSignatures); } @@ -307,12 +370,14 @@ public class ConsumerOptions { * By default, PGPainless will require encrypted messages to make use of SEIP data packets. * Those are Symmetrically Encrypted Integrity Protected Data packets. * Symmetrically Encrypted Data Packets without integrity protection are rejected by default. - * Furthermore, PGPainless will throw an exception if verification of the MDC error detection code of the SEIP packet - * fails. + * Furthermore, PGPainless will throw an exception if verification of the MDC error detection + * code of the SEIP packet fails. * - * Failure of MDC verification indicates a tampered ciphertext, which might be the cause of an attack or data corruption. + * Failure of MDC verification indicates a tampered ciphertext, which might be the cause of an + * attack or data corruption. * - * This method can be used to ignore MDC errors and allow PGPainless to consume encrypted data without integrity protection. + * This method can be used to ignore MDC errors and allow PGPainless to consume encrypted data + * without integrity protection. * If the flag
    ignoreMDCErrors
    is set to true, PGPainless will *
      *
    • not throw exceptions for SEIP packets with tampered ciphertext
    • @@ -323,7 +388,8 @@ public class ConsumerOptions { * * It will however still throw an exception if it encounters a SEIP packet with missing or truncated MDC * - * @see Sym. Encrypted Integrity Protected Data Packet + * @see + * Sym. Encrypted Integrity Protected Data Packet * @param ignoreMDCErrors true if MDC errors or missing MDCs shall be ignored, false otherwise. * @return options */ @@ -353,6 +419,11 @@ public class ConsumerOptions { return this; } + /** + * Return true, if the ciphertext should be handled as binary non-OpenPGP data. + * + * @return true if non-OpenPGP data is forced + */ boolean isForceNonOpenPgpData() { return forceNonOpenPgpData; } @@ -407,6 +478,10 @@ public class ConsumerOptions { return multiPassStrategy; } + /** + * Source for OpenPGP certificates. + * When verifying signatures on a message, this object holds available signer certificates. + */ public static class CertificateSource { private List stores = new ArrayList<>(); From 980daeca31f5f1554f86f34eeb67164a7d73fc2b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 4 Jan 2023 18:55:57 +0100 Subject: [PATCH 0652/1204] Add missing javadoc to CustomPublicKeyDataDecryptorFactory --- .../CustomPublicKeyDataDecryptorFactory.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java index 37dc10a9..91902cc7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java @@ -7,8 +7,21 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.pgpainless.key.SubkeyIdentifier; +/** + * Custom {@link PublicKeyDataDecryptorFactory} which can enable customized implementations of message decryption + * using public keys. + * This class can for example be used to implement message encryption using hardware tokens like smartcards or + * TPMs. + * @see ConsumerOptions#addCustomDecryptorFactory(CustomPublicKeyDataDecryptorFactory) + */ public interface CustomPublicKeyDataDecryptorFactory extends PublicKeyDataDecryptorFactory { + /** + * Return the {@link SubkeyIdentifier} for which this particular {@link CustomPublicKeyDataDecryptorFactory} + * is intended. + * + * @return subkey identifier + */ SubkeyIdentifier getSubkeyIdentifier(); } From 3b2d0795f7e62462a17a0d9651d8eb9adcfa11bf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Dec 2022 12:25:35 +0100 Subject: [PATCH 0653/1204] Fix NPE when sop generate-key --with-key-password is used with multiple uids Fixes #351 --- CHANGELOG.md | 2 ++ .../src/main/java/org/pgpainless/sop/GenerateKeyImpl.java | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a35d4d5..d5be2adc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ SPDX-License-Identifier: CC0-1.0 ## 1.4.2-SNAPSHOT - Properly decrypt messages without MDC packets when `ConsumerOptions.setIgnoreMDCErrors(true)` is set +- Fix crash in `sop generate-key --with-key-password` when more than one user-id is given + ## 1.4.1 - Add `UserId.parse()` method to parse user-ids into their components diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index c75087c8..70561693 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -51,6 +51,7 @@ public class GenerateKeyImpl implements GenerateKey { @Override public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { Iterator userIdIterator = userIds.iterator(); + Passphrase passphraseCopy = new Passphrase(passphrase.getChars()); // generateKeyRing clears the original passphrase PGPSecretKeyRing key; try { String primaryUserId = userIdIterator.hasNext() ? userIdIterator.next() : null; @@ -61,7 +62,7 @@ public class GenerateKeyImpl implements GenerateKey { SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(key); while (userIdIterator.hasNext()) { - editor.addUserId(userIdIterator.next(), SecretKeyRingProtector.unprotectedKeys()); + editor.addUserId(userIdIterator.next(), SecretKeyRingProtector.unlockAnyKeyWith(passphraseCopy)); } key = editor.done(); From ab6b6ca2e74fc192781d74db183ae47fab38e1ec Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 Dec 2022 12:30:30 +0100 Subject: [PATCH 0654/1204] Add regression test for #351 --- .../org/pgpainless/sop/GenerateKeyTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java index a71eda12..3a6e4476 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -6,15 +6,20 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; import sop.SOP; public class GenerateKeyTest { @@ -67,4 +72,24 @@ public class GenerateKeyTest { assertFalse(new String(bytes).startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----")); } + + @Test + public void protectedMultiUserIdKey() throws IOException, PGPException { + byte[] bytes = sop.generateKey() + .userId("Alice") + .userId("Bob") + .withKeyPassword("sw0rdf1sh") + .generate() + .getBytes(); + + PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(bytes); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + + assertTrue(info.getUserIds().contains("Alice")); + assertTrue(info.getUserIds().contains("Bob")); + + for (PGPSecretKey key : secretKey) { + assertNotNull(UnlockSecretKey.unlockSecretKey(key, Passphrase.fromPassword("sw0rdf1sh"))); + } + } } From 7a2c9d864c2ecb9db1025dfe23df3872a6b6881e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Jan 2023 17:52:59 +0100 Subject: [PATCH 0655/1204] Add javadoc to DecryptionBuilder --- .../decryption_verification/DecryptionBuilder.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java index 0baf4124..96b4ad60 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java @@ -10,6 +10,11 @@ import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPException; +/** + * Builder class that takes an {@link InputStream} of ciphertext (or plaintext signed data) + * and combines it with a configured {@link ConsumerOptions} object to form a {@link DecryptionStream} which + * can be used to decrypt an OpenPGP message or verify signatures. + */ public class DecryptionBuilder implements DecryptionBuilderInterface { @Override From 8cb773841b0c7881fa3e878dda424e746f565ab0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Jan 2023 19:18:02 +0100 Subject: [PATCH 0656/1204] Revert certificate-store integration Integration of certificate-store and pgpainless-cert-d makes packaging complicated. Alternatively, users can simply integrate the certificate-store with PGPainless themselves. --- pgpainless-core/build.gradle | 4 - .../ConsumerOptions.java | 40 --- .../encryption_signing/EncryptionOptions.java | 29 --- .../EncryptWithKeyFromKeyStoreTest.java | 230 ------------------ version.gradle | 2 - 5 files changed, 305 deletions(-) delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 84b4273f..3c73121f 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -24,10 +24,6 @@ dependencies { api "org.bouncycastle:bcpg-jdk15to18:$bouncyPgVersion" // api(files("../libs/bcpg-jdk18on-1.70.jar")) - // certificate store - api "org.pgpainless:pgp-certificate-store:$pgpCertDJavaVersion" - testImplementation "org.pgpainless:pgpainless-cert-d:$pgpainlessCertDVersion" - // @Nullable, @Nonnull annotations implementation "com.google.code.findbugs:jsr305:3.0.2" } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java index 33404c48..d0d9230b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -6,12 +6,10 @@ package org.pgpainless.decryption_verification; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -25,7 +23,6 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.key.SubkeyIdentifier; @@ -33,9 +30,6 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; import org.pgpainless.util.SessionKey; -import pgp.certificate_store.PGPCertificateStore; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.exception.BadDataException; /** * Options for decryption and signature verification. @@ -135,17 +129,6 @@ public class ConsumerOptions { return this; } - /** - * Pass in a {@link PGPCertificateStore} from which certificates can be sourced for signature verification. - * - * @param certificateStore certificate store - * @return options - */ - public ConsumerOptions addVerificationCerts(PGPCertificateStore certificateStore) { - this.certificates.addStore(certificateStore); - return this; - } - /** * Add some detached signatures from the given {@link InputStream} for verification. * @@ -484,18 +467,8 @@ public class ConsumerOptions { */ public static class CertificateSource { - private List stores = new ArrayList<>(); private Set explicitCertificates = new HashSet<>(); - /** - * Add a certificate store as source for verification certificates. - * - * @param certificateStore cert store - */ - public void addStore(PGPCertificateStore certificateStore) { - this.stores.add(certificateStore); - } - /** * Add a certificate as verification cert explicitly. * @@ -529,19 +502,6 @@ public class ConsumerOptions { } } - for (PGPCertificateStore store : stores) { - try { - Iterator certs = store.getCertificatesBySubkeyId(keyId); - if (!certs.hasNext()) { - continue; - } - Certificate cert = certs.next(); - PGPPublicKeyRing publicKey = PGPainless.readKeyRing().publicKeyRing(cert.getInputStream()); - return publicKey; - } catch (IOException | BadDataException e) { - continue; - } - } return null; } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index cf3b426a..612dcd50 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -4,7 +4,6 @@ package org.pgpainless.encryption_signing; -import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -14,7 +13,6 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; - import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPPublicKey; @@ -22,7 +20,6 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; -import org.pgpainless.PGPainless; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.exception.KeyException; @@ -32,10 +29,6 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyAccessor; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.Passphrase; -import pgp.certificate_store.PGPCertificateStore; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.exception.BadDataException; -import pgp.certificate_store.exception.BadNameException; /** * Options for the encryption process. @@ -241,28 +234,6 @@ public class EncryptionOptions { return this; } - /** - * Add a recipient by providing a {@link PGPCertificateStore} and the {@link OpenPgpFingerprint} of the recipients key. - * If no such certificate is found in the store, a {@link NoSuchElementException is thrown}. - * - * @param certificateStore certificate store - * @param certificateFingerprint fingerprint of the recipient certificate - * @return builder - * @throws BadDataException if the certificate contains bad data - * @throws BadNameException if the fingerprint is not in a recognizable form for the store - * @throws IOException in case of an IO error - * @throws NoSuchElementException if the store does not contain a certificate for the given fingerprint - */ - public EncryptionOptions addRecipient(@Nonnull PGPCertificateStore certificateStore, - @Nonnull OpenPgpFingerprint certificateFingerprint) - throws BadDataException, BadNameException, IOException { - String fingerprint = certificateFingerprint.toString().toLowerCase(); - Certificate certificateRecord = certificateStore.getCertificate(fingerprint); - PGPPublicKeyRing recipientCertificate = PGPainless.readKeyRing() - .publicKeyRing(certificateRecord.getInputStream()); - return addRecipient(recipientCertificate); - } - private void addRecipientKey(PGPPublicKeyRing keyRing, PGPPublicKey key) { encryptionKeys.add(new SubkeyIdentifier(keyRing, key.getKeyID())); PGPKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java deleted file mode 100644 index 48f1bbd7..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptWithKeyFromKeyStoreTest.java +++ /dev/null @@ -1,230 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.certificate_store.MergeCallbacks; -import org.pgpainless.certificate_store.PGPainlessCertD; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import pgp.cert_d.PGPCertificateStoreAdapter; -import pgp.certificate_store.certificate.Certificate; -import pgp.certificate_store.exception.BadDataException; -import pgp.certificate_store.exception.BadNameException; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class EncryptWithKeyFromKeyStoreTest { - - // Collection of 3 keys (fingerprints below) - private static final String KEY_COLLECTION = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "lFgEYwerQBYJKwYBBAHaRw8BAQdAl3XjFMXQdmhMuFEIbE7IJUP1k+5utUT6IAW3\n" + - "zlWguvQAAQDK7Qh5Q9EAB5cTh2OWsPeydfDqRmnuxlZjlwf4WWQLhRAltBRBIDxh\n" + - "QHBncGFpbmxlc3Mub3JnPoiPBBMWCgBBBQJjB6tBCRBoj2Vso6FpsxYhBNqK9ZX8\n" + - "QfcbxPJmCGiPZWyjoWmzAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAACEaAP9P\n" + - "49Q/E19vyx2rV8EjQd+XBFnDuYxBjw80ZVC0TaKJNgEAgWsQqcg/ARkG9XGxaE3X\n" + - "IE9tFHh4wpjQhnK1Ta/wJAOcXQRjB6tBEgorBgEEAZdVAQUBAQdATJM1XKfKVF+C\n" + - "B2/xrGU+F89Ir9viOut4sna4aWfvwHoDAQgHAAD/UN84yv5jxKsPgfw/XZCDwoey\n" + - "Y69ompSiBuZjzOWrjegToIh1BBgWCgAdBQJjB6tBAp4BApsMBRYCAwEABAsJCAcF\n" + - "FQoJCAsACgkQaI9lbKOhabP/PAEApov4hYuhIENq26z+w4s3A1gakN+gax54F7+M\n" + - "YSUm16sBAPiuEdpVJOwTk3WMXKyLOYaVU3JstlP2H1ouguvYTt4CnFgEYwerQRYJ\n" + - "KwYBBAHaRw8BAQdA5xpeGHNy9v+QUbl+Rs7Mx0c6D913gksW1eZ4Qeg31B0AAQCx\n" + - "6b3P5lRBAraZstlRupymrt6vF2JpeJB8JOOQ+rdVYBJpiNUEGBYKAH0FAmMHq0EC\n" + - "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjB6tBAAoJENH9GnI3A/RM\n" + - "IVMA/1GU9E+vA8bs0vJVDjp1ri3J4S7u+abwmlivDw8g8XCWAPwKWWfHLgJCsAHk\n" + - "INuDgJdqbNPATFiXxH9FqYnOvWy6DAAKCRBoj2Vso6Fps884AP9D5ZOwuBEXyT/j\n" + - "0G8CWBZ0lT14kRGFucjQi9kZStAuVgEA5cd3eUWofnekd/P6R3UgmvhVOqvxwUUg\n" + - "Y3mEArH7+waUWARjB6tBFgkrBgEEAdpHDwEBB0BCYWjTs0pfBnKYgO0O07djiMSB\n" + - "tUJVpUFo6zrVK92RgAAA/38G6IEK5rJs1OCusmmhHJk1vDu0hbesK7JH7dh75mVY\n" + - "Ep20FEIgPGJAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMHq0EJEAnsE6FTTHNl\n" + - "FiEE2/L5HBba6IFDHu8cCewToVNMc2UCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + - "AQAAS7MBAI74uYLK7XR6oCwWYk7C6nwdgu3t478MaEpVHQz/9nEGAQCvJCYqqOd6\n" + - "cAG6fwFaIJ3h99/Y5o2NaiN17S2zOXEZDJxdBGMHq0ESCisGAQQBl1UBBQEBB0BU\n" + - "EjXQCT4xwJryksXsMLaFo43pFTwWaTzduiWgCy2KMgMBCAcAAP9lXlnMYtBfXpgH\n" + - "doUZZk3cvWBOH3awc12V3jZSLtSE8BAJiHUEGBYKAB0FAmMHq0ECngECmwwFFgID\n" + - "AQAECwkIBwUVCgkICwAKCRAJ7BOhU0xzZf5lAQDOgzMhqg3fE8Hg4Hbt4+B0fAD0\n" + - "kp6EJgsKRWT7KbZ0SQD/aVGFv7VRVqiiqOT/YMQKBBwHnq/CGJqxUwUmavBMRAqc\n" + - "WARjB6tBFgkrBgEEAdpHDwEBB0A5kv3bpsnlxs2LrAzeBx4RgtXQNBhGRhzko1to\n" + - "4q+ebQAA/1SU1hvrqd9gNmcc4wff1iwJ1dnqnrbGbO1Yz9rYZjXRE4iI1QQYFgoA\n" + - "fQUCYwerQQKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMHq0EACgkQ\n" + - "pYWdiAVpxGRW4AD+Lade9kJrvcBMSq8EERhYTH6DFka4eMgFB76kH31WmpQA+gOU\n" + - "7kwqKmtyVsXVgCLGMcdTvbZr+73C5m8R7LsdY5kEAAoJEAnsE6FTTHNl7BAA/2v8\n" + - "Wzfmg1OO6IWCohmmNgF4rIDBW8Q9s3+1I/mWlMyjAP9YGR+fnN/YOQrlSG9UiXE5\n" + - "fGwUhaPB0LEGWp0wmmQYA5RYBGMHq0EWCSsGAQQB2kcPAQEHQI8C53+C8crLCQ48\n" + - "OKQa1dEKc8XWQSA6Ckg5j73tOJRLAAD/VRvioGU2M9G6+eKTn68mBVZ8G512HELr\n" + - "apK9M5UFGUMPXLQUQyA8Y0BwZ3BhaW5sZXNzLm9yZz6IjwQTFgoAQQUCYwerQQkQ\n" + - "ommXHYx1l94WIQQp+Mrw86EV1myUgUKiaZcdjHWX3gKeAQKbAQUWAgMBAAQLCQgH\n" + - "BRUKCQgLApkBAAAQ5wEAvahnnRuwY+Y7EPSQG+sqhsdvSTumleYPtEOnHfKctpkA\n" + - "/iaTp4OoUw/RtyWUAk8MLN47CAW5wwhFUbVfZOaS88wMnF0EYwerQRIKKwYBBAGX\n" + - "VQEFAQEHQNz/s68ZGUBfDmMz510cFgHz+mAdC2nXeE4hHKV/HIVsAwEIBwAA/1HB\n" + - "vRl84B8r/PY+5j/X6A+4J08QB/vd5wIHVdkrX+xQELGIdQQYFgoAHQUCYwerQQKe\n" + - "AQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEKJplx2MdZfeqzYA/jLtjRmy42MCOxnF\n" + - "3A95WZIDoEohFU0QAeE/yVTLGoDTAP4xhTznleABK7VbD9GJXfD6DkEC749tOsST\n" + - "eYO/GOxKDpxYBGMHq0EWCSsGAQQB2kcPAQEHQFnvyWSgOv4gn3Ch3RY74pRg+7hX\n" + - "OBJAf6ybwvx9t4olAAEAwYG1CL0JozVD1216yrENkP8La132O1MI28kqMsoF6FcP\n" + - "I4jVBBgWCgB9BQJjB6tBAp4BApsCBRYCAwEABAsJCAcFFQoJCAtfIAQZFgoABgUC\n" + - "YwerQQAKCRB8jJGVps/ENgz7AP9ZMENJH+rIKMjynb9WPBlvJ8yJ9dMhzCxcssxg\n" + - "EVZYXAEA5ZsE5xJLQC/cVMGFvqaQ8iPo5jhDZpQJ8RCVlb8XzQwACgkQommXHYx1\n" + - "l96SkgD/f0FYkK4yB8FWuntJ3n0FUfE31wDwpxvvpvP+o3d2GB4BAP9LRKBXMwj4\n" + - "jzJc4ViKmwiNJAPttDQCpYjzJT7LUKAA\n" + - "=EAvh\n" + - "-----END PGP PRIVATE KEY BLOCK-----"; - - // Collection of 3 certificates (fingerprints below) - private static final String CERT_COLLECTION = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "Version: BCPG v1.71\n" + - "\n" + - "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\n" + - "Mwj4jzJc4ViKmwiNJAPttDQCpYjzJT7LUKAA\n" + - "=WaRm\n" + - "-----END PGP PUBLIC KEY BLOCK-----"; - private static final OpenPgpFingerprint cert1fp = OpenPgpFingerprint.parse("DA8AF595FC41F71BC4F26608688F656CA3A169B3"); - private static final OpenPgpFingerprint cert2fp = OpenPgpFingerprint.parse("DBF2F91C16DAE881431EEF1C09EC13A1534C7365"); - private static final OpenPgpFingerprint cert3fp = OpenPgpFingerprint.parse("29F8CAF0F3A115D66C948142A269971D8C7597DE"); - - @Test - public void encryptWithCertFromCertificateStore() throws PGPException, IOException, BadDataException, InterruptedException, BadNameException { - // In-Memory certificate store - PGPainlessCertD certificateDirectory = PGPainlessCertD.inMemory(); - PGPCertificateStoreAdapter adapter = new PGPCertificateStoreAdapter(certificateDirectory); - - // Populate store - PGPPublicKeyRingCollection certificates = PGPainless.readKeyRing().publicKeyRingCollection(CERT_COLLECTION); - for (PGPPublicKeyRing cert : certificates) { - certificateDirectory.insert(new ByteArrayInputStream(cert.getEncoded()), MergeCallbacks.mergeWithExisting()); - } - - // Encrypt message - ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() - .onOutputStream(ciphertextOut) - .withOptions(ProducerOptions.encrypt( - EncryptionOptions.encryptCommunications() - .addRecipient(adapter, cert2fp))); - ByteArrayInputStream plaintext = new ByteArrayInputStream("Hello, World! This message is encrypted using a cert from a store!".getBytes()); - Streams.pipeAll(plaintext, encryptionStream); - encryptionStream.close(); - - // Get cert from store - Certificate cert = adapter.getCertificate(cert2fp.toString()); - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(cert.getInputStream()); - - // check if message was encrypted for cert - assertTrue(encryptionStream.getResult().isEncryptedFor(publicKeys)); - } - - @Test - public void verifyWithCertFromCertificateStore() - throws PGPException, IOException, BadDataException, InterruptedException, BadNameException { - // In-Memory certificate store - PGPainlessCertD certificateDirectory = PGPainlessCertD.inMemory(); - PGPCertificateStoreAdapter adapter = new PGPCertificateStoreAdapter(certificateDirectory); - - // Populate store - PGPPublicKeyRingCollection certificates = PGPainless.readKeyRing().publicKeyRingCollection(CERT_COLLECTION); - for (PGPPublicKeyRing cert : certificates) { - certificateDirectory.insert(new ByteArrayInputStream(cert.getEncoded()), MergeCallbacks.mergeWithExisting()); - } - - // Prepare keys - OpenPgpFingerprint cryptFp = cert3fp; - OpenPgpFingerprint signFp = cert1fp; - PGPSecretKeyRingCollection secretKeys = PGPainless.readKeyRing().secretKeyRingCollection(KEY_COLLECTION); - PGPSecretKeyRing signingKey = secretKeys.getSecretKeyRing(signFp.getKeyId()); - PGPSecretKeyRing decryptionKey = secretKeys.getSecretKeyRing(cryptFp.getKeyId()); - SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - - // Encrypt and sign message - ByteArrayInputStream plaintextIn = new ByteArrayInputStream( - "This message was encrypted with a cert from a store and gets verified with a cert from a store as well".getBytes()); - ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() - .onOutputStream(ciphertext) - .withOptions( - ProducerOptions.signAndEncrypt( - EncryptionOptions.encryptCommunications() - .addRecipient(adapter, cryptFp), - SigningOptions.get() - .addSignature(protector, signingKey) - )); - Streams.pipeAll(plaintextIn, encryptionStream); - encryptionStream.close(); - - // Prepare ciphertext for decryption - ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertext.toByteArray()); - ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); - // Decrypt and verify - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(ciphertextIn) - .withOptions( - new ConsumerOptions() - .addDecryptionKey(decryptionKey, protector) - .addVerificationCerts(adapter)); - Streams.pipeAll(decryptionStream, plaintextOut); - decryptionStream.close(); - - // Check that message can be decrypted and is verified - OpenPgpMetadata result = decryptionStream.getResult(); - assertTrue(result.isEncrypted()); - assertTrue(result.isVerified()); - assertTrue(result.containsVerifiedSignatureFrom(signFp)); - } -} diff --git a/version.gradle b/version.gradle index c6f7104e..53705861 100644 --- a/version.gradle +++ b/version.gradle @@ -17,8 +17,6 @@ allprojects { junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' - pgpainlessCertDVersion = '0.2.0' - pgpCertDJavaVersion = '0.2.1' slf4jVersion = '1.7.36' sopJavaVersion = '4.0.7' } From 4bf2e07ddbab6f00f2f6b079948fabd61439bc3b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Jan 2023 19:30:35 +0100 Subject: [PATCH 0657/1204] Bump sop-java to 4.1.0 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 53705861..fd3129e3 100644 --- a/version.gradle +++ b/version.gradle @@ -18,6 +18,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '4.0.7' + sopJavaVersion = '4.1.0' } } From 75feb167f0a12756f5b99c0f281fd0c2206fe5a5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Jan 2023 19:30:41 +0100 Subject: [PATCH 0658/1204] Update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5be2adc..25272b18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ SPDX-License-Identifier: CC0-1.0 ## 1.4.2-SNAPSHOT - Properly decrypt messages without MDC packets when `ConsumerOptions.setIgnoreMDCErrors(true)` is set - Fix crash in `sop generate-key --with-key-password` when more than one user-id is given - +- Revert integration with `pgp-certificate-store` +- Bump `sop-java` to `4.1.0` ## 1.4.1 - Add `UserId.parse()` method to parse user-ids into their components From 2d33a7e5d3cefa110608b6c4a70ae04c58c85fe2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Jan 2023 19:33:26 +0100 Subject: [PATCH 0659/1204] PGPainless 1.4.2 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25272b18..370dace7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.4.2-SNAPSHOT +## 1.4.2 - Properly decrypt messages without MDC packets when `ConsumerOptions.setIgnoreMDCErrors(true)` is set - Fix crash in `sop generate-key --with-key-password` when more than one user-id is given - Revert integration with `pgp-certificate-store` diff --git a/README.md b/README.md index 49320f94..3b6dc7cb 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.1' + implementation 'org.pgpainless:pgpainless-core:1.4.2' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index c1b2a988..97b7765a 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.1" + implementation "org.pgpainless:pgpainless-sop:1.4.2" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.1 + 1.4.2 ... diff --git a/version.gradle b/version.gradle index fd3129e3..83ea0d7d 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.4.2' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 6c0bb6c6272166e0cd53f766b9126afc9c7f0f74 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Jan 2023 19:38:37 +0100 Subject: [PATCH 0660/1204] PGPainless 1.4.3-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 83ea0d7d..b49f7930 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.2' - isSnapshot = false + shortVersion = '1.4.3' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From b58861635d4206909f2c0b65b47af0f04d08a7fe Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 16 Jan 2023 19:38:52 +0100 Subject: [PATCH 0661/1204] Add some missing javadoc --- build.gradle | 12 ++++++++++++ .../java/org/pgpainless/cli/PGPainlessCLI.java | 10 ++++++++++ .../main/java/org/pgpainless/sop/ArmorImpl.java | 3 +++ .../java/org/pgpainless/sop/DearmorImpl.java | 3 +++ .../java/org/pgpainless/sop/DecryptImpl.java | 3 +++ .../org/pgpainless/sop/DetachedSignImpl.java | 3 +++ .../org/pgpainless/sop/DetachedVerifyImpl.java | 3 +++ .../java/org/pgpainless/sop/EncryptImpl.java | 3 +++ .../org/pgpainless/sop/ExtractCertImpl.java | 3 +++ .../org/pgpainless/sop/GenerateKeyImpl.java | 3 +++ .../org/pgpainless/sop/InlineDetachImpl.java | 3 +++ .../java/org/pgpainless/sop/InlineSignImpl.java | 3 +++ .../org/pgpainless/sop/InlineVerifyImpl.java | 3 +++ .../main/java/org/pgpainless/sop/KeyReader.java | 3 +++ .../sop/MatchMakingSecretKeyRingProtector.java | 17 +++++++++++++++++ .../main/java/org/pgpainless/sop/SOPImpl.java | 6 ++++++ .../java/org/pgpainless/sop/VersionImpl.java | 3 +++ 17 files changed, 84 insertions(+) diff --git a/build.gradle b/build.gradle index 6e086754..eb4ff723 100644 --- a/build.gradle +++ b/build.gradle @@ -258,6 +258,18 @@ task javadocAll(type: Javadoc) { ] as String[] } +if (JavaVersion.current().isJava8Compatible()) { + tasks.withType(Javadoc) { + // The '-quiet' as second argument is actually a hack, + // since the one paramater addStringOption doesn't seem to + // work, we extra add '-quiet', which is added anyway by + // gradle. See https://github.com/gradle/gradle/issues/2354 + // See JDK-8200363 (https://bugs.openjdk.java.net/browse/JDK-8200363) + // for information about the -Xwerror option. + options.addStringOption('Xwerror', '-quiet') + } +} + /** * Fetch sha256 checksums of artifacts published to maven central. * diff --git a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java index 35791a3e..938bf1aa 100644 --- a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java +++ b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java @@ -18,6 +18,10 @@ public class PGPainlessCLI { SopCLI.setSopInstance(new SOPImpl()); } + /** + * Main method of the CLI application. + * @param args arguments + */ public static void main(String[] args) { int result = execute(args); if (result != 0) { @@ -25,6 +29,12 @@ public class PGPainlessCLI { } } + /** + * Execute the given command and return the exit code of the program. + * + * @param args command string array (e.g. ["pgpainless-cli", "generate-key", "Alice"]) + * @return exit code + */ public static int execute(String... args) { return SopCLI.execute(args); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java index d65c2925..daee3a9b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java @@ -18,6 +18,9 @@ import sop.enums.ArmorLabel; import sop.exception.SOPGPException; import sop.operation.Armor; +/** + * Implementation of the
      armor
      operation using PGPainless. + */ public class ArmorImpl implements Armor { @Override diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java index f0b21a6d..29483437 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java @@ -15,6 +15,9 @@ import sop.Ready; import sop.exception.SOPGPException; import sop.operation.Dearmor; +/** + * Implementation of the
      dearmor
      operation using PGPainless. + */ public class DearmorImpl implements Dearmor { @Override diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 867024ef..f7876799 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -34,6 +34,9 @@ import sop.Verification; import sop.exception.SOPGPException; import sop.operation.Decrypt; +/** + * Implementation of the
      decrypt
      operation using PGPainless. + */ public class DecryptImpl implements Decrypt { private final ConsumerOptions consumerOptions = ConsumerOptions.get(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java index f6ab8172..c32cb219 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java @@ -36,6 +36,9 @@ import sop.enums.SignAs; import sop.exception.SOPGPException; import sop.operation.DetachedSign; +/** + * Implementation of the
      sign
      operation using PGPainless. + */ public class DetachedSignImpl implements DetachedSign { private boolean armor = true; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index 1ed43941..93ad398c 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -23,6 +23,9 @@ import sop.Verification; import sop.exception.SOPGPException; import sop.operation.DetachedVerify; +/** + * Implementation of the
      verify
      operation using PGPainless. + */ public class DetachedVerifyImpl implements DetachedVerify { private final ConsumerOptions options = ConsumerOptions.get(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 61a46731..1874d9e1 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -34,6 +34,9 @@ import sop.exception.SOPGPException; import sop.operation.Encrypt; import sop.util.ProxyOutputStream; +/** + * Implementation of the
      encrypt
      operation using PGPainless. + */ public class EncryptImpl implements Encrypt { EncryptionOptions encryptionOptions = EncryptionOptions.get(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java index 5be3ad31..be7fc9c3 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java @@ -19,6 +19,9 @@ import sop.Ready; import sop.exception.SOPGPException; import sop.operation.ExtractCert; +/** + * Implementation of the
      extract-cert
      operation using PGPainless. + */ public class ExtractCertImpl implements ExtractCert { private boolean armor = true; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index 70561693..da99c854 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -24,6 +24,9 @@ import sop.Ready; import sop.exception.SOPGPException; import sop.operation.GenerateKey; +/** + * Implementation of the
      generate-key
      operation using PGPainless. + */ public class GenerateKeyImpl implements GenerateKey { private boolean armor = true; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java index 178018aa..bafc2794 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java @@ -30,6 +30,9 @@ import sop.Signatures; import sop.exception.SOPGPException; import sop.operation.InlineDetach; +/** + * Implementation of the
      inline-detach
      operation using PGPainless. + */ public class InlineDetachImpl implements InlineDetach { private boolean armor = true; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index 82c3603b..dd4ab0cf 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -29,6 +29,9 @@ import sop.enums.InlineSignAs; import sop.exception.SOPGPException; import sop.operation.InlineSign; +/** + * Implementation of the
      inline-sign
      operation using PGPainless. + */ public class InlineSignImpl implements InlineSign { private boolean armor = true; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index f33f718b..7665a7bb 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -26,6 +26,9 @@ import sop.Verification; import sop.exception.SOPGPException; import sop.operation.InlineVerify; +/** + * Implementation of the
      inline-verify
      operation using PGPainless. + */ public class InlineVerifyImpl implements InlineVerify { private final ConsumerOptions options = ConsumerOptions.get(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java index 5e6f3a7d..036ec126 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java @@ -13,6 +13,9 @@ import sop.exception.SOPGPException; import java.io.IOException; import java.io.InputStream; +/** + * Reader for OpenPGP keys and certificates with error matching according to the SOP spec. + */ class KeyReader { static PGPSecretKeyRingCollection readSecretKeys(InputStream keyInputStream, boolean requireContent) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java index df54583e..0b88cb5d 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java @@ -20,12 +20,21 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; +/** + * Implementation of the {@link SecretKeyRingProtector} which can be handed passphrases and keys separately, + * and which then matches up passphrases and keys when needed. + */ public class MatchMakingSecretKeyRingProtector implements SecretKeyRingProtector { private final Set passphrases = new HashSet<>(); private final Set keys = new HashSet<>(); private final CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(); + /** + * Add a single passphrase to the protector. + * + * @param passphrase passphrase + */ public void addPassphrase(Passphrase passphrase) { if (passphrase.isEmpty()) { return; @@ -46,6 +55,11 @@ public class MatchMakingSecretKeyRingProtector implements SecretKeyRingProtector } } + /** + * Add a single {@link PGPSecretKeyRing} to the protector. + * + * @param key secret keys + */ public void addSecretKey(PGPSecretKeyRing key) { if (!keys.add(key)) { return; @@ -89,6 +103,9 @@ public class MatchMakingSecretKeyRingProtector implements SecretKeyRingProtector return protector.getEncryptor(keyId); } + /** + * Clear all known passphrases from the protector. + */ public void clear() { for (Passphrase passphrase : passphrases) { passphrase.clear(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java index 28772f10..a49f7e34 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -19,6 +19,12 @@ import sop.operation.InlineSign; import sop.operation.InlineVerify; import sop.operation.Version; +/** + * Implementation of the
      sop
      API using PGPainless. + *
       {@code
      + * SOP sop = new SOPImpl();
      + * }
      + */ public class SOPImpl implements SOP { static { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index dbd1cf35..4449af10 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -12,6 +12,9 @@ import java.util.Properties; import org.bouncycastle.jce.provider.BouncyCastleProvider; import sop.operation.Version; +/** + * Implementation of the
      version
      operation using PGPainless. + */ public class VersionImpl implements Version { // draft version From a50c2d97142c30b492d7a57a6a352ef957cfa3d7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 16 Jan 2023 20:15:57 +0100 Subject: [PATCH 0662/1204] More missing javadoc --- .../pgpainless/algorithm/AEADAlgorithm.java | 5 +++++ .../protection/BaseSecretKeyRingProtector.java | 16 ++++++++++++++++ .../key/protection/SecretKeyRingProtector.java | 6 ++++++ .../java/org/pgpainless/util/ArmorUtils.java | 18 ++++++++++++++++++ .../util/ArmoredInputStreamFactory.java | 11 +++++++++++ .../util/ArmoredOutputStreamFactory.java | 10 ++++++++++ 6 files changed, 66 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java index 106d6bff..a5885005 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java @@ -41,6 +41,11 @@ public enum AEADAlgorithm { this.tagLength = tagLength; } + /** + * Return the ID of the AEAD algorithm. + * + * @return algorithm ID + */ public int getAlgorithmId() { return algorithmId; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java index 5b545c12..1a31d0e8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java @@ -13,15 +13,31 @@ import org.pgpainless.util.Passphrase; import javax.annotation.Nullable; +/** + * Basic {@link SecretKeyRingProtector} implementation that respects the users {@link KeyRingProtectionSettings} when + * encrypting keys. + */ public class BaseSecretKeyRingProtector implements SecretKeyRingProtector { private final SecretKeyPassphraseProvider passphraseProvider; private final KeyRingProtectionSettings protectionSettings; + /** + * Constructor that uses the given {@link SecretKeyPassphraseProvider} to retrieve passphrases and PGPainless' + * default {@link KeyRingProtectionSettings}. + * + * @param passphraseProvider provider for passphrases + */ public BaseSecretKeyRingProtector(SecretKeyPassphraseProvider passphraseProvider) { this(passphraseProvider, KeyRingProtectionSettings.secureDefaultSettings()); } + /** + * Constructor that uses the given {@link SecretKeyPassphraseProvider} and {@link KeyRingProtectionSettings}. + * + * @param passphraseProvider provider for passphrases + * @param protectionSettings protection settings + */ public BaseSecretKeyRingProtector(SecretKeyPassphraseProvider passphraseProvider, KeyRingProtectionSettings protectionSettings) { this.passphraseProvider = passphraseProvider; this.protectionSettings = protectionSettings; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java index ee461a4e..d7ed5c85 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java @@ -29,6 +29,12 @@ import org.pgpainless.util.Passphrase; */ public interface SecretKeyRingProtector { + /** + * Returns true, if the protector has a passphrase for the key with the given key-id. + * + * @param keyId key id + * @return true if it has a passphrase, false otherwise + */ boolean hasPassphraseFor(Long keyId); /** diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index b3e60023..87170638 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -33,15 +33,33 @@ import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.decryption_verification.OpenPgpInputStream; import org.pgpainless.key.OpenPgpFingerprint; +/** + * Utility class for dealing with ASCII armored OpenPGP data. + */ public final class ArmorUtils { // MessageIDs are 32 printable characters private static final Pattern PATTERN_MESSAGE_ID = Pattern.compile("^\\S{32}$"); + /** + * Constant armor key for comments. + */ public static final String HEADER_COMMENT = "Comment"; + /** + * Constant armor key for program versions. + */ public static final String HEADER_VERSION = "Version"; + /** + * Constant armor key for message IDs. Useful for split messages. + */ public static final String HEADER_MESSAGEID = "MessageID"; + /** + * Constant armor key for used hash algorithms in clearsigned messages. + */ public static final String HEADER_HASH = "Hash"; + /** + * Constant armor key for message character sets. + */ public static final String HEADER_CHARSET = "Charset"; private ArmorUtils() { diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java index 0c5ceeaf..e48fb4c0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java @@ -9,12 +9,23 @@ import java.io.InputStream; import org.bouncycastle.bcpg.ArmoredInputStream; +/** + * Factory class for instantiating preconfigured {@link ArmoredInputStream ArmoredInputStreams}. + * {@link #get(InputStream)} will return an {@link ArmoredInputStream} that is set up to properly detect CRC errors. + */ public final class ArmoredInputStreamFactory { private ArmoredInputStreamFactory() { } + /** + * Return an instance of {@link ArmoredInputStream} which will detect CRC errors. + * + * @param inputStream input stream + * @return armored input stream + * @throws IOException in case of an IO error + */ public static ArmoredInputStream get(InputStream inputStream) throws IOException { if (inputStream instanceof CRCingArmoredInputStreamWrapper) { return (ArmoredInputStream) inputStream; diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java index fb2cd4f5..f61bacb1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java @@ -15,6 +15,9 @@ import org.pgpainless.encryption_signing.ProducerOptions; */ public final class ArmoredOutputStreamFactory { + /** + * Name of the program. + */ public static final String PGPAINLESS = "PGPainless"; private static String version = PGPAINLESS; private static String[] comment = new String[0]; @@ -42,6 +45,13 @@ public final class ArmoredOutputStreamFactory { return armoredOutputStream; } + /** + * Return an instance of the {@link ArmoredOutputStream} which might have pre-populated armor headers. + * + * @param outputStream output stream + * @param options options + * @return armored output stream + */ public static ArmoredOutputStream get(OutputStream outputStream, ProducerOptions options) { if (options.isHideArmorHeaders()) { ArmoredOutputStream armorOut = new ArmoredOutputStream(outputStream); From 4cf5a32cc02bc9d3c1f83425e68b86564e9e7ec6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 21 Jan 2023 19:17:49 +0100 Subject: [PATCH 0663/1204] Update SECURITY.md --- SECURITY.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index fad439b5..0549d1a0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,12 +12,11 @@ SPDX-License-Identifier: Apache-2.0 Use this section to tell people about which versions of your project are currently being supported with security updates. -| Version | Supported | -|----------|--------------------| -| 1.4.X-rc | :white_check_mark: | -| 1.3.X | :white_check_mark: | -| 1.2.X | :white_check_mark: | -| < 1.2.0 | :x: | +| Version | Supported | +|---------|--------------------| +| 1.4.X | :white_check_mark: | +| 1.3.X | :white_check_mark: | +| < 1.3.X | :x: | ## Reporting a Vulnerability From 67cc59efa26b33696381eeab1cd318c505fe7bd5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 21 Jan 2023 19:16:39 +0100 Subject: [PATCH 0664/1204] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 370dace7..62740014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,10 @@ SPDX-License-Identifier: CC0-1.0 - Add `KeyRingUtils.publicKeys(PGPKeyRing keys)` - Remove `BCUtil` class +## 1.3.16 +- Bump `sop-java` to `4.1.0` +- Bump `gradlew` to `7.5` + ## 1.3.15 - Fix crash in `sop generate-key --with-key-password` when more than one user-id is given - `sop generate-key`: Allow key generation without user-ids From 9f98e4ce371670d8cc05c5e9cdbd095d1b899271 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 23 Jan 2023 10:02:02 +0200 Subject: [PATCH 0665/1204] Fixed redundant dot an exception message. --- .../decryption_verification/OpenPgpMessageInputStream.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index bb5d67b3..7fe11bbf 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -970,7 +970,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); inbandSignaturesWithMissingCert.add(new SignatureVerification.Failure( new SignatureVerification(signature, null), - new SignatureValidationException("Missing verification key."))); + new SignatureValidationException("Missing verification key"))); } } From 695e03f8b6cc01e0fc98b6ce2d9894f9f5e605d2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Jan 2023 18:19:08 +0100 Subject: [PATCH 0666/1204] Add EncryptionOptions.hasEncryptionMethod() --- .../encryption_signing/EncryptionOptions.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 612dcd50..b5baee83 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -311,6 +311,16 @@ public class EncryptionOptions { return this; } + /** + * Return
      true
      iff the user specified at least one encryption method, + *
      false
      otherwise. + * + * @return encryption methods is not empty + */ + public boolean hasEncryptionMethod() { + return !encryptionMethods.isEmpty(); + } + public interface EncryptionKeySelector { List selectEncryptionSubkeys(List encryptionCapableKeys); } From 83ef9cfe80cfbf12d59f7ad7beac3f7eda6c2631 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Jan 2023 18:20:03 +0100 Subject: [PATCH 0667/1204] SOP encrypt: Throw MissingArg if no encryption method was provided. --- .../src/main/java/org/pgpainless/sop/EncryptImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 1874d9e1..62d20bf0 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -113,6 +113,9 @@ public class EncryptImpl implements Encrypt { @Override public Ready plaintext(InputStream plaintext) throws IOException { + if (!encryptionOptions.hasEncryptionMethod()) { + throw new SOPGPException.MissingArg("Missing encryption method."); + } ProducerOptions producerOptions = signingOptions != null ? ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions) : ProducerOptions.encrypt(encryptionOptions); From f4bd17ade8f253575c47dff03492b306c4364944 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Jan 2023 19:02:49 +0100 Subject: [PATCH 0668/1204] Bump sop-java to 4.1.1 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index b49f7930..7154bca5 100644 --- a/version.gradle +++ b/version.gradle @@ -18,6 +18,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '4.1.0' + sopJavaVersion = '4.1.1' } } From d53cd6d0bdf7e8740de7359f14d1a9f2fc591222 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Jan 2023 19:03:11 +0100 Subject: [PATCH 0669/1204] pgpainless-sop: reuse shared sop-java test suite --- pgpainless-sop/build.gradle | 4 +++ .../PGPainlessSopInstanceFactory.java | 20 ++++++++++++ .../operation/PGPainlessArmorDearmorTest.java | 11 +++++++ .../PGPainlessDecryptWIthSessionKeyTest.java | 11 +++++++ ...ainlessDetachedSignDetachedVerifyTest.java | 18 +++++++++++ .../PGPainlessEncryptDecryptTest.java | 11 +++++++ .../operation/PGPainlessExtractCertTest.java | 32 +++++++++++++++++++ .../operation/PGPainlessGenerateKeyTest.java | 11 +++++++ ...ineSignInlineDetachDetachedVerifyTest.java | 12 +++++++ .../PGPainlessInlineSignInlineVerifyTest.java | 11 +++++++ .../operation/PGPainlessVersionTest.java | 11 +++++++ 11 files changed, 152 insertions(+) create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/PGPainlessSopInstanceFactory.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessArmorDearmorTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWIthSessionKeyTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDetachedSignDetachedVerifyTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessEncryptDecryptTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessExtractCertTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessGenerateKeyTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineDetachDetachedVerifyTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineVerifyTest.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessVersionTest.java diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index 4eca8a7f..1a40bb27 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -21,10 +21,14 @@ dependencies { // Logging testImplementation "ch.qos.logback:logback-classic:$logbackVersion" + // Depend on "shared" sop-java test suite (fixtures are turned into tests by inheritance inside test sources) + testImplementation(testFixtures("org.pgpainless:sop-java:$sopJavaVersion")) + implementation(project(":pgpainless-core")) api "org.pgpainless:sop-java:$sopJavaVersion" } test { useJUnitPlatform() + environment("test.implementation", "sop.testsuite.pgpainless.PGPainlessSopInstanceFactory") } diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/PGPainlessSopInstanceFactory.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/PGPainlessSopInstanceFactory.java new file mode 100644 index 00000000..a9aac0e9 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/PGPainlessSopInstanceFactory.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless; + +import java.util.Collections; +import java.util.Map; + +import org.pgpainless.sop.SOPImpl; +import sop.SOP; +import sop.testsuite.SOPInstanceFactory; + +public class PGPainlessSopInstanceFactory extends SOPInstanceFactory { + + @Override + public Map provideSOPInstances() { + return Collections.singletonMap("PGPainless-SOP", new SOPImpl()); + } +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessArmorDearmorTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessArmorDearmorTest.java new file mode 100644 index 00000000..9161706d --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessArmorDearmorTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.ArmorDearmorTest; + +public class PGPainlessArmorDearmorTest extends ArmorDearmorTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWIthSessionKeyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWIthSessionKeyTest.java new file mode 100644 index 00000000..2825db5f --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWIthSessionKeyTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.DecryptWithSessionKeyTest; + +public class PGPainlessDecryptWIthSessionKeyTest extends DecryptWithSessionKeyTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDetachedSignDetachedVerifyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDetachedSignDetachedVerifyTest.java new file mode 100644 index 00000000..dff9e86f --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDetachedSignDetachedVerifyTest.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import org.junit.jupiter.api.Disabled; +import sop.SOP; +import sop.testsuite.operation.DetachedSignDetachedVerifyTest; + +public class PGPainlessDetachedSignDetachedVerifyTest extends DetachedSignDetachedVerifyTest { + + @Override + @Disabled("Since we allow for dynamic cert loading, we can ignore this test") + public void verifyMissingCertCausesMissingArg(SOP sop) { + super.verifyMissingCertCausesMissingArg(sop); + } +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessEncryptDecryptTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessEncryptDecryptTest.java new file mode 100644 index 00000000..b6264d3a --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessEncryptDecryptTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.EncryptDecryptTest; + +public class PGPainlessEncryptDecryptTest extends EncryptDecryptTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessExtractCertTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessExtractCertTest.java new file mode 100644 index 00000000..ee4e9684 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessExtractCertTest.java @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import java.io.IOException; + +import org.junit.jupiter.api.Disabled; +import sop.SOP; +import sop.testsuite.operation.ExtractCertTest; + +public class PGPainlessExtractCertTest extends ExtractCertTest { + + @Disabled("BC uses old CTBs causing mismatching byte arrays :/") + @Override + public void extractAliceCertFromAliceKeyTest(SOP sop) throws IOException { + super.extractAliceCertFromAliceKeyTest(sop); + } + + @Disabled("BC uses old CTBs causing mismatching byte arrays :/") + @Override + public void extractBobsCertFromBobsKeyTest(SOP sop) throws IOException { + super.extractBobsCertFromBobsKeyTest(sop); + } + + @Disabled("BC uses old CTBs causing mismatching byte arrays :/") + @Override + public void extractCarolsCertFromCarolsKeyTest(SOP sop) throws IOException { + super.extractCarolsCertFromCarolsKeyTest(sop); + } +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessGenerateKeyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessGenerateKeyTest.java new file mode 100644 index 00000000..fe78bed0 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessGenerateKeyTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.GenerateKeyTest; + +public class PGPainlessGenerateKeyTest extends GenerateKeyTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineDetachDetachedVerifyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineDetachDetachedVerifyTest.java new file mode 100644 index 00000000..20fdc262 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineDetachDetachedVerifyTest.java @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.InlineSignInlineDetachDetachedVerifyTest; + +public class PGPainlessInlineSignInlineDetachDetachedVerifyTest + extends InlineSignInlineDetachDetachedVerifyTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineVerifyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineVerifyTest.java new file mode 100644 index 00000000..16166eb1 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineVerifyTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.InlineSignInlineVerifyTest; + +public class PGPainlessInlineSignInlineVerifyTest extends InlineSignInlineVerifyTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessVersionTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessVersionTest.java new file mode 100644 index 00000000..7a8f7db4 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessVersionTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.VersionTest; + +public class PGPainlessVersionTest extends VersionTest { + +} From 9509fff92913af662d13c94a6aad4d18404e06d8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Jan 2023 19:10:14 +0100 Subject: [PATCH 0670/1204] Update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62740014..d730daed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.3-SNAPSHOT +- Bump `sop-java` to `4.1.1` +- Reuse shared test suite of `sop-java` +- Add `EncryptionOptions.hasEncryptionMethod()` +- SOP `encrypt`: Throw `MissingArg` exception if no encryption method was provided +- Fix redundant dot in exception message (thanks @DenBond7) + ## 1.4.2 - Properly decrypt messages without MDC packets when `ConsumerOptions.setIgnoreMDCErrors(true)` is set - Fix crash in `sop generate-key --with-key-password` when more than one user-id is given From 1257c52ede623fd377a594ab4daa09ea5647ea1b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Jan 2023 19:12:45 +0100 Subject: [PATCH 0671/1204] PGPainless 1.4.3 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d730daed..d3475dd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.4.3-SNAPSHOT +## 1.4.3 - Bump `sop-java` to `4.1.1` - Reuse shared test suite of `sop-java` - Add `EncryptionOptions.hasEncryptionMethod()` diff --git a/README.md b/README.md index 3b6dc7cb..bc3484b8 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.2' + implementation 'org.pgpainless:pgpainless-core:1.4.3' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 97b7765a..74772110 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.2" + implementation "org.pgpainless:pgpainless-sop:1.4.3" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.2 + 1.4.3 ... diff --git a/version.gradle b/version.gradle index 7154bca5..914de80f 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.4.3' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 6c2331d4e61342042fefb7c495755c119976c4c0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Jan 2023 19:15:31 +0100 Subject: [PATCH 0672/1204] PGPainless 1.4.4-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 914de80f..68d74212 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.3' - isSnapshot = false + shortVersion = '1.4.4' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From 30771f470a31e6fc094dd6824eb85cdfd1cc9cc0 Mon Sep 17 00:00:00 2001 From: Bastien JANSEN Date: Wed, 8 Feb 2023 09:16:53 +0100 Subject: [PATCH 0673/1204] Support version 3 signature packets --- .../consumer/SignatureValidator.java | 10 ++- .../subpackets/SignatureSubpacketsUtil.java | 3 + .../VerifyVersion3SignaturePacketTest.java | 65 +++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java index af245235..cf0dc1fb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java @@ -158,8 +158,10 @@ public abstract class SignatureValidator { @Override public void verify(PGPSignature signature) throws SignatureValidationException { signatureIsNotMalformed(signingKey).verify(signature); - signatureDoesNotHaveCriticalUnknownNotations(policy.getNotationRegistry()).verify(signature); - signatureDoesNotHaveCriticalUnknownSubpackets().verify(signature); + if (signature.getVersion() >= 4) { + signatureDoesNotHaveCriticalUnknownNotations(policy.getNotationRegistry()).verify(signature); + signatureDoesNotHaveCriticalUnknownSubpackets().verify(signature); + } signatureUsesAcceptableHashAlgorithm(policy).verify(signature); signatureUsesAcceptablePublicKeyAlgorithm(policy, signingKey).verify(signature); } @@ -373,7 +375,9 @@ public abstract class SignatureValidator { return new SignatureValidator() { @Override public void verify(PGPSignature signature) throws SignatureValidationException { - signatureHasHashedCreationTime().verify(signature); + if (signature.getVersion() >= 4) { + signatureHasHashedCreationTime().verify(signature); + } signatureDoesNotPredateSigningKey(creator).verify(signature); if (signature.getSignatureType() != SignatureType.PRIMARYKEY_BINDING.getCode()) { signatureDoesNotPredateSigningKeyBindingDate(creator).verify(signature); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index cbcbeafc..1105a813 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -141,6 +141,9 @@ public final class SignatureSubpacketsUtil { * @return signature creation time subpacket */ public static @Nullable SignatureCreationTime getSignatureCreationTime(PGPSignature signature) { + if (signature.getVersion() == 3) { + return new SignatureCreationTime(false, signature.getCreationTime()); + } return hashed(signature, SignatureSubpacket.signatureCreationTime); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java new file mode 100644 index 00000000..54b9c94b --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java @@ -0,0 +1,65 @@ +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPV3SignatureGenerator; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.protection.SecretKeyRingProtector; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class VerifyVersion3SignaturePacketTest { + + + protected static final byte[] DATA = "hello".getBytes(StandardCharsets.UTF_8); + + @Test + void verifyDetachedVersion3Signature() throws PGPException, IOException { + PGPSignature version3Signature = generateV3Signature(); + + ConsumerOptions options = new ConsumerOptions() + .addVerificationCert(TestKeys.getEmilPublicKeyRing()) + .addVerificationOfDetachedSignatures(new ByteArrayInputStream(version3Signature.getEncoded())); + + DecryptionStream verifier = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(DATA)) + .withOptions(options); + + OpenPgpMetadata metadata = processSignedData(verifier); + assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.getEmilPublicKeyRing())); + } + + private static PGPSignature generateV3Signature() throws IOException, PGPException { + PGPContentSignerBuilder builder = ImplementationFactory.getInstance().getPGPContentSignerBuilder(PublicKeyAlgorithm.ECDSA, HashAlgorithm.SHA512); + PGPV3SignatureGenerator signatureGenerator = new PGPV3SignatureGenerator(builder); + + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPPrivateKey privateKey = secretKeys.getSecretKey().extractPrivateKey(protector.getDecryptor(secretKeys.getSecretKey().getKeyID())); + + signatureGenerator.init(SignatureType.CANONICAL_TEXT_DOCUMENT.getCode(), privateKey); + signatureGenerator.update(DATA); + + return signatureGenerator.generate(); + } + + private OpenPgpMetadata processSignedData(DecryptionStream verifier) throws IOException { + Streams.drain(verifier); + verifier.close(); + return verifier.getResult(); + } +} From d03f84f415ff43716303e810b7dd8c700559392c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 8 Feb 2023 14:49:10 +0100 Subject: [PATCH 0674/1204] Add reuse header to VerifyVersion3SignaturePacketTest --- .../VerifyVersion3SignaturePacketTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java index 54b9c94b..2a12e74a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Bastien Jansen +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification; import org.bouncycastle.openpgp.PGPException; From 997a6c8c5d109b11585c9ff065afa9e63c11cbd0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 8 Feb 2023 14:51:44 +0100 Subject: [PATCH 0675/1204] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3475dd5..867980ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.4-SNAPSHOT +- Fix expectations on subpackets of v3 signatures (thanks @bjansen) + - Properly verify v3 signatures, which do not yet have signature subpackets, yet we required them to have + a hashed creation date subpacket. + ## 1.4.3 - Bump `sop-java` to `4.1.1` - Reuse shared test suite of `sop-java` From afc7d491445d641429474feb633f26effad2d348 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 9 Feb 2023 21:55:10 +0100 Subject: [PATCH 0676/1204] PGPainless 1.4.4 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 867980ea..bd7f3505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.4.4-SNAPSHOT +## 1.4.4 - Fix expectations on subpackets of v3 signatures (thanks @bjansen) - Properly verify v3 signatures, which do not yet have signature subpackets, yet we required them to have a hashed creation date subpacket. diff --git a/README.md b/README.md index bc3484b8..2a91789e 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.3' + implementation 'org.pgpainless:pgpainless-core:1.4.4' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 74772110..3aef6d05 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.3" + implementation "org.pgpainless:pgpainless-sop:1.4.4" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.3 + 1.4.4 ... diff --git a/version.gradle b/version.gradle index 68d74212..2593724d 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.4.4' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From a25ea542d69651fa0dfbccf0d3cb1151c83f772a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 9 Feb 2023 21:58:37 +0100 Subject: [PATCH 0677/1204] PGPainless 1.4.5-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 2593724d..55e9b151 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.4' - isSnapshot = false + shortVersion = '1.4.5' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72' From ed2c53f5d6362afec315e43a84cb35c5745ccd55 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 25 Feb 2023 11:26:58 +0100 Subject: [PATCH 0678/1204] Make getLastModified() @Nonnull --- .../src/main/java/org/pgpainless/key/info/KeyRingInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index fa4168dd..46dd500b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -611,7 +611,7 @@ public class KeyRingInfo { * * @return last modification date. */ - public @Nullable Date getLastModified() { + public @Nonnull Date getLastModified() { PGPSignature mostRecent = getMostRecentSignature(); if (mostRecent == null) { // No sigs found. Return public key creation date instead. From acb5d3fd9e98041948e6742804b3c1c439f124e1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 7 Apr 2023 11:26:38 +0200 Subject: [PATCH 0679/1204] getEncryptionSubkeys(): Compare expirations against reference date --- .../src/main/java/org/pgpainless/key/info/KeyRingInfo.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 46dd500b..1ebd023e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -903,7 +903,7 @@ public class KeyRingInfo { */ public @Nonnull List getEncryptionSubkeys(EncryptionPurpose purpose) { Date primaryExpiration = getPrimaryKeyExpirationDate(); - if (primaryExpiration != null && primaryExpiration.before(new Date())) { + if (primaryExpiration != null && primaryExpiration.before(referenceDate)) { return Collections.emptyList(); } @@ -917,7 +917,7 @@ public class KeyRingInfo { } Date subkeyExpiration = getSubkeyExpirationDate(OpenPgpFingerprint.of(subKey)); - if (subkeyExpiration != null && subkeyExpiration.before(new Date())) { + if (subkeyExpiration != null && subkeyExpiration.before(referenceDate)) { continue; } From e744668f5ae63dec785b02e48d13f7bb5f8aedd6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 7 Apr 2023 11:47:40 +0200 Subject: [PATCH 0680/1204] Deprecate OpenPgpFingerprint.parse() methods --- .../src/main/java/org/pgpainless/key/OpenPgpFingerprint.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java index 86ac8265..0525f4c9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java @@ -55,7 +55,9 @@ public abstract class OpenPgpFingerprint implements CharSequence, Comparable Date: Fri, 7 Apr 2023 12:28:27 +0200 Subject: [PATCH 0681/1204] Introduce OpenPgpv6Fingerprint --- .../pgpainless/key/OpenPgpFingerprint.java | 11 +- .../pgpainless/key/OpenPgpV5Fingerprint.java | 67 +------- .../pgpainless/key/OpenPgpV6Fingerprint.java | 58 +++++++ .../pgpainless/key/_64DigitFingerprint.java | 119 ++++++++++++++ .../key/OpenPgpV5FingerprintTest.java | 69 +------- .../key/OpenPgpV6FingerprintTest.java | 154 ++++++++++++++++++ .../key/_64DigitFingerprintTest.java | 85 ++++++++++ 7 files changed, 430 insertions(+), 133 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV6FingerprintTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java index 0525f4c9..1ac900a0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java @@ -36,6 +36,9 @@ public abstract class OpenPgpFingerprint implements CharSequence, Comparable +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import javax.annotation.Nonnull; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; + +/** + * This class represents a hex encoded, upper case OpenPGP v6 fingerprint. + */ +public class OpenPgpV6Fingerprint extends _64DigitFingerprint { + + /** + * Create an {@link OpenPgpV6Fingerprint}. + * + * @param fingerprint uppercase hexadecimal fingerprint of length 64 + */ + public OpenPgpV6Fingerprint(@Nonnull String fingerprint) { + super(fingerprint); + } + + public OpenPgpV6Fingerprint(@Nonnull byte[] bytes) { + super(bytes); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPPublicKey key) { + super(key); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPSecretKey key) { + this(key.getPublicKey()); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPPublicKeyRing ring) { + super(ring); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPSecretKeyRing ring) { + super(ring); + } + + public OpenPgpV6Fingerprint(@Nonnull PGPKeyRing ring) { + super(ring); + } + + @Override + public int getVersion() { + return 6; + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java new file mode 100644 index 00000000..11f18058 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import javax.annotation.Nonnull; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.encoders.Hex; + +/** + * This class represents a hex encoded, upper case OpenPGP v5 or v6 fingerprint. + * Since both fingerprints use the same format, this class is used when parsing the fingerprint without knowing the + * key version. + */ +public class _64DigitFingerprint extends OpenPgpFingerprint { + + /** + * Create an {@link _64DigitFingerprint}. + * + * @param fingerprint uppercase hexadecimal fingerprint of length 64 + */ + protected _64DigitFingerprint(@Nonnull String fingerprint) { + super(fingerprint); + } + + protected _64DigitFingerprint(@Nonnull byte[] bytes) { + super(Hex.encode(bytes)); + } + + protected _64DigitFingerprint(@Nonnull PGPPublicKey key) { + super(key); + } + + protected _64DigitFingerprint(@Nonnull PGPSecretKey key) { + this(key.getPublicKey()); + } + + protected _64DigitFingerprint(@Nonnull PGPPublicKeyRing ring) { + super(ring); + } + + protected _64DigitFingerprint(@Nonnull PGPSecretKeyRing ring) { + super(ring); + } + + protected _64DigitFingerprint(@Nonnull PGPKeyRing ring) { + super(ring); + } + + @Override + public int getVersion() { + return -1; // might be v5 or v6 + } + + @Override + protected boolean isValid(@Nonnull String fp) { + return fp.matches("^[0-9A-F]{64}$"); + } + + @Override + public long getKeyId() { + byte[] bytes = Hex.decode(toString().getBytes(utf8)); + ByteBuffer buf = ByteBuffer.wrap(bytes); + + // The key id is the left-most 8 bytes (conveniently a long). + // We have to cast here in order to be compatible with java 8 + // https://github.com/eclipse/jetty.project/issues/3244 + ((Buffer) buf).position(0); + + return buf.getLong(); + } + + @Override + public String prettyPrint() { + String fp = toString(); + StringBuilder pretty = new StringBuilder(); + + for (int i = 0; i < 4; i++) { + pretty.append(fp, i * 8, (i + 1) * 8).append(' '); + } + pretty.append(' '); + for (int i = 4; i < 7; i++) { + pretty.append(fp, i * 8, (i + 1) * 8).append(' '); + } + pretty.append(fp, 56, 64); + return pretty.toString(); + } + + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + + if (!(other instanceof CharSequence)) { + return false; + } + + return this.toString().equals(other.toString()); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public int compareTo(OpenPgpFingerprint openPgpFingerprint) { + return toString().compareTo(openPgpFingerprint.toString()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java index a250bef4..57c98928 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV5FingerprintTest.java @@ -5,8 +5,6 @@ package org.pgpainless.key; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -29,77 +27,12 @@ public class OpenPgpV5FingerprintTest { OpenPgpV5Fingerprint fingerprint = new OpenPgpV5Fingerprint(fp); assertEquals(fp, fingerprint.toString()); assertEquals(pretty, fingerprint.prettyPrint()); + assertEquals(5, fingerprint.getVersion()); long id = fingerprint.getKeyId(); assertEquals("76543210abcdefab", Long.toHexString(id)); } - @Test - public void testParse() { - String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; - OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); - - assertTrue(parsed instanceof OpenPgpV5Fingerprint); - OpenPgpV5Fingerprint v5fp = (OpenPgpV5Fingerprint) parsed; - assertEquals(prettyPrint, v5fp.prettyPrint()); - assertEquals(5, v5fp.getVersion()); - } - - @Test - public void testParseFromBinary() { - String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; - byte[] binary = Hex.decode(hex); - - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); - assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); - assertEquals(hex, fingerprint.toString()); - - OpenPgpV5Fingerprint constructed = new OpenPgpV5Fingerprint(binary); - assertEquals(fingerprint, constructed); - } - - @Test - public void testParseFromBinary_leadingZeros() { - String hex = "000000000000000001AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; - byte[] binary = Hex.decode(hex); - - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); - assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); - assertEquals(hex, fingerprint.toString()); - } - - @Test - public void testParseFromBinary_trailingZeros() { - String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA100000000000000000"; - byte[] binary = Hex.decode(hex); - - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); - assertTrue(fingerprint instanceof OpenPgpV5Fingerprint); - assertEquals(hex, fingerprint.toString()); - } - - @Test - public void testParseFromBinary_wrongLength() { - String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA012345"; // missing 2 digits - byte[] binary = Hex.decode(hex); - - assertThrows(IllegalArgumentException.class, () -> OpenPgpFingerprint.parseFromBinary(binary)); - } - - @Test - public void equalsTest() { - String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; - OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); - - assertNotEquals(parsed, null); - assertNotEquals(parsed, new Object()); - assertEquals(parsed, parsed.toString()); - - OpenPgpFingerprint parsed2 = new OpenPgpV5Fingerprint(prettyPrint); - assertEquals(parsed.hashCode(), parsed2.hashCode()); - assertEquals(0, parsed.compareTo(parsed2)); - } - @Test public void constructFromMockedPublicKey() { String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV6FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV6FingerprintTest.java new file mode 100644 index 00000000..7a7b0e45 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV6FingerprintTest.java @@ -0,0 +1,154 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OpenPgpV6FingerprintTest { + + @Test + public void testFingerprintFormatting() { + String pretty = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + String fp = pretty.replace(" ", ""); + + OpenPgpV6Fingerprint fingerprint = new OpenPgpV6Fingerprint(fp); + assertEquals(fp, fingerprint.toString()); + assertEquals(pretty, fingerprint.prettyPrint()); + assertEquals(6, fingerprint.getVersion()); + + long id = fingerprint.getKeyId(); + assertEquals("76543210abcdefab", Long.toHexString(id)); + } + + @Test + public void testParseFromBinary_leadingZeros() { + String hex = "000000000000000001AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = new OpenPgpV6Fingerprint(binary); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_trailingZeros() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA100000000000000000"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = new OpenPgpV6Fingerprint(binary); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_wrongLength() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA012345"; // missing 2 digits + byte[] binary = Hex.decode(hex); + + assertThrows(IllegalArgumentException.class, () -> new OpenPgpV6Fingerprint(binary)); + } + + @Test + public void equalsTest() { + String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + OpenPgpFingerprint parsed = new OpenPgpV6Fingerprint(prettyPrint); + + assertNotEquals(parsed, null); + assertNotEquals(parsed, new Object()); + assertEquals(parsed, parsed.toString()); + + OpenPgpFingerprint parsed2 = new OpenPgpV6Fingerprint(prettyPrint); + assertEquals(parsed.hashCode(), parsed2.hashCode()); + assertEquals(0, parsed.compareTo(parsed2)); + } + + @Test + public void constructFromMockedPublicKey() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); + assertTrue(fingerprint instanceof OpenPgpV6Fingerprint); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedSecretKey() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPSecretKey secretKey = mock(PGPSecretKey.class); + when(secretKey.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = new OpenPgpV6Fingerprint(secretKey); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedPublicKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPPublicKeyRing publicKeys = mock(PGPPublicKeyRing.class); + when(publicKeys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKeys); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV6Fingerprint(publicKeys); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedSecretKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPSecretKeyRing secretKeys = mock(PGPSecretKeyRing.class); + when(secretKeys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(secretKeys); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV6Fingerprint(secretKeys); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void constructFromMockedKeyRing() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + PGPPublicKey publicKey = getMockedPublicKey(hex); + PGPKeyRing keys = mock(PGPKeyRing.class); + when(keys.getPublicKey()).thenReturn(publicKey); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(keys); + assertEquals(6, fingerprint.getVersion()); + assertEquals(hex, fingerprint.toString()); + + fingerprint = new OpenPgpV6Fingerprint(keys); + assertEquals(hex, fingerprint.toString()); + } + + private PGPPublicKey getMockedPublicKey(String hex) { + byte[] binary = Hex.decode(hex); + + PGPPublicKey mocked = mock(PGPPublicKey.class); + when(mocked.getVersion()).thenReturn(6); + when(mocked.getFingerprint()).thenReturn(binary); + return mocked; + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java new file mode 100644 index 00000000..21ff31c3 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java @@ -0,0 +1,85 @@ +package org.pgpainless.key; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class _64DigitFingerprintTest { + + @Test + public void testParse() { + String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); + + assertTrue(parsed instanceof _64DigitFingerprint); + assertEquals(prettyPrint, parsed.prettyPrint()); + assertEquals(-1, parsed.getVersion()); + } + + @Test + public void testParseFromBinary() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof _64DigitFingerprint); + assertEquals(hex, fingerprint.toString()); + + OpenPgpV5Fingerprint v5 = new OpenPgpV5Fingerprint(binary); + assertEquals(fingerprint, v5); + + OpenPgpV6Fingerprint v6 = new OpenPgpV6Fingerprint(binary); + assertEquals(fingerprint, v6); + } + + @Test + public void testParseFromBinary_leadingZeros() { + String hex = "000000000000000001AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA01234567"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof _64DigitFingerprint); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_trailingZeros() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA100000000000000000"; + byte[] binary = Hex.decode(hex); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); + assertTrue(fingerprint instanceof _64DigitFingerprint); + assertEquals(hex, fingerprint.toString()); + } + + @Test + public void testParseFromBinary_wrongLength() { + String hex = "76543210ABCDEFAB01AB23CD1C0FFEE11EEFF0C1DC32BA10BAFEDCBA012345"; // missing 2 digits + byte[] binary = Hex.decode(hex); + + assertThrows(IllegalArgumentException.class, () -> OpenPgpFingerprint.parseFromBinary(binary)); + } + + @Test + public void equalsTest() { + String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; + OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); + + assertNotEquals(parsed, null); + assertNotEquals(parsed, new Object()); + assertEquals(parsed, parsed.toString()); + + OpenPgpFingerprint v5 = new OpenPgpV5Fingerprint(prettyPrint); + assertEquals(parsed.hashCode(), v5.hashCode()); + assertEquals(0, parsed.compareTo(v5)); + + OpenPgpFingerprint v6 = new OpenPgpV6Fingerprint(prettyPrint); + assertEquals(parsed.hashCode(), v6.hashCode()); + assertEquals(0, parsed.compareTo(v6)); + } + +} From 2587f19df345075b6c55949aee06dd08c346e156 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 9 Apr 2023 18:49:20 +0200 Subject: [PATCH 0682/1204] BC173: Fix CRC error detection by improving error check --- .../pgpainless/decryption_verification/TeeBCPGInputStream.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index 28b415e0..bdbd9bcd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -120,7 +120,7 @@ public class TeeBCPGInputStream { last = inputStream.read(); return last; } catch (IOException e) { - if ("crc check failed in armored message.".equals(e.getMessage())) { + if (e.getMessage().contains("crc check failed in armored message")) { throw e; } return -1; From 44608744c26f2df63daba7f3a8e24d706b8d5db0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 16:17:58 +0200 Subject: [PATCH 0683/1204] Add missing license header --- .../test/java/org/pgpainless/key/_64DigitFingerprintTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java index 21ff31c3..a38fa61d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.key; import org.bouncycastle.util.encoders.Hex; From e35287a66658af3213628be09b6d025f401cb40c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 14:31:48 +0200 Subject: [PATCH 0684/1204] Add support for SOP05 features --- .../org/pgpainless/sop/GenerateKeyImpl.java | 48 ++++++++++++++++++- .../org/pgpainless/sop/ListProfilesImpl.java | 29 +++++++++++ .../main/java/org/pgpainless/sop/SOPImpl.java | 6 +++ version.gradle | 2 +- 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index da99c854..693ca454 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -8,18 +8,22 @@ import java.io.IOException; import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import java.util.Iterator; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.pgpainless.PGPainless; +import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.Passphrase; +import sop.Profile; import sop.Ready; import sop.exception.SOPGPException; import sop.operation.GenerateKey; @@ -29,9 +33,16 @@ import sop.operation.GenerateKey; */ public class GenerateKeyImpl implements GenerateKey { + public static final Profile DEFAULT_PROFILE = new Profile("default", "Generate keys based on XDH and EdDSA"); + public static final Profile RSA3072_PROFILE = new Profile("rfc4880-rsa3072@pgpainless.org", "Generate 3072-bit RSA keys"); + public static final Profile RSA4096_PROFILE = new Profile("rfc4880-rsa4096@pgpainless.org", "Generate 4096-bit RSA keys"); + + public static final List SUPPORTED_PROFILES = Arrays.asList(DEFAULT_PROFILE, RSA3072_PROFILE, RSA4096_PROFILE); + private boolean armor = true; private final Set userIds = new LinkedHashSet<>(); private Passphrase passphrase = Passphrase.emptyPassphrase(); + private String profile = DEFAULT_PROFILE.getName(); @Override public GenerateKey noArmor() { @@ -51,6 +62,18 @@ public class GenerateKeyImpl implements GenerateKey { return this; } + @Override + public GenerateKey profile(String profileName) { + for (Profile profile : SUPPORTED_PROFILES) { + if (profile.getName().equals(profileName)) { + this.profile = profileName; + return this; + } + } + + throw new SOPGPException.UnsupportedProfile("generate-key", profileName); + } + @Override public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { Iterator userIdIterator = userIds.iterator(); @@ -58,8 +81,7 @@ public class GenerateKeyImpl implements GenerateKey { PGPSecretKeyRing key; try { String primaryUserId = userIdIterator.hasNext() ? userIdIterator.next() : null; - key = PGPainless.generateKeyRing() - .modernKeyRing(primaryUserId, passphrase); + key = generateKeyWithProfile(profile, primaryUserId, passphrase); if (userIdIterator.hasNext()) { SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(key); @@ -90,4 +112,26 @@ public class GenerateKeyImpl implements GenerateKey { throw new RuntimeException(e); } } + + private PGPSecretKeyRing generateKeyWithProfile(String profile, String primaryUserId, Passphrase passphrase) + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing key; + // XDH + EdDSA + if (profile.equals(DEFAULT_PROFILE.getName())) { + key = PGPainless.generateKeyRing() + .modernKeyRing(primaryUserId, passphrase); + } + else if (profile.equals(RSA3072_PROFILE.getName())) { + key = PGPainless.generateKeyRing() + .simpleRsaKeyRing(primaryUserId, RsaLength._3072, passphrase); + } + else if (profile.equals(RSA4096_PROFILE.getName())) { + key = PGPainless.generateKeyRing() + .simpleRsaKeyRing(primaryUserId, RsaLength._4096, passphrase); + } + else { + throw new SOPGPException.UnsupportedProfile("generate-key", profile); + } + return key; + } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java new file mode 100644 index 00000000..06519bb3 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import java.util.List; + +import sop.Profile; +import sop.exception.SOPGPException; +import sop.operation.ListProfiles; + +public class ListProfilesImpl implements ListProfiles { + + @Override + public List subcommand(String command) { + if (command == null) { + throw new SOPGPException.UnsupportedProfile("null"); + } + + switch (command) { + case "generate-key": + return GenerateKeyImpl.SUPPORTED_PROFILES; + + default: + throw new SOPGPException.UnsupportedProfile(command); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java index a49f7e34..a0e5f631 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -17,6 +17,7 @@ import sop.operation.ExtractCert; import sop.operation.GenerateKey; import sop.operation.InlineSign; import sop.operation.InlineVerify; +import sop.operation.ListProfiles; import sop.operation.Version; /** @@ -96,6 +97,11 @@ public class SOPImpl implements SOP { return new DearmorImpl(); } + @Override + public ListProfiles listProfiles() { + return new ListProfilesImpl(); + } + @Override public InlineDetach inlineDetach() { return new InlineDetachImpl(); diff --git a/version.gradle b/version.gradle index 55e9b151..1adf151a 100644 --- a/version.gradle +++ b/version.gradle @@ -18,6 +18,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '4.1.1' + sopJavaVersion = '5.0.0-SNAPSHOT' } } From b79e706d65c33defebd132e9ec3db5f6725c89db Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 14:35:55 +0200 Subject: [PATCH 0685/1204] Bump SOP version in VersionImpl to 05 --- .../src/main/java/org/pgpainless/sop/VersionImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index 4449af10..13a5dc94 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -18,7 +18,7 @@ import sop.operation.Version; public class VersionImpl implements Version { // draft version - private static final String SOP_VERSION = "04"; + private static final String SOP_VERSION = "05"; @Override public String getName() { From f3a4a01d199bd3782d2e34b065ac16c47dd5253b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 16:17:32 +0200 Subject: [PATCH 0686/1204] Add basic tests for new functionality --- .../org/pgpainless/sop/GenerateKeyTest.java | 14 ++++++++ .../org/pgpainless/sop/ListProfilesTest.java | 34 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java index 3a6e4476..5894bfa7 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -7,6 +7,7 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; @@ -21,6 +22,7 @@ import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; import sop.SOP; +import sop.exception.SOPGPException; public class GenerateKeyTest { @@ -92,4 +94,16 @@ public class GenerateKeyTest { assertNotNull(UnlockSecretKey.unlockSecretKey(key, Passphrase.fromPassword("sw0rdf1sh"))); } } + + @Test + public void invalidProfile() { + assertThrows(SOPGPException.UnsupportedProfile.class, () -> + sop.generateKey().profile("invalid")); + } + + @Test + public void nullProfile() { + assertThrows(SOPGPException.UnsupportedProfile.class, () -> + sop.generateKey().profile((String) null)); + } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java new file mode 100644 index 00000000..0103bf4d --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import sop.SOP; +import sop.exception.SOPGPException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ListProfilesTest { + + private SOP sop; + + @BeforeEach + public void prepare() { + this.sop = new SOPImpl(); + } + + @Test + public void listProfilesOfGenerateKey() { + assertFalse(sop.listProfiles().subcommand("generate-key").isEmpty()); + } + + @Test + public void listProfilesOfHelpCommandThrows() { + assertThrows(SOPGPException.UnsupportedProfile.class, () -> + sop.listProfiles().subcommand("help")); + } +} From 702fdf085c2eef616be59b233988d5ab2a28f417 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 12:49:23 +0200 Subject: [PATCH 0687/1204] Thin out and rename profiles of generate-key --- .../java/org/pgpainless/sop/GenerateKeyImpl.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index 693ca454..d2baf29b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -33,16 +33,15 @@ import sop.operation.GenerateKey; */ public class GenerateKeyImpl implements GenerateKey { - public static final Profile DEFAULT_PROFILE = new Profile("default", "Generate keys based on XDH and EdDSA"); - public static final Profile RSA3072_PROFILE = new Profile("rfc4880-rsa3072@pgpainless.org", "Generate 3072-bit RSA keys"); - public static final Profile RSA4096_PROFILE = new Profile("rfc4880-rsa4096@pgpainless.org", "Generate 4096-bit RSA keys"); + public static final Profile CURVE25519_PROFILE = new Profile("draft-koch-eddsa-for-openpgp-00", "Generate EdDSA / ECDH keys using Curve25519"); + public static final Profile RSA4096_PROFILE = new Profile("rfc4880", "Generate 4096-bit RSA keys"); - public static final List SUPPORTED_PROFILES = Arrays.asList(DEFAULT_PROFILE, RSA3072_PROFILE, RSA4096_PROFILE); + public static final List SUPPORTED_PROFILES = Arrays.asList(CURVE25519_PROFILE, RSA4096_PROFILE); private boolean armor = true; private final Set userIds = new LinkedHashSet<>(); private Passphrase passphrase = Passphrase.emptyPassphrase(); - private String profile = DEFAULT_PROFILE.getName(); + private String profile = CURVE25519_PROFILE.getName(); @Override public GenerateKey noArmor() { @@ -117,14 +116,11 @@ public class GenerateKeyImpl implements GenerateKey { throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing key; // XDH + EdDSA - if (profile.equals(DEFAULT_PROFILE.getName())) { + if (profile.equals(CURVE25519_PROFILE.getName())) { key = PGPainless.generateKeyRing() .modernKeyRing(primaryUserId, passphrase); } - else if (profile.equals(RSA3072_PROFILE.getName())) { - key = PGPainless.generateKeyRing() - .simpleRsaKeyRing(primaryUserId, RsaLength._3072, passphrase); - } + // RSA 4096 else if (profile.equals(RSA4096_PROFILE.getName())) { key = PGPainless.generateKeyRing() .simpleRsaKeyRing(primaryUserId, RsaLength._4096, passphrase); From 63714859292c7bac51b3e40d127439cf469a93ad Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 14:51:50 +0200 Subject: [PATCH 0688/1204] Add some clarifying comments to GenerateKeyImpl --- .../src/main/java/org/pgpainless/sop/GenerateKeyImpl.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index d2baf29b..ba788dac 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -63,13 +63,16 @@ public class GenerateKeyImpl implements GenerateKey { @Override public GenerateKey profile(String profileName) { + // Sanitize the profile name to make sure we support the given profile for (Profile profile : SUPPORTED_PROFILES) { if (profile.getName().equals(profileName)) { this.profile = profileName; + // return if we found the profile return this; } } + // profile not found, throw throw new SOPGPException.UnsupportedProfile("generate-key", profileName); } @@ -126,6 +129,7 @@ public class GenerateKeyImpl implements GenerateKey { .simpleRsaKeyRing(primaryUserId, RsaLength._4096, passphrase); } else { + // Missing else-if branch for profile. Oops. throw new SOPGPException.UnsupportedProfile("generate-key", profile); } return key; From 11eda9be953487e2124fba4b48c31f72dbb13a94 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 15:09:15 +0200 Subject: [PATCH 0689/1204] Bump sop-java to 5.0.0 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 1adf151a..4a532368 100644 --- a/version.gradle +++ b/version.gradle @@ -18,6 +18,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '5.0.0-SNAPSHOT' + sopJavaVersion = '5.0.0' } } From 772a98b4ae0701571136be3dd7ff66f7c17042c0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 15:22:38 +0200 Subject: [PATCH 0690/1204] Update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd7f3505..6212d8c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.5.0-SNAPSHOT +- Introduce `OpenPgpv6Fingerprint` class +- Bump `sop-java` to `5.0.0`, implementing [SOP Spec Revision 05](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html) + - Add support for `list-profiles` subcommand (`generate-key` only for now) + - `generate-key`: Add support for `--profile=` option + - Add profile `draft-koch-eddsa-for-openpgp-00` which represents status quo. + - Add profile `rfc4880` which generates keys based on 4096-bit RSA. + ## 1.4.4 - Fix expectations on subpackets of v3 signatures (thanks @bjansen) - Properly verify v3 signatures, which do not yet have signature subpackets, yet we required them to have From 66d81660052e6a3720eec3026509dcd9d1288491 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 14:35:11 +0200 Subject: [PATCH 0691/1204] Bump sop-java to 6.0.0-SNAPSHOT --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 4a532368..aa025a9e 100644 --- a/version.gradle +++ b/version.gradle @@ -18,6 +18,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '5.0.0' + sopJavaVersion = '6.0.0' } } From 446d121777b47f82aa4bfd993d13ef27880e9d77 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 14:36:31 +0200 Subject: [PATCH 0692/1204] Bump SOP version in VersionImpl to 06 --- .../src/main/java/org/pgpainless/sop/VersionImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index 13a5dc94..5e851706 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -18,7 +18,7 @@ import sop.operation.Version; public class VersionImpl implements Version { // draft version - private static final String SOP_VERSION = "05"; + private static final String SOP_VERSION = "06"; @Override public String getName() { From 5b363de6e42a9cec16175f6afb5552548b389384 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 14:36:57 +0200 Subject: [PATCH 0693/1204] Implement VersionImpl.getSopSpecVersion() --- .../src/main/java/org/pgpainless/sop/VersionImpl.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index 5e851706..8986e295 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -63,4 +63,9 @@ public class VersionImpl implements Version { "Using " + getBackendVersion() + "\n" + "https://www.bouncycastle.org/java.html"; } + + @Override + public String getSopSpecVersion() { + return "draft-dkg-openpgp-stateless-cli-" + SOP_VERSION; + } } From 3b1edb076c5aa74403777f58f82064d38ef9d2ce Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 15:13:43 +0200 Subject: [PATCH 0694/1204] Basic support for sop encrypt --profile=XXX --- .../java/org/pgpainless/sop/EncryptImpl.java | 20 +++++++++++++++++++ .../org/pgpainless/sop/ListProfilesImpl.java | 3 +++ 2 files changed, 23 insertions(+) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 62d20bf0..689e07be 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -8,7 +8,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; +import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; import org.bouncycastle.openpgp.PGPException; @@ -28,6 +30,7 @@ import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.Passphrase; +import sop.Profile; import sop.Ready; import sop.enums.EncryptAs; import sop.exception.SOPGPException; @@ -39,10 +42,15 @@ import sop.util.ProxyOutputStream; */ public class EncryptImpl implements Encrypt { + private static final Profile DEFAULT_PROFILE = new Profile("default", "Use the implementer's recommendations"); + + public static final List SUPPORTED_PROFILES = Arrays.asList(DEFAULT_PROFILE); + EncryptionOptions encryptionOptions = EncryptionOptions.get(); SigningOptions signingOptions = null; MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); private final Set signingKeys = new HashSet<>(); + private String profile = DEFAULT_PROFILE.getName(); // TODO: Use in future releases private EncryptAs encryptAs = EncryptAs.Binary; boolean armor = true; @@ -111,6 +119,18 @@ public class EncryptImpl implements Encrypt { return this; } + @Override + public Encrypt profile(String profileName) { + for (Profile profile : SUPPORTED_PROFILES) { + if (profile.getName().equals(profileName)) { + this.profile = profile.getName(); + return this; + } + } + + throw new SOPGPException.UnsupportedProfile("encrypt", profileName); + } + @Override public Ready plaintext(InputStream plaintext) throws IOException { if (!encryptionOptions.hasEncryptionMethod()) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java index 06519bb3..c0f5027a 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java @@ -22,6 +22,9 @@ public class ListProfilesImpl implements ListProfiles { case "generate-key": return GenerateKeyImpl.SUPPORTED_PROFILES; + case "encrypt": + return EncryptImpl.SUPPORTED_PROFILES; + default: throw new SOPGPException.UnsupportedProfile(command); } From 926e540016dd6edf8ec7de2234847e3fbbbbc4dc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 15:57:29 +0200 Subject: [PATCH 0695/1204] Test fine-grained SOP spec version --- .../cli/commands/VersionCmdTest.java | 8 +++++++ .../java/org/pgpainless/sop/VersionImpl.java | 21 +++++++++++++---- .../java/org/pgpainless/sop/VersionTest.java | 23 +++++++++++++++++++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java index 2e4aa7e4..87f535a8 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/VersionCmdTest.java @@ -41,4 +41,12 @@ public class VersionCmdTest extends CLITest { assertTrue(info.contains("Bouncy Castle")); assertTrue(info.contains("Stateless OpenPGP Protocol")); } + + @Test + public void testSopSpecVersion() throws IOException { + ByteArrayOutputStream out = pipeStdoutToStream(); + assertSuccess(executeCommand("version", "--sop-spec")); + String info = out.toString(); + assertTrue(info.startsWith("draft-dkg-openpgp-stateless-cli-")); + } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index 8986e295..82995edf 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -18,7 +18,7 @@ import sop.operation.Version; public class VersionImpl implements Version { // draft version - private static final String SOP_VERSION = "06"; + private static final int SOP_VERSION = 6; @Override public String getName() { @@ -51,11 +51,12 @@ public class VersionImpl implements Version { @Override public String getExtendedVersion() { + String FORMAT_VERSION = String.format("%02d", SOP_VERSION); return getName() + " " + getVersion() + "\n" + "https://codeberg.org/PGPainless/pgpainless/src/branch/master/pgpainless-sop\n" + "\n" + - "Implementation of the Stateless OpenPGP Protocol Version " + SOP_VERSION + "\n" + - "https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-" + SOP_VERSION + "\n" + + "Implementation of the Stateless OpenPGP Protocol Version " + FORMAT_VERSION + "\n" + + "https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-" + FORMAT_VERSION + "\n" + "\n" + "Based on pgpainless-core " + getVersion() + "\n" + "https://pgpainless.org\n" + @@ -65,7 +66,17 @@ public class VersionImpl implements Version { } @Override - public String getSopSpecVersion() { - return "draft-dkg-openpgp-stateless-cli-" + SOP_VERSION; + public int getSopSpecVersionNumber() { + return SOP_VERSION; + } + + @Override + public boolean isSopSpecImplementationIncomplete() { + return false; + } + + @Override + public String getSopSpecImplementationIncompletenessRemarks() { + return null; } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java index c9739471..fa4af6a8 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java @@ -7,6 +7,7 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -49,4 +50,26 @@ public class VersionTest { String firstLine = extendedVersion.split("\n")[0]; assertEquals(sop.version().getName() + " " + sop.version().getVersion(), firstLine); } + + @Test + public void testGetSopSpecVersion() { + boolean incomplete = sop.version().isSopSpecImplementationIncomplete(); + int revisionNumber = sop.version().getSopSpecVersionNumber(); + + String revisionString = sop.version().getSopSpecRevisionString(); + assertEquals("draft-dkg-openpgp-stateless-cli-" + String.format("%02d", revisionNumber), revisionString); + + String incompletenessRemarks = sop.version().getSopSpecImplementationIncompletenessRemarks(); + + String fullSopSpecVersion = sop.version().getSopSpecVersion(); + if (incomplete) { + assertTrue(fullSopSpecVersion.startsWith("~" + revisionString)); + } else { + assertTrue(fullSopSpecVersion.startsWith(revisionString)); + } + + if (incompletenessRemarks != null) { + assertTrue(fullSopSpecVersion.endsWith(incompletenessRemarks)); + } + } } From 676e7d166a22854b53f01a50c6416a8c2f5c1920 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 16:06:45 +0200 Subject: [PATCH 0696/1204] EncryptImpl: Rename default profile, add documentation --- .../src/main/java/org/pgpainless/sop/EncryptImpl.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 689e07be..18bcabcc 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -42,15 +42,15 @@ import sop.util.ProxyOutputStream; */ public class EncryptImpl implements Encrypt { - private static final Profile DEFAULT_PROFILE = new Profile("default", "Use the implementer's recommendations"); + private static final Profile RFC4880_PROFILE = new Profile("rfc4880", "Follow the packet format of rfc4880"); - public static final List SUPPORTED_PROFILES = Arrays.asList(DEFAULT_PROFILE); + public static final List SUPPORTED_PROFILES = Arrays.asList(RFC4880_PROFILE); EncryptionOptions encryptionOptions = EncryptionOptions.get(); SigningOptions signingOptions = null; MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); private final Set signingKeys = new HashSet<>(); - private String profile = DEFAULT_PROFILE.getName(); // TODO: Use in future releases + private String profile = RFC4880_PROFILE.getName(); // TODO: Use in future releases private EncryptAs encryptAs = EncryptAs.Binary; boolean armor = true; @@ -121,13 +121,16 @@ public class EncryptImpl implements Encrypt { @Override public Encrypt profile(String profileName) { + // sanitize profile name to make sure we only accept supported profiles for (Profile profile : SUPPORTED_PROFILES) { if (profile.getName().equals(profileName)) { + // profile is supported, return this.profile = profile.getName(); return this; } } + // Profile is not supported, throw throw new SOPGPException.UnsupportedProfile("encrypt", profileName); } From 003423a165631e8015cbe05f424fc0555b3ae0cf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 16:09:51 +0200 Subject: [PATCH 0697/1204] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6212d8c4..7d408157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ SPDX-License-Identifier: CC0-1.0 - `generate-key`: Add support for `--profile=` option - Add profile `draft-koch-eddsa-for-openpgp-00` which represents status quo. - Add profile `rfc4880` which generates keys based on 4096-bit RSA. +- Bump `sop-java` to `6.0.0`, implementing [SOP Spec Revision 06](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-06.html) + - `encrypt`: Add support for `--profile=` option + - Add profile `rfc4880` to reflect status quo + - `version`: Add support for `--sop-spec` option ## 1.4.4 - Fix expectations on subpackets of v3 signatures (thanks @bjansen) From e465ae60a7150faabfeedb32dee3d727fb01662e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 16:22:54 +0200 Subject: [PATCH 0698/1204] VersionImpl: Fix outdated method names --- .../src/main/java/org/pgpainless/sop/VersionImpl.java | 5 +++-- .../src/test/java/org/pgpainless/sop/VersionTest.java | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index 82995edf..0794c708 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -66,7 +66,7 @@ public class VersionImpl implements Version { } @Override - public int getSopSpecVersionNumber() { + public int getSopSpecRevisionNumber() { return SOP_VERSION; } @@ -76,7 +76,8 @@ public class VersionImpl implements Version { } @Override - public String getSopSpecImplementationIncompletenessRemarks() { + public String getSopSpecImplementationRemarks() { return null; } + } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java index fa4af6a8..32d2c2e0 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java @@ -54,12 +54,12 @@ public class VersionTest { @Test public void testGetSopSpecVersion() { boolean incomplete = sop.version().isSopSpecImplementationIncomplete(); - int revisionNumber = sop.version().getSopSpecVersionNumber(); + int revisionNumber = sop.version().getSopSpecRevisionNumber(); - String revisionString = sop.version().getSopSpecRevisionString(); + String revisionString = sop.version().getSopSpecRevisionName(); assertEquals("draft-dkg-openpgp-stateless-cli-" + String.format("%02d", revisionNumber), revisionString); - String incompletenessRemarks = sop.version().getSopSpecImplementationIncompletenessRemarks(); + String incompletenessRemarks = sop.version().getSopSpecImplementationRemarks(); String fullSopSpecVersion = sop.version().getSopSpecVersion(); if (incomplete) { From 3a3e193bb0466ec4b5618c5fc40645f0e2645a23 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 16:23:16 +0200 Subject: [PATCH 0699/1204] Bump bouncycastle to 1.73 --- version.gradle | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/version.gradle b/version.gradle index aa025a9e..cbd64528 100644 --- a/version.gradle +++ b/version.gradle @@ -8,12 +8,8 @@ allprojects { isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 - bouncyCastleVersion = '1.72' - // When using bouncyCastleVersion 1.72: - // unfortunately we rely on 1.72.1 or 1.72.3 for a patch for https://github.com/bcgit/bc-java/issues/1257 - // which is a bug we introduced with a PR against BC :/ oops - // When bouncyCastleVersion is 1.71, bouncyPgVersion can simply be set to 1.71 as well. - bouncyPgVersion = '1.72.3' + bouncyCastleVersion = '1.73' + bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' logbackVersion = '1.2.11' mockitoVersion = '4.5.1' From d8f32b668910b7c1a53ae288bcffbdb013464b07 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 16:24:55 +0200 Subject: [PATCH 0700/1204] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d408157..08184f70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog ## 1.5.0-SNAPSHOT +- Bump `bcpg-jdk15to18` to `1.73` +- Bump `bcprov-jdk15to18` to `1.73` - Introduce `OpenPgpv6Fingerprint` class - Bump `sop-java` to `5.0.0`, implementing [SOP Spec Revision 05](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html) - Add support for `list-profiles` subcommand (`generate-key` only for now) From 94a609127e75f1569e42e17f68a32029c039389f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 16:35:17 +0200 Subject: [PATCH 0701/1204] PGPainless 1.5.0 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 6 +++--- version.gradle | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08184f70..51f7a815 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.5.0-SNAPSHOT +## 1.5.0 - Bump `bcpg-jdk15to18` to `1.73` - Bump `bcprov-jdk15to18` to `1.73` - Introduce `OpenPgpv6Fingerprint` class diff --git a/README.md b/README.md index 2a91789e..14ff388d 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.4.4' + implementation 'org.pgpainless:pgpainless-core:1.5.0' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 3aef6d05..b4e755dd 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless-SOP -[![Spec Revision: 4](https://img.shields.io/badge/Spec%20Revision-4-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) +[![Spec Revision: 6](https://img.shields.io/badge/Spec%20Revision-6-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) [![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-sop)](https://search.maven.org/artifact/org.pgpainless/pgpainless-sop) [![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-sop/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-sop) @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.4.4" + implementation "org.pgpainless:pgpainless-sop:1.5.0" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.4.4 + 1.5.0 ... diff --git a/version.gradle b/version.gradle index cbd64528..86239e88 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.4.5' - isSnapshot = true + shortVersion = '1.5.0' + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.73' From 36a52a3e34c76287b0a37ee22c70b8ec57657180 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 16:37:37 +0200 Subject: [PATCH 0702/1204] PGPainless 1.5.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 86239e88..3d271a75 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.5.0' - isSnapshot = false + shortVersion = '1.5.1' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.73' From 9a0b60ac7e45e2f6cae2c6d88e334507348e17d1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 17:41:02 +0200 Subject: [PATCH 0703/1204] Update quickstart document --- docs/source/pgpainless-sop/quickstart.md | 40 +++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/docs/source/pgpainless-sop/quickstart.md b/docs/source/pgpainless-sop/quickstart.md index 10ef0a72..60e1df29 100644 --- a/docs/source/pgpainless-sop/quickstart.md +++ b/docs/source/pgpainless-sop/quickstart.md @@ -75,10 +75,21 @@ In both cases, the resulting output will be the UTF8 encoded, ASCII armored Open To disable ASCII armoring, call `noArmor()` before calling `generate()`. -At the time of writing, the resulting OpenPGP secret key will consist of a certification-capable 256-bits +Revision `05` of the Stateless OpenPGP Protocol specification introduced the concept of profiles for +certain operations. +The key generation feature is the first operation to make use of profiles to specify different key algorithms. +To set a profile, simply call `profile(String profileName)` and pass in one of the available profile identifiers. + +To explore, which profiles are available, refer to the dedicated [section](#explore-profiles). + +The default profile used by `pgpainless-sop` is called `draft-koch-eddsa-for-openpgp-00`. +If this profile is used, the resulting OpenPGP secret key will consist of a certification-capable 256-bits ed25519 EdDSA primary key, a 256-bits ed25519 EdDSA subkey used for signing, as well as a 256-bits X25519 ECDH subkey for encryption. +Another profile defined by `pgpainless-sop` is `rfc4880`, which changes the key generation behaviour such that +the resulting key is a single 4096-bit RSA key capable of certifying, signing and encrypting. + The whole key does not have an expiration date set. ### Extract a Certificate @@ -186,6 +197,13 @@ If any keys used for signing are password protected, you need to provide the sig It does not matter in which order signing keys and key passwords are provided, the implementation will figure out matches on its own. If different key passwords are used, the `withKeyPassword(_)` method can be called multiple times. +You can modify the behaviour of the encrypt operation by switching between different profiles via the +`profile(String profileName)` method. +At the time of writing, the only available profile for this operation is `rfc4880` which applies encryption +as defined in [rfc4880](https://datatracker.ietf.org/doc/html/rfc4880). + +To explore, which profiles are available, refer to the dedicated [section](#explore-profiles). + By default, the encrypted message will be ASCII armored. To disable ASCII armor, call `noArmor()` before the `plaintext(_)` method call. @@ -464,3 +482,23 @@ By default, the signatures output will be ASCII armored. This can be disabled by prior to `message(_)`. The detached signatures can now be verified like in the section above. + +### Explore Profiles + +Certain operations allow modification of their behaviour by selecting between different profiles. +An example for this is the `generateKey()` operation, where different profiles result in different algorithms used +during key generation. + +To explore, which profiles are supported by a certain operation, you can use the `listProfiles()` operation. +For example, this is how you can get a list of profiles supported by the `generateKey()` operation: + +```java +List profiles = sop.listProfiles().subcommand("generate-key"); +``` + +:::{note} +As you can see, the argument passed into the `subcommand()` method must match the operation name as defined in the +[Stateless OpenPGP Protocol specification](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/). +::: + +At the time of writing (the latest revision of the SOP spec is 06), only `generate-key` and `encrypt` accept profiles. \ No newline at end of file From 05968533a5b64005a38702b7e2fdf787c59265ba Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 18:35:47 +0200 Subject: [PATCH 0704/1204] InlineVerifyImpl: Export signature mode in Verification result --- .../RoundTripInlineSignInlineVerifyCmdTest.java | 2 +- .../org/pgpainless/sop/InlineVerifyImpl.java | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index d36ee58f..0676f213 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -409,7 +409,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { assertEquals("Hello, World!\n", out.toString()); String ver = readStringFromFile(verifications); assertEquals( - "2022-11-18T14:55:33Z 7A073EDF273C902796D259528FBDD36D01831673 AEA0FD2C899D3FC077815F0026560D2AE53DB86F\n", ver); + "2022-11-18T14:55:33Z 7A073EDF273C902796D259528FBDD36D01831673 AEA0FD2C899D3FC077815F0026560D2AE53DB86F mode:binary\n", ver); } @Test diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 7665a7bb..eea5ebfb 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -13,6 +13,7 @@ import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; @@ -23,6 +24,7 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MissingDecryptionMethodException; import sop.ReadyWithResult; import sop.Verification; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; import sop.operation.InlineVerify; @@ -96,6 +98,19 @@ public class InlineVerifyImpl implements InlineVerify { private Verification map(SignatureVerification sigVerification) { return new Verification(sigVerification.getSignature().getCreationTime(), sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString()); + sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString(), + getMode(sigVerification.getSignature()), + null); + } + + private static SignatureMode getMode(PGPSignature signature) { + if (signature.getSignatureType() == PGPSignature.BINARY_DOCUMENT) { + return SignatureMode.binary; + } + if (signature.getSignatureType() == PGPSignature.CANONICAL_TEXT_DOCUMENT) { + return SignatureMode.text; + } + + return null; } } From 2ec176e9388e281f98171b89008e84d0f0cb125c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 18:39:52 +0200 Subject: [PATCH 0705/1204] DetachedVerifyImpl: Export signature mode in Verification result --- .../cli/commands/InlineDetachCmdTest.java | 4 ++-- .../commands/RoundTripSignVerifyCmdTest.java | 4 ++-- .../org/pgpainless/sop/DetachedVerifyImpl.java | 17 ++++++++++++++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java index 8854d837..19bc9aa5 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/InlineDetachCmdTest.java @@ -90,7 +90,7 @@ public class InlineDetachCmdTest extends CLITest { pipeStringToStdin(msgOut.toString()); ByteArrayOutputStream verifyOut = pipeStdoutToStream(); assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), certFile.getAbsolutePath())); - assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C\n", + assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C mode:text\n", verifyOut.toString()); } @@ -115,7 +115,7 @@ public class InlineDetachCmdTest extends CLITest { ByteArrayOutputStream verifyOut = pipeStdoutToStream(); File certFile = writeFile("cert.asc", CERT); assertSuccess(executeCommand("verify", sigFile.getAbsolutePath(), certFile.getAbsolutePath())); - assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C\n", + assertEquals("2021-05-15T16:08:06Z 4F665C4DC2C4660BC6425E415736E6931ACF370C 4F665C4DC2C4660BC6425E415736E6931ACF370C mode:text\n", verifyOut.toString()); } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java index 6196a847..97bfae7e 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java @@ -94,7 +94,7 @@ public class RoundTripSignVerifyCmdTest extends CLITest { "=VWAZ\n" + "-----END PGP SIGNATURE-----"; private static final String BINARY_SIG_VERIFICATION = - "2022-11-09T18:40:24Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E\n"; + "2022-11-09T18:40:24Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:binary\n"; private static final String TEXT_SIG = "-----BEGIN PGP SIGNATURE-----\n" + "Version: PGPainless\n" + "\n" + @@ -104,7 +104,7 @@ public class RoundTripSignVerifyCmdTest extends CLITest { "=s5xn\n" + "-----END PGP SIGNATURE-----"; private static final String TEXT_SIG_VERIFICATION = - "2022-11-09T18:41:18Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E\n"; + "2022-11-09T18:41:18Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:text\n"; private static final Date TEXT_SIG_CREATION = UTCUtil.parseUTCDate("2022-11-09T18:41:18Z"); @Test diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index 93ad398c..f0cb1161 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -12,6 +12,7 @@ import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; @@ -20,6 +21,7 @@ import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.exception.MalformedOpenPgpMessageException; import sop.Verification; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; import sop.operation.DetachedVerify; @@ -94,6 +96,19 @@ public class DetachedVerifyImpl implements DetachedVerify { private Verification map(SignatureVerification sigVerification) { return new Verification(sigVerification.getSignature().getCreationTime(), sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString()); + sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString(), + getMode(sigVerification.getSignature()), + null); + } + + private static SignatureMode getMode(PGPSignature signature) { + if (signature.getSignatureType() == PGPSignature.BINARY_DOCUMENT) { + return SignatureMode.binary; + } + if (signature.getSignatureType() == PGPSignature.CANONICAL_TEXT_DOCUMENT) { + return SignatureMode.text; + } + + return null; } } From b0974c6ade4569c9f7342884d45d1a80a212e5c4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 18:40:25 +0200 Subject: [PATCH 0706/1204] Add more tests for inline-sign-verify roundtrips --- .../sop/InlineSignVerifyRoundtripTest.java | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java index b24b729c..e5ce518b 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java @@ -9,13 +9,14 @@ import sop.ByteArrayAndResult; import sop.SOP; import sop.Verification; import sop.enums.InlineSignAs; +import sop.enums.SignatureMode; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertEquals; public class InlineSignVerifyRoundtripTest { @@ -46,7 +47,11 @@ public class InlineSignVerifyRoundtripTest { byte[] verified = result.getBytes(); - assertFalse(result.getResult().isEmpty()); + List verificationList = result.getResult(); + assertEquals(1, verificationList.size()); + Verification verification = verificationList.get(0); + assertEquals(SignatureMode.text, verification.getSignatureMode()); + assertArrayEquals(message, verified); } @@ -65,6 +70,7 @@ public class InlineSignVerifyRoundtripTest { byte[] inlineSigned = sop.inlineSign() .key(key) .withKeyPassword("sw0rdf1sh") + .mode(InlineSignAs.binary) .data(message).getBytes(); ByteArrayAndResult> result = sop.inlineVerify() @@ -74,7 +80,45 @@ public class InlineSignVerifyRoundtripTest { byte[] verified = result.getBytes(); - assertFalse(result.getResult().isEmpty()); + List verificationList = result.getResult(); + assertEquals(1, verificationList.size()); + Verification verification = verificationList.get(0); + assertEquals(SignatureMode.binary, verification.getSignatureMode()); + + assertArrayEquals(message, verified); + } + + + @Test + public void testInlineSignAndVerifyWithTextSignatures() throws IOException { + byte[] key = sop.generateKey() + .userId("Mark") + .withKeyPassword("y3110w5ubm4r1n3") + .generate().getBytes(); + + byte[] cert = sop.extractCert() + .key(key).getBytes(); + + byte[] message = "Give me a plaintext that I can sign and verify, pls.".getBytes(StandardCharsets.UTF_8); + + byte[] inlineSigned = sop.inlineSign() + .key(key) + .withKeyPassword("y3110w5ubm4r1n3") + .mode(InlineSignAs.text) + .data(message).getBytes(); + + ByteArrayAndResult> result = sop.inlineVerify() + .cert(cert) + .data(inlineSigned) + .toByteArrayAndResult(); + + byte[] verified = result.getBytes(); + + List verificationList = result.getResult(); + assertEquals(1, verificationList.size()); + Verification verification = verificationList.get(0); + assertEquals(SignatureMode.text, verification.getSignatureMode()); + assertArrayEquals(message, verified); } From 06c924d41dff1a358ea07eb9ddefe4b1984c6185 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 18:43:56 +0200 Subject: [PATCH 0707/1204] Add tests for mode to DetachedSignTest --- .../org/pgpainless/sop/DetachedSignTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java index c6fcc267..4f274691 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java @@ -23,6 +23,7 @@ import org.pgpainless.signature.SignatureUtils; import sop.SOP; import sop.Verification; import sop.enums.SignAs; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; public class DetachedSignTest { @@ -49,6 +50,7 @@ public class DetachedSignTest { public void signArmored() throws IOException { byte[] signature = sop.sign() .key(key) + .mode(SignAs.Binary) .data(data) .toByteArrayAndResult().getBytes(); @@ -62,6 +64,7 @@ public class DetachedSignTest { .data(data); assertEquals(1, verifications.size()); + assertEquals(SignatureMode.binary, verifications.get(0).getSignatureMode()); } @Test @@ -84,6 +87,26 @@ public class DetachedSignTest { assertEquals(1, verifications.size()); } + @Test + public void textSig() throws IOException { + byte[] signature = sop.sign() + .key(key) + .noArmor() + .mode(SignAs.Text) + .data(data) + .toByteArrayAndResult().getBytes(); + + List verifications = sop.verify() + .cert(cert) + .notAfter(new Date(new Date().getTime() + 10000)) + .notBefore(new Date(new Date().getTime() - 10000)) + .signatures(signature) + .data(data); + + assertEquals(1, verifications.size()); + assertEquals(SignatureMode.text, verifications.get(0).getSignatureMode()); + } + @Test public void rejectSignatureAsTooOld() throws IOException { byte[] signature = sop.sign() From e3bacdbe35cef9aa7bf0d36d08fe7399747d5478 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 18:53:50 +0200 Subject: [PATCH 0708/1204] Introduce VerificationHelper class and export signature mode in decrypt operation --- .../RoundTripEncryptDecryptCmdTest.java | 4 +- .../java/org/pgpainless/sop/DecryptImpl.java | 8 +-- .../pgpainless/sop/DetachedVerifyImpl.java | 23 +------- .../org/pgpainless/sop/InlineVerifyImpl.java | 23 +------- .../pgpainless/sop/VerificationHelper.java | 52 +++++++++++++++++++ 5 files changed, 57 insertions(+), 53 deletions(-) create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java index d314de20..8c294e59 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java @@ -129,7 +129,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { String romeosVerif = readStringFromFile(anotherVerificationsFile); assertEquals(julietsVerif, romeosVerif); assertFalse(julietsVerif.isEmpty()); - assertEquals(103, julietsVerif.length()); // 103 is number of symbols in [DATE, FINGER, FINGER] for V4 + assertEquals(115, julietsVerif.length()); // 115 is number of symbols in [DATE, FINGER, FINGER, MODE] for V4 } @Test @@ -274,7 +274,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { assertEquals(plaintext, out.toString()); String verificationString = readStringFromFile(verifications); - assertEquals("2022-11-09T17:22:48Z C0DCEC44B1A173664B05DABCECD0BF863F65C9A5 A2EC077FC977E15DD799EFF92C0D3C123CF51C08\n", + assertEquals("2022-11-09T17:22:48Z C0DCEC44B1A173664B05DABCECD0BF863F65C9A5 A2EC077FC977E15DD799EFF92C0D3C123CF51C08 mode:binary\n", verificationString); } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index f7876799..d15713ca 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -147,7 +147,7 @@ public class DecryptImpl implements Decrypt { List verificationList = new ArrayList<>(); for (SignatureVerification signatureVerification : metadata.getVerifiedInlineSignatures()) { - verificationList.add(map(signatureVerification)); + verificationList.add(VerificationHelper.mapVerification(signatureVerification)); } SessionKey sessionKey = null; @@ -163,10 +163,4 @@ public class DecryptImpl implements Decrypt { } }; } - - private Verification map(SignatureVerification sigVerification) { - return new Verification(sigVerification.getSignature().getCreationTime(), - sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString()); - } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index f0cb1161..cdae0215 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -12,7 +12,6 @@ import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; @@ -21,7 +20,6 @@ import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.exception.MalformedOpenPgpMessageException; import sop.Verification; -import sop.enums.SignatureMode; import sop.exception.SOPGPException; import sop.operation.DetachedVerify; @@ -78,7 +76,7 @@ public class DetachedVerifyImpl implements DetachedVerify { List verificationList = new ArrayList<>(); for (SignatureVerification signatureVerification : metadata.getVerifiedDetachedSignatures()) { - verificationList.add(map(signatureVerification)); + verificationList.add(VerificationHelper.mapVerification(signatureVerification)); } if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { @@ -92,23 +90,4 @@ public class DetachedVerifyImpl implements DetachedVerify { throw new SOPGPException.BadData(e); } } - - private Verification map(SignatureVerification sigVerification) { - return new Verification(sigVerification.getSignature().getCreationTime(), - sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString(), - getMode(sigVerification.getSignature()), - null); - } - - private static SignatureMode getMode(PGPSignature signature) { - if (signature.getSignatureType() == PGPSignature.BINARY_DOCUMENT) { - return SignatureMode.binary; - } - if (signature.getSignatureType() == PGPSignature.CANONICAL_TEXT_DOCUMENT) { - return SignatureMode.text; - } - - return null; - } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index eea5ebfb..aecb891b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -13,7 +13,6 @@ import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; @@ -24,7 +23,6 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MissingDecryptionMethodException; import sop.ReadyWithResult; import sop.Verification; -import sop.enums.SignatureMode; import sop.exception.SOPGPException; import sop.operation.InlineVerify; @@ -76,7 +74,7 @@ public class InlineVerifyImpl implements InlineVerify { metadata.getVerifiedInlineSignatures(); for (SignatureVerification signatureVerification : verifications) { - verificationList.add(map(signatureVerification)); + verificationList.add(VerificationHelper.mapVerification(signatureVerification)); } if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { @@ -94,23 +92,4 @@ public class InlineVerifyImpl implements InlineVerify { } }; } - - private Verification map(SignatureVerification sigVerification) { - return new Verification(sigVerification.getSignature().getCreationTime(), - sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString(), - getMode(sigVerification.getSignature()), - null); - } - - private static SignatureMode getMode(PGPSignature signature) { - if (signature.getSignatureType() == PGPSignature.BINARY_DOCUMENT) { - return SignatureMode.binary; - } - if (signature.getSignatureType() == PGPSignature.CANONICAL_TEXT_DOCUMENT) { - return SignatureMode.text; - } - - return null; - } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java new file mode 100644 index 00000000..126a5e3b --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.decryption_verification.SignatureVerification; +import sop.Verification; +import sop.enums.SignatureMode; + +/** + * Helper class for shared methods related to {@link Verification Verifications}. + */ +public class VerificationHelper { + + /** + * Map a {@link SignatureVerification} object to a {@link Verification}. + * + * @param sigVerification signature verification + * @return verification + */ + public static Verification mapVerification(SignatureVerification sigVerification) { + return new Verification( + sigVerification.getSignature().getCreationTime(), + sigVerification.getSigningKey().getSubkeyFingerprint().toString(), + sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString(), + getMode(sigVerification.getSignature()), + null); + } + + /** + * Map an OpenPGP signature type to a {@link SignatureMode} enum. + * Note: This method only maps {@link PGPSignature#BINARY_DOCUMENT} and {@link PGPSignature#CANONICAL_TEXT_DOCUMENT}. + * Other values are mapped to
      null
      . + * + * @param signature signature + * @return signature mode enum or null + */ + private static SignatureMode getMode(PGPSignature signature) { + + if (signature.getSignatureType() == PGPSignature.BINARY_DOCUMENT) { + return SignatureMode.binary; + } + + if (signature.getSignatureType() == PGPSignature.CANONICAL_TEXT_DOCUMENT) { + return SignatureMode.text; + } + + return null; + } +} From d5f3dc80bca5706c7381e8163f42a0584bde6fb7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 19:00:33 +0200 Subject: [PATCH 0709/1204] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51f7a815..893638c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.5.1-SNAPSHOT +- SOP: Emit signature `mode:{binary|text}` in `Verification` results + ## 1.5.0 - Bump `bcpg-jdk15to18` to `1.73` - Bump `bcprov-jdk15to18` to `1.73` From d10841c57a0a76a2092ef7f7ab5d9436d2fcf51c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 24 Apr 2023 16:13:11 +0200 Subject: [PATCH 0710/1204] Add workflow for pull requests --- .github/workflows/pr.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/pr.yml diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 00000000..cc1ca4e0 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2021 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Build + +on: + pull_request: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + - name: Build, Check and Coverage + uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 + with: + arguments: check jacocoRootReport From 0cb088525193ce845ee4cb51d66ac7c21513ddc1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Apr 2023 13:28:07 +0200 Subject: [PATCH 0711/1204] Relax constraints on decryption keys to improve interop with faulty, broken legacy clients that have been very naughty and need punishment --- .../OpenPgpMessageInputStream.java | 9 ++-- .../org/pgpainless/key/info/KeyRingInfo.java | 46 +++++++++++++++++++ ...ntDecryptionUsingNonEncryptionKeyTest.java | 18 ++++++++ 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 7fe11bbf..19a01fbf 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -43,15 +43,14 @@ import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; import org.bouncycastle.util.io.TeeInputStream; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.OpenPgpPacket; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; +import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.decryption_verification.syntax_check.InputSymbol; import org.pgpainless.decryption_verification.syntax_check.PDA; import org.pgpainless.decryption_verification.syntax_check.StackSymbol; -import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; -import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MessageNotIntegrityProtectedException; import org.pgpainless.exception.MissingDecryptionMethodException; @@ -674,7 +673,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - for (PGPPublicKey publicKey : info.getEncryptionSubkeys(EncryptionPurpose.ANY)) { + for (PGPPublicKey publicKey : info.getDecryptionSubkeys()) { if (publicKey.getAlgorithm() == algorithm && info.isSecretKeyAvailable(publicKey.getKeyID())) { PGPSecretKey candidate = secretKeys.getSecretKey(publicKey.getKeyID()); decryptionKeyCandidates.add(new Tuple<>(secretKeys, candidate)); @@ -692,7 +691,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } KeyRingInfo info = new KeyRingInfo(secretKeys, policy, new Date()); - List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); + List encryptionKeys = info.getDecryptionSubkeys(); for (PGPPublicKey key : encryptionKeys) { if (key.getKeyID() == keyID) { return secretKeys; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 1ebd023e..75d9e16c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -41,11 +41,15 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.exception.KeyException; import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.util.KeyIdUtil; import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.consumer.SignaturePicker; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; +import org.pgpainless.util.DateUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Utility class to quickly extract certain information from a {@link PGPPublicKeyRing}/{@link PGPSecretKeyRing}. @@ -55,6 +59,8 @@ public class KeyRingInfo { private static final Pattern PATTERN_EMAIL_FROM_USERID = Pattern.compile("<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)>"); private static final Pattern PATTERN_EMAIL_EXPLICIT = Pattern.compile("^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)$"); + private static final Logger LOGGER = LoggerFactory.getLogger(KeyRingInfo.class); + private final PGPKeyRing keys; private final Signatures signatures; private final Date referenceDate; @@ -904,6 +910,7 @@ public class KeyRingInfo { public @Nonnull List getEncryptionSubkeys(EncryptionPurpose purpose) { Date primaryExpiration = getPrimaryKeyExpirationDate(); if (primaryExpiration != null && primaryExpiration.before(referenceDate)) { + LOGGER.debug("Certificate is expired: Primary key is expired on " + DateUtil.formatUTCDate(primaryExpiration)); return Collections.emptyList(); } @@ -913,15 +920,18 @@ public class KeyRingInfo { PGPPublicKey subKey = subkeys.next(); if (!isKeyValidlyBound(subKey.getKeyID())) { + LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is not validly bound."); continue; } Date subkeyExpiration = getSubkeyExpirationDate(OpenPgpFingerprint.of(subKey)); if (subkeyExpiration != null && subkeyExpiration.before(referenceDate)) { + LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is expired on " + DateUtil.formatUTCDate(subkeyExpiration)); continue; } if (!subKey.isEncryptionKey()) { + LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " algorithm is not capable of encryption."); continue; } @@ -947,6 +957,42 @@ public class KeyRingInfo { return encryptionKeys; } + /** + * Return a list of all subkeys that could potentially be used to decrypt a message. + * Contrary to {@link #getEncryptionSubkeys(EncryptionPurpose)}, this method also includes revoked, expired keys, + * as well as keys which do not carry any encryption keyflags. + * Merely keys which use algorithms that cannot be used for encryption at all are excluded. + * That way, decryption of messages produced by faulty implementations can still be decrypted. + * + * @return decryption keys + */ + public @Nonnull List getDecryptionSubkeys() { + Iterator subkeys = keys.getPublicKeys(); + List decryptionKeys = new ArrayList<>(); + + while (subkeys.hasNext()) { + PGPPublicKey subKey = subkeys.next(); + + // subkeys have been valid at some point + if (subKey.getKeyID() != getKeyId()) { + PGPSignature binding = signatures.subkeyBindings.get(subKey.getKeyID()); + if (binding == null) { + LOGGER.debug("Subkey " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " was never validly bound."); + continue; + } + } + + // Public-Key algorithm can encrypt + if (!subKey.isEncryptionKey()) { + LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is not encryption-capable."); + continue; + } + + decryptionKeys.add(subKey); + } + return decryptionKeys; + } + /** * Return a list of all keys which carry the provided key flag in their signature. * diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java index ba80c69d..04a98265 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java @@ -14,6 +14,7 @@ import java.nio.charset.StandardCharsets; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.exception.MissingDecryptionMethodException; @@ -189,6 +190,23 @@ public class PreventDecryptionUsingNonEncryptionKeyTest { } @Test + public void canDecryptMessageDespiteMissingKeyFlag() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_INCAPABLE_KEY); + + ByteArrayInputStream msgIn = new ByteArrayInputStream(MSG.getBytes(StandardCharsets.UTF_8)); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(msgIn) + .withOptions(new ConsumerOptions().addDecryptionKey(secretKeys)); + + Streams.drain(decryptionStream); + decryptionStream.close(); + OpenPgpMetadata metadata = decryptionStream.getResult(); + + assertEquals(new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID()), metadata.getDecryptionKey()); + } + + @Test + @Disabled public void nonEncryptionKeyCannotDecrypt() throws IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_INCAPABLE_KEY); From 699381238c97089b4c9e3e27cb9c091ed687c654 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Apr 2023 13:28:38 +0200 Subject: [PATCH 0712/1204] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 893638c3..22e452e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ SPDX-License-Identifier: CC0-1.0 ## 1.5.1-SNAPSHOT - SOP: Emit signature `mode:{binary|text}` in `Verification` results +- core: Relax constraints on decryption subkeys to improve interoperability with broken clients + - Allow decryption with revoked keys + - Allow decryption with expired keys + - Allow decryption with erroneously addressed keys without encryption key flags ## 1.5.0 - Bump `bcpg-jdk15to18` to `1.73` From bf2bb31b711d14ca872f6d6d6f49034f98540797 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Apr 2023 13:36:48 +0200 Subject: [PATCH 0713/1204] PGPainless 1.5.1 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e452e6..d59bf372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.5.1-SNAPSHOT +## 1.5.1 - SOP: Emit signature `mode:{binary|text}` in `Verification` results - core: Relax constraints on decryption subkeys to improve interoperability with broken clients - Allow decryption with revoked keys diff --git a/README.md b/README.md index 14ff388d..06e9f2ed 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.5.0' + implementation 'org.pgpainless:pgpainless-core:1.5.1' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index b4e755dd..7ae0a63c 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.5.0" + implementation "org.pgpainless:pgpainless-sop:1.5.1" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.5.0 + 1.5.1 ... diff --git a/version.gradle b/version.gradle index 3d271a75..88c3babd 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.5.1' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.73' From 23fd630670e1d734d22effad5843a5e9ada49b49 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Apr 2023 13:39:44 +0200 Subject: [PATCH 0714/1204] PGPainless 1.5.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 88c3babd..c53ce511 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.5.1' - isSnapshot = false + shortVersion = '1.5.2' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.73' From eb1ff27a900e56d97872fd0b167dbb2b07f3ac2c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 15:15:42 +0200 Subject: [PATCH 0715/1204] Bump sop-java to 6.1.0 --- .../commands/RoundTripSignVerifyCmdTest.java | 11 ++++++- .../sop/CarolKeySignEncryptRoundtripTest.java | 10 +++--- .../org/pgpainless/sop/DetachedSignTest.java | 14 +++++--- .../sop/EncryptDecryptRoundTripTest.java | 31 ++++++++++++------ .../org/pgpainless/sop/InlineDetachTest.java | 24 ++++++++------ .../sop/InlineSignVerifyRoundtripTest.java | 32 +++++++++---------- version.gradle | 2 +- 7 files changed, 79 insertions(+), 45 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java index 97bfae7e..0ff83144 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java @@ -13,6 +13,7 @@ import java.io.File; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.text.ParseException; import java.util.Date; import org.bouncycastle.openpgp.PGPException; @@ -105,7 +106,15 @@ public class RoundTripSignVerifyCmdTest extends CLITest { "-----END PGP SIGNATURE-----"; private static final String TEXT_SIG_VERIFICATION = "2022-11-09T18:41:18Z 444C10AB011EF8424C83F0A9DA9F413986211DC6 9DA09423C9F94BA4CCA30951099B11BF296A373E mode:text\n"; - private static final Date TEXT_SIG_CREATION = UTCUtil.parseUTCDate("2022-11-09T18:41:18Z"); + private static final Date TEXT_SIG_CREATION; + + static { + try { + TEXT_SIG_CREATION = UTCUtil.parseUTCDate("2022-11-09T18:41:18Z"); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } @Test public void createArmoredSignature() throws IOException { diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java index 81bf7645..58dfaa62 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java @@ -4,15 +4,15 @@ package org.pgpainless.sop; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + import java.io.IOException; import org.junit.jupiter.api.Test; import sop.ByteArrayAndResult; import sop.DecryptionResult; import sop.Ready; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; +import sop.testsuite.assertions.VerificationListAssert; public class CarolKeySignEncryptRoundtripTest { @@ -290,6 +290,8 @@ public class CarolKeySignEncryptRoundtripTest { byte[] plaintext = decryption.getBytes(); assertArrayEquals(msg, plaintext); - assertEquals(1, decryption.getResult().getVerifications().size()); + VerificationListAssert.assertThatVerificationList(decryption.getResult().getVerifications()) + .hasSingleItem() + .issuedBy("71FFDA004409E5DDB0C3E8F19BA789DC76D6849A", "71FFDA004409E5DDB0C3E8F19BA789DC76D6849A"); } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java index 4f274691..316c4a37 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java @@ -25,6 +25,7 @@ import sop.Verification; import sop.enums.SignAs; import sop.enums.SignatureMode; import sop.exception.SOPGPException; +import sop.testsuite.assertions.VerificationListAssert; public class DetachedSignTest { @@ -63,8 +64,9 @@ public class DetachedSignTest { .signatures(signature) .data(data); - assertEquals(1, verifications.size()); - assertEquals(SignatureMode.binary, verifications.get(0).getSignatureMode()); + VerificationListAssert.assertThatVerificationList(verifications) + .hasSingleItem() + .hasMode(SignatureMode.binary); } @Test @@ -84,7 +86,8 @@ public class DetachedSignTest { .signatures(signature) .data(data); - assertEquals(1, verifications.size()); + VerificationListAssert.assertThatVerificationList(verifications) + .hasSingleItem(); } @Test @@ -103,8 +106,9 @@ public class DetachedSignTest { .signatures(signature) .data(data); - assertEquals(1, verifications.size()); - assertEquals(SignatureMode.text, verifications.get(0).getSignatureMode()); + VerificationListAssert.assertThatVerificationList(verifications) + .hasSingleItem() + .hasMode(SignatureMode.text); } @Test diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java index 22af5b70..f51b711d 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -21,7 +21,9 @@ import sop.ByteArrayAndResult; import sop.DecryptionResult; import sop.SOP; import sop.SessionKey; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; +import sop.testsuite.assertions.VerificationListAssert; public class EncryptDecryptRoundTripTest { @@ -75,7 +77,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted.toByteArray()); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(1, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .hasSingleItem(); } @Test @@ -106,7 +109,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(1, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .hasSingleItem(); } @Test @@ -125,7 +129,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(0, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -144,7 +149,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(0, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -163,7 +169,8 @@ public class EncryptDecryptRoundTripTest { assertArrayEquals(message, decrypted); DecryptionResult result = bytesAndResult.getResult(); - assertEquals(0, result.getVerifications().size()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -180,7 +187,8 @@ public class EncryptDecryptRoundTripTest { .toByteArrayAndResult() .getResult(); - assertTrue(result.getVerifications().isEmpty()); + VerificationListAssert.assertThatVerificationList(result.getVerifications()) + .isEmpty(); } @Test @@ -486,14 +494,19 @@ public class EncryptDecryptRoundTripTest { sop.decrypt().withKey(key).verifyWithCert(cert).ciphertext(ciphertext).toByteArrayAndResult(); assertEquals(sessionKey, bytesAndResult.getResult().getSessionKey().get().toString()); assertArrayEquals(plaintext, bytesAndResult.getBytes()); - assertEquals(1, bytesAndResult.getResult().getVerifications().size()); - + VerificationListAssert.assertThatVerificationList(bytesAndResult.getResult().getVerifications()) + .hasSingleItem() + .issuedBy("9C26EFAB1C6500A228E8A9C2658EE420C824D191") + .hasMode(SignatureMode.binary); // Decrypt with session key bytesAndResult = sop.decrypt().withSessionKey(SessionKey.fromString(sessionKey)) .verifyWithCert(cert).ciphertext(ciphertext).toByteArrayAndResult(); assertEquals(sessionKey, bytesAndResult.getResult().getSessionKey().get().toString()); assertArrayEquals(plaintext, bytesAndResult.getBytes()); - assertEquals(1, bytesAndResult.getResult().getVerifications().size()); + VerificationListAssert.assertThatVerificationList(bytesAndResult.getResult().getVerifications()) + .hasSingleItem() + .issuedBy("9C26EFAB1C6500A228E8A9C2658EE420C824D191") + .hasMode(SignatureMode.binary); } @Test diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java index 1054babb..98279e4f 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java @@ -5,8 +5,6 @@ package org.pgpainless.sop; 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 java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -34,7 +32,9 @@ import sop.SOP; import sop.Signatures; import sop.Verification; import sop.enums.InlineSignAs; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; +import sop.testsuite.assertions.VerificationListAssert; public class InlineDetachTest { @@ -79,9 +79,11 @@ public class InlineDetachTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); - assertEquals(1, verificationList.size()); - assertEquals(new OpenPgpV4Fingerprint(secretKey).toString(), verificationList.get(0).getSigningCertFingerprint()); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .issuedBy(new OpenPgpV4Fingerprint(secretKey).toString()) + .hasMode(SignatureMode.text); + assertArrayEquals(data, message); } @@ -121,8 +123,10 @@ public class InlineDetachTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); - assertEquals(1, verificationList.size()); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.binary); + assertArrayEquals(data, message); } @@ -191,8 +195,10 @@ public class InlineDetachTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); - assertEquals(1, verificationList.size()); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.binary); + assertArrayEquals(data, message); } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java index e5ce518b..f3a50fc3 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineSignVerifyRoundtripTest.java @@ -4,19 +4,19 @@ package org.pgpainless.sop; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + import org.junit.jupiter.api.Test; import sop.ByteArrayAndResult; import sop.SOP; import sop.Verification; import sop.enums.InlineSignAs; import sop.enums.SignatureMode; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; +import sop.testsuite.assertions.VerificationListAssert; public class InlineSignVerifyRoundtripTest { @@ -48,9 +48,9 @@ public class InlineSignVerifyRoundtripTest { byte[] verified = result.getBytes(); List verificationList = result.getResult(); - assertEquals(1, verificationList.size()); - Verification verification = verificationList.get(0); - assertEquals(SignatureMode.text, verification.getSignatureMode()); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.text); assertArrayEquals(message, verified); } @@ -81,9 +81,9 @@ public class InlineSignVerifyRoundtripTest { byte[] verified = result.getBytes(); List verificationList = result.getResult(); - assertEquals(1, verificationList.size()); - Verification verification = verificationList.get(0); - assertEquals(SignatureMode.binary, verification.getSignatureMode()); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.binary); assertArrayEquals(message, verified); } @@ -115,9 +115,9 @@ public class InlineSignVerifyRoundtripTest { byte[] verified = result.getBytes(); List verificationList = result.getResult(); - assertEquals(1, verificationList.size()); - Verification verification = verificationList.get(0); - assertEquals(SignatureMode.text, verification.getSignatureMode()); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .hasMode(SignatureMode.text); assertArrayEquals(message, verified); } diff --git a/version.gradle b/version.gradle index c53ce511..ce7eebc7 100644 --- a/version.gradle +++ b/version.gradle @@ -14,6 +14,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '6.0.0' + sopJavaVersion = '6.1.0' } } From f51685c1266a17ae55bdd209ce1515c042e9f8c6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 15:16:37 +0200 Subject: [PATCH 0716/1204] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d59bf372..506ded3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.5.2-SNAPSHOT +- Bump `sop-java` to `6.1.0` + ## 1.5.1 - SOP: Emit signature `mode:{binary|text}` in `Verification` results - core: Relax constraints on decryption subkeys to improve interoperability with broken clients From eb45dee04fdeb9634e78cde1c766b2bef8bec9e9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 15:22:43 +0200 Subject: [PATCH 0717/1204] rewriteManPages script: Remind to run Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0. You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins. See https://docs.gradle.org/7.5.1/userguide/command_line_interface.html#sec:command_line_warnings in sop repo --- pgpainless-cli/rewriteManPages.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-cli/rewriteManPages.sh b/pgpainless-cli/rewriteManPages.sh index 321dbdde..51d2aa04 100755 --- a/pgpainless-cli/rewriteManPages.sh +++ b/pgpainless-cli/rewriteManPages.sh @@ -4,7 +4,7 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) SOP_DIR=$(realpath $SCRIPT_DIR/../../sop-java) [ ! -d "$SOP_DIR" ] && echo "sop-java repository MUST be cloned next to pgpainless repo" && exit 1; SRC_DIR=$SOP_DIR/sop-java-picocli/build/docs/manpage -[ ! -d "$SRC_DIR" ] && echo "No sop manpages found." && exit 1; +[ ! -d "$SRC_DIR" ] && echo "No sop manpages found. Please run `gradle asciidoctor` in the sop-java repo." && exit 1; DEST_DIR=$SCRIPT_DIR/packaging/man mkdir -p $DEST_DIR From 558036c485d6afbade4380ba5aaddf68ada25906 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 15:23:37 +0200 Subject: [PATCH 0718/1204] Update man pages --- .../packaging/man/pgpainless-cli-decrypt.1 | 36 ++++----------- .../packaging/man/pgpainless-cli-encrypt.1 | 12 +++-- .../man/pgpainless-cli-generate-completion.1 | 10 ++++ .../man/pgpainless-cli-generate-key.1 | 9 +++- .../packaging/man/pgpainless-cli-help.1 | 10 ++++ .../man/pgpainless-cli-inline-sign.1 | 9 ++-- .../man/pgpainless-cli-list-profiles.1 | 46 +++++++++++++++++++ .../packaging/man/pgpainless-cli-sign.1 | 4 +- .../packaging/man/pgpainless-cli-verify.1 | 13 +++++- .../packaging/man/pgpainless-cli-version.1 | 7 ++- pgpainless-cli/packaging/man/pgpainless-cli.1 | 15 ++++++ 11 files changed, 129 insertions(+), 42 deletions(-) create mode 100644 pgpainless-cli/packaging/man/pgpainless-cli-list-profiles.1 diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 index 17d59134..258078aa 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 @@ -30,41 +30,21 @@ pgpainless\-cli\-decrypt \- Decrypt a message from standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] -[\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP] [\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP] -[\fB\-\-verify\-with\fP=\fICERT\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... -[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP]... -[\fIKEY\fP...] +\fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP] +[\fB\-\-verify\-not\-after\fP=\fIDATE\fP] [\fB\-\-verify\-not\-before\fP=\fIDATE\fP] +[\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP] [\fB\-\-verify\-with\fP=\fICERT\fP]... +[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]... +[\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP]... [\fIKEY\fP...] .SH "DESCRIPTION" .SH "OPTIONS" .sp -\fB\-\-not\-after\fP=\fIDATE\fP -.RS 4 -ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) -.sp -Reject signatures with a creation date not in range. -.sp -Defaults to current system time (\(aqnow\(aq). -.sp -Accepts special value \(aq\-\(aq for end of time. -.RE -.sp -\fB\-\-not\-before\fP=\fIDATE\fP -.RS 4 -ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) -.sp -Reject signatures with a creation date not in range. -.sp -Defaults to beginning of time (\(aq\-\(aq). -.RE -.sp \fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP .RS 4 Can be used to learn the session key on successful decryption .RE .sp -\fB\-\-stacktrace\fP, \fB\-\-verify\-out, \-\-verifications\-out\fP=\fIVERIFICATIONS\fP +\fB\-\-stacktrace\fP, \fB\-\-verify\-not\-after\fP=\fIDATE\fP, \fB\-\-verify\-not\-before\fP=\fIDATE\fP, \fB\-\-verify\-out, \-\-verifications\-out\fP=\fIVERIFICATIONS\fP .RS 4 Emits signature verification status to the designated output .RE @@ -87,7 +67,7 @@ Symmetric passphrase to decrypt the message with. .sp Enables decryption based on any "SKESK" packets in the "CIPHERTEXT". .sp -Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). .RE .sp \fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP @@ -96,7 +76,7 @@ Symmetric message key (session key). .sp Enables decryption of the "CIPHERTEXT" using the session key directly against the "SEIPD" packet. .sp -Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). .RE .SH "ARGUMENTS" .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 index f1d804d0..79002302 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 @@ -31,8 +31,9 @@ pgpainless\-cli\-encrypt \- Encrypt a message from standard input .SH "SYNOPSIS" .sp \fBpgpainless\-cli encrypt\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP] -[\fB\-\-sign\-with\fP=\fIKEY\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... -[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fICERTS\fP...] +[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-sign\-with\fP=\fIKEY\fP]... +[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]... +[\fICERTS\fP...] .SH "DESCRIPTION" .SH "OPTIONS" @@ -47,6 +48,11 @@ Type of the input data. Defaults to \(aqbinary\(aq ASCII armor the output .RE .sp +\fB\-\-profile\fP=\fIPROFILE\fP +.RS 4 +Profile identifier to switch between profiles +.RE +.sp \fB\-\-sign\-with\fP=\fIKEY\fP .RS 4 Sign the output with a private key @@ -63,7 +69,7 @@ Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). .RS 4 Encrypt the message with a password. .sp -Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) +Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). .RE .SH "ARGUMENTS" .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 index 5ab3d673..5c50ee96 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-completion.1 @@ -152,4 +152,14 @@ Ambiguous input (a filename matching the designator already exists) \fB79\fP .RS 4 Key is not signing capable +.RE +.sp +\fB83\fP +.RS 4 +Options were supplied that are incompatible with each other +.RE +.sp +\fB89\fP +.RS 4 +The requested profile is unsupported, or the indicated subcommand does not accept profiles .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 index 96b069f3..a5317665 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-generate-key.1 @@ -30,8 +30,8 @@ pgpainless\-cli\-generate\-key \- Generate a secret key .SH "SYNOPSIS" .sp -\fBpgpainless\-cli generate\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP] -[\fIUSERID\fP...] +\fBpgpainless\-cli generate\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-profile\fP=\fIPROFILE\fP] +[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP] [\fIUSERID\fP...] .SH "DESCRIPTION" .SH "OPTIONS" @@ -41,6 +41,11 @@ pgpainless\-cli\-generate\-key \- Generate a secret key ASCII armor the output .RE .sp +\fB\-\-profile\fP=\fIPROFILE\fP +.RS 4 +Profile identifier to switch between profiles +.RE +.sp \fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP .RS 4 Password to protect the private key with diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-help.1 b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 index 6152fc87..1e7c2b08 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-help.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-help.1 @@ -147,4 +147,14 @@ Ambiguous input (a filename matching the designator already exists) \fB79\fP .RS 4 Key is not signing capable +.RE +.sp +\fB83\fP +.RS 4 +Options were supplied that are incompatible with each other +.RE +.sp +\fB89\fP +.RS 4 +The requested profile is unsupported, or the indicated subcommand does not accept profiles .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 index 7deb568c..db041c0c 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-inline-sign.1 @@ -30,20 +30,19 @@ pgpainless\-cli\-inline\-sign \- Create an inline\-signed message from data on standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli inline\-sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP= -\fI{binary|text|cleartextsigned}\fP] +\fBpgpainless\-cli inline\-sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text|clearsigned}\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fIKEYS\fP...] .SH "DESCRIPTION" .SH "OPTIONS" .sp -\fB\-\-as\fP=\fI{binary|text|cleartextsigned}\fP +\fB\-\-as\fP=\fI{binary|text|clearsigned}\fP .RS 4 -Specify the signature format of the signed message +Specify the signature format of the signed message. .sp \(aqtext\(aq and \(aqbinary\(aq will produce inline\-signed messages. .sp -\(aqcleartextsigned\(aq will make use of the cleartext signature framework. +\(aqclearsigned\(aq will make use of the cleartext signature framework. .sp Defaults to \(aqbinary\(aq. .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-list-profiles.1 b/pgpainless-cli/packaging/man/pgpainless-cli-list-profiles.1 new file mode 100644 index 00000000..9bcfa17f --- /dev/null +++ b/pgpainless-cli/packaging/man/pgpainless-cli-list-profiles.1 @@ -0,0 +1,46 @@ +'\" t +.\" Title: pgpainless-cli-list-profiles +.\" Author: [see the "AUTHOR(S)" section] +.\" Generator: Asciidoctor 2.0.10 +.\" Manual: PGPainless-CLI Manual +.\" Source: +.\" Language: English +.\" +.TH "PGPAINLESS\-CLI\-LIST\-PROFILES" "1" "" "" "PGPainless\-CLI Manual" +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.ss \n[.ss] 0 +.nh +.ad l +.de URL +\fI\\$2\fP <\\$1>\\$3 +.. +.als MTO URL +.if \n[.g] \{\ +. mso www.tmac +. am URL +. ad l +. . +. am MTO +. ad l +. . +. LINKSTYLE blue R < > +.\} +.SH "NAME" +pgpainless\-cli\-list\-profiles \- Emit a list of profiles supported by the identified subcommand +.SH "SYNOPSIS" +.sp +\fBpgpainless\-cli list\-profiles\fP [\fB\-\-stacktrace\fP] \fICOMMAND\fP +.SH "DESCRIPTION" + +.SH "OPTIONS" +.sp +\fB\-\-stacktrace\fP +.RS 4 +.RE +.SH "ARGUMENTS" +.sp +\fICOMMAND\fP +.RS 4 +Subcommand for which to list profiles +.RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 index 6519e0ec..5bb22e90 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-sign.1 @@ -38,7 +38,7 @@ pgpainless\-cli\-sign \- Create a detached signature on the data from standard i .sp \fB\-\-as\fP=\fI{binary|text}\fP .RS 4 -Specify the output format of the signed message +Specify the output format of the signed message. .sp Defaults to \(aqbinary\(aq. .sp @@ -47,7 +47,7 @@ If \(aq\-\-as=text\(aq and the input data is not valid UTF\-8, sign fails with r .sp \fB\-\-micalg\-out\fP=\fIMICALG\fP .RS 4 -Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content\-Type (RFC3156) +Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content\-Type (RFC3156). .RE .sp \fB\-\-[no\-]armor\fP diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 index 5cf0020c..714064f6 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-verify.1 @@ -36,7 +36,18 @@ pgpainless\-cli\-verify \- Verify a detached signature over the data from standa .SH "OPTIONS" .sp -\fB\-\-not\-after\fP=\fIDATE\fP, \fB\-\-not\-before\fP=\fIDATE\fP +\fB\-\-not\-after\fP=\fIDATE\fP +.RS 4 +ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) +.sp +Reject signatures with a creation date not in range. +.sp +Defaults to current system time ("now"). +.sp +Accepts special value "\-" for end of time. +.RE +.sp +\fB\-\-not\-before\fP=\fIDATE\fP .RS 4 ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) .sp diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 index 003e549f..f1bea312 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 @@ -30,7 +30,7 @@ pgpainless\-cli\-version \- Display version information about the tool .SH "SYNOPSIS" .sp -\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP] +\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\fP] .SH "DESCRIPTION" .SH "OPTIONS" @@ -45,6 +45,11 @@ Print information about the cryptographic backend Print an extended version string .RE .sp +\fB\-\-pgpainless\-cli\-spec\fP +.RS 4 +Print the latest revision of the SOP specification targeted by the implementation +.RE +.sp \fB\-\-stacktrace\fP .RS 4 .RE \ No newline at end of file diff --git a/pgpainless-cli/packaging/man/pgpainless-cli.1 b/pgpainless-cli/packaging/man/pgpainless-cli.1 index 686f728f..e5cc8129 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli.1 @@ -101,6 +101,11 @@ Create an inline\-signed message from data on standard input Verify inline\-signed data from standard input .RE .sp +\fBlist\-profiles\fP +.RS 4 +Emit a list of profiles supported by the identified subcommand +.RE +.sp \fBversion\fP .RS 4 Display version information about the tool @@ -205,4 +210,14 @@ Ambiguous input (a filename matching the designator already exists) \fB79\fP .RS 4 Key is not signing capable +.RE +.sp +\fB83\fP +.RS 4 +Options were supplied that are incompatible with each other +.RE +.sp +\fB89\fP +.RS 4 +The requested profile is unsupported, or the indicated subcommand does not accept profiles .RE \ No newline at end of file From 52fa7e4d46331bacb6839b73012d612d147d6bfa Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 1 May 2023 09:35:28 +0200 Subject: [PATCH 0719/1204] OpenPgpMessageInputStream: Return -1 instead of throwing MalformedOpenPgpMessageException when calling read() on drained stream --- .../OpenPgpMessageInputStream.java | 2 ++ .../syntax_check/OpenPgpMessageSyntax.java | 4 ++++ .../OpenPgpMessageInputStreamTest.java | 17 +++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 19a01fbf..d51a6cf7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -744,6 +744,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throws IOException { if (nestedInputStream == null) { if (packetInputStream != null) { + syntaxVerifier.next(InputSymbol.EndOfSequence); syntaxVerifier.assertValid(); } return -1; @@ -774,6 +775,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { super.close(); if (closed) { if (packetInputStream != null) { + syntaxVerifier.next(InputSymbol.EndOfSequence); syntaxVerifier.assertValid(); } return; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java index 6abb507a..9d20e0a8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -132,6 +132,10 @@ public class OpenPgpMessageSyntax implements Syntax { @Nonnull Transition fromValid(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) throws MalformedOpenPgpMessageException { + if (input == InputSymbol.EndOfSequence) { + // allow subsequent read() calls. + return new Transition(State.Valid); + } // There is no applicable transition rule out of Valid throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index 01966bbe..a0ec7c25 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -34,6 +34,7 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.io.Streams; import org.junit.JUtils; import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -652,6 +653,22 @@ public class OpenPgpMessageInputStreamTest { assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); } + @Test + public void readAfterCloseTest() throws PGPException, IOException { + OpenPgpMessageInputStream pgpIn = get(SENC_LIT, ConsumerOptions.get() + .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + Streams.drain(pgpIn); // read all + + byte[] buf = new byte[1024]; + assertEquals(-1, pgpIn.read(buf)); + assertEquals(-1, pgpIn.read()); + assertEquals(-1, pgpIn.read(buf)); + assertEquals(-1, pgpIn.read()); + + pgpIn.close(); + pgpIn.getMetadata(); + } + private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) throws PGPException, IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); From 2e730a2c4873b01940bee213dd4a4d8390f2d223 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 1 May 2023 10:10:50 +0200 Subject: [PATCH 0720/1204] Update changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 506ded3a..59868c3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,11 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.5.2-SNAPSHOT +## 1.5.2-rc1 - Bump `sop-java` to `6.1.0` +- Normalize `OpenPgpMessageInputStream.read()` behaviour when reading past the stream + - Instead of throwing a `MalformedOpenPgpMessageException` which could throw off unsuspecting parsers, + we now simply return `-1` like every other `InputStream`. ## 1.5.1 - SOP: Emit signature `mode:{binary|text}` in `Verification` results From de5926fc472976b7a9fe24f0568364c51204c7f8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 1 May 2023 10:12:37 +0200 Subject: [PATCH 0721/1204] PGPainless 1.5.2-rc1 --- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 06e9f2ed..903d21b8 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.5.1' + implementation 'org.pgpainless:pgpainless-core:1.5.2-rc1' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 7ae0a63c..a082f09c 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.5.1" + implementation "org.pgpainless:pgpainless-sop:1.5.2-rc1" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.5.1 + 1.5.2-rc1 ... diff --git a/version.gradle b/version.gradle index ce7eebc7..46a173b3 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.5.2' - isSnapshot = true + shortVersion = '1.5.2-rc1' + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.73' From 671d45a9116e36a3124610e735c3fb122968923b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 1 May 2023 10:15:24 +0200 Subject: [PATCH 0722/1204] PGPainless 1.5.2-rc2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 46a173b3..564100d4 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.5.2-rc1' - isSnapshot = false + shortVersion = '1.5.2-rc2' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.73' From 9c81137f4884c9d401dbc0435d40ba872dcb704e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 13:51:34 +0200 Subject: [PATCH 0723/1204] Add template methods to generate RSA keys with primary and subkeys --- .../key/generation/KeyRingTemplates.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java index 42eb7efa..07f2235f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java @@ -26,6 +26,78 @@ public final class KeyRingTemplates { } + /** + * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, + * a dedicated RSA subkey used for signing and a third RSA subkey used for encryption. + * + * @param userId userId or null + * @param length length of the RSA keys + * @return key + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error + */ + public PGPSecretKeyRing rsaKeyRing(@Nullable CharSequence userId, + @Nonnull RsaLength length) + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + return rsaKeyRing(userId, length, Passphrase.emptyPassphrase()); + } + + /** + * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, + * a dedicated RSA subkey used for signing and a third RSA subkey used for encryption. + * + * @param userId userId or null + * @param length length of the RSA keys + * @param password passphrase to encrypt the key with + * @return key + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error + */ + public PGPSecretKeyRing rsaKeyRing(@Nullable CharSequence userId, + @Nonnull RsaLength length, + @Nonnull String password) + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + Passphrase passphrase = Passphrase.emptyPassphrase(); + if (!isNullOrEmpty(password)) { + passphrase = Passphrase.fromPassword(password); + } + return rsaKeyRing(userId, length, passphrase); + } + + /** + * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, + * a dedicated RSA subkey used for signing and a third RSA subkey used for encryption. + * + * @param userId userId or null + * @param length length of the RSA keys + * @param passphrase passphrase to encrypt the key with + * @return key + * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters + * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider + * @throws PGPException in case of an OpenPGP related error + */ + public PGPSecretKeyRing rsaKeyRing(@Nullable CharSequence userId, + @Nonnull RsaLength length, + @Nonnull Passphrase passphrase) + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + KeyRingBuilder builder = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)); + + if (userId != null) { + builder.addUserId(userId.toString()); + } + + if (!passphrase.isEmpty()) { + builder.setPassphrase(passphrase); + } + + return builder.build(); + } + /** * Creates a simple, unencrypted RSA KeyPair of length {@code length} with user-id {@code userId}. * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. From 8869d9bd783ead32c70725f7dd1cd17d070f085f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 13:51:59 +0200 Subject: [PATCH 0724/1204] Simplify key template methods by replacing String and UserID args with CharSequence --- .../key/generation/KeyRingTemplates.java | 113 +++--------------- 1 file changed, 19 insertions(+), 94 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java index 07f2235f..6966232b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java @@ -17,7 +17,6 @@ import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa.EdDSACurve; import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.generation.type.xdh.XDHSpec; -import org.pgpainless.key.util.UserId; import org.pgpainless.util.Passphrase; public final class KeyRingTemplates { @@ -111,46 +110,20 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable UserId userId, @Nonnull RsaLength length) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId == null ? null : userId.toString(), length); - } - - /** - * Creates a simple, unencrypted RSA KeyPair of length {@code length} with user-id {@code userId}. - * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. - * - * @param userId user id. - * @param length length in bits. - * - * @return {@link PGPSecretKeyRing} containing the KeyPair. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length) + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable CharSequence userId, @Nonnull RsaLength length) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { return simpleRsaKeyRing(userId, length, Passphrase.emptyPassphrase()); } - /** - * Creates a simple RSA KeyPair of length {@code length} with user-id {@code userId}. - * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. - * - * @param userId user id. - * @param length length in bits. - * @param password Password of the key. Can be null for unencrypted keys. - * - * @return {@link PGPSecretKeyRing} containing the KeyPair. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable UserId userId, @Nonnull RsaLength length, @Nullable String password) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId == null ? null : userId.toString(), length, password); + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable CharSequence userId, @Nonnull RsaLength length, @Nonnull Passphrase passphrase) + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + KeyRingBuilder builder = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) + .setPassphrase(passphrase); + if (userId != null) { + builder.addUserId(userId.toString()); + } + return builder.build(); } /** @@ -167,7 +140,7 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length, @Nullable String password) + public PGPSecretKeyRing simpleRsaKeyRing(@Nullable CharSequence userId, @Nonnull RsaLength length, @Nullable String password) throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { Passphrase passphrase = Passphrase.emptyPassphrase(); if (!isNullOrEmpty(password)) { @@ -176,17 +149,6 @@ public final class KeyRingTemplates { return simpleRsaKeyRing(userId, length, passphrase); } - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable String userId, @Nonnull RsaLength length, @Nonnull Passphrase passphrase) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - KeyRingBuilder builder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) - .setPassphrase(passphrase); - if (userId != null) { - builder.addUserId(userId); - } - return builder.build(); - } - /** * Creates a key ring consisting of an ed25519 EdDSA primary key and a curve25519 XDH subkey. * The EdDSA primary key is used for signing messages and certifying the sub key. @@ -200,48 +162,11 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleEcKeyRing(@Nullable UserId userId) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId == null ? null : userId.toString()); - } - - /** - * Creates a key ring consisting of an ed25519 EdDSA primary key and a curve25519 XDH subkey. - * The EdDSA primary key is used for signing messages and certifying the sub key. - * The XDH subkey is used for encryption and decryption of messages. - * - * @param userId user-id - * - * @return {@link PGPSecretKeyRing} containing the key pairs. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable CharSequence userId) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { return simpleEcKeyRing(userId, Passphrase.emptyPassphrase()); } - /** - * Creates a key ring consisting of an ed25519 EdDSA primary key and a curve25519 XDH subkey. - * The EdDSA primary key is used for signing messages and certifying the sub key. - * The XDH subkey is used for encryption and decryption of messages. - * - * @param userId user-id - * @param password Password of the private key. Can be null for an unencrypted key. - * - * @return {@link PGPSecretKeyRing} containing the key pairs. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleEcKeyRing(@Nullable UserId userId, String password) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId == null ? null : userId.toString(), password); - } - /** * Creates a key ring consisting of an ed25519 EdDSA primary key and a X25519 XDH subkey. * The EdDSA primary key is used for signing messages and certifying the sub key. @@ -256,7 +181,7 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId, String password) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable CharSequence userId, String password) throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { Passphrase passphrase = Passphrase.emptyPassphrase(); if (!isNullOrEmpty(password)) { @@ -265,14 +190,14 @@ public final class KeyRingTemplates { return simpleEcKeyRing(userId, passphrase); } - public PGPSecretKeyRing simpleEcKeyRing(@Nullable String userId, @Nonnull Passphrase passphrase) + public PGPSecretKeyRing simpleEcKeyRing(@Nullable CharSequence userId, @Nonnull Passphrase passphrase) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { KeyRingBuilder builder = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) .setPassphrase(passphrase); if (userId != null) { - builder.addUserId(userId); + builder.addUserId(userId.toString()); } return builder.build(); } @@ -288,7 +213,7 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing modernKeyRing(@Nullable String userId) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public PGPSecretKeyRing modernKeyRing(@Nullable CharSequence userId) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { return modernKeyRing(userId, Passphrase.emptyPassphrase()); } @@ -304,13 +229,13 @@ public final class KeyRingTemplates { * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider * @throws PGPException in case of an OpenPGP related error */ - public PGPSecretKeyRing modernKeyRing(@Nullable String userId, @Nullable String password) + public PGPSecretKeyRing modernKeyRing(@Nullable CharSequence userId, @Nullable String password) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { Passphrase passphrase = (password != null ? Passphrase.fromPassword(password) : Passphrase.emptyPassphrase()); return modernKeyRing(userId, passphrase); } - public PGPSecretKeyRing modernKeyRing(@Nullable String userId, @Nonnull Passphrase passphrase) + public PGPSecretKeyRing modernKeyRing(@Nullable CharSequence userId, @Nonnull Passphrase passphrase) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { KeyRingBuilder builder = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) @@ -318,7 +243,7 @@ public final class KeyRingTemplates { .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) .setPassphrase(passphrase); if (userId != null) { - builder.addUserId(userId); + builder.addUserId(userId.toString()); } return builder.build(); } From a8ab93a49a3faff3f0c9bd7d3074e53fa0f46cee Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 14:07:33 +0200 Subject: [PATCH 0725/1204] SOP: GenerateKey with --profile=rfc4880 now generates RSA key with subkeys --- .../src/main/java/org/pgpainless/sop/GenerateKeyImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index ba788dac..f86d2893 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -126,7 +126,7 @@ public class GenerateKeyImpl implements GenerateKey { // RSA 4096 else if (profile.equals(RSA4096_PROFILE.getName())) { key = PGPainless.generateKeyRing() - .simpleRsaKeyRing(primaryUserId, RsaLength._4096, passphrase); + .rsaKeyRing(primaryUserId, RsaLength._4096, passphrase); } else { // Missing else-if branch for profile. Oops. From 15f6cc70b1979882a0ccb0b68db4c0c32e534a86 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 14:30:08 +0200 Subject: [PATCH 0726/1204] Add MessageMetadata.getRecipientKeyIds() Fixes #376 --- .../decryption_verification/MessageMetadata.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 648b99ab..cc97e81a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -92,6 +92,21 @@ public class MessageMetadata { return false; } + /** + * Return a list containing all recipient keyIDs. + * + * @return list of recipients + */ + public List getRecipientKeyIds() { + List keyIds = new ArrayList<>(); + Iterator encLayers = getEncryptionLayers(); + while (encLayers.hasNext()) { + EncryptedData layer = encLayers.next(); + keyIds.addAll(layer.getRecipients()); + } + return keyIds; + } + public @Nonnull Iterator getEncryptionLayers() { return new LayerIterator(message) { @Override From 304350fe5c7b771fb6ab3f8a0dd17cbbd5efd0c2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 14:38:38 +0200 Subject: [PATCH 0727/1204] Add p-tags to EncryptionOptions javadoc --- .../pgpainless/encryption_signing/EncryptionOptions.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index b5baee83..2d0fb156 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -33,7 +33,7 @@ import org.pgpainless.util.Passphrase; /** * Options for the encryption process. * This class can be used to set encryption parameters, like encryption keys and passphrases, algorithms etc. - * + *

      * A typical use might look like follows: *

        * {@code
      @@ -42,11 +42,11 @@ import org.pgpainless.util.Passphrase;
        * opt.addPassphrase(Passphrase.fromPassword("AdditionalDecryptionPassphrase123"));
        * }
        * 
      - * + *

      * To use a custom symmetric encryption algorithm, use {@link #overrideEncryptionAlgorithm(SymmetricKeyAlgorithm)}. * This will cause PGPainless to use the provided algorithm for message encryption, instead of negotiating an algorithm * by inspecting the provided recipient keys. - * + *

      * By default, PGPainless will encrypt to all suitable, encryption capable subkeys on each recipient's certificate. * This behavior can be changed per recipient, e.g. by calling *

      @@ -83,7 +83,7 @@ public class EncryptionOptions {
            * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys
            * which carry either the {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS} or
            * {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE} flag.
      -     *
      +     * 

      * Use this if you are not sure. * * @return encryption options From 64c6d7a90409c57837e47cb5ee1eb4f95c73a5a4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 14:38:52 +0200 Subject: [PATCH 0728/1204] Annotate EncryptionOptions methods with @Nonnull --- .../encryption_signing/EncryptionOptions.java | 34 +++++++++++-------- .../EncryptionOptionsTest.java | 6 ++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java index 2d0fb156..63320853 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -75,7 +75,7 @@ public class EncryptionOptions { this(EncryptionPurpose.ANY); } - public EncryptionOptions(EncryptionPurpose purpose) { + public EncryptionOptions(@Nonnull EncryptionPurpose purpose) { this.purpose = purpose; } @@ -118,7 +118,7 @@ public class EncryptionOptions { * @param keys keys * @return this */ - public EncryptionOptions addRecipients(Iterable keys) { + public EncryptionOptions addRecipients(@Nonnull Iterable keys) { if (!keys.iterator().hasNext()) { throw new IllegalArgumentException("Set of recipient keys cannot be empty."); } @@ -154,7 +154,7 @@ public class EncryptionOptions { * @param userId user id * @return this */ - public EncryptionOptions addRecipient(PGPPublicKeyRing key, String userId) { + public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, @Nonnull CharSequence userId) { return addRecipient(key, userId, encryptionKeySelector); } @@ -167,11 +167,13 @@ public class EncryptionOptions { * @param encryptionKeySelectionStrategy strategy to select one or more encryption subkeys to encrypt to * @return this */ - public EncryptionOptions addRecipient(PGPPublicKeyRing key, String userId, EncryptionKeySelector encryptionKeySelectionStrategy) { + public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, + @Nonnull CharSequence userId, + @Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) { KeyRingInfo info = new KeyRingInfo(key, new Date()); List encryptionSubkeys = encryptionKeySelectionStrategy - .selectEncryptionSubkeys(info.getEncryptionSubkeys(userId, purpose)); + .selectEncryptionSubkeys(info.getEncryptionSubkeys(userId.toString(), purpose)); if (encryptionSubkeys.isEmpty()) { throw new KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)); } @@ -179,7 +181,7 @@ public class EncryptionOptions { for (PGPPublicKey encryptionSubkey : encryptionSubkeys) { SubkeyIdentifier keyId = new SubkeyIdentifier(key, encryptionSubkey.getKeyID()); keyRingInfo.put(keyId, info); - keyViews.put(keyId, new KeyAccessor.ViaUserId(info, keyId, userId)); + keyViews.put(keyId, new KeyAccessor.ViaUserId(info, keyId, userId.toString())); addRecipientKey(key, encryptionSubkey); } @@ -192,7 +194,7 @@ public class EncryptionOptions { * @param key key ring * @return this */ - public EncryptionOptions addRecipient(PGPPublicKeyRing key) { + public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key) { return addRecipient(key, encryptionKeySelector); } @@ -203,7 +205,8 @@ public class EncryptionOptions { * @param encryptionKeySelectionStrategy strategy used to select one or multiple encryption subkeys. * @return this */ - public EncryptionOptions addRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy) { + public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, + @Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) { Date evaluationDate = new Date(); KeyRingInfo info; info = new KeyRingInfo(key, evaluationDate); @@ -234,7 +237,8 @@ public class EncryptionOptions { return this; } - private void addRecipientKey(PGPPublicKeyRing keyRing, PGPPublicKey key) { + private void addRecipientKey(@Nonnull PGPPublicKeyRing keyRing, + @Nonnull PGPPublicKey key) { encryptionKeys.add(new SubkeyIdentifier(keyRing, key.getKeyID())); PGPKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory .getInstance().getPublicKeyKeyEncryptionMethodGenerator(key); @@ -247,7 +251,7 @@ public class EncryptionOptions { * @param passphrase passphrase * @return this */ - public EncryptionOptions addPassphrase(Passphrase passphrase) { + public EncryptionOptions addPassphrase(@Nonnull Passphrase passphrase) { if (passphrase.isEmpty()) { throw new IllegalArgumentException("Passphrase must not be empty."); } @@ -267,7 +271,7 @@ public class EncryptionOptions { * @param encryptionMethod encryption method * @return this */ - public EncryptionOptions addEncryptionMethod(PGPKeyEncryptionMethodGenerator encryptionMethod) { + public EncryptionOptions addEncryptionMethod(@Nonnull PGPKeyEncryptionMethodGenerator encryptionMethod) { encryptionMethods.add(encryptionMethod); return this; } @@ -303,7 +307,7 @@ public class EncryptionOptions { * @param encryptionAlgorithm encryption algorithm override * @return this */ - public EncryptionOptions overrideEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) { + public EncryptionOptions overrideEncryptionAlgorithm(@Nonnull SymmetricKeyAlgorithm encryptionAlgorithm) { if (encryptionAlgorithm == SymmetricKeyAlgorithm.NULL) { throw new IllegalArgumentException("Plaintext encryption can only be used to denote unencrypted secret keys."); } @@ -322,7 +326,7 @@ public class EncryptionOptions { } public interface EncryptionKeySelector { - List selectEncryptionSubkeys(List encryptionCapableKeys); + List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys); } /** @@ -333,7 +337,7 @@ public class EncryptionOptions { public static EncryptionKeySelector encryptToFirstSubkey() { return new EncryptionKeySelector() { @Override - public List selectEncryptionSubkeys(List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { return encryptionCapableKeys.isEmpty() ? Collections.emptyList() : Collections.singletonList(encryptionCapableKeys.get(0)); } }; @@ -347,7 +351,7 @@ public class EncryptionOptions { public static EncryptionKeySelector encryptToAllCapableSubkeys() { return new EncryptionKeySelector() { @Override - public List selectEncryptionSubkeys(List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { return encryptionCapableKeys; } }; diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 3436ba69..7d2fa453 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -36,6 +36,8 @@ import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.util.Passphrase; +import javax.annotation.Nonnull; + public class EncryptionOptionsTest { private static PGPSecretKeyRing secretKeys; @@ -149,7 +151,7 @@ public class EncryptionOptionsTest { assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, new EncryptionOptions.EncryptionKeySelector() { @Override - public List selectEncryptionSubkeys(List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { return Collections.emptyList(); } })); @@ -157,7 +159,7 @@ public class EncryptionOptionsTest { assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, "test@pgpainless.org", new EncryptionOptions.EncryptionKeySelector() { @Override - public List selectEncryptionSubkeys(List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { return Collections.emptyList(); } })); From 1d26751b45b00519894f91f80dbf5d3bc52b4d04 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 15:59:21 +0200 Subject: [PATCH 0729/1204] Remove unused KeyRingEditorTest --- .../key/modification/KeyRingEditorTest.java | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/modification/KeyRingEditorTest.java diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/KeyRingEditorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/KeyRingEditorTest.java deleted file mode 100644 index 68774cfc..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/KeyRingEditorTest.java +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.modification; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; -import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor; - -public class KeyRingEditorTest { - - @Test - public void testConstructorThrowsNpeForNull() { - assertThrows(NullPointerException.class, - () -> new SecretKeyRingEditor(null)); - } -} From 3b8a1b47d7e2cd24d63a562cd246254159b645b5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 16:03:12 +0200 Subject: [PATCH 0730/1204] Add javadoc p-tags --- .../src/main/java/org/pgpainless/PGPainless.java | 6 +++--- .../encryption_signing/SigningOptions.java | 12 ++++++------ .../java/org/pgpainless/key/info/KeyAccessor.java | 2 +- .../java/org/pgpainless/key/info/KeyRingInfo.java | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 6da77c80..16928210 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -152,7 +152,7 @@ public final class PGPainless { /** * Make changes to a secret key. * This method can be used to change key expiration dates and passphrases, or add/revoke subkeys. - * + *

      * After making the desired changes in the builder, the modified key ring can be extracted using {@link SecretKeyRingEditorInterface#done()}. * * @param secretKeys secret key ring @@ -165,7 +165,7 @@ public final class PGPainless { /** * Make changes to a secret key at the given reference time. * This method can be used to change key expiration dates and passphrases, or add/revoke user-ids and subkeys. - * + *

      * After making the desired changes in the builder, the modified key can be extracted using {@link SecretKeyRingEditorInterface#done()}. * * @param secretKeys secret key ring @@ -179,7 +179,7 @@ public final class PGPainless { /** * Quickly access information about a {@link org.bouncycastle.openpgp.PGPPublicKeyRing} / {@link PGPSecretKeyRing}. * This method can be used to determine expiration dates, key flags and other information about a key. - * + *

      * To evaluate a key at a given date (e.g. to determine if the key was allowed to create a certain signature) * use {@link #inspectKeyRing(PGPKeyRing, Date)} instead. * diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java index 0af07fc9..77f95efb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -158,7 +158,7 @@ public final class SigningOptions { * Add an inline-signature. * Inline signatures are being embedded into the message itself and can be processed in one pass, thanks to the use * of one-pass-signature packets. - * + *

      * This method uses the passed in user-id to select user-specific hash algorithms. * * @param secretKeyDecryptor decryptor to unlock the signing secret key @@ -182,7 +182,7 @@ public final class SigningOptions { * Add an inline-signature. * Inline signatures are being embedded into the message itself and can be processed in one pass, thanks to the use * of one-pass-signature packets. - * + *

      * This method uses the passed in user-id to select user-specific hash algorithms. * * @param secretKeyDecryptor decryptor to unlock the signing secret key @@ -295,7 +295,7 @@ public final class SigningOptions { * Detached signatures are not being added into the PGP message itself. * Instead, they can be distributed separately to the message. * Detached signatures are useful if the data that is being signed shall not be modified (e.g. when signing a file). - * + *

      * This method uses the passed in user-id to select user-specific hash algorithms. * * @param secretKeyDecryptor decryptor to unlock the secret signing key @@ -320,7 +320,7 @@ public final class SigningOptions { * Detached signatures are not being added into the PGP message itself. * Instead, they can be distributed separately to the message. * Detached signatures are useful if the data that is being signed shall not be modified (e.g. when signing a file). - * + *

      * This method uses the passed in user-id to select user-specific hash algorithms. * * @param secretKeyDecryptor decryptor to unlock the secret signing key @@ -406,7 +406,7 @@ public final class SigningOptions { /** * Negotiate, which hash algorithm to use. - * + *

      * This method gives the highest priority to the algorithm override, which can be set via {@link #overrideHashAlgorithm(HashAlgorithm)}. * After that, the signing keys hash algorithm preferences are iterated to find the first acceptable algorithm. * Lastly, should no acceptable algorithm be found, the {@link Policy Policies} default signature hash algorithm is @@ -451,7 +451,7 @@ public final class SigningOptions { /** * Override hash algorithm negotiation by dictating which hash algorithm needs to be used. * If no override has been set, an accetable algorithm will be negotiated instead. - * + *

      * Note: To override the hash algorithm for signing, call this method *before* calling * {@link #addInlineSignature(SecretKeyRingProtector, PGPSecretKeyRing, DocumentSignatureType)} or * {@link #addDetachedSignature(SecretKeyRingProtector, PGPSecretKeyRing, DocumentSignatureType)}. diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java index 5fa71d46..48102931 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java @@ -28,7 +28,7 @@ public abstract class KeyAccessor { /** * Depending on the way we address the key (key-id or user-id), return the respective {@link PGPSignature} * which contains the algorithm preferences we are going to use. - * + *

      * If we address a key via its user-id, we want to rely on the algorithm preferences in the user-id certification, * while we would instead rely on those in the direct-key signature if we'd address the key by key-id. * diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 75d9e16c..45de52f3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -292,7 +292,7 @@ public class KeyRingInfo { /** * Return the current primary user-id of the key ring. - * + *

      * Note: If no user-id is marked as primary key using a {@link PrimaryUserID} packet, * this method returns the first user-id on the key, otherwise null. * @@ -472,7 +472,7 @@ public class KeyRingInfo { /** * Return the latest direct-key self signature. - * + *

      * Note: This signature might be expired (check with {@link SignatureUtils#isSignatureExpired(PGPSignature)}). * * @return latest direct key self-signature or null @@ -782,7 +782,7 @@ public class KeyRingInfo { * Return the latest date on which the key ring is still usable for the given key flag. * If only a subkey is carrying the required flag and the primary key expires earlier than the subkey, * the expiry date of the primary key is returned. - * + *

      * This method might return null, if the primary key and a subkey with the required flag does not expire. * @param use key flag representing the use case, e.g. {@link KeyFlag#SIGN_DATA} or * {@link KeyFlag#ENCRYPT_COMMS}/{@link KeyFlag#ENCRYPT_STORAGE}. @@ -1133,7 +1133,7 @@ public class KeyRingInfo { /** * Returns true, if this {@link KeyRingInfo} is based on a {@link PGPSecretKeyRing}, which has a valid signing key * which is ready to be used (i.e. secret key is present and is not on a smart-card). - * + *

      * If you just want to check, whether a key / certificate has signing capable subkeys, * use {@link #isSigningCapable()} instead. * From 953206b4ed39b692a4c7caa9d9b586382bf81729 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 16:03:50 +0200 Subject: [PATCH 0731/1204] Make more of the API null-safe by using @Nonnull/@Nullable --- .../main/java/org/pgpainless/PGPainless.java | 26 ++- .../OpenPgpMessageInputStream.java | 2 +- .../BcPGPHashContextContentSignerBuilder.java | 9 +- .../encryption_signing/SigningOptions.java | 114 ++++++----- .../org/pgpainless/key/info/KeyAccessor.java | 39 +++- .../org/pgpainless/key/info/KeyRingInfo.java | 183 ++++++++++++------ .../secretkeyring/SecretKeyRingEditor.java | 10 +- .../algorithm/RevocationStateTest.java | 5 - .../key/info/UserIdRevocationTest.java | 1 + .../SignatureSubpacketsUtilTest.java | 3 + ...artyCertificationSignatureBuilderTest.java | 6 +- .../subpackets/SignatureSubpacketsTest.java | 2 +- 12 files changed, 263 insertions(+), 137 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 16928210..3da54177 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -40,6 +40,7 @@ public final class PGPainless { * Generate a fresh OpenPGP key ring from predefined templates. * @return templates */ + @Nonnull public static KeyRingTemplates generateKeyRing() { return new KeyRingTemplates(); } @@ -49,6 +50,7 @@ public final class PGPainless { * * @return builder */ + @Nonnull public static KeyRingBuilder buildKeyRing() { return new KeyRingBuilder(); } @@ -57,6 +59,7 @@ public final class PGPainless { * Read an existing OpenPGP key ring. * @return builder */ + @Nonnull public static KeyRingReader readKeyRing() { return new KeyRingReader(); } @@ -67,6 +70,7 @@ public final class PGPainless { * @param secretKey secret key * @return public key certificate */ + @Nonnull public static PGPPublicKeyRing extractCertificate(@Nonnull PGPSecretKeyRing secretKey) { return KeyRingUtils.publicKeyRingFrom(secretKey); } @@ -79,6 +83,7 @@ public final class PGPainless { * @return merged certificate * @throws PGPException in case of an error */ + @Nonnull public static PGPPublicKeyRing mergeCertificate( @Nonnull PGPPublicKeyRing originalCopy, @Nonnull PGPPublicKeyRing updatedCopy) @@ -94,6 +99,7 @@ public final class PGPainless { * * @throws IOException in case of an error in the {@link ArmoredOutputStream} */ + @Nonnull public static String asciiArmor(@Nonnull PGPKeyRing key) throws IOException { if (key instanceof PGPSecretKeyRing) { @@ -111,6 +117,7 @@ public final class PGPainless { * * @throws IOException in case of an error in the {@link ArmoredOutputStream} */ + @Nonnull public static String asciiArmor(@Nonnull PGPSignature signature) throws IOException { return ArmorUtils.toAsciiArmoredString(signature); @@ -136,6 +143,7 @@ public final class PGPainless { * * @return builder */ + @Nonnull public static EncryptionBuilder encryptAndOrSign() { return new EncryptionBuilder(); } @@ -145,6 +153,7 @@ public final class PGPainless { * * @return builder */ + @Nonnull public static DecryptionBuilder decryptAndOrVerify() { return new DecryptionBuilder(); } @@ -158,8 +167,9 @@ public final class PGPainless { * @param secretKeys secret key ring * @return builder */ - public static SecretKeyRingEditorInterface modifyKeyRing(PGPSecretKeyRing secretKeys) { - return modifyKeyRing(secretKeys, null); + @Nonnull + public static SecretKeyRingEditorInterface modifyKeyRing(@Nonnull PGPSecretKeyRing secretKeys) { + return modifyKeyRing(secretKeys, new Date()); } /** @@ -172,7 +182,9 @@ public final class PGPainless { * @param referenceTime reference time used as signature creation date * @return builder */ - public static SecretKeyRingEditorInterface modifyKeyRing(PGPSecretKeyRing secretKeys, Date referenceTime) { + @Nonnull + public static SecretKeyRingEditorInterface modifyKeyRing(@Nonnull PGPSecretKeyRing secretKeys, + @Nonnull Date referenceTime) { return new SecretKeyRingEditor(secretKeys, referenceTime); } @@ -186,7 +198,8 @@ public final class PGPainless { * @param keyRing key ring * @return access object */ - public static KeyRingInfo inspectKeyRing(PGPKeyRing keyRing) { + @Nonnull + public static KeyRingInfo inspectKeyRing(@Nonnull PGPKeyRing keyRing) { return new KeyRingInfo(keyRing); } @@ -198,7 +211,8 @@ public final class PGPainless { * @param referenceTime date of inspection * @return access object */ - public static KeyRingInfo inspectKeyRing(PGPKeyRing keyRing, Date referenceTime) { + @Nonnull + public static KeyRingInfo inspectKeyRing(@Nonnull PGPKeyRing keyRing, @Nonnull Date referenceTime) { return new KeyRingInfo(keyRing, referenceTime); } @@ -207,6 +221,7 @@ public final class PGPainless { * * @return policy */ + @Nonnull public static Policy getPolicy() { return Policy.getInstance(); } @@ -216,6 +231,7 @@ public final class PGPainless { * * @return builder */ + @Nonnull public static CertifyCertificate certify() { return new CertifyCertificate(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index d51a6cf7..04d823d1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -359,7 +359,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPCompressedData compressedData = packetInputStream.readCompressedData(); // Extract Metadata MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( - CompressionAlgorithm.fromId(compressedData.getAlgorithm()), + CompressionAlgorithm.requireFromId(compressedData.getAlgorithm()), metadata.depth + 1); LOGGER.debug("Compressed Data Packet (" + compressionLayer.algorithm + ") at depth " + metadata.depth + " encountered"); diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java index df6f6ca3..5cdf9e36 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java @@ -44,10 +44,15 @@ class BcPGPHashContextContentSignerBuilder extends PGPHashContextContentSignerBu BcPGPHashContextContentSignerBuilder(MessageDigest messageDigest) { this.messageDigest = messageDigest; - this.hashAlgorithm = HashAlgorithm.fromName(messageDigest.getAlgorithm()); + this.hashAlgorithm = requireFromName(messageDigest.getAlgorithm()); + } + + private static HashAlgorithm requireFromName(String digestName) { + HashAlgorithm hashAlgorithm = HashAlgorithm.fromName(digestName); if (hashAlgorithm == null) { - throw new IllegalArgumentException("Cannot recognize OpenPGP Hash Algorithm: " + messageDigest.getAlgorithm()); + throw new IllegalArgumentException("Cannot recognize OpenPGP Hash Algorithm: " + digestName); } + return hashAlgorithm; } @Override diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java index 77f95efb..a899bd12 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -10,6 +10,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; @@ -46,7 +47,9 @@ public final class SigningOptions { private final boolean detached; private final HashAlgorithm hashAlgorithm; - private SigningMethod(PGPSignatureGenerator signatureGenerator, boolean detached, HashAlgorithm hashAlgorithm) { + private SigningMethod(@Nonnull PGPSignatureGenerator signatureGenerator, + boolean detached, + @Nonnull HashAlgorithm hashAlgorithm) { this.signatureGenerator = signatureGenerator; this.detached = detached; this.hashAlgorithm = hashAlgorithm; @@ -60,7 +63,8 @@ public final class SigningOptions { * @param hashAlgorithm hash algorithm used to generate the signature * @return inline signing method */ - public static SigningMethod inlineSignature(PGPSignatureGenerator signatureGenerator, HashAlgorithm hashAlgorithm) { + public static SigningMethod inlineSignature(@Nonnull PGPSignatureGenerator signatureGenerator, + @Nonnull HashAlgorithm hashAlgorithm) { return new SigningMethod(signatureGenerator, false, hashAlgorithm); } @@ -73,7 +77,8 @@ public final class SigningOptions { * @param hashAlgorithm hash algorithm used to generate the signature * @return detached signing method */ - public static SigningMethod detachedSignature(PGPSignatureGenerator signatureGenerator, HashAlgorithm hashAlgorithm) { + public static SigningMethod detachedSignature(@Nonnull PGPSignatureGenerator signatureGenerator, + @Nonnull HashAlgorithm hashAlgorithm) { return new SigningMethod(signatureGenerator, true, hashAlgorithm); } @@ -93,6 +98,7 @@ public final class SigningOptions { private final Map signingMethods = new HashMap<>(); private HashAlgorithm hashAlgorithmOverride; + @Nonnull public static SigningOptions get() { return new SigningOptions(); } @@ -107,8 +113,9 @@ public final class SigningOptions { * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be unlocked or a signing method cannot be created */ - public SigningOptions addSignature(SecretKeyRingProtector signingKeyProtector, - PGPSecretKeyRing signingKey) + @Nonnull + public SigningOptions addSignature(@Nonnull SecretKeyRingProtector signingKeyProtector, + @Nonnull PGPSecretKeyRing signingKey) throws PGPException { return addInlineSignature(signingKeyProtector, signingKey, DocumentSignatureType.BINARY_DOCUMENT); } @@ -124,9 +131,10 @@ public final class SigningOptions { * @throws KeyException if something is wrong with any of the keys * @throws PGPException if any of the keys cannot be unlocked or a signing method cannot be created */ - public SigningOptions addInlineSignatures(SecretKeyRingProtector secrectKeyDecryptor, - Iterable signingKeys, - DocumentSignatureType signatureType) + @Nonnull + public SigningOptions addInlineSignatures(@Nonnull SecretKeyRingProtector secrectKeyDecryptor, + @Nonnull Iterable signingKeys, + @Nonnull DocumentSignatureType signatureType) throws KeyException, PGPException { for (PGPSecretKeyRing signingKey : signingKeys) { addInlineSignature(secrectKeyDecryptor, signingKey, signatureType); @@ -147,9 +155,10 @@ public final class SigningOptions { * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be unlocked or the signing method cannot be created */ - public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - DocumentSignatureType signatureType) + @Nonnull + public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + @Nonnull DocumentSignatureType signatureType) throws KeyException, PGPException { return addInlineSignature(secretKeyDecryptor, secretKey, null, signatureType); } @@ -170,10 +179,11 @@ public final class SigningOptions { * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be unlocked or the signing method cannot be created */ - public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType) + @Nonnull + public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + @Nullable CharSequence userId, + @Nonnull DocumentSignatureType signatureType) throws KeyException, PGPException { return addInlineSignature(secretKeyDecryptor, secretKey, userId, signatureType, null); } @@ -195,17 +205,18 @@ public final class SigningOptions { * @throws KeyException if the key is invalid * @throws PGPException if the key cannot be unlocked or the signing method cannot be created */ - public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType, + @Nonnull + public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + @Nullable CharSequence userId, + @Nonnull DocumentSignatureType signatureType, @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) throws KeyException, PGPException { KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); if (userId != null && !keyRingInfo.isUserIdValid(userId)) { throw new KeyException.UnboundUserIdException( OpenPgpFingerprint.of(secretKey), - userId, + userId.toString(), keyRingInfo.getLatestUserIdCertification(userId), keyRingInfo.getUserIdRevocation(userId) ); @@ -242,9 +253,10 @@ public final class SigningOptions { * @throws KeyException if something is wrong with any of the keys * @throws PGPException if any of the keys cannot be validated or unlocked, or if any signing method cannot be created */ - public SigningOptions addDetachedSignatures(SecretKeyRingProtector secretKeyDecryptor, - Iterable signingKeys, - DocumentSignatureType signatureType) + @Nonnull + public SigningOptions addDetachedSignatures(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull Iterable signingKeys, + @Nonnull DocumentSignatureType signatureType) throws PGPException { for (PGPSecretKeyRing signingKey : signingKeys) { addDetachedSignature(secretKeyDecryptor, signingKey, signatureType); @@ -263,8 +275,9 @@ public final class SigningOptions { * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created */ - public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing signingKey) + @Nonnull + public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing signingKey) throws PGPException { return addDetachedSignature(secretKeyDecryptor, signingKey, DocumentSignatureType.BINARY_DOCUMENT); } @@ -283,9 +296,10 @@ public final class SigningOptions { * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created */ - public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - DocumentSignatureType signatureType) + @Nonnull + public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + @Nonnull DocumentSignatureType signatureType) throws PGPException { return addDetachedSignature(secretKeyDecryptor, secretKey, null, signatureType); } @@ -307,10 +321,11 @@ public final class SigningOptions { * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created */ - public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType) + @Nonnull + public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + @Nullable CharSequence userId, + @Nonnull DocumentSignatureType signatureType) throws PGPException { return addDetachedSignature(secretKeyDecryptor, secretKey, userId, signatureType, null); } @@ -333,17 +348,18 @@ public final class SigningOptions { * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created */ - public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType, + @Nonnull + public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing secretKey, + @Nullable CharSequence userId, + @Nonnull DocumentSignatureType signatureType, @Nullable BaseSignatureSubpackets.Callback subpacketCallback) throws PGPException { KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); if (userId != null && !keyRingInfo.isUserIdValid(userId)) { throw new KeyException.UnboundUserIdException( OpenPgpFingerprint.of(secretKey), - userId, + userId.toString(), keyRingInfo.getLatestUserIdCertification(userId), keyRingInfo.getUserIdRevocation(userId) ); @@ -369,11 +385,11 @@ public final class SigningOptions { return this; } - private void addSigningMethod(PGPSecretKeyRing secretKey, - PGPPrivateKey signingSubkey, + private void addSigningMethod(@Nonnull PGPSecretKeyRing secretKey, + @Nonnull PGPPrivateKey signingSubkey, @Nullable BaseSignatureSubpackets.Callback subpacketCallback, - HashAlgorithm hashAlgorithm, - DocumentSignatureType signatureType, + @Nonnull HashAlgorithm hashAlgorithm, + @Nonnull DocumentSignatureType signatureType, boolean detached) throws PGPException { SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(secretKey, signingSubkey.getKeyID()); @@ -416,7 +432,9 @@ public final class SigningOptions { * @param policy policy * @return selected hash algorithm */ - private HashAlgorithm negotiateHashAlgorithm(Set preferences, Policy policy) { + @Nonnull + private HashAlgorithm negotiateHashAlgorithm(@Nonnull Set preferences, + @Nonnull Policy policy) { if (hashAlgorithmOverride != null) { return hashAlgorithmOverride; } @@ -425,9 +443,10 @@ public final class SigningOptions { .negotiateHashAlgorithm(preferences); } - private PGPSignatureGenerator createSignatureGenerator(PGPPrivateKey privateKey, - HashAlgorithm hashAlgorithm, - DocumentSignatureType signatureType) + @Nonnull + private PGPSignatureGenerator createSignatureGenerator(@Nonnull PGPPrivateKey privateKey, + @Nonnull HashAlgorithm hashAlgorithm, + @Nonnull DocumentSignatureType signatureType) throws PGPException { int publicKeyAlgorithm = privateKey.getPublicKeyPacket().getAlgorithm(); PGPContentSignerBuilder signerBuilder = ImplementationFactory.getInstance() @@ -444,6 +463,7 @@ public final class SigningOptions { * * @return signing methods */ + @Nonnull Map getSigningMethods() { return Collections.unmodifiableMap(signingMethods); } @@ -459,7 +479,8 @@ public final class SigningOptions { * @param hashAlgorithmOverride override hash algorithm * @return this */ - public SigningOptions overrideHashAlgorithm(HashAlgorithm hashAlgorithmOverride) { + @Nonnull + public SigningOptions overrideHashAlgorithm(@Nonnull HashAlgorithm hashAlgorithmOverride) { this.hashAlgorithmOverride = hashAlgorithmOverride; return this; } @@ -469,6 +490,7 @@ public final class SigningOptions { * * @return hash algorithm override */ + @Nullable public HashAlgorithm getHashAlgorithmOverride() { return hashAlgorithmOverride; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java index 48102931..8ab8a9c4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java @@ -20,7 +20,7 @@ public abstract class KeyAccessor { protected final KeyRingInfo info; protected final SubkeyIdentifier key; - KeyAccessor(KeyRingInfo info, SubkeyIdentifier key) { + KeyAccessor(@Nonnull KeyRingInfo info, @Nonnull SubkeyIdentifier key) { this.info = info; this.key = key; } @@ -34,13 +34,15 @@ public abstract class KeyAccessor { * * @return signature */ - public abstract @Nonnull PGPSignature getSignatureWithPreferences(); + @Nonnull + public abstract PGPSignature getSignatureWithPreferences(); /** * Return preferred symmetric key encryption algorithms. * * @return preferred symmetric algorithms */ + @Nonnull public Set getPreferredSymmetricKeyAlgorithms() { return SignatureSubpacketsUtil.parsePreferredSymmetricKeyAlgorithms(getSignatureWithPreferences()); } @@ -50,6 +52,7 @@ public abstract class KeyAccessor { * * @return preferred hash algorithms */ + @Nonnull public Set getPreferredHashAlgorithms() { return SignatureSubpacketsUtil.parsePreferredHashAlgorithms(getSignatureWithPreferences()); } @@ -59,6 +62,7 @@ public abstract class KeyAccessor { * * @return preferred compression algorithms */ + @Nonnull public Set getPreferredCompressionAlgorithms() { return SignatureSubpacketsUtil.parsePreferredCompressionAlgorithms(getSignatureWithPreferences()); } @@ -78,13 +82,16 @@ public abstract class KeyAccessor { * @param key id of the subkey * @param userId user-id */ - public ViaUserId(KeyRingInfo info, SubkeyIdentifier key, String userId) { + public ViaUserId(@Nonnull KeyRingInfo info, + @Nonnull SubkeyIdentifier key, + @Nonnull String userId) { super(info, key); this.userId = userId; } @Override - public @Nonnull PGPSignature getSignatureWithPreferences() { + @Nonnull + public PGPSignature getSignatureWithPreferences() { PGPSignature signature = info.getLatestUserIdCertification(userId); if (signature != null) { return signature; @@ -104,19 +111,26 @@ public abstract class KeyAccessor { * @param info info about the key at a given date * @param key key-id */ - public ViaKeyId(KeyRingInfo info, SubkeyIdentifier key) { + public ViaKeyId(@Nonnull KeyRingInfo info, + @Nonnull SubkeyIdentifier key) { super(info, key); } @Override - public @Nonnull PGPSignature getSignatureWithPreferences() { + @Nonnull + public PGPSignature getSignatureWithPreferences() { String primaryUserId = info.getPrimaryUserId(); // If the key is located by Key ID, the algorithm of the primary User ID of the key provides the // preferred symmetric algorithm. - PGPSignature signature = info.getLatestUserIdCertification(primaryUserId); + PGPSignature signature = null; + if (primaryUserId != null) { + signature = info.getLatestUserIdCertification(primaryUserId); + } + if (signature == null) { signature = info.getLatestDirectKeySelfSignature(); } + if (signature == null) { throw new IllegalStateException("No valid signature found."); } @@ -126,22 +140,27 @@ public abstract class KeyAccessor { public static class SubKey extends KeyAccessor { - public SubKey(KeyRingInfo info, SubkeyIdentifier key) { + public SubKey(@Nonnull KeyRingInfo info, + @Nonnull SubkeyIdentifier key) { super(info, key); } @Override - public @Nonnull PGPSignature getSignatureWithPreferences() { + @Nonnull + public PGPSignature getSignatureWithPreferences() { PGPSignature signature; if (key.getPrimaryKeyId() == key.getSubkeyId()) { signature = info.getLatestDirectKeySelfSignature(); - if (signature == null) { + if (signature == null && info.getPrimaryUserId() != null) { signature = info.getLatestUserIdCertification(info.getPrimaryUserId()); } } else { signature = info.getCurrentSubkeyBindingSignature(key.getSubkeyId()); } + if (signature == null) { + throw new IllegalStateException("No valid signature found."); + } return signature; } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 45de52f3..1c20a06c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -74,7 +74,9 @@ public class KeyRingInfo { * @param signature signature * @return info of key ring at signature creation time */ - public static KeyRingInfo evaluateForSignature(PGPKeyRing keyRing, PGPSignature signature) { + @Nonnull + public static KeyRingInfo evaluateForSignature(@Nonnull PGPKeyRing keyRing, + @Nonnull PGPSignature signature) { return new KeyRingInfo(keyRing, signature.getCreationTime()); } @@ -83,7 +85,7 @@ public class KeyRingInfo { * * @param keys key ring */ - public KeyRingInfo(PGPKeyRing keys) { + public KeyRingInfo(@Nonnull PGPKeyRing keys) { this(keys, new Date()); } @@ -93,7 +95,8 @@ public class KeyRingInfo { * @param keys key ring * @param referenceDate date of validation */ - public KeyRingInfo(PGPKeyRing keys, Date referenceDate) { + public KeyRingInfo(@Nonnull PGPKeyRing keys, + @Nonnull Date referenceDate) { this(keys, PGPainless.getPolicy(), referenceDate); } @@ -104,14 +107,17 @@ public class KeyRingInfo { * @param policy policy * @param referenceDate validation date */ - public KeyRingInfo(PGPKeyRing keys, Policy policy, Date referenceDate) { - this.referenceDate = referenceDate != null ? referenceDate : new Date(); + public KeyRingInfo(@Nonnull PGPKeyRing keys, + @Nonnull Policy policy, + @Nonnull Date referenceDate) { + this.referenceDate = referenceDate; this.keys = keys; this.signatures = new Signatures(keys, this.referenceDate, policy); this.primaryUserId = findPrimaryUserId(); this.revocationState = findRevocationState(); } + @Nonnull private RevocationState findRevocationState() { PGPSignature revocation = signatures.primaryKeyRevocation; if (revocation != null) { @@ -126,6 +132,7 @@ public class KeyRingInfo { * * @return public key */ + @Nonnull public PGPPublicKey getPublicKey() { return keys.getPublicKey(); } @@ -136,7 +143,8 @@ public class KeyRingInfo { * @param fingerprint fingerprint * @return public key or null */ - public @Nullable PGPPublicKey getPublicKey(OpenPgpFingerprint fingerprint) { + @Nullable + public PGPPublicKey getPublicKey(@Nonnull OpenPgpFingerprint fingerprint) { return getPublicKey(fingerprint.getKeyId()); } @@ -146,7 +154,8 @@ public class KeyRingInfo { * @param keyId key id * @return public key or null */ - public @Nullable PGPPublicKey getPublicKey(long keyId) { + @Nullable + public PGPPublicKey getPublicKey(long keyId) { return getPublicKey(keys, keyId); } @@ -157,7 +166,8 @@ public class KeyRingInfo { * @param keyId key id * @return public key or null */ - public static @Nullable PGPPublicKey getPublicKey(PGPKeyRing keyRing, long keyId) { + @Nullable + public static PGPPublicKey getPublicKey(@Nonnull PGPKeyRing keyRing, long keyId) { return keyRing.getPublicKey(keyId); } @@ -210,6 +220,7 @@ public class KeyRingInfo { * * @return list of public keys */ + @Nonnull public List getPublicKeys() { Iterator iterator = keys.getPublicKeys(); List list = iteratorToList(iterator); @@ -221,7 +232,8 @@ public class KeyRingInfo { * * @return primary secret key or null if the key ring is public */ - public @Nullable PGPSecretKey getSecretKey() { + @Nullable + public PGPSecretKey getSecretKey() { if (keys instanceof PGPSecretKeyRing) { PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) keys; return secretKeys.getSecretKey(); @@ -235,7 +247,8 @@ public class KeyRingInfo { * @param fingerprint fingerprint * @return secret key or null */ - public @Nullable PGPSecretKey getSecretKey(OpenPgpFingerprint fingerprint) { + @Nullable + public PGPSecretKey getSecretKey(@Nonnull OpenPgpFingerprint fingerprint) { return getSecretKey(fingerprint.getKeyId()); } @@ -245,7 +258,8 @@ public class KeyRingInfo { * @param keyId key id * @return secret key or null */ - public @Nullable PGPSecretKey getSecretKey(long keyId) { + @Nullable + public PGPSecretKey getSecretKey(long keyId) { if (keys instanceof PGPSecretKeyRing) { return ((PGPSecretKeyRing) keys).getSecretKey(keyId); } @@ -259,6 +273,7 @@ public class KeyRingInfo { * * @return list of secret keys */ + @Nonnull public List getSecretKeys() { if (keys instanceof PGPSecretKeyRing) { PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) keys; @@ -282,11 +297,13 @@ public class KeyRingInfo { * * @return fingerprint */ + @Nonnull public OpenPgpFingerprint getFingerprint() { return OpenPgpFingerprint.of(getPublicKey()); } - public @Nullable String getPrimaryUserId() { + @Nullable + public String getPrimaryUserId() { return primaryUserId; } @@ -298,6 +315,7 @@ public class KeyRingInfo { * * @return primary user-id or null */ + @Nullable private String findPrimaryUserId() { String primaryUserId = null; Date currentModificationDate = null; @@ -342,10 +360,10 @@ public class KeyRingInfo { * * @return list of user-ids */ + @Nonnull public List getUserIds() { Iterator iterator = getPublicKey().getUserIDs(); - List userIds = iteratorToList(iterator); - return userIds; + return iteratorToList(iterator); } /** @@ -353,6 +371,7 @@ public class KeyRingInfo { * * @return valid user-ids */ + @Nonnull public List getValidUserIds() { List valid = new ArrayList<>(); List userIds = getUserIds(); @@ -369,6 +388,7 @@ public class KeyRingInfo { * * @return bound user-ids */ + @Nonnull public List getValidAndExpiredUserIds() { List probablyExpired = new ArrayList<>(); List userIds = getUserIds(); @@ -407,21 +427,25 @@ public class KeyRingInfo { * @param userId user-id * @return true if user-id is valid */ - public boolean isUserIdValid(String userId) { + public boolean isUserIdValid(@Nonnull CharSequence userId) { + if (primaryUserId == null) { + // No primary userID? No userID at all! + return false; + } + if (!userId.equals(primaryUserId)) { if (!isUserIdBound(primaryUserId)) { - // primary user-id not valid + // primary user-id not valid? UserID not valid! return false; } } return isUserIdBound(userId); } - - private boolean isUserIdBound(String userId) { - - PGPSignature certification = signatures.userIdCertifications.get(userId); - PGPSignature revocation = signatures.userIdRevocations.get(userId); + private boolean isUserIdBound(@Nonnull CharSequence userId) { + String userIdString = userId.toString(); + PGPSignature certification = signatures.userIdCertifications.get(userIdString); + PGPSignature revocation = signatures.userIdRevocations.get(userIdString); if (certification == null) { return false; @@ -453,6 +477,7 @@ public class KeyRingInfo { * * @return email addresses */ + @Nonnull public List getEmailAddresses() { List userIds = getUserIds(); List emails = new ArrayList<>(); @@ -477,7 +502,8 @@ public class KeyRingInfo { * * @return latest direct key self-signature or null */ - public @Nullable PGPSignature getLatestDirectKeySelfSignature() { + @Nullable + public PGPSignature getLatestDirectKeySelfSignature() { return signatures.primaryKeySelfSignature; } @@ -486,7 +512,8 @@ public class KeyRingInfo { * * @return revocation or null */ - public @Nullable PGPSignature getRevocationSelfSignature() { + @Nullable + public PGPSignature getRevocationSelfSignature() { return signatures.primaryKeyRevocation; } @@ -496,8 +523,9 @@ public class KeyRingInfo { * @param userId user-id * @return certification signature or null */ - public @Nullable PGPSignature getLatestUserIdCertification(String userId) { - return signatures.userIdCertifications.get(userId); + @Nullable + public PGPSignature getLatestUserIdCertification(@Nonnull CharSequence userId) { + return signatures.userIdCertifications.get(userId.toString()); } /** @@ -506,8 +534,9 @@ public class KeyRingInfo { * @param userId user-id * @return revocation or null */ - public @Nullable PGPSignature getUserIdRevocation(String userId) { - return signatures.userIdRevocations.get(userId); + @Nullable + public PGPSignature getUserIdRevocation(@Nonnull CharSequence userId) { + return signatures.userIdRevocations.get(userId.toString()); } /** @@ -516,7 +545,8 @@ public class KeyRingInfo { * @param keyId subkey id * @return subkey binding signature or null */ - public @Nullable PGPSignature getCurrentSubkeyBindingSignature(long keyId) { + @Nullable + public PGPSignature getCurrentSubkeyBindingSignature(long keyId) { return signatures.subkeyBindings.get(keyId); } @@ -526,7 +556,8 @@ public class KeyRingInfo { * @param keyId subkey id * @return subkey binding revocation or null */ - public @Nullable PGPSignature getSubkeyRevocationSignature(long keyId) { + @Nullable + public PGPSignature getSubkeyRevocationSignature(long keyId) { return signatures.subkeyRevocations.get(keyId); } @@ -535,7 +566,8 @@ public class KeyRingInfo { * @param keyId key-id * @return list of key flags */ - public @Nonnull List getKeyFlagsOf(long keyId) { + @Nonnull + public List getKeyFlagsOf(long keyId) { // key is primary key if (getPublicKey().getKeyID() == keyId) { @@ -575,7 +607,8 @@ public class KeyRingInfo { * @param userId user-id * @return key flags */ - public @Nonnull List getKeyFlagsOf(String userId) { + @Nonnull + public List getKeyFlagsOf(String userId) { if (!isUserIdValid(userId)) { return Collections.emptyList(); } @@ -607,6 +640,7 @@ public class KeyRingInfo { * * @return creation date */ + @Nonnull public Date getCreationDate() { return getPublicKey().getCreationTime(); } @@ -617,7 +651,8 @@ public class KeyRingInfo { * * @return last modification date. */ - public @Nonnull Date getLastModified() { + @Nonnull + public Date getLastModified() { PGPSignature mostRecent = getMostRecentSignature(); if (mostRecent == null) { // No sigs found. Return public key creation date instead. @@ -631,7 +666,8 @@ public class KeyRingInfo { * * @return latest key creation time */ - public @Nonnull Date getLatestKeyCreationDate() { + @Nonnull + public Date getLatestKeyCreationDate() { Date latestCreation = null; for (PGPPublicKey key : getPublicKeys()) { if (!isKeyValidlyBound(key.getKeyID())) { @@ -648,7 +684,8 @@ public class KeyRingInfo { return latestCreation; } - private @Nullable PGPSignature getMostRecentSignature() { + @Nullable + private PGPSignature getMostRecentSignature() { Set allSignatures = new HashSet<>(); PGPSignature mostRecentSelfSignature = getLatestDirectKeySelfSignature(); PGPSignature revocationSelfSignature = getRevocationSelfSignature(); @@ -668,6 +705,7 @@ public class KeyRingInfo { return mostRecent; } + @Nonnull public RevocationState getRevocationState() { return revocationState; } @@ -677,7 +715,8 @@ public class KeyRingInfo { * * @return revocation date or null */ - public @Nullable Date getRevocationDate() { + @Nullable + public Date getRevocationDate() { return getRevocationState().isSoftRevocation() ? getRevocationState().getDate() : null; } @@ -686,7 +725,8 @@ public class KeyRingInfo { * * @return expiration date */ - public @Nullable Date getPrimaryKeyExpirationDate() { + @Nullable + public Date getPrimaryKeyExpirationDate() { PGPSignature directKeySig = getLatestDirectKeySelfSignature(); Date directKeyExpirationDate = null; if (directKeySig != null) { @@ -722,6 +762,7 @@ public class KeyRingInfo { return userIdExpirationDate; } + @Nullable public String getPossiblyExpiredPrimaryUserId() { String validPrimaryUserId = getPrimaryUserId(); if (validPrimaryUserId != null) { @@ -760,7 +801,8 @@ public class KeyRingInfo { * @param fingerprint subkey fingerprint * @return expiration date or null */ - public @Nullable Date getSubkeyExpirationDate(OpenPgpFingerprint fingerprint) { + @Nullable + public Date getSubkeyExpirationDate(OpenPgpFingerprint fingerprint) { if (getPublicKey().getKeyID() == fingerprint.getKeyId()) { return getPrimaryKeyExpirationDate(); } @@ -788,6 +830,7 @@ public class KeyRingInfo { * {@link KeyFlag#ENCRYPT_COMMS}/{@link KeyFlag#ENCRYPT_STORAGE}. * @return latest date on which the key ring can be used for the given use case, or null if it can be used indefinitely. */ + @Nullable public Date getExpirationDateForUse(KeyFlag use) { if (use == KeyFlag.SPLIT || use == KeyFlag.SHARED) { throw new IllegalArgumentException("SPLIT and SHARED are not uses, but properties."); @@ -814,20 +857,18 @@ public class KeyRingInfo { } if (nonExpiringSubkeys.isEmpty()) { - if (latestSubkeyExpirationDate != null) { - if (primaryExpiration == null) { - return latestSubkeyExpirationDate; - } - if (latestSubkeyExpirationDate.before(primaryExpiration)) { - return latestSubkeyExpirationDate; - } + if (primaryExpiration == null) { + return latestSubkeyExpirationDate; + } + if (latestSubkeyExpirationDate.before(primaryExpiration)) { + return latestSubkeyExpirationDate; } } return primaryExpiration; } - public boolean isHardRevoked(String userId) { - PGPSignature revocation = signatures.userIdRevocations.get(userId); + public boolean isHardRevoked(@Nonnull CharSequence userId) { + PGPSignature revocation = signatures.userIdRevocations.get(userId.toString()); if (revocation == null) { return false; } @@ -907,7 +948,8 @@ public class KeyRingInfo { * @param purpose purpose (encrypt data at rest / communications) * @return encryption subkeys */ - public @Nonnull List getEncryptionSubkeys(EncryptionPurpose purpose) { + @Nonnull + public List getEncryptionSubkeys(@Nonnull EncryptionPurpose purpose) { Date primaryExpiration = getPrimaryKeyExpirationDate(); if (primaryExpiration != null && primaryExpiration.before(referenceDate)) { LOGGER.debug("Certificate is expired: Primary key is expired on " + DateUtil.formatUTCDate(primaryExpiration)); @@ -966,7 +1008,8 @@ public class KeyRingInfo { * * @return decryption keys */ - public @Nonnull List getDecryptionSubkeys() { + @Nonnull + public List getDecryptionSubkeys() { Iterator subkeys = keys.getPublicKeys(); List decryptionKeys = new ArrayList<>(); @@ -999,7 +1042,8 @@ public class KeyRingInfo { * @param flag flag * @return keys with flag */ - public List getKeysWithKeyFlag(KeyFlag flag) { + @Nonnull + public List getKeysWithKeyFlag(@Nonnull KeyFlag flag) { List keysWithFlag = new ArrayList<>(); for (PGPPublicKey key : getPublicKeys()) { List keyFlags = getKeyFlagsOf(key.getKeyID()); @@ -1021,11 +1065,13 @@ public class KeyRingInfo { * @param purpose encryption purpose * @return encryption subkeys */ - public @Nonnull List getEncryptionSubkeys(String userId, EncryptionPurpose purpose) { + @Nonnull + public List getEncryptionSubkeys(@Nullable CharSequence userId, + @Nonnull EncryptionPurpose purpose) { if (userId != null && !isUserIdValid(userId)) { throw new KeyException.UnboundUserIdException( OpenPgpFingerprint.of(keys), - userId, + userId.toString(), getLatestUserIdCertification(userId), getUserIdRevocation(userId) ); @@ -1039,7 +1085,8 @@ public class KeyRingInfo { * * @return signing keys */ - public @Nonnull List getSigningSubkeys() { + @Nonnull + public List getSigningSubkeys() { Iterator subkeys = keys.getPublicKeys(); List signingKeys = new ArrayList<>(); while (subkeys.hasNext()) { @@ -1057,39 +1104,48 @@ public class KeyRingInfo { return signingKeys; } + @Nonnull public Set getPreferredHashAlgorithms() { return getPreferredHashAlgorithms(getPrimaryUserId()); } - public Set getPreferredHashAlgorithms(String userId) { + @Nonnull + public Set getPreferredHashAlgorithms(@Nullable CharSequence userId) { return getKeyAccessor(userId, getKeyId()).getPreferredHashAlgorithms(); } + @Nonnull public Set getPreferredHashAlgorithms(long keyId) { return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)) .getPreferredHashAlgorithms(); } + @Nonnull public Set getPreferredSymmetricKeyAlgorithms() { return getPreferredSymmetricKeyAlgorithms(getPrimaryUserId()); } - public Set getPreferredSymmetricKeyAlgorithms(String userId) { + @Nonnull + public Set getPreferredSymmetricKeyAlgorithms(@Nullable CharSequence userId) { return getKeyAccessor(userId, getKeyId()).getPreferredSymmetricKeyAlgorithms(); } + @Nonnull public Set getPreferredSymmetricKeyAlgorithms(long keyId) { return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)).getPreferredSymmetricKeyAlgorithms(); } + @Nonnull public Set getPreferredCompressionAlgorithms() { return getPreferredCompressionAlgorithms(getPrimaryUserId()); } - public Set getPreferredCompressionAlgorithms(String userId) { + @Nonnull + public Set getPreferredCompressionAlgorithms(@Nullable CharSequence userId) { return getKeyAccessor(userId, getKeyId()).getPreferredCompressionAlgorithms(); } + @Nonnull public Set getPreferredCompressionAlgorithms(long keyId) { return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)).getPreferredCompressionAlgorithms(); } @@ -1173,15 +1229,20 @@ public class KeyRingInfo { return true; } - private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) { + private KeyAccessor getKeyAccessor(@Nullable CharSequence userId, long keyID) { if (getPublicKey(keyID) == null) { throw new NoSuchElementException("No subkey with key id " + Long.toHexString(keyID) + " found on this key."); } - if (userId != null && !getUserIds().contains(userId)) { + + if (userId != null && !getUserIds().contains(userId.toString())) { throw new NoSuchElementException("No user-id '" + userId + "' found on this key."); } - return userId == null ? new KeyAccessor.ViaKeyId(this, new SubkeyIdentifier(keys, keyID)) - : new KeyAccessor.ViaUserId(this, new SubkeyIdentifier(keys, keyID), userId); + + if (userId != null) { + return new KeyAccessor.ViaUserId(this, new SubkeyIdentifier(keys, keyID), userId.toString()); + } else { + return new KeyAccessor.ViaKeyId(this, new SubkeyIdentifier(keys, keyID)); + } } public static class Signatures { @@ -1193,7 +1254,9 @@ public class KeyRingInfo { private final Map subkeyRevocations; private final Map subkeyBindings; - public Signatures(PGPKeyRing keyRing, Date referenceDate, Policy policy) { + public Signatures(@Nonnull PGPKeyRing keyRing, + @Nonnull Date referenceDate, + @Nonnull Policy policy) { primaryKeyRevocation = SignaturePicker.pickCurrentRevocationSelfSignature(keyRing, policy, referenceDate); primaryKeySelfSignature = SignaturePicker.pickLatestDirectKeySignature(keyRing, policy, referenceDate); userIdRevocations = new HashMap<>(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index 01c09903..6210ef5b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -72,14 +72,12 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { private PGPSecretKeyRing secretKeyRing; private final Date referenceTime; - public SecretKeyRingEditor(PGPSecretKeyRing secretKeyRing) { - this(secretKeyRing, null); + public SecretKeyRingEditor(@Nonnull PGPSecretKeyRing secretKeyRing) { + this(secretKeyRing, new Date()); } - public SecretKeyRingEditor(PGPSecretKeyRing secretKeyRing, Date referenceTime) { - if (secretKeyRing == null) { - throw new NullPointerException("SecretKeyRing MUST NOT be null."); - } + public SecretKeyRingEditor(@Nonnull PGPSecretKeyRing secretKeyRing, + @Nonnull Date referenceTime) { this.secretKeyRing = secretKeyRing; this.referenceTime = referenceTime; } diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java index e23d92d3..c24084b4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/RevocationStateTest.java @@ -60,11 +60,6 @@ public class RevocationStateTest { assertEquals("softRevoked (2022-08-03 18:26:35 UTC)", state.toString()); } - @Test - public void testSoftRevokedNullDateThrows() { - assertThrows(NullPointerException.class, () -> RevocationState.softRevoked(null)); - } - @Test public void orderTest() { assertEquals(RevocationState.notRevoked(), RevocationState.notRevoked()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java index fd665901..0caf8b75 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java @@ -95,6 +95,7 @@ public class UserIdRevocationTest { KeyRingInfo info = new KeyRingInfo(secretKeys); PGPSignature signature = info.getUserIdRevocation("secondary@key.id"); + assertNotNull(signature); RevocationReason reason = (RevocationReason) signature.getHashedSubPackets() .getSubpacket(SignatureSubpacketTags.REVOCATION_REASON); assertNotNull(reason); diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java index d5cfb4f5..5b93415e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java @@ -145,6 +145,7 @@ public class SignatureSubpacketsUtilTest { PGPSignature signature = generator.generateCertification(secretKeys.getPublicKey()); Set featureSet = SignatureSubpacketsUtil.parseFeatures(signature); + assertNotNull(featureSet); assertEquals(2, featureSet.size()); assertTrue(featureSet.contains(Feature.MODIFICATION_DETECTION)); assertTrue(featureSet.contains(Feature.AEAD_ENCRYPTED_DATA)); @@ -216,6 +217,7 @@ public class SignatureSubpacketsUtilTest { PGPSignature signature = generator.generateCertification(secretKeys.getPublicKey()); RevocationKey revocationKey = SignatureSubpacketsUtil.getRevocationKey(signature); + assertNotNull(revocationKey); assertArrayEquals(secretKeys.getPublicKey().getFingerprint(), revocationKey.getFingerprint()); assertEquals(secretKeys.getPublicKey().getAlgorithm(), revocationKey.getAlgorithm()); } @@ -277,6 +279,7 @@ public class SignatureSubpacketsUtilTest { PGPSignature signature = generator.generateCertification(secretKeys.getPublicKey()); TrustSignature trustSignature = SignatureSubpacketsUtil.getTrustSignature(signature); + assertNotNull(trustSignature); assertEquals(10, trustSignature.getDepth()); assertEquals(3, trustSignature.getTrustAmount()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java index ced6a466..bf1cb694 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java @@ -4,6 +4,7 @@ package org.pgpainless.signature.builder; +import org.bouncycastle.bcpg.sig.Exportable; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; @@ -22,6 +23,7 @@ import java.security.NoSuchAlgorithmException; 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.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -61,7 +63,9 @@ public class ThirdPartyCertificationSignatureBuilderTest { assertEquals(SignatureType.GENERIC_CERTIFICATION, SignatureType.valueOf(certification.getSignatureType())); assertEquals(secretKeys.getPublicKey().getKeyID(), certification.getKeyID()); assertArrayEquals(secretKeys.getPublicKey().getFingerprint(), certification.getHashedSubPackets().getIssuerFingerprint().getFingerprint()); - assertFalse(SignatureSubpacketsUtil.getExportableCertification(certification).isExportable()); + Exportable exportable = SignatureSubpacketsUtil.getExportableCertification(certification); + assertNotNull(exportable); + assertFalse(exportable.isExportable()); // test sig correctness certification.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), secretKeys.getPublicKey()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java index 988bc776..3c2eda91 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java @@ -376,7 +376,7 @@ public class SignatureSubpacketsTest { public void testSetSignatureTarget() { byte[] hash = new byte[20]; new Random().nextBytes(hash); - wrapper.setSignatureTarget(PublicKeyAlgorithm.fromId(key.getAlgorithm()), HashAlgorithm.SHA512, hash); + wrapper.setSignatureTarget(PublicKeyAlgorithm.requireFromId(key.getAlgorithm()), HashAlgorithm.SHA512, hash); PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); SignatureTarget target = vector.getSignatureTarget(); From d05ffd0451f47633c7def76d6e6fc834c4cc492d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 16:11:06 +0200 Subject: [PATCH 0732/1204] Make DateUtil null-safe --- .../src/main/java/org/pgpainless/util/DateUtil.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java index ad1fce09..f56020d4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java @@ -4,6 +4,7 @@ package org.pgpainless.util; +import javax.annotation.Nonnull; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; @@ -16,6 +17,7 @@ public final class DateUtil { } // Java's SimpleDateFormat is not thread-safe, therefore we return a new instance on every invocation. + @Nonnull public static SimpleDateFormat getParser() { SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); parser.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -28,11 +30,12 @@ public final class DateUtil { * @param dateString timestamp * @return date */ - public static Date parseUTCDate(String dateString) { + @Nonnull + public static Date parseUTCDate(@Nonnull String dateString) { try { return getParser().parse(dateString); } catch (ParseException e) { - return null; + throw new IllegalArgumentException("Malformed UTC timestamp: " + dateString, e); } } @@ -42,6 +45,7 @@ public final class DateUtil { * @param date date * @return timestamp */ + @Nonnull public static String formatUTCDate(Date date) { return getParser().format(date); } @@ -51,7 +55,8 @@ public final class DateUtil { * @param date date * @return floored date */ - public static Date toSecondsPrecision(Date date) { + @Nonnull + public static Date toSecondsPrecision(@Nonnull Date date) { long millis = date.getTime(); long seconds = millis / 1000; long floored = seconds * 1000; @@ -63,6 +68,7 @@ public final class DateUtil { * * @return now */ + @Nonnull public static Date now() { return toSecondsPrecision(new Date()); } From 21ae48d8c11cc8686fb9c7b3d50ec02116f4a58d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 17:13:29 +0200 Subject: [PATCH 0733/1204] Use assert statements to flag impossible NPEs --- .../decryption_verification/OpenPgpMessageInputStream.java | 2 ++ .../pgpainless/key/certification/CertifyCertificate.java | 1 + .../key/modification/secretkeyring/SecretKeyRingEditor.java | 6 +++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index 04d823d1..07098269 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -544,6 +544,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { for (Tuple k : postponedDueToMissingPassphrase) { PGPSecretKey key = k.getA(); PGPSecretKeyRing keys = getDecryptionKey(key.getKeyID()); + assert (keys != null); keyIds.add(new SubkeyIdentifier(keys, key.getKeyID())); } if (!keyIds.isEmpty()) { @@ -556,6 +557,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { PGPSecretKey secretKey = missingPassphrases.getA(); long keyId = secretKey.getKeyID(); PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); + assert (decryptionKey != null); SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { continue; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java index 05e64879..9340d93d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java @@ -275,6 +275,7 @@ public class CertifyCertificate { // We only support certification-capable primary keys OpenPgpFingerprint fingerprint = info.getFingerprint(); PGPPublicKey certificationPubKey = info.getPublicKey(fingerprint); + assert (certificationPubKey != null); if (!info.isKeyValidlyBound(certificationPubKey.getKeyID())) { throw new KeyException.RevokedKeyException(fingerprint); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index 6210ef5b..73c39c25 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -182,7 +182,10 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { } // We need to unmark this user-id as primary - if (info.getLatestUserIdCertification(otherUserId).getHashedSubPackets().isPrimaryUserID()) { + PGPSignature userIdCertification = info.getLatestUserIdCertification(otherUserId); + assert (userIdCertification != null); + + if (userIdCertification.getHashedSubPackets().isPrimaryUserID()) { addUserId(otherUserId, new SelfSignatureSubpackets.Callback() { @Override public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { @@ -601,6 +604,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { } if (prevUserIdSig.getHashedSubPackets().isPrimaryUserID()) { + assert (primaryUserId != null); PGPSignature userIdSig = reissueNonPrimaryUserId(secretKeyRingProtector, userId, prevUserIdSig); secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryUserId, userIdSig); } From 09bacd40d1701d52dbb23d3360c7dca1532e2000 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 17:14:18 +0200 Subject: [PATCH 0734/1204] SecretKeyRingEditor: referenceTime cannot be null anymore --- .../secretkeyring/SecretKeyRingEditor.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index 73c39c25..7c577600 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -124,9 +124,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { } SelfSignatureBuilder builder = new SelfSignatureBuilder(primaryKey, protector); - if (referenceTime != null) { - builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } + builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); builder.setSignatureType(SignatureType.POSITIVE_CERTIFICATION); // Retain signature subpackets of previous signatures @@ -351,16 +349,12 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { .getV4FingerprintCalculator(), false, subkeyProtector.getEncryptor(subkey.getKeyID())); SubkeyBindingSignatureBuilder skBindingBuilder = new SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector, hashAlgorithm); - if (referenceTime != null) { - skBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } + skBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); skBindingBuilder.getHashedSubpackets().setKeyFlags(flags); if (subkeyAlgorithm.isSigningCapable()) { PrimaryKeyBindingSignatureBuilder pkBindingBuilder = new PrimaryKeyBindingSignatureBuilder(secretSubkey, subkeyProtector, hashAlgorithm); - if (referenceTime != null) { - pkBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } + pkBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); PGPSignature pkBinding = pkBindingBuilder.build(primaryKey.getPublicKey()); skBindingBuilder.getHashedSubpackets().addEmbeddedSignature(pkBinding); } From 7a194c517ac9ce03d3e9eaffc770c12a1655580a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 17:15:30 +0200 Subject: [PATCH 0735/1204] Remove KeyRingUtils.removeSecretKey() in favor of stripSecretKey() --- .../org/pgpainless/key/util/KeyRingUtils.java | 24 ------------------- .../CertificateWithMissingSecretKeyTest.java | 2 +- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 2ce058fd..23361c13 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -440,30 +440,6 @@ public final class KeyRingUtils { return newSecretKey; } - /** - * Remove the secret key of the subkey identified by the given secret key id from the key ring. - * The public part stays attached to the key ring, so that it can still be used for encryption / verification of signatures. - * - * This method is intended to be used to remove secret primary keys from live keys when those are kept in offline storage. - * - * @param secretKeys secret key ring - * @param secretKeyId id of the secret key to remove - * @return secret key ring with removed secret key - * - * @throws IOException in case of an error during serialization / deserialization of the key - * @throws PGPException in case of a broken key - * - * @deprecated use {@link #stripSecretKey(PGPSecretKeyRing, long)} instead. - * TODO: Remove in 1.2.X - */ - @Nonnull - @Deprecated - public static PGPSecretKeyRing removeSecretKey(@Nonnull PGPSecretKeyRing secretKeys, - long secretKeyId) - throws IOException, PGPException { - return stripSecretKey(secretKeys, secretKeyId); - } - /** * Remove the secret key of the subkey identified by the given secret key id from the key ring. * The public part stays attached to the key ring, so that it can still be used for encryption / verification of signatures. diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java index a53999a6..e5f9e370 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CertificateWithMissingSecretKeyTest.java @@ -83,7 +83,7 @@ public class CertificateWithMissingSecretKeyTest { encryptionSubkeyId = PGPainless.inspectKeyRing(secretKeys) .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID(); // remove the encryption/decryption secret key - missingDecryptionSecKey = KeyRingUtils.removeSecretKey(secretKeys, encryptionSubkeyId); + missingDecryptionSecKey = KeyRingUtils.stripSecretKey(secretKeys, encryptionSubkeyId); } @Test From 5c76f9046f28f013bf3e3ce70c7ac8f092abec6f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 17:16:10 +0200 Subject: [PATCH 0736/1204] Turn empty catch block into test failure --- .../IgnoreUnknownSignatureVersionsTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java index 2b60ea02..aa1da741 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java @@ -5,6 +5,7 @@ package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -71,7 +72,7 @@ public class IgnoreUnknownSignatureVersionsTest { try { cert = PGPainless.readKeyRing().publicKeyRing(CERT); } catch (IOException e) { - + fail("Cannot parse certificate.", e); } } From 78cb2ec3d0f3b14ff16ff87d4dc5343c803b6c8e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 17:16:56 +0200 Subject: [PATCH 0737/1204] Do not catch and immediatelly rethrow exception --- .../decryption_verification/TeeBCPGInputStream.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index bdbd9bcd..e1d33f70 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -7,7 +7,6 @@ package org.pgpainless.decryption_verification; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.NoSuchElementException; import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.bcpg.MarkerPacket; @@ -49,13 +48,7 @@ public class TeeBCPGInputStream { return null; } - OpenPgpPacket packet; - try { - packet = OpenPgpPacket.requireFromTag(tag); - } catch (NoSuchElementException e) { - throw e; - } - return packet; + return OpenPgpPacket.requireFromTag(tag); } public Packet readPacket() throws IOException { From 3cea98536506dfa2a44d5898c27e096cd53cdfb9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 17:19:18 +0200 Subject: [PATCH 0738/1204] TeeBCPGInputStream: Annotate byte[] arg as @Nonnull --- .../decryption_verification/TeeBCPGInputStream.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java index e1d33f70..725c6f6e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -19,6 +19,8 @@ import org.bouncycastle.openpgp.PGPOnePassSignature; import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.algorithm.OpenPgpPacket; +import javax.annotation.Nonnull; + /** * Since we need to update signatures with data from the underlying stream, this class is used to tee out the data. * Unfortunately we cannot simply override {@link BCPGInputStream#read()} to tee the data out though, since @@ -121,7 +123,7 @@ public class TeeBCPGInputStream { } @Override - public int read(byte[] b, int off, int len) throws IOException { + public int read(@Nonnull byte[] b, int off, int len) throws IOException { if (last != -1) { outputStream.write(last); } From fb581f11c792c17f516ec50683b50bcdad24ca53 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 3 May 2023 17:20:02 +0200 Subject: [PATCH 0739/1204] UserId.parse(): Prevent self-referencing javadoc --- .../src/main/java/org/pgpainless/key/util/UserId.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java index 427f9060..b115afda 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java @@ -88,7 +88,7 @@ public final class UserId implements CharSequence { *

    • John Doe <john@pgpainless.org>
    • *
    • John Doe (work email) <john@pgpainless.org>
    • *
    - * In these cases, {@link #parse(String)} will detect email addresses, names and comments and expose those + * In these cases, this method will detect email addresses, names and comments and expose those * via the respective getters. * This method does not support parsing mail addresses of the following formats: *