diff --git a/.github/ISSUE_TEMPLATE/cli-application.md b/.github/ISSUE_TEMPLATE/cli-application.md index 68f35b74..b0d7b452 100644 --- a/.github/ISSUE_TEMPLATE/cli-application.md +++ b/.github/ISSUE_TEMPLATE/cli-application.md @@ -7,6 +7,9 @@ assignees: '' --- +**Preliminary** +[ ] This bug was found using AI assistance. + **Describe the bug** diff --git a/.github/ISSUE_TEMPLATE/library.md b/.github/ISSUE_TEMPLATE/library.md index 74f5f666..eae6f2d4 100644 --- a/.github/ISSUE_TEMPLATE/library.md +++ b/.github/ISSUE_TEMPLATE/library.md @@ -7,6 +7,9 @@ assignees: '' --- +**Preliminary** +[ ] This bug was found using AI assistance. + **Describe the bug** diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 031ba56d..d45b16a3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,7 +36,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'java' ] + language: [ 'java-kotlin' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support @@ -46,7 +46,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -57,7 +57,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -71,4 +71,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 00000000..8c4f4e6c --- /dev/null +++ b/BUILD.md @@ -0,0 +1,22 @@ + + +# Build PGPainless + +There are a number of different artifacts that can be built from the PGPainless source code: + +## `pgpainless-cli/build/libs/pgpainless-cli-X.Y.Z-all.jar` + +This is a fat jar, built using the Shadow plugin. +It bundles all necessary dependencies required by the CLI application at runtime. +This artifact will be produced by the `gradle shadowJar` task, which is run as part of the `gradle assemble` task. + +## `pgpainless-cli/build/native/nativeCompile/pgpainless-cli` + +This is a native image, that can be built using GraalVM which compared to the executable jar file above +offers greatly improved performance by skipping the JVM startup overhead. + +To build this image, you need to run `gradle nativeCompile` using a GraalVM-enabled Java SDK. diff --git a/README.md b/README.md index e305e43a..ecc5826f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ 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 Bouncy Castle java library and can be used on Android down to API level 10. +PGPainless is based around the Bouncy Castle java library and can be used on Android. It can be configured to either use the Java Cryptographic Engine (JCE), or Bouncy Castles lightweight reimplementation. While signature verification in Bouncy Castle is limited to signature correctness, PGPainless goes much further. @@ -32,7 +32,7 @@ It also checks if signing subkeys are properly bound to their primary key, if ke if keys are allowed to create signatures in the first place. These rigorous checks make PGPainless stand out from other Java-based OpenPGP libraries and are the reason why -PGPainless currently [*scores first place* on Sequoia-PGPs Interoperability Test-Suite](https://tests.sequoia-pgp.org). +PGPainless currently scores above average on Sequoia-PGPs [Interoperability Test-Suite](https://tests.sequoia-pgp.org). > At FlowCrypt we are using PGPainless in our Kotlin code bases on Android and on server side. > The ergonomics of legacy PGP tooling on Java is not very good, and PGPainless improves it greatly. @@ -65,24 +65,23 @@ 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); + OpenPGPKey secretKey = PGPainless.getInstance().readKey() + .parseKey(key); ``` Similarly, keys can quickly be exported:: ```java - PGPSecretKeyRing secretKey = ...; - String armored = PGPainless.asciiArmor(secretKey); - ByteArrayOutputStream binary = new ByteArrayOutputStream(); - secretKey.encode(binary); + OpenPGPKey secretKey = ...; + String armored = secretKey.toAsciiArmoredString(); + byte[] binary = secretKey.getEncoded(); ``` Extract a public key certificate from a secret key: ```java - PGPSecretKeyRing secretKey = ...; - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); + OpenPGPKey secretKey = ...; + OpenPGPCertificate certificate = secretKey.toCertificate(); ``` ### Easily Generate Keys @@ -90,16 +89,17 @@ PGPainless comes with a simple to use `KeyRingBuilder` class that helps you to q There are some predefined key archetypes, but it is possible to fully customize key generation to your needs. ```java + PGPainless api = PGPainless.getInstance(); // RSA key without additional subkeys - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + OpenPGPKey secretKeys = api.generateKey() .simpleRsaKeyRing("Juliet ", RsaLength._4096); // EdDSA primary key with EdDSA signing- and XDH encryption subkeys - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Romeo ", "I defy you, stars!"); // Customized key - PGPSecretKeyRing keyRing = PGPainless.buildKeyRing() + OpenPGPKey keyRing = api.buildKey() .setPrimaryKey(KeySpec.getBuilder( RSA.withLength(RsaLength._8192), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) @@ -124,24 +124,26 @@ algorithms accordingly. Still it allows you to manually specify which algorithms to use of course. ```java - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + PGPainless api = PGPainless.getInstance(); + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(outputStream) .withOptions( ProducerOptions.signAndEncrypt( - new EncryptionOptions() + EncryptionOptions.get(api) .addRecipient(aliceKey) .addRecipient(bobsKey) // optionally encrypt to a passphrase .addMessagePassphrase(Passphrase.fromPassword("password123")) // optionally override symmetric encryption algorithm .overrideEncryptionAlgorithm(SymmetricKeyAlgorithm.AES_192), - new SigningOptions() + SigningOptions.get(api) // Sign in-line (using one-pass-signature packet) .addInlineSignature(secretKeyDecryptor, aliceSecKey, signatureType) // Sign using a detached signature .addDetachedSignature(secretKeyDecryptor, aliceSecKey, signatureType) // optionally override hash algorithm - .overrideHashAlgorithm(HashAlgorithm.SHA256) + .overrideHashAlgorithm(HashAlgorithm.SHA256), + api ).setAsciiArmor(true) // Ascii armor or not ); @@ -161,9 +163,9 @@ Furthermore, PGPainless will reject signatures made using weak algorithms like S This behaviour can be modified though using the `Policy` class. ```java - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(encryptedInputStream) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get(api) .addDecryptionKey(bobSecKeys, secretKeyProtector) .addVerificationCert(alicePubKeys) ); @@ -215,10 +217,10 @@ which contains the bug you are fixing. That way we can update older revisions of Please follow the [code of conduct](CODE_OF_CONDUCT.md) if you want to be part of the project. ## Acknowledgements -Development on PGPainless is generously sponsored by [FlowCrypt.com](https://flowcrypt.com). Thank you very very very much! +In the past, development on PGPainless has been generously sponsored by [FlowCrypt.com](https://flowcrypt.com). Thank you very very very much! [![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). +Parts of PGPainless development ([project page](https://nlnet.nl/project/PGPainless/)) has been 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://blog.jabberhead.tk/wp-content/uploads/2022/05/NGIAssure_tag.svg)](https://nlnet.nl/assure/) diff --git a/build.gradle b/build.gradle index 93100f4d..38b21d5f 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { } plugins { - id 'org.jetbrains.kotlin.jvm' version "1.8.10" + id 'org.jetbrains.kotlin.jvm' version "1.9.21" id 'com.diffplug.spotless' version '6.22.0' apply false } @@ -37,7 +37,6 @@ allprojects { // without this we would generate an empty pgpainless.jar for the project root // https://stackoverflow.com/a/25445035 jar { - reproducibleFileOrder = true onlyIf { !sourceSets.main.allSource.files.isEmpty() } } diff --git a/docs/source/index.rst b/docs/source/index.rst index 06c115ec..6ff985e0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -35,4 +35,5 @@ Contents quickstart.md pgpainless-cli/usage.md sop.md - pgpainless-core/indepth.rst \ No newline at end of file + pgpainless-core/indepth.rst + pgpainless-core/migration_2.0.md \ No newline at end of file diff --git a/docs/source/pgpainless-core/migration_2.0.md b/docs/source/pgpainless-core/migration_2.0.md new file mode 100644 index 00000000..cfed6744 --- /dev/null +++ b/docs/source/pgpainless-core/migration_2.0.md @@ -0,0 +1,137 @@ +# Migration Guide PGPainless 2.0 + +PGPainless 2.0 makes use of Bouncy Castles new High-Level API. +As a consequence, the use of certain "mid-level" classes, such as `PGPPublicKeyRing`, `PGPSecretKeyRing` is now +discouraged in favor of their high-level counterparts, e.g. `OpenPGPCertificate`, `OpenPGPKey`. + + +## Terminology Changes + +Bouncy Castles high-level API uses OpenPGP terminology as described in the book [OpenPGP for application developers](https://openpgp.dev/book/). +Therefore, some terms used in the mid-level API are no longer used. + +| Old Term | New Term | Description | +|------------------------|----------------------------|------------------------------------------------------| +| key ring | OpenPGP certificate or key | | +| master key | primary key | | +| public key ring | (OpenPGP) certificate | | +| secret key ring | (OpenPGP) key | | +| subkey | component key | A component key is either a primary key, or a subkey | +| primary key identifier | certificate identifier | | +| subkey identifier | component key identifier | | + + +## API + +PGPainless 2.0 switches away from the Singleton pattern. + +The API entrypoints for PGPainless 1.X were static methods of the `PGPainless` class. +Configuration was done by modifying singletons, e.g. `Policy`. + +With PGPainless 2.X, the recommended way to use the API is to create individual instances of the `PGPainless` class, +which provide non-static methods for different OpenPGP operations. +That way, you can have multiple API instances with different, per-instance configurations. + + +## Key Material + +The use of `PGPPublicKeyRing` objects is now discouraged in favor of `OpenPGPCertificate`. +Appropriately, `OpenPGPKey` replaces `PGPSecretKeyRing`. `OpenPGPKey` extends the `OpenPGPCertificate` class, but also +contains secret key material. + +An `OpenPGPCertificate` consists of `OpenPGPCertificateComponent`s such as `OpenPGPComponentKey`s and +`OpenPGPIdentityComponent`s, which are bound to the certificate with `OpenPGPComponentSignature`s. +`OpenPGPIdentityComponent`s are either `OpenPGPUserId`s or `OpenPGPUserAttribute`s (the latter being more or less +deprecated). +Components of an OpenPGP certificate, which contain key material (public keys, secret keys, subkeys...) are represented +by the `OpenPGPComponentKey` class, from which `OpenPGPPrimaryKey`, `OpenPGPSubkey` and `OpenPGPSecretKey` inherit. + +As stated above, `OpenPGPCertificateComponent`s are bound to the certificate using `OpenPGPSignature`s, +which Bouncy Castle arranges into `OpenPGPSignatureChains` internally. +This chain structure is evaluated to determine the status of a certificate component at a given time, as well as +its applicable properties (algorithm preferences, features, key flags...) + +In places, where you cannot switch to using `OpenPGPCertificate`, you can access the underlying `PGPPublicKeyRing` +by calling `certificate.getPGPPublicKeyRing()`. +Analog, you can access the underlying `PGPSecretKeyRing` of an `OpenPGPKey` via `key.getPGPSecretKeyRing()`. + + +### Key Versions + +PGPainless 1.X primarily supported OpenPGP keys of version 4. +The 2.X release introduces support for OpenPGP v6 as well, which makes it necessary to specify the desired key version +e.g. when generating keys. + +This can be done by passing an `OpenPGPKeyVersion` enum. + + +## `KeyIdentifier` + +OpenPGP has evolved over time and with it the way to identify individual keys. +Old protocol versions rely on 64-bit key-ids, which are nowadays deprecated, as 64-bits are not exactly +collision-resistant. +For some time already, the use of fingerprints is therefore encouraged as a replacement. +However, key-ids were not everywhere at once in the protocol, so many artifacts still contain elements with +key-ids in them. +An example for this are public-key encrypted session-key packets, which in version 1 still only contain the recipients +key-id. +In signatures, both key-ids and fingerprints are present. + +To solve this inconsistency, Bouncy Castle introduced the `KeyIdentifier` type as an abstraction of both key-ids +and fingerprints. +Now most methods that take some sort of identifier, be it fingerprint or key-id, now also accept a `KeyIdentifier` +object. + +Consequently, `KeyIdentifier` is now also the preferred way to reference keys in PGPainless and many places where +previously a key-id or fingerprint was expected, now also accept `KeyIdentifier` objects. +In places, where you need to access a 64-bit key-id, you can call `keyIdentifier.getKeyId()`. + +## `SecretKeyRingProtector` + +When an OpenPGP v6 key is encrypted, the public key parts are incorporated as authenticated data into the encryption +process. Therefore, when instantiating a `PBESecretKeyEncryptor`, the public key needs to be passed in. +As a consequence, the API of `SecretKeyRingProtector` changed and now a `PGPPublicKey` needs to be passed in, +instead of merely a key-id or `KeyIdentifier`. + +## Differences between BCs high-level API and PGPainless + +With Bouncy Castle now introducing its own high-level API, you might ask, what differences there are between +high-level PGPainless classes and their new Bouncy Castle counterparts. + +### `KeyRingInfo` vs. `OpenPGPCertificate`/`OpenPGPKey` + +PGPainless' `KeyRingInfo` class fulfils a similar task as the new `OpenPGPCertificate`/`OpenPGPKey` classes, +namely evaluating OpenPGP key material, checking self signatures, exposing certain properties like +subkeys, algorithm preferences etc. in a way accessible for the user, all with respect to a given reference time. + +However, `KeyRingInfo` historically gets instantiated *per reference time*, while`OpenPGPCertificate`/`OpenPGPKey` +is instantiated only *once* and expects you to pass in the reference time each time you are using a +property getter, lazily evaluating applicable signatures as needed. +Under the hood, the Bouncy Castle classes now cache expensive signature verification results for later use. +Consequently, `KeyRingInfo` now wraps `OpenPGPCertificate`/`OpenPGPKey`, forwarding method calls while passing along +the chosen reference time and mapping basic data types to PGPainless' high-level types / enums. + + +## Type Replacements + +| Old | New | Comment | +|------------------------------|------------------------------|---------------------------------------------------------------------| +| `PGPPublicKeyRing` | `OpenPGPCertificate` | Self-Signatures are automagically evaluated | +| `PGPSecretKeyRing` | `OpenPGPKey` | Same as `OpenPGPCertificate`, but also contains secret key material | +| `PGPPublicKey` (primary key) | `OpenPGPPrimaryKey` | Primary keys provide getters to access bound user identities | +| `PGPPublicKey` (subkey) | `OpenPGPComponentKey` | - | +| `PGPSecretKey` (primary key) | `OpenPGPSecretKey` | - | +| `PGPSecretKey` (subkey) | `OpenPGPSecretKey` | - | +| `PGPPrivateKey` | `OpenPGPPrivateKey` | - | +| `Long` (Key-ID) | `KeyIdentifier` | - | +| `byte[]` (Key Fingerprint) | `KeyIdentifier` | - | +| `MissingPublicKeyCallback` | `OpenPGPCertificateProvider` | - | +| (detached) `PGPSignature` | `OpenPGPDocumentSignature` | - | + + +## Algorithm Support + +The use of ElGamal as public key algorithm is now deprecated. Consequently, it is no longer possible to generate +ElGamal keys. + +RFC9580 introduced new key types `Ed25519`, `Ed448`, `X25519`, `X448`. \ No newline at end of file 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 f1f69912..45b20084 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 @@ -11,12 +11,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -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.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.info.KeyRingInfo; @@ -25,24 +22,26 @@ import sop.exception.SOPGPException; public class ExtractCertCmdTest extends CLITest { + private final PGPainless api = PGPainless.getInstance(); + public ExtractCertCmdTest() { super(LoggerFactory.getLogger(ExtractCertCmdTest.class)); } @Test public void testExtractCert() - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + throws IOException { + OpenPGPKey key = api.generateKey() .simpleEcKeyRing("Juliet Capulet "); - pipeBytesToStdin(secretKeys.getEncoded()); + pipeBytesToStdin(key.getEncoded()); ByteArrayOutputStream out = pipeStdoutToStream(); assertSuccess(executeCommand("extract-cert", "--armor")); assertTrue(out.toString().startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n")); - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(out.toByteArray()); - KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys); + OpenPGPCertificate certificate = api.readKey().parseCertificate(out.toByteArray()); + KeyRingInfo info = api.inspect(certificate); assertFalse(info.isSecretKey()); assertTrue(info.isUserIdValid("Juliet Capulet ")); } 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 f8d56bc3..fdc341b0 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 @@ -11,13 +11,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -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.junit.jupiter.api.Disabled; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; @@ -25,6 +21,8 @@ import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.sop.EncryptImpl; +import org.pgpainless.sop.GenerateKeyImpl; import org.slf4j.LoggerFactory; import sop.exception.SOPGPException; @@ -138,7 +136,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { } @Test - @Disabled("Disabled, since we now read certificates from secret keys") + // @Disabled("Disabled, since we now read certificates from secret keys") public void testEncryptingForKeyFails() throws IOException { File notACert = writeFile("key.asc", KEY); @@ -298,14 +296,14 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { } @Test - public void testEncryptWithIncapableCert() throws PGPException, - InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + public void testEncryptWithIncapableCert() throws IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey key = api.buildKey() .addUserId("No Crypt ") .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .build(); - PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); + OpenPGPCertificate cert = key.toCertificate(); File certFile = writeFile("cert.pgp", cert.getEncoded()); pipeStringToStdin("Hello, World!\n"); @@ -318,15 +316,16 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { @Test public void testSignWithIncapableKey() - throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + throws IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey key = api.buildKey() .addUserId("Cannot Sign ") .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder( KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .build(); - File keyFile = writeFile("key.pgp", secretKeys.getEncoded()); - File certFile = writeFile("cert.pgp", PGPainless.extractCertificate(secretKeys).getEncoded()); + File keyFile = writeFile("key.pgp", key.getEncoded()); + File certFile = writeFile("cert.pgp", key.toCertificate().getEncoded()); pipeStringToStdin("Hello, World!\n"); ByteArrayOutputStream out = pipeStdoutToStream(); @@ -650,7 +649,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { // Generate key File passwordFile = writeFile("password", "sw0rdf1sh"); File keyFile = pipeStdoutToFile("key.asc"); - assertSuccess(executeCommand("generate-key", "--profile=rfc4880", "--with-key-password", passwordFile.getAbsolutePath(), "Alice ")); + assertSuccess(executeCommand("generate-key", "--profile=" + GenerateKeyImpl.RFC4880_RSA4096_PROFILE.getName(), "--with-key-password", passwordFile.getAbsolutePath(), "Alice ")); File certFile = pipeStdoutToFile("cert.asc"); pipeFileToStdin(keyFile); @@ -662,7 +661,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { // Encrypt File ciphertextFile = pipeStdoutToFile("msg.asc"); pipeFileToStdin(plaintextFile); - assertSuccess(executeCommand("encrypt", "--profile=rfc4880", certFile.getAbsolutePath())); + assertSuccess(executeCommand("encrypt", "--profile=" + EncryptImpl.RFC4880_PROFILE.getName(), certFile.getAbsolutePath())); ByteArrayOutputStream decrypted = pipeStdoutToStream(); pipeFileToStdin(ciphertextFile); 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 057cec98..50eeadab 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 @@ -15,8 +15,8 @@ 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.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; @@ -350,12 +350,13 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { @Test public void createMalformedMessage() throws IOException, PGPException { + PGPainless api = PGPainless.getInstance(); String msg = "Hello, World!\n"; - PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY_2); + OpenPGPKey key = api.readKey().parseKey(KEY_2); ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(ciphertext) - .withOptions(ProducerOptions.sign(SigningOptions.get() + .withOptions(ProducerOptions.sign(SigningOptions.get(api) .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key) ).overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) .setAsciiArmor(false)); 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 9dcb3aca..fea9a5d2 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 @@ -11,12 +11,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; 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; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; @@ -199,12 +196,13 @@ public class RoundTripSignVerifyCmdTest extends CLITest { @Test public void testSignWithIncapableKey() - throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + throws IOException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("Cannot Sign ") .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) - .build(); + .build() + .getPGPSecretKeyRing(); File keyFile = writeFile("key.pgp", secretKeys.getEncoded()); pipeStringToStdin("Hello, World!\n"); @@ -252,7 +250,7 @@ public class RoundTripSignVerifyCmdTest extends CLITest { String verification = verificationsOut.toString(); String[] split = verification.split(" "); OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(cert); - OpenPgpV4Fingerprint signingKeyFingerprint = new OpenPgpV4Fingerprint(info.getSigningSubkeys().get(0)); + OpenPgpV4Fingerprint signingKeyFingerprint = new OpenPgpV4Fingerprint(info.getSigningSubkeys().get(0).getPGPPublicKey()); assertEquals(signingKeyFingerprint.toString(), split[1].trim(), verification); assertEquals(primaryKeyFingerprint.toString(), split[2].trim()); diff --git a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java deleted file mode 100644 index d744e222..00000000 --- a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.gnupg; - -import org.bouncycastle.bcpg.S2K; - -public enum GnuPGDummyExtension { - - /** - * 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), - ; - - private final int id; - - GnuPGDummyExtension(int id) { - this.id = id; - } - - public int getId() { - return id; - } -} diff --git a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java deleted file mode 100644 index 42af92d8..00000000 --- a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java +++ /dev/null @@ -1,210 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.gnupg; - -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 org.pgpainless.key.SubkeyIdentifier; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -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 - * stub secret keys in the style of GnuPGs proprietary extensions. - * - * @see - * GnuPGs doc/DETAILS - GNU extensions to the S2K algorithm - */ -public final class GnuPGDummyKeyUtil { - - private 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(@Nonnull 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}. - * - * @param secretKeys secret keys - * @return builder - */ - public static Builder modify(@Nonnull PGPSecretKeyRing secretKeys) { - return new Builder(secretKeys); - } - - public static final class Builder { - - private final PGPSecretKeyRing keys; - - private Builder(@Nonnull PGPSecretKeyRing keys) { - 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 GnuPGDummyExtension#NO_PRIVATE_KEY}. - * - * @param filter filter to select keys for removal - * @return modified key ring - */ - public PGPSecretKeyRing removePrivateKeys(@Nonnull KeyFilter 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 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(@Nonnull 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 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(@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(@Nonnull GnuPGDummyExtension extension, - @Nullable byte[] serial, - @Nonnull 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, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial); - PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); - secretKeyList.add(onCard); - } else { - SecretSubkeyPacket keyPacket = new SecretSubkeyPacket(publicKeyPacket, - 0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial); - PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); - secretKeyList.add(onCard); - } - } - - return new PGPSecretKeyRing(secretKeyList); - } - - private byte[] encodeSerial(@Nonnull byte[] serial) { - byte[] encoded = new byte[serial.length + 1]; - encoded[0] = (byte) (serial.length & 0xff); - System.arraycopy(serial, 0, encoded, 1, serial.length); - return encoded; - } - - 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 { - - /** - * Return true, if the given key should be selected, false otherwise. - * - * @param keyId id of the key - * @return select - */ - 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); - } - } -} diff --git a/pgpainless-core/src/main/java/org/gnupg/package-info.java b/pgpainless-core/src/main/java/org/gnupg/package-info.java deleted file mode 100644 index 03268619..00000000 --- a/pgpainless-core/src/main/java/org/gnupg/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Utility classes related to creating keys with GNU DUMMY S2K values. - */ -package org.gnupg; 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 deleted file mode 100644 index 3522f509..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java +++ /dev/null @@ -1,417 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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 java.util.NoSuchElementException; - -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; - -/** - * InputStream used to determine the nature of potential OpenPGP data. - */ -public class OpenPgpInputStream extends BufferedInputStream { - - @SuppressWarnings("CharsetObjectCanBeUsed") - 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 * 2; - - private final byte[] buffer; - private final int bufferLen; - - private boolean containsArmorHeader; - private boolean containsOpenPgpPackets; - private boolean isLikelyOpenPgpMessage; - - public OpenPgpInputStream(InputStream in, boolean check) throws IOException { - super(in, MAX_BUFFER_SIZE); - - mark(MAX_BUFFER_SIZE); - buffer = new byte[MAX_BUFFER_SIZE]; - bufferLen = read(buffer); - reset(); - - if (check) { - inspectBuffer(); - } - } - - public OpenPgpInputStream(InputStream in) throws IOException { - this(in, true); - } - - private void inspectBuffer() throws IOException { - if (checkForAsciiArmor()) { - return; - } - - checkForBinaryOpenPgp(); - } - - private boolean checkForAsciiArmor() { - if (startsWithIgnoringWhitespace(buffer, ARMOR_HEADER, bufferLen)) { - containsArmorHeader = true; - return true; - } - 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 checkForBinaryOpenPgp() throws IOException { - if (bufferLen == -1) { - // Empty data - return; - } - - ByteArrayInputStream bufferIn = new ByteArrayInputStream(buffer, 0, bufferLen); - nonExhaustiveParseAndCheckPlausibility(bufferIn); - } - - private void nonExhaustiveParseAndCheckPlausibility(ByteArrayInputStream bufferIn) throws IOException { - // Read the packet header - 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; - - // Determine the packet length - 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); - } - } 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; - } - } - - // 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: - // How to handle this? Probably discard as garbage... - return; - - 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.requireFromCode(sigType); - } catch (NoSuchElementException e) { - return; - } - - containsOpenPgpPackets = 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.requireFromCode(opsSigType); - } catch (NoSuchElementException 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; - // While this is a valid OpenPGP message, enabling the line below would lead to too many false positives - // isLikelyOpenPgpMessage = 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; - } - } - - 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; - } - - /** - * 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/main/java/org/pgpainless/decryption_verification/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/package-info.java deleted file mode 100644 index 07e7cd3d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes used to decryption and verification of OpenPGP encrypted / signed data. - */ -package org.pgpainless.decryption_verification; diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java deleted file mode 100644 index 65d27390..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java +++ /dev/null @@ -1,132 +0,0 @@ -// 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 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) { - 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 UnacceptableThirdPartyCertificationKeyException extends KeyException { - - public UnacceptableThirdPartyCertificationKeyException(@Nonnull OpenPgpFingerprint fingerprint) { - super("Key " + fingerprint + " has no acceptable certification key.", fingerprint); - } - } - - 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 deleted file mode 100644 index b7a87ab7..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyIntegrityException.java +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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() { - super("Key Integrity Exception"); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java deleted file mode 100644 index f98a4048..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import org.pgpainless.decryption_verification.syntax_check.InputSymbol; -import org.pgpainless.decryption_verification.syntax_check.StackSymbol; -import org.pgpainless.decryption_verification.syntax_check.State; - -/** - * 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 RuntimeException { - - public MalformedOpenPgpMessageException(String message) { - super(message); - } - - 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."); - } - - public MalformedOpenPgpMessageException(String s, MalformedOpenPgpMessageException e) { - super(s, e); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MessageNotIntegrityProtectedException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MessageNotIntegrityProtectedException.java deleted file mode 100644 index 1d8559e1..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MessageNotIntegrityProtectedException.java +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import org.bouncycastle.openpgp.PGPException; - -public class MessageNotIntegrityProtectedException extends PGPException { - - public MessageNotIntegrityProtectedException() { - super("Message is encrypted using a 'Symmetrically Encrypted Data' (SED) packet, which enables certain types of attacks. " + - "A 'Symmetrically Encrypted Integrity Protected' (SEIP) packet should be used instead."); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MissingDecryptionMethodException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MissingDecryptionMethodException.java deleted file mode 100644 index 0e856ba6..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MissingDecryptionMethodException.java +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import org.bouncycastle.openpgp.PGPException; - -/** - * Exception that is thrown when decryption fails due to a missing decryption key or decryption passphrase. - * This can happen when the user does not provide the right set of keys / the right password when decrypting - * a message. - */ -public class MissingDecryptionMethodException extends PGPException { - - public MissingDecryptionMethodException(String message) { - super(message); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MissingPassphraseException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MissingPassphraseException.java deleted file mode 100644 index 3f8e0799..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MissingPassphraseException.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Set; - -import org.bouncycastle.openpgp.PGPException; -import org.pgpainless.key.SubkeyIdentifier; - -public class MissingPassphraseException extends PGPException { - - private final Set keyIds; - - public MissingPassphraseException(Set keyIds) { - super("Missing passphrase encountered for keys " + Arrays.toString(keyIds.toArray())); - this.keyIds = Collections.unmodifiableSet(keyIds); - } - - public Set getKeyIds() { - return keyIds; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/ModificationDetectionException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/ModificationDetectionException.java deleted file mode 100644 index 5be1b359..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/ModificationDetectionException.java +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import java.io.IOException; - -/** - * Exception that gets thrown when the verification of a modification detection code failed. - */ -public class ModificationDetectionException extends IOException { - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/SignatureValidationException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/SignatureValidationException.java deleted file mode 100644 index 2141ec5c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/SignatureValidationException.java +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import java.util.Map; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.SignatureType; - -public class SignatureValidationException extends PGPException { - - public SignatureValidationException(String message) { - super(message); - } - - public SignatureValidationException(String message, Exception underlying) { - super(message, underlying); - } - - public SignatureValidationException(String message, Map rejections) { - super(message + ": " + exceptionMapToString(rejections)); - } - - private static String exceptionMapToString(Map rejections) { - StringBuilder sb = new StringBuilder(); - sb.append(rejections.size()).append(" rejected signatures:\n"); - for (PGPSignature signature : rejections.keySet()) { - String typeString; - SignatureType type = SignatureType.fromCode(signature.getSignatureType()); - if (type == null) { - typeString = "0x" + Long.toHexString(signature.getSignatureType()); - } else { - typeString = type.toString(); - } - sb.append(typeString).append(' ') - .append(signature.getCreationTime()).append(": ") - .append(rejections.get(signature).getMessage()).append('\n'); - } - return sb.toString(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/UnacceptableAlgorithmException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/UnacceptableAlgorithmException.java deleted file mode 100644 index aa3c8603..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/UnacceptableAlgorithmException.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 unacceptable algorithms are encountered. - */ -public class UnacceptableAlgorithmException extends PGPException { - - public UnacceptableAlgorithmException(String message) { - super(message); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/WrongConsumingMethodException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/WrongConsumingMethodException.java deleted file mode 100644 index 93d2e9c5..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/WrongConsumingMethodException.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.PGPException; - -public class WrongConsumingMethodException extends PGPException { - - public WrongConsumingMethodException(String message) { - super(message); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/WrongPassphraseException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/WrongPassphraseException.java deleted file mode 100644 index 409db3e2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/WrongPassphraseException.java +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import org.bouncycastle.openpgp.PGPException; - -public class WrongPassphraseException extends PGPException { - - public WrongPassphraseException(String message) { - super(message); - } - - public WrongPassphraseException(long keyId, PGPException cause) { - this("Wrong passphrase provided for key " + Long.toHexString(keyId), cause); - } - - public WrongPassphraseException(String message, PGPException cause) { - super(message, cause); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/exception/package-info.java deleted file mode 100644 index 01a786aa..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Exceptions. - */ -package org.pgpainless.exception; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/package-info.java deleted file mode 100644 index 060cd540..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP keys. - */ -package org.pgpainless.key; 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 deleted file mode 100644 index f7a78404..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/OpenPgpKeyAttributeUtil.java +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.util; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.SignatureType; - -public final class OpenPgpKeyAttributeUtil { - - private OpenPgpKeyAttributeUtil() { - - } - - public static List getPreferredHashAlgorithms(PGPPublicKey publicKey) { - List hashAlgorithms = new ArrayList<>(); - Iterator keySignatures = publicKey.getSignatures(); - while (keySignatures.hasNext()) { - PGPSignature signature = (PGPSignature) keySignatures.next(); - - if (signature.getKeyID() != publicKey.getKeyID()) { - // Signature from a foreign key. Skip. - continue; - } - - SignatureType signatureType = SignatureType.fromCode(signature.getSignatureType()); - if (signatureType == null) { - // unknown signature type - continue; - } - if (signatureType == SignatureType.POSITIVE_CERTIFICATION - || signatureType == SignatureType.GENERIC_CERTIFICATION) { - int[] hashAlgos = signature.getHashedSubPackets().getPreferredHashAlgorithms(); - if (hashAlgos == null) { - continue; - } - for (int h : hashAlgos) { - HashAlgorithm algorithm = HashAlgorithm.fromId(h); - if (algorithm != null) { - hashAlgorithms.add(algorithm); - } - } - // Exit the loop after the first key signature with hash algorithms. - break; - } - } - return hashAlgorithms; - } - - /** - * Return the hash algorithm that was used in the latest self signature. - * - * @param publicKey public key - * @return list of hash algorithm - */ - public static List guessPreferredHashAlgorithms(PGPPublicKey publicKey) { - HashAlgorithm hashAlgorithm = null; - Date lastCreationDate = null; - - Iterator keySignatures = publicKey.getSignatures(); - while (keySignatures.hasNext()) { - PGPSignature signature = (PGPSignature) keySignatures.next(); - if (signature.getKeyID() != publicKey.getKeyID()) { - continue; - } - - SignatureType signatureType = SignatureType.fromCode(signature.getSignatureType()); - if (signatureType == null || signatureType != SignatureType.POSITIVE_CERTIFICATION - && signatureType != SignatureType.GENERIC_CERTIFICATION) { - continue; - } - - Date creationDate = signature.getCreationTime(); - if (lastCreationDate == null || lastCreationDate.before(creationDate)) { - lastCreationDate = creationDate; - hashAlgorithm = HashAlgorithm.fromId(signature.getHashAlgorithm()); - } - } - - if (hashAlgorithm == null) { - return Collections.emptyList(); - } - return Collections.singletonList(hashAlgorithm); - } - - /** - * Try to extract hash algorithm preferences from self signatures. - * If no self-signature containing hash algorithm preferences is found, - * try to derive a hash algorithm preference by inspecting the hash algorithm used by existing - * self-signatures. - * - * @param publicKey key - * @return hash algorithm preferences (might be empty!) - */ - public static Set getOrGuessPreferredHashAlgorithms(PGPPublicKey publicKey) { - List preferredHashAlgorithms = OpenPgpKeyAttributeUtil.getPreferredHashAlgorithms(publicKey); - if (preferredHashAlgorithms.isEmpty()) { - preferredHashAlgorithms = OpenPgpKeyAttributeUtil.guessPreferredHashAlgorithms(publicKey); - } - return new LinkedHashSet<>(preferredHashAlgorithms); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/package-info.java deleted file mode 100644 index 4609c126..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Utility functions to deal with OpenPGP keys. - */ -package org.pgpainless.key.util; diff --git a/pgpainless-core/src/main/java/org/pgpainless/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/package-info.java deleted file mode 100644 index 3573fcdc..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/package-info.java +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * PGPainless - Use OpenPGP Painlessly! - * - * @see org.pgpainless.core.org - */ -package org.pgpainless; diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/util/package-info.java deleted file mode 100644 index 6c525229..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Utility classes. - */ -package org.pgpainless.util; 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 deleted file mode 100644 index 26180214..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/KeyRingSelectionStrategy.java +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util.selection.keyring; - -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}). - * @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); -} 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 deleted file mode 100644 index 7dbf7c93..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util.selection.keyring; - -import javax.annotation.Nonnull; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; - -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 - public Set selectKeyRingsFromCollection(@Nonnull O identifier, @Nonnull PGPPublicKeyRingCollection keyRingCollection) { - Set accepted = new HashSet<>(); - for (Iterator i = keyRingCollection.getKeyRings(); i.hasNext(); ) { - PGPPublicKeyRing ring = i.next(); - if (accept(identifier, ring)) accepted.add(ring); - } - return accepted; - } - - @Override - public MultiMap selectKeyRingsFromCollections(@Nonnull MultiMap keyRingCollections) { - MultiMap keyRings = new MultiMap<>(); - for (Map.Entry> entry : keyRingCollections.entrySet()) { - for (PGPPublicKeyRingCollection collection : entry.getValue()) { - keyRings.plus(entry.getKey(), selectKeyRingsFromCollection(entry.getKey(), collection)); - } - } - return keyRings; - } -} 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 deleted file mode 100644 index 9e57b575..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util.selection.keyring; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; -import javax.annotation.Nonnull; - -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) { - Set accepted = new HashSet<>(); - for (Iterator i = keyRingCollection.getKeyRings(); i.hasNext(); ) { - PGPSecretKeyRing ring = i.next(); - if (accept(identifier, ring)) accepted.add(ring); - } - return accepted; - } - - @Override - public MultiMap selectKeyRingsFromCollections(@Nonnull MultiMap keyRingCollections) { - MultiMap keyRings = new MultiMap<>(); - for (Map.Entry> entry : keyRingCollections.entrySet()) { - for (PGPSecretKeyRingCollection collection : entry.getValue()) { - keyRings.plus(entry.getKey(), selectKeyRingsFromCollection(entry.getKey(), collection)); - } - } - return keyRings; - } -} 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 deleted file mode 100644 index ffda8867..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/ExactUserId.java +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util.selection.keyring.impl; - -import java.util.List; - -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.pgpainless.key.util.KeyRingUtils; -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 - public boolean accept(String identifier, PGPPublicKeyRing keyRing) { - List userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keyRing.getPublicKey()); - for (String userId : userIds) { - if (userId.equals(identifier)) return true; - } - return false; - } - } - - /** - * {@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 - public boolean accept(String identifier, PGPSecretKeyRing keyRing) { - List userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keyRing.getPublicKey()); - for (String userId : userIds) { - if (userId.equals(identifier)) return true; - } - return false; - } - } -} 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 deleted file mode 100644 index 934f5577..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Whitelist.java +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util.selection.keyring.impl; - -import java.util.Map; -import java.util.Set; - -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -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; - - public PubRingSelectionStrategy(MultiMap whitelist) { - this.whitelist = whitelist; - } - - public PubRingSelectionStrategy(Map> whitelist) { - this(new MultiMap<>(whitelist)); - } - - @Override - public boolean accept(O identifier, PGPPublicKeyRing keyRing) { - Set whitelistedKeyIds = whitelist.get(identifier); - - if (whitelistedKeyIds == null) { - return false; - } - - return whitelistedKeyIds.contains(keyRing.getPublicKey().getKeyID()); - } - } - - /** - * {@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; - - public SecRingSelectionStrategy(MultiMap whitelist) { - this.whitelist = whitelist; - } - - public SecRingSelectionStrategy(Map> whitelist) { - this(new MultiMap<>(whitelist)); - } - - @Override - public boolean accept(O identifier, PGPSecretKeyRing keyRing) { - Set whitelistedKeyIds = whitelist.get(identifier); - - if (whitelistedKeyIds == null) { - return false; - } - - return whitelistedKeyIds.contains(keyRing.getPublicKey().getKeyID()); - } - - } -} 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 deleted file mode 100644 index d1929028..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/Wildcard.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util.selection.keyring.impl; - -import org.bouncycastle.openpgp.PGPPublicKeyRing; -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() { - - } - - public static class PubRingSelectionStrategy extends PublicKeyRingSelectionStrategy { - - @Override - public boolean accept(O identifier, PGPPublicKeyRing keyRing) { - return true; - } - } - - public static class SecRingSelectionStrategy extends SecretKeyRingSelectionStrategy { - - @Override - public boolean accept(O identifier, PGPSecretKeyRing keyRing) { - return true; - } - } -} 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 deleted file mode 100644 index edc38006..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/XMPP.java +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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 - public boolean accept(String jid, PGPPublicKeyRing keyRing) { - if (!jid.matches("^xmpp:.+$")) { - jid = "xmpp:" + jid; - } - return super.accept(jid, keyRing); - } - } - - /** - * {@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 - public boolean accept(String jid, PGPSecretKeyRing keyRing) { - if (!jid.matches("^xmpp:.+$")) { - jid = "xmpp:" + jid; - } - return super.accept(jid, keyRing); - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/package-info.java deleted file mode 100644 index 9b4bf1a4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/impl/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Implementations of Key Ring Selection Strategies. - */ -package org.pgpainless.util.selection.keyring.impl; diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/package-info.java deleted file mode 100644 index 3c93ce47..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Different Key Ring Selection Strategies. - */ -package org.pgpainless.util.selection.keyring; diff --git a/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt b/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt index 2763cb55..0dc6de40 100644 --- a/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt +++ b/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt @@ -14,7 +14,6 @@ import java.util.* * Since '0' is a special date value in the OpenPGP specification (e.g. '0' means no expiration for * expiration dates), this method will return 'null' if seconds is 0. * - * @param date date * @param seconds number of seconds to be added * @return date plus seconds or null if seconds is '0' */ @@ -25,9 +24,19 @@ fun Date.plusSeconds(seconds: Long): Date? { return if (seconds == 0L) null else Date(this.time + 1000 * seconds) } +/** This date in seconds since epoch. */ val Date.asSeconds: Long get() = time / 1000 +/** + * Return the number of seconds that would need to be added to this date in order to reach the later + * date. Note: This method requires the [later] date to be indeed greater or equal to this date, + * otherwise an [IllegalArgumentException] is thrown. + * + * @param later later date + * @return difference between this and [later] in seconds + * @throws IllegalArgumentException if later is not greater or equal to this date + */ fun Date.secondsTill(later: Date): Long { require(this <= later) { "Timestamp MUST be before the later timestamp." } return later.asSeconds - this.asSeconds diff --git a/pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt b/pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt index c6c318b3..c6f3039e 100644 --- a/pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt +++ b/pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt @@ -9,7 +9,7 @@ fun Long.openPgpKeyId(): String { return String.format("%016X", this).uppercase() } -/** Parse a Long form a 16 digit hex encoded OpenPgp key-ID. */ +/** Parse a Long from a 16 digit hex encoded OpenPgp key-ID. */ fun Long.Companion.fromOpenPgpKeyId(hexKeyId: String): Long { require("^[0-9A-Fa-f]{16}$".toRegex().matches(hexKeyId)) { "Provided long key-id does not match expected format. " + diff --git a/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyExtension.kt b/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyExtension.kt new file mode 100644 index 00000000..4831878a --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyExtension.kt @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.gnupg + +import org.bouncycastle.bcpg.S2K + +enum class GnuPGDummyExtension(val id: Int) { + + /** 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/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt b/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt new file mode 100644 index 00000000..eafe2cf2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt @@ -0,0 +1,206 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.gnupg + +import kotlin.experimental.and +import org.bouncycastle.bcpg.KeyIdentifier +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 org.bouncycastle.openpgp.api.OpenPGPKey +import org.gnupg.GnuPGDummyKeyUtil.KeyFilter +import org.pgpainless.key.SubkeyIdentifier + +/** + * 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](https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;hb=HEAD#l1489) + */ +class GnuPGDummyKeyUtil private constructor() { + + companion object { + + /** + * 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 [S2K.GNU_DUMMY_S2K] and protection mode + * [GnuPGDummyExtension.DIVERT_TO_CARD] + */ + @JvmStatic + fun getIdsOfKeysWithGnuPGS2KDivertedToCard( + secretKeys: PGPSecretKeyRing + ): Set = + secretKeys + .filter { + it.s2K?.type == S2K.GNU_DUMMY_S2K && + it.s2K?.protectionMode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD + } + .map { SubkeyIdentifier(secretKeys, it.keyIdentifier) } + .toSet() + + @JvmStatic fun modify(key: OpenPGPKey): Builder = modify(key.pgpSecretKeyRing) + + /** + * Modify the given [PGPSecretKeyRing]. + * + * @param secretKeys secret keys + * @return builder + */ + @JvmStatic fun modify(secretKeys: PGPSecretKeyRing) = Builder(secretKeys) + } + + class Builder(private val keys: PGPSecretKeyRing) { + + /** + * Remove all private keys that match the given [KeyFilter] from the key ring and replace + * them with GNU_DUMMY keys with S2K protection mode [GnuPGDummyExtension.NO_PRIVATE_KEY]. + * + * @param filter filter to select keys for removal + * @return modified key ring + */ + fun removePrivateKeys(filter: KeyFilter): PGPSecretKeyRing { + return replacePrivateKeys(GnuPGDummyExtension.NO_PRIVATE_KEY, null, filter) + } + + /** + * Remove all private keys that match the given [KeyFilter] from the key ring and replace + * them with GNU_DUMMY keys with S2K protection mode [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 + */ + fun divertPrivateKeysToCard(filter: KeyFilter): PGPSecretKeyRing { + return divertPrivateKeysToCard(filter, ByteArray(16)) + } + + /** + * Remove all private keys that match the given [KeyFilter] from the key ring and replace + * them with GNU_DUMMY keys with S2K protection mode [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 + */ + fun divertPrivateKeysToCard( + filter: KeyFilter, + cardSerialNumber: ByteArray? + ): PGPSecretKeyRing { + require(cardSerialNumber == null || cardSerialNumber.size <= 16) { + "Card serial number length cannot exceed 16 bytes." + } + return replacePrivateKeys(GnuPGDummyExtension.DIVERT_TO_CARD, cardSerialNumber, filter) + } + + private fun replacePrivateKeys( + extension: GnuPGDummyExtension, + serial: ByteArray?, + filter: KeyFilter + ): PGPSecretKeyRing { + val encodedSerial: ByteArray? = serial?.let { encodeSerial(it) } + val s2k: S2K = extensionToS2K(extension) + + return PGPSecretKeyRing( + keys + .map { + if (!filter.filter(it.keyIdentifier)) { + // Leave non-filtered key intact + it + } else { + val publicKeyPacket = it.publicKey.publicKeyPacket + // Convert key packet + val keyPacket: SecretKeyPacket = + if (it.isMasterKey) { + SecretKeyPacket( + publicKeyPacket, + 0, + SecretKeyPacket.USAGE_SHA1, + s2k, + null, + encodedSerial) + } else { + SecretSubkeyPacket( + publicKeyPacket, + 0, + SecretKeyPacket.USAGE_SHA1, + s2k, + null, + encodedSerial) + } + PGPSecretKey(keyPacket, it.publicKey) + } + } + .toList()) + } + + private fun encodeSerial(serial: ByteArray): ByteArray { + val encoded = ByteArray(serial.size + 1) + encoded[0] = serial.size.toByte().and(0xff.toByte()) + System.arraycopy(serial, 0, encoded, 1, serial.size) + return encoded + } + + private fun extensionToS2K(extension: GnuPGDummyExtension): S2K { + return S2K.gnuDummyS2K( + if (extension == GnuPGDummyExtension.DIVERT_TO_CARD) + S2K.GNUDummyParams.divertToCard() + else S2K.GNUDummyParams.noPrivateKey()) + } + } + + /** Filter for selecting keys. */ + fun interface KeyFilter { + fun filter(keyIdentifier: KeyIdentifier): Boolean + + companion object { + + /** + * Select any key. + * + * @return filter + */ + @JvmStatic fun any(): KeyFilter = KeyFilter { true } + + /** + * Select only the given keyId. + * + * @param onlyKeyId only acceptable key id + * @return filter + */ + @JvmStatic + @Deprecated("Use only(KeyIdentifier) instead.") + fun only(onlyKeyId: Long) = only(KeyIdentifier(onlyKeyId)) + + /** + * Select only the given keyIdentifier. + * + * @param onlyKeyIdentifier only acceptable key identifier + * @return filter + */ + @JvmStatic + fun only(onlyKeyIdentifier: KeyIdentifier) = KeyFilter { + it.matchesExplicit(onlyKeyIdentifier) + } + + /** + * Select all keyIds which are contained in the given set of ids. + * + * @param ids set of acceptable keyIds + * @return filter + */ + @JvmStatic fun selected(ids: Collection) = KeyFilter { ids.contains(it) } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt index 81d9e605..602510cb 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt @@ -4,12 +4,27 @@ package org.pgpainless +import java.io.ByteArrayOutputStream import java.io.OutputStream import java.util.* +import org.bouncycastle.bcpg.ArmoredOutputStream +import org.bouncycastle.bcpg.BCPGOutputStream +import org.bouncycastle.bcpg.PacketFormat import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPSecretKeyRing import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.api.OpenPGPApi +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPImplementation +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.api.OpenPGPKeyGenerator +import org.bouncycastle.openpgp.api.OpenPGPKeyReader +import org.bouncycastle.openpgp.api.OpenPGPSignature +import org.bouncycastle.openpgp.api.bc.BcOpenPGPApi +import org.pgpainless.algorithm.OpenPGPKeyVersion +import org.pgpainless.bouncycastle.PolicyAdapter +import org.pgpainless.bouncycastle.extensions.setAlgorithmSuite import org.pgpainless.decryption_verification.DecryptionBuilder import org.pgpainless.encryption_signing.EncryptionBuilder import org.pgpainless.key.certification.CertifyCertificate @@ -18,34 +33,270 @@ import org.pgpainless.key.generation.KeyRingTemplates import org.pgpainless.key.info.KeyRingInfo import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor import org.pgpainless.key.parsing.KeyRingReader -import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.policy.Policy import org.pgpainless.util.ArmorUtils -class PGPainless private constructor() { +/** + * Main entry point to the PGPainless OpenPGP API. Historically, this class was used through static + * factory methods only, and configuration was done using the Singleton pattern. However, now it is + * recommended to instantiate the API and apply configuration on a per-instance manner. The benefit + * of this being that you can have multiple differently configured instances at the same time. + * + * @param implementation OpenPGP Implementation - either BCs lightweight + * [org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation] or JCAs + * [org.bouncycastle.openpgp.api.jcajce.JcaOpenPGPImplementation]. + * @param algorithmPolicy policy, deciding acceptable algorithms + */ +class PGPainless( + val implementation: OpenPGPImplementation = OpenPGPImplementation.getInstance(), + val algorithmPolicy: Policy = Policy() +) { + + constructor( + algorithmPolicy: Policy + ) : this(OpenPGPImplementation.getInstance(), algorithmPolicy) + + private val api: OpenPGPApi + + init { + implementation.setPolicy( + PolicyAdapter(algorithmPolicy)) // adapt PGPainless' Policy to BCs OpenPGPPolicy + api = BcOpenPGPApi(implementation) + } + + @JvmOverloads + fun toAsciiArmor( + certOrKey: OpenPGPCertificate, + packetFormat: PacketFormat = PacketFormat.ROUNDTRIP + ): String { + val armorBuilder = ArmoredOutputStream.builder().clearHeaders() + ArmorUtils.keyToHeader(certOrKey.primaryKey.pgpPublicKey) + .getOrDefault(ArmorUtils.HEADER_COMMENT, setOf()) + .forEach { armorBuilder.addComment(it) } + return certOrKey.toAsciiArmoredString(packetFormat, armorBuilder) + } + + @JvmOverloads + fun toAsciiArmor( + signature: OpenPGPSignature, + packetFormat: PacketFormat = PacketFormat.ROUNDTRIP + ): String { + val armorBuilder = ArmoredOutputStream.builder().clearHeaders() + armorBuilder.addComment(signature.keyIdentifier.toPrettyPrint()) + return signature.toAsciiArmoredString(packetFormat, armorBuilder) + } + + @JvmOverloads + fun toAsciiArmor( + signature: PGPSignature, + packetFormat: PacketFormat = PacketFormat.ROUNDTRIP + ): String { + val armorBuilder = ArmoredOutputStream.builder().clearHeaders() + OpenPGPSignature.getMostExpressiveIdentifier(signature.keyIdentifiers)?.let { + armorBuilder.addComment(it.toPrettyPrint()) + } + val bOut = ByteArrayOutputStream() + val aOut = armorBuilder.build(bOut) + val pOut = BCPGOutputStream(aOut, packetFormat) + signature.encode(pOut) + pOut.close() + aOut.close() + return bOut.toString() + } + + /** + * Generate a new [OpenPGPKey] from predefined templates. + * + * @param version [OpenPGPKeyVersion], defaults to [OpenPGPKeyVersion.v4] + * @param creationTime of the key, defaults to now + * @return [KeyRingTemplates] api + */ + @JvmOverloads + fun generateKey( + version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4, + creationTime: Date = Date() + ): KeyRingTemplates = KeyRingTemplates(version, creationTime, this) + + /** + * Build a fresh, custom [OpenPGPKey] using PGPainless' API. + * + * @param version [OpenPGPKeyVersion], defaults to [OpenPGPKeyVersion.v4] + * @return [KeyRingBuilder] api + */ + @JvmOverloads + fun buildKey(version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4): KeyRingBuilder = + KeyRingBuilder(version, this) + + /** + * Build a fresh, custom [OpenPGPKey] using BCs new API. + * + * @param version [OpenPGPKeyVersion], defaults to [OpenPGPKeyVersion.v4] + * @param creationTime creation time of the key, defaults to now + * @return [OpenPGPKeyGenerator] api + */ + @JvmOverloads + fun _buildKey( + version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4, + creationTime: Date = Date() + ): OpenPGPKeyGenerator = + OpenPGPKeyGenerator( + implementation, + version.numeric, + algorithmPolicy.keyProtectionSettings.aead, + creationTime) + .setAlgorithmSuite(algorithmPolicy.keyGenerationAlgorithmSuite) + + /** + * Inspect an [OpenPGPKey] or [OpenPGPCertificate], gaining convenient access to its properties. + * + * @param keyOrCertificate [OpenPGPKey] or [OpenPGPCertificate] + * @param referenceTime reference time for evaluation + * @return [KeyRingInfo] wrapper + */ + @JvmOverloads + fun inspect(keyOrCertificate: OpenPGPCertificate, referenceTime: Date = Date()): KeyRingInfo = + KeyRingInfo(keyOrCertificate, this, referenceTime) + + /** + * Modify an [OpenPGPKey], adding new components and signatures. This API can be used to add new + * subkeys, user-ids or user-attributes to the key, extend or alter its expiration time, revoke + * individual components of the entire certificate, etc. + * + * @param key key to modify + * @param referenceTime timestamp for modifications + * @return [SecretKeyRingEditor] api + */ + @JvmOverloads + fun modify(key: OpenPGPKey, referenceTime: Date = Date()): SecretKeyRingEditor = + SecretKeyRingEditor(key, this, referenceTime) + + /** + * Parse [OpenPGPKey]/[OpenPGPCertificate] material from binary or ASCII armored encoding. + * + * @return [OpenPGPKeyReader] api + */ + fun readKey(): OpenPGPKeyReader = api.readKeyOrCertificate() + + /** + * Convert a [PGPSecretKeyRing] into an [OpenPGPKey]. + * + * @param secretKeyRing mid-level API [PGPSecretKeyRing] object + * @return high-level API [OpenPGPKey] object + */ + fun toKey(secretKeyRing: PGPSecretKeyRing): OpenPGPKey = + OpenPGPKey(secretKeyRing, implementation) + + /** + * Convert a [PGPPublicKeyRing] into an [OpenPGPCertificate]. + * + * @param certificate mid-level API [PGPSecretKeyRing] object + * @return high-level API [OpenPGPCertificate] object + */ + fun toCertificate(certificate: PGPPublicKeyRing): OpenPGPCertificate = + OpenPGPCertificate(certificate, implementation) + + /** + * Depending on the type, convert either a [PGPSecretKeyRing] into an [OpenPGPKey] or a + * [PGPPublicKeyRing] into an [OpenPGPCertificate]. + * + * @param keyOrCertificate [PGPKeyRing], either [PGPSecretKeyRing] or [PGPPublicKeyRing] + * @return depending on the type of [keyOrCertificate], either an [OpenPGPKey] or + * [OpenPGPCertificate] + */ + fun toKeyOrCertificate(keyOrCertificate: PGPKeyRing): OpenPGPCertificate = + when (keyOrCertificate) { + is PGPSecretKeyRing -> toKey(keyOrCertificate) + is PGPPublicKeyRing -> toCertificate(keyOrCertificate) + else -> + throw IllegalArgumentException( + "Unexpected PGPKeyRing subclass: ${keyOrCertificate.javaClass.name}") + } + + /** + * Merge two copies of an [OpenPGPCertificate] into a single copy. This method can be used to + * import new third-party signatures into a certificate. + * + * @param originalCopy local copy of the certificate + * @param updatedCopy copy of the same certificate, potentially carrying new signatures and + * components + * @return merged [OpenPGPCertificate] + */ + fun mergeCertificate( + originalCopy: OpenPGPCertificate, + updatedCopy: OpenPGPCertificate + ): OpenPGPCertificate { + return OpenPGPCertificate.join(originalCopy, updatedCopy) + } + + /** + * Generate an encrypted and/or signed OpenPGP message. + * + * @return [EncryptionBuilder] api + */ + fun generateMessage(): EncryptionBuilder = EncryptionBuilder(this) + + /** + * Process an OpenPGP message. This method attempts decryption of encrypted messages and + * performs signature verification. + * + * @return [DecryptionBuilder] api + */ + fun processMessage(): DecryptionBuilder = DecryptionBuilder(this) + + /** + * Create certification signatures on third-party [OpenPGPCertificates][OpenPGPCertificate]. + * + * @return [CertifyCertificate] api + */ + fun generateCertification(): CertifyCertificate = CertifyCertificate(this) companion object { + @Volatile private var instance: PGPainless? = null + + @JvmStatic + fun getInstance(): PGPainless = + instance ?: synchronized(this) { instance ?: PGPainless().also { instance = it } } + + @JvmStatic + fun setInstance(api: PGPainless) { + instance = api + } + /** - * Generate a fresh OpenPGP key ring from predefined templates. + * Generate a fresh [OpenPGPKey] from predefined templates. * * @return templates */ - @JvmStatic fun generateKeyRing() = KeyRingTemplates() + @JvmStatic + @JvmOverloads + @Deprecated( + "Call .generateKey() on an instance of PGPainless instead.", + replaceWith = ReplaceWith("generateKey(version)")) + fun generateKeyRing(version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4): KeyRingTemplates = + getInstance().generateKey(version) /** * Build a custom OpenPGP key ring. * * @return builder */ - @JvmStatic fun buildKeyRing() = KeyRingBuilder() + @JvmStatic + @JvmOverloads + @Deprecated( + "Call buildKey() on an instance of PGPainless instead.", + replaceWith = ReplaceWith("buildKey(version)")) + fun buildKeyRing(version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4): KeyRingBuilder = + getInstance().buildKey(version) /** * Read an existing OpenPGP key ring. * * @return builder */ - @JvmStatic fun readKeyRing() = KeyRingReader() + @JvmStatic + @Deprecated("Use readKey() instead.", replaceWith = ReplaceWith("readKey()")) + fun readKeyRing(): KeyRingReader = KeyRingReader() /** * Extract a public key certificate from a secret key. @@ -54,8 +305,9 @@ class PGPainless private constructor() { * @return public key certificate */ @JvmStatic - fun extractCertificate(secretKey: PGPSecretKeyRing) = - KeyRingUtils.publicKeyRingFrom(secretKey) + @Deprecated("Use .toKey() and then .toCertificate() instead.") + fun extractCertificate(secretKey: PGPSecretKeyRing): PGPPublicKeyRing = + secretKey.toCertificate() /** * Merge two copies of the same certificate (e.g. an old copy, and one retrieved from a key @@ -67,8 +319,12 @@ class PGPainless private constructor() { * @throws PGPException in case of an error */ @JvmStatic - fun mergeCertificate(originalCopy: PGPPublicKeyRing, updatedCopy: PGPPublicKeyRing) = - PGPPublicKeyRing.join(originalCopy, updatedCopy) + @Deprecated( + "Use mergeCertificate() instead.", replaceWith = ReplaceWith("mergeCertificate()")) + fun mergeCertificate( + originalCopy: PGPPublicKeyRing, + updatedCopy: PGPPublicKeyRing + ): PGPPublicKeyRing = PGPPublicKeyRing.join(originalCopy, updatedCopy) /** * Wrap a key or certificate in ASCII armor. @@ -78,9 +334,14 @@ class PGPainless private constructor() { * @throws IOException in case of an error during the armoring process */ @JvmStatic - fun asciiArmor(key: PGPKeyRing) = - if (key is PGPSecretKeyRing) ArmorUtils.toAsciiArmoredString(key) - else ArmorUtils.toAsciiArmoredString(key as PGPPublicKeyRing) + fun asciiArmor(key: PGPKeyRing): String = + getInstance().toAsciiArmor(getInstance().toKeyOrCertificate(key)) + + @JvmStatic + @Deprecated( + "Call getInstance().toAsciiArmor(cert) instead.", + replaceWith = ReplaceWith("getInstance().toAsciiArmor(cert)")) + fun asciiArmor(cert: OpenPGPCertificate): String = getInstance().toAsciiArmor(cert) /** * Wrap a key of certificate in ASCII armor and write the result into the given @@ -105,7 +366,10 @@ class PGPainless private constructor() { * @throws IOException in case of an error during the armoring process */ @JvmStatic - fun asciiArmor(signature: PGPSignature) = ArmorUtils.toAsciiArmoredString(signature) + @Deprecated( + "Call toAsciiArmor(signature) on an instance of PGPainless instead.", + replaceWith = ReplaceWith("getInstance().toAsciiArmor(signature)")) + fun asciiArmor(signature: PGPSignature): String = getInstance().toAsciiArmor(signature) /** * Create an [EncryptionBuilder], which can be used to encrypt and/or sign data using @@ -113,7 +377,11 @@ class PGPainless private constructor() { * * @return builder */ - @JvmStatic fun encryptAndOrSign() = EncryptionBuilder() + @Deprecated( + "Call generateMessage() on an instance of PGPainless instead.", + replaceWith = ReplaceWith("generateMessage()")) + @JvmStatic + fun encryptAndOrSign(): EncryptionBuilder = getInstance().generateMessage() /** * Create a [DecryptionBuilder], which can be used to decrypt and/or verify data using @@ -121,7 +389,11 @@ class PGPainless private constructor() { * * @return builder */ - @JvmStatic fun decryptAndOrVerify() = DecryptionBuilder() + @Deprecated( + "Call processMessage() on an instance of PGPainless instead.", + replaceWith = ReplaceWith("processMessage()")) + @JvmStatic + fun decryptAndOrVerify(): DecryptionBuilder = getInstance().processMessage() /** * Make changes to a secret key at the given reference time. This method can be used to @@ -137,35 +409,56 @@ class PGPainless private constructor() { */ @JvmStatic @JvmOverloads - fun modifyKeyRing(secretKey: PGPSecretKeyRing, referenceTime: Date = Date()) = - SecretKeyRingEditor(secretKey, referenceTime) + fun modifyKeyRing( + secretKey: PGPSecretKeyRing, + referenceTime: Date = Date() + ): SecretKeyRingEditor = getInstance().modify(getInstance().toKey(secretKey), referenceTime) /** * Quickly access information about a [org.bouncycastle.openpgp.PGPPublicKeyRing] / * [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 key key ring * @param referenceTime date of inspection * @return access object */ @JvmStatic @JvmOverloads - fun inspectKeyRing(key: PGPKeyRing, referenceTime: Date = Date()) = - KeyRingInfo(key, referenceTime) + @Deprecated( + "Use inspect(key) on an instance of PGPainless instead.", + replaceWith = ReplaceWith("inspect(key)")) + fun inspectKeyRing(key: PGPKeyRing, referenceTime: Date = Date()): KeyRingInfo = + getInstance().inspect(getInstance().toKeyOrCertificate(key), referenceTime) + + @JvmStatic + @JvmOverloads + @Deprecated( + "Use inspect(key) on an instance of PGPainless instead.", + replaceWith = ReplaceWith("inspect(key)")) + fun inspectKeyRing(key: OpenPGPCertificate, referenceTime: Date = Date()): KeyRingInfo = + getInstance().inspect(key, referenceTime) /** * Access, and make changes to PGPainless policy on acceptable/default algorithms etc. * * @return policy */ - @JvmStatic fun getPolicy() = Policy.getInstance() + @Deprecated( + "Use PGPainless.getInstance().getAlgorithmPolicy() instead.", + replaceWith = ReplaceWith("getInstance().algorithmPolicy")) + @JvmStatic + fun getPolicy(): Policy = getInstance().algorithmPolicy /** * Create different kinds of signatures on other keys. * * @return builder */ - @JvmStatic fun certify() = CertifyCertificate() + @Deprecated( + "Call .generateCertification() on an instance of PGPainless instead.", + replaceWith = ReplaceWith("generateCertification()")) + @JvmStatic + fun certify(): CertifyCertificate = getInstance().generateCertification() } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt index 6a3a6214..6e06abe3 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt @@ -4,35 +4,75 @@ package org.pgpainless.algorithm +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism + +/** + * AEAD Algorithm. + * + * @param algorithmId numeric algorithm id + * @param ivLength length of the initialization vector + * @param tagLength length of the tag + * @see + * [RFC9580 - AEAD Algorithms](https://www.rfc-editor.org/rfc/rfc9580.html#name-aead-algorithms) + */ enum class AEADAlgorithm(val algorithmId: Int, val ivLength: Int, val tagLength: Int) { /** * Encrypt-then-Authenticate-then-Translate mode. - * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-eax-mode + * + * @see [RFC9580 - EAX Mode](https://www.rfc-editor.org/rfc/rfc9580.html#name-eax-mode) */ EAX(1, 16, 16), /** * Offset-Codebook mode. OCB is mandatory to implement in crypto-refresh. Favored by GnuPG. Is * not yet FIPS compliant, but supported by most implementations and therefore favorable. - * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-ocb-mode + * + * @see [RFC9580 - OCB Mode](https://www.rfc-editor.org/rfc/rfc9580.html#name-ocb-mode) */ OCB(2, 15, 16), /** * Galois/Counter-Mode. GCM is controversial. Some say it is hard to get right. Some * implementations like GnuPG therefore avoid it. May be necessary to achieve FIPS compliance. - * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-gcm-mode + * + * @see [RFC9580 - GCM Mode](https://www.rfc-editor.org/rfc/rfc9580.html#name-gcm-mode) */ GCM(3, 12, 16), ; + /** + * Return a [MessageEncryptionMechanism] instance representing AEAD using this algorithm and the + * given [SymmetricKeyAlgorithm]. + * + * @param ciphermode symmetric key algorithm + * @return MessageEncryptionMechanism representing aead(this, ciphermode) + */ + fun toMechanism(ciphermode: SymmetricKeyAlgorithm): MessageEncryptionMechanism = + MessageEncryptionMechanism.aead(ciphermode.algorithmId, this.algorithmId) + companion object { + + /** + * Parse an [AEADAlgorithm] from an algorithm id. If no matching [AEADAlgorithm] is known, + * return `null`. + * + * @param id algorithm id + * @return aeadAlgorithm or null + */ @JvmStatic fun fromId(id: Int): AEADAlgorithm? { return values().firstOrNull { algorithm -> algorithm.algorithmId == id } } + /** + * Parse an [AEADAlgorithm] from an algorithm id. If no matching [AEADAlgorithm] is known, + * throw a [NoSuchElementException]. + * + * @param id algorithm id + * @return aeadAlgorithm + * @throws NoSuchElementException for unknown algorithm ids + */ @JvmStatic fun requireFromId(id: Int): AEADAlgorithm { return fromId(id) ?: throw NoSuchElementException("No AEADAlgorithm found for id $id") diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADCipherMode.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADCipherMode.kt new file mode 100644 index 00000000..5cd29e6c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADCipherMode.kt @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites.Combination + +data class AEADCipherMode(val aeadAlgorithm: AEADAlgorithm, val ciphermode: SymmetricKeyAlgorithm) { + constructor( + combination: Combination + ) : this( + AEADAlgorithm.requireFromId(combination.aeadAlgorithm), + SymmetricKeyAlgorithm.requireFromId(combination.symmetricAlgorithm)) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AlgorithmSuite.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AlgorithmSuite.kt index 867bf1b8..71a618a2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AlgorithmSuite.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AlgorithmSuite.kt @@ -4,18 +4,91 @@ package org.pgpainless.algorithm -class AlgorithmSuite( - symmetricKeyAlgorithms: List, - hashAlgorithms: List, - compressionAlgorithms: List +class AlgorithmSuite +private constructor( + val symmetricKeyAlgorithms: Set?, + val hashAlgorithms: Set?, + val compressionAlgorithms: Set?, + val aeadAlgorithms: Set?, + val features: Set? ) { - val symmetricKeyAlgorithms: Set = symmetricKeyAlgorithms.toSet() - val hashAlgorithms: Set = hashAlgorithms.toSet() - val compressionAlgorithms: Set = compressionAlgorithms.toSet() + constructor( + symmetricKeyAlgorithms: List?, + hashAlgorithms: List?, + compressionAlgorithms: List?, + aeadAlgorithms: List?, + features: List? + ) : this( + symmetricKeyAlgorithms?.toSet(), + hashAlgorithms?.toSet(), + compressionAlgorithms?.toSet(), + aeadAlgorithms?.toSet(), + features?.toSet()) + + fun modify(): Builder = Builder(this) + + class Builder(suite: AlgorithmSuite? = null) { + private var symmetricKeyAlgorithms: Set? = + suite?.symmetricKeyAlgorithms + private var hashAlgorithms: Set? = suite?.hashAlgorithms + private var compressionAlgorithms: Set? = suite?.compressionAlgorithms + private var aeadAlgorithms: Set? = suite?.aeadAlgorithms + private var features: Set? = suite?.features + + fun overrideSymmetricKeyAlgorithms( + vararg symmetricKeyAlgorithms: SymmetricKeyAlgorithm + ): Builder = overrideSymmetricKeyAlgorithms(symmetricKeyAlgorithms.toSet()) + + fun overrideSymmetricKeyAlgorithms( + symmetricKeyAlgorithms: Collection? + ): Builder = apply { this.symmetricKeyAlgorithms = symmetricKeyAlgorithms?.toSet() } + + fun overrideHashAlgorithms(vararg hashAlgorithms: HashAlgorithm): Builder = + overrideHashAlgorithms(hashAlgorithms.toSet()) + + fun overrideHashAlgorithms(hashAlgorithms: Collection?): Builder = apply { + this.hashAlgorithms = hashAlgorithms?.toSet() + } + + fun overrideCompressionAlgorithms( + vararg compressionAlgorithms: CompressionAlgorithm + ): Builder = overrideCompressionAlgorithms(compressionAlgorithms.toSet()) + + fun overrideCompressionAlgorithms( + compressionAlgorithms: Collection? + ): Builder = apply { this.compressionAlgorithms = compressionAlgorithms?.toSet() } + + fun overrideAeadAlgorithms(vararg aeadAlgorithms: AEADCipherMode): Builder = + overrideAeadAlgorithms(aeadAlgorithms.toSet()) + + fun overrideAeadAlgorithms(aeadAlgorithms: Collection?): Builder = apply { + this.aeadAlgorithms = aeadAlgorithms?.toSet() + } + + fun overrideFeatures(vararg features: Feature): Builder = overrideFeatures(features.toSet()) + + fun overrideFeatures(features: Collection?): Builder = apply { + this.features = features?.toSet() + } + + fun build(): AlgorithmSuite { + return AlgorithmSuite( + symmetricKeyAlgorithms, + hashAlgorithms, + compressionAlgorithms, + aeadAlgorithms, + features) + } + } companion object { + @JvmStatic + fun emptyBuilder(): Builder { + return Builder() + } + @JvmStatic val defaultSymmetricKeyAlgorithms = listOf( @@ -39,9 +112,26 @@ class AlgorithmSuite( CompressionAlgorithm.ZIP, CompressionAlgorithm.UNCOMPRESSED) + @JvmStatic + val defaultAEADAlgorithmSuites = + listOf( + AEADCipherMode(AEADAlgorithm.EAX, SymmetricKeyAlgorithm.AES_256), + AEADCipherMode(AEADAlgorithm.OCB, SymmetricKeyAlgorithm.AES_256), + AEADCipherMode(AEADAlgorithm.GCM, SymmetricKeyAlgorithm.AES_256), + AEADCipherMode(AEADAlgorithm.EAX, SymmetricKeyAlgorithm.AES_192), + AEADCipherMode(AEADAlgorithm.EAX, SymmetricKeyAlgorithm.AES_192)) + + @JvmStatic + val defaultFeatures = + listOf(Feature.MODIFICATION_DETECTION, Feature.MODIFICATION_DETECTION_2) + @JvmStatic val defaultAlgorithmSuite = AlgorithmSuite( - defaultSymmetricKeyAlgorithms, defaultHashAlgorithms, defaultCompressionAlgorithms) + defaultSymmetricKeyAlgorithms, + defaultHashAlgorithms, + defaultCompressionAlgorithms, + defaultAEADAlgorithmSuites, + defaultFeatures) } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/EncryptionPurpose.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/EncryptionPurpose.kt index 1b4bbe6e..2ba984e7 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/EncryptionPurpose.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/EncryptionPurpose.kt @@ -4,11 +4,13 @@ package org.pgpainless.algorithm -enum class EncryptionPurpose { +import org.bouncycastle.bcpg.sig.KeyFlags + +enum class EncryptionPurpose(val code: Int) { /** The stream will encrypt communication that goes over the wire. E.g. EMail, Chat... */ - COMMUNICATIONS, + COMMUNICATIONS(KeyFlags.ENCRYPT_COMMS), /** The stream will encrypt data at rest. E.g. Encrypted backup... */ - STORAGE, + STORAGE(KeyFlags.ENCRYPT_STORAGE), /** The stream will use keys with either flags to encrypt the data. */ - ANY + ANY(KeyFlags.ENCRYPT_COMMS or KeyFlags.ENCRYPT_STORAGE) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/HashAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/HashAlgorithm.kt index 3360e7fe..686666cf 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/HashAlgorithm.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/HashAlgorithm.kt @@ -11,15 +11,19 @@ package org.pgpainless.algorithm */ enum class HashAlgorithm(val algorithmId: Int, val algorithmName: String) { + // 0 is reserved @Deprecated("MD5 is deprecated") MD5(1, "MD5"), SHA1(2, "SHA1"), RIPEMD160(3, "RIPEMD160"), + // 4 - 7 are reserved SHA256(8, "SHA256"), SHA384(9, "SHA384"), SHA512(10, "SHA512"), SHA224(11, "SHA224"), SHA3_256(12, "SHA3-256"), + // 13 is reserved SHA3_512(14, "SHA3-512"), + // 100 - 110 are private / experimental ; companion object { @@ -57,13 +61,15 @@ enum class HashAlgorithm(val algorithmId: Int, val algorithmName: String) { * for a list of algorithms and names. * * @param name text name - * @return enum value + * @return enum value or null */ @JvmStatic fun fromName(name: String): HashAlgorithm? { return name.uppercase().let { algoName -> + // find value where it.algorithmName == ALGO-NAME values().firstOrNull { it.algorithmName == algoName } - ?: values().firstOrNull { it.algorithmName == algoName.replace("-", "") } + // else, find value where it.algorithmName == ALGONAME + ?: values().firstOrNull { it.algorithmName == algoName.replace("-", "") } } } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPGPKeyVersion.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPGPKeyVersion.kt new file mode 100644 index 00000000..f053dd06 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPGPKeyVersion.kt @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +enum class OpenPGPKeyVersion(val numeric: Int) { + // Version 2 packets are identical in format to Version 3 packets, but are generated by + // PGP 2.5 or before. V2 packets are deprecated and they MUST NOT be generated. + + /** + * Version 3 packets were first generated by PGP 2.6. Version 3 keys are deprecated. They + * contain three weaknesses. First, it is relatively easy to construct a version 3 key that has + * the same Key ID as any other key because the Key ID is simply the low 64 bits of the public + * modulus. Second, because the fingerprint of a version 3 key hashes the key material, but not + * its length, there is an increased opportunity for fingerprint collisions. Third, there are + * weaknesses in the MD5 hash algorithm that cause developers to prefer other algorithms. + */ + @Deprecated("V3 keys are deprecated.") v3(3), + + /** + * Version 4 packets are used in RFC2440, RFC4880, RFC9580. The version 4 format is widely + * supported by various implementations. + * + * @see [RFC2440](https://www.rfc-editor.org/rfc/rfc2440.html) + * @see [RFC4880](https://www.rfc-editor.org/rfc/rfc4880.html) + * @see [RFC9580](https://www.rfc-editor.org/rfc/rfc9580.html) + */ + v4(4), + + /** + * "V5"-keys are introduced in the LibrePGP document. These are NOT OpenPGP keys and are + * primarily supported by GnuPG and RNP. + * + * @see [LibrePGP](https://datatracker.ietf.org/doc/draft-koch-librepgp/) + */ + librePgp(5), + + /** + * Version 6 packets are introduced in RFC9580. The version 6 format is similar to the version 4 + * format except for the addition of a count for the key material. This count helps parsing + * Secret Key packets (which are an extension of the Public Key packet format) in the case of an + * unknown algorithm. In addition, fingerprints of version 6 keys are calculated differently + * from version 4 keys, preventing the KOpenPGP attack. + * + * @see [RFC9580](https://www.rfc-editor.org/rfc/rfc9580.html) + */ + v6(6), + ; + + companion object { + @JvmStatic + fun from(numeric: Int): OpenPGPKeyVersion { + return values().find { numeric == it.numeric } + ?: throw IllegalArgumentException("Unknown key version $numeric") + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt index b8fc6836..a5da51b5 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt @@ -4,76 +4,105 @@ package org.pgpainless.algorithm +import org.bouncycastle.bcpg.PublicKeyUtils + /** - * Enumeration of public key algorithms as defined in RFC4880. + * Enumeration of public key algorithms as defined in RFC4880, RFC9580, Persistent Symmetric Keys. * - * See [RFC4880: Public-Key Algorithms](https://tools.ietf.org/html/rfc4880#section-9.1) + * @see [RFC4880: Public-Key Algorithms](https://tools.ietf.org/html/rfc4880#section-9.1) + * @see + * [RFC9580: Public-Key Algorithms](https://www.rfc-editor.org/rfc/rfc9580.html#name-public-key-algorithms) + * @see + * [Persistent Symmetric Keys in OpenPGP](https://www.ietf.org/archive/id/draft-ietf-openpgp-persistent-symmetric-keys-01.html#name-persistent-symmetric-key-al) */ -enum class PublicKeyAlgorithm( - val algorithmId: Int, - val signingCapable: Boolean, - val encryptionCapable: Boolean -) { +enum class PublicKeyAlgorithm(val algorithmId: Int) { + + // RFC4880 /** RSA capable of encryption and signatures. */ - RSA_GENERAL(1, true, true), + RSA_GENERAL(1), /** * RSA with usage encryption. * - * @deprecated see Deprecation - * notice + * @deprecated see [Deprecation notice](https://tools.ietf.org/html/rfc4880#section-13.5) */ @Deprecated("RSA_ENCRYPT is deprecated in favor of RSA_GENERAL", ReplaceWith("RSA_GENERAL")) - RSA_ENCRYPT(2, false, true), + RSA_ENCRYPT(2), /** * RSA with usage of creating signatures. * - * @deprecated see Deprecation - * notice + * @deprecated see [Deprecation notice](https://tools.ietf.org/html/rfc4880#section-13.5) */ @Deprecated("RSA_SIGN is deprecated in favor of RSA_GENERAL", ReplaceWith("RSA_GENERAL")) - RSA_SIGN(3, true, false), + RSA_SIGN(3), /** ElGamal with usage encryption. */ - ELGAMAL_ENCRYPT(16, false, true), + ELGAMAL_ENCRYPT(16), /** Digital Signature Algorithm. */ - DSA(17, true, false), + DSA(17), /** Elliptic Curve Diffie-Hellman. */ - ECDH(18, false, true), + ECDH(18), /** Elliptic Curve Digital Signature Algorithm. */ - ECDSA(19, true, false), + ECDSA(19), /** * ElGamal General. * - * @deprecated see Deprecation - * notice + * @deprecated see [Deprecation notice](https://tools.ietf.org/html/rfc4880#section-13.8) */ - @Deprecated("ElGamal is deprecated") ELGAMAL_GENERAL(20, true, true), + @Deprecated("ElGamal is deprecated") ELGAMAL_GENERAL(20), /** Diffie-Hellman key exchange algorithm. */ - DIFFIE_HELLMAN(21, false, true), + DIFFIE_HELLMAN(21), /** Digital Signature Algorithm based on twisted Edwards Curves. */ - EDDSA_LEGACY(22, true, false), + EDDSA_LEGACY(22), + + // RFC9580 /** X25519 encryption algorithm. */ - X25519(25, false, true), + X25519(25), /** X448 encryption algorithm. */ - X448(26, false, true), + X448(26), /** Ed25519 signature algorithm. */ - ED25519(27, true, false), + ED25519(27), /** Ed448 signature algorithm. */ - ED448(28, true, false), - ; + ED448(28), + + // Persistent Symmetric Keys in OpenPGP + + /** + * AEAD can be used as a persistent key symmetric encryption algorithm for message encryption. + * + * @see + * [Persistent Symmetric Keys in OpenPGP](https://datatracker.ietf.org/doc/draft-ietf-openpgp-persistent-symmetric-keys/) + */ + AEAD(128) { + override val signingCapable = false + override val encryptionCapable = true + }, + + /** + * HMAC can be used as a persistent key symmetric signing algorithm for message signing. + * + * @see + * [Persistent Symmetric Keys in OpenPGP](https://datatracker.ietf.org/doc/draft-ietf-openpgp-persistent-symmetric-keys/) + */ + HMAC(129) { + override val signingCapable = true + override val encryptionCapable = false + }; + + open val signingCapable: Boolean = PublicKeyUtils.isSigningAlgorithm(algorithmId) + open val encryptionCapable: Boolean = PublicKeyUtils.isEncryptionAlgorithm(algorithmId) fun isSigningCapable(): Boolean = signingCapable diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/CompressionAlgorithmNegotiator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/CompressionAlgorithmNegotiator.kt new file mode 100644 index 00000000..ab3128db --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/CompressionAlgorithmNegotiator.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm.negotiation + +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.policy.Policy + +fun interface CompressionAlgorithmNegotiator { + + /** + * Negotiate a suitable [CompressionAlgorithm] by taking into consideration the [Policy], a + * user-provided [compressionAlgorithmOverride] and the users set of [orderedPreferences]. + * + * @param policy implementations [Policy] + * @param compressionAlgorithmOverride user-provided [CompressionAlgorithm] override. + * @param orderedPreferences preferred compression algorithms taken from the users certificate + * @return negotiated [CompressionAlgorithm] + */ + fun negotiate( + policy: Policy, + compressionAlgorithmOverride: CompressionAlgorithm?, + orderedPreferences: Set? + ): CompressionAlgorithm + + companion object { + + /** + * Static negotiation of compression algorithms. This implementation discards compression + * algorithm preferences and instead either returns the non-null algorithm override, + * otherwise the policies default hash algorithm. + * + * @return delegate implementation + */ + @JvmStatic + fun staticNegotiation(): CompressionAlgorithmNegotiator = + CompressionAlgorithmNegotiator { policy, override, _ -> + override ?: policy.compressionAlgorithmPolicy.defaultCompressionAlgorithm + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/EncryptionMechanismNegotiator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/EncryptionMechanismNegotiator.kt new file mode 100644 index 00000000..238e3212 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/EncryptionMechanismNegotiator.kt @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm.negotiation + +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism +import org.pgpainless.algorithm.AEADAlgorithm +import org.pgpainless.algorithm.AEADCipherMode +import org.pgpainless.algorithm.Feature +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.policy.Policy + +fun interface EncryptionMechanismNegotiator { + + fun negotiate( + policy: Policy, + override: MessageEncryptionMechanism?, + features: List>, + aeadAlgorithmPreferences: List>, + symmetricAlgorithmPreferences: List> + ): MessageEncryptionMechanism + + companion object { + @JvmStatic + fun modificationDetectionOrBetter( + symmetricKeyAlgorithmNegotiator: SymmetricKeyAlgorithmNegotiator + ): EncryptionMechanismNegotiator = + object : EncryptionMechanismNegotiator { + + override fun negotiate( + policy: Policy, + override: MessageEncryptionMechanism?, + features: List>, + aeadAlgorithmPreferences: List>, + symmetricAlgorithmPreferences: List> + ): MessageEncryptionMechanism { + + // If the user supplied an override, use that + if (override != null) { + return override + } + + // If all support SEIPD2, use SEIPD2 + if (features.all { it.contains(Feature.MODIFICATION_DETECTION_2) }) { + // Find best supported algorithm combination + val counted = mutableMapOf() + for (pref in aeadAlgorithmPreferences) { + for (mode in pref) { + counted[mode] = counted.getOrDefault(mode, 0) + 1 + } + } + // filter for supported combinations and find most widely supported + val bestSupportedMode: AEADCipherMode = + counted + .filter { + policy.messageEncryptionAlgorithmPolicy.isAcceptable( + MessageEncryptionMechanism.aead( + it.key.ciphermode.algorithmId, + it.key.aeadAlgorithm.algorithmId)) + } + .maxByOrNull { it.value } + ?.key + ?: AEADCipherMode(AEADAlgorithm.OCB, SymmetricKeyAlgorithm.AES_128) + + // return best supported mode or symmetric key fallback mechanism + return MessageEncryptionMechanism.aead( + bestSupportedMode.ciphermode.algorithmId, + bestSupportedMode.aeadAlgorithm.algorithmId) + } else if (features.all { it.contains(Feature.LIBREPGP_OCB_ENCRYPTED_DATA) }) { + return MessageEncryptionMechanism.librePgp( + symmetricKeyAlgorithmNegotiator + .negotiate( + policy.messageEncryptionAlgorithmPolicy + .symmetricAlgorithmPolicy, + null, + symmetricAlgorithmPreferences) + .algorithmId) + } + // If all support SEIPD1, negotiate SEIPD1 using symmetricKeyAlgorithmNegotiator + else if (features.all { it.contains(Feature.MODIFICATION_DETECTION) }) { + return MessageEncryptionMechanism.integrityProtected( + symmetricKeyAlgorithmNegotiator + .negotiate( + policy.messageEncryptionAlgorithmPolicy + .symmetricAlgorithmPolicy, + null, + symmetricAlgorithmPreferences) + .algorithmId) + } + // Else fall back to fallback mechanism from policy + else { + return policy.messageEncryptionAlgorithmPolicy.asymmetricFallbackMechanism + } + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt index b9474247..cf398806 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt @@ -21,7 +21,7 @@ interface HashAlgorithmNegotiator { * @param orderedPrefs hash algorithm preferences * @return picked algorithms */ - fun negotiateHashAlgorithm(orderedPrefs: Set): HashAlgorithm + fun negotiateHashAlgorithm(orderedPrefs: Set?): HashAlgorithm companion object { @@ -62,9 +62,9 @@ interface HashAlgorithmNegotiator { ): HashAlgorithmNegotiator { return object : HashAlgorithmNegotiator { override fun negotiateHashAlgorithm( - orderedPrefs: Set + orderedPrefs: Set? ): HashAlgorithm { - return orderedPrefs.firstOrNull { hashAlgorithmPolicy.isAcceptable(it) } + return orderedPrefs?.firstOrNull { hashAlgorithmPolicy.isAcceptable(it) } ?: hashAlgorithmPolicy.defaultHashAlgorithm() } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt index d11f2c03..909b8c1c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt @@ -4,7 +4,6 @@ package org.pgpainless.algorithm.negotiation -import java.lang.IllegalArgumentException import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.policy.Policy @@ -36,9 +35,8 @@ interface SymmetricKeyAlgorithmNegotiator { override: SymmetricKeyAlgorithm?, keyPreferences: List> ): SymmetricKeyAlgorithm { - if (override == SymmetricKeyAlgorithm.NULL) { - throw IllegalArgumentException( - "Algorithm override cannot be NULL (plaintext).") + require(override != SymmetricKeyAlgorithm.NULL) { + "Algorithm override cannot be NULL (plaintext)." } if (override != null) { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthenticity.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthenticity.kt index f3d60bf6..650b3722 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthenticity.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthenticity.kt @@ -5,14 +5,46 @@ package org.pgpainless.authentication import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.pgpainless.PGPainless +/** + * A certificate authenticity record, indicating, to what degree the certificate is authenticated. + * + * @param userId identity, was changed to [CharSequence] instead of [String] starting with + * PGPainless 2.0. + * @param certificate certificate, was changed to [OpenPGPCertificate] instead of + * [PGPPublicKeyRing]. Use [pgpPublicKeyRing] if you need to access a [PGPPublicKeyRing]. + * @param certificationChains map of chains and their trust degrees + * @param targetAmount targeted trust amount + */ class CertificateAuthenticity( - val userId: String, - val certificate: PGPPublicKeyRing, + val userId: CharSequence, + val certificate: OpenPGPCertificate, val certificationChains: Map, val targetAmount: Int ) { + /** Legacy constructor accepting a [PGPPublicKeyRing]. */ + @Deprecated("Pass in an OpenPGPCertificate instead of a PGPPublicKeyRing.") + constructor( + userId: String, + certificate: PGPPublicKeyRing, + certificationChains: Map, + targetAmount: Int + ) : this( + userId, + PGPainless.getInstance().toCertificate(certificate), + certificationChains, + targetAmount) + + /** + * Field was introduced to allow backwards compatibility with pre-2.0 API as replacement for + * [certificate]. + */ + @Deprecated("Use certificate instead.", replaceWith = ReplaceWith("certificate")) + val pgpPublicKeyRing: PGPPublicKeyRing = certificate.pgpPublicKeyRing + val totalTrustAmount: Int get() = certificationChains.values.sum() @@ -44,5 +76,22 @@ class CertificateAuthenticity( */ class CertificationChain(val trustAmount: Int, val chainLinks: List) {} -/** A chain link contains a node in the trust chain. */ -class ChainLink(val certificate: PGPPublicKeyRing) {} +/** + * A chain link contains a node in the trust chain. + * + * @param certificate chain link certificate, was changed from [PGPPublicKeyRing] to + * [OpenPGPCertificate] with PGPainless 2.0. Use [pgpPublicKeyRing] if you need to access the + * field as [PGPPublicKeyRing]. + */ +class ChainLink(val certificate: OpenPGPCertificate) { + constructor( + certificate: PGPPublicKeyRing + ) : this(PGPainless.getInstance().toCertificate(certificate)) + + /** + * Field was introduced to allow backwards compatibility with pre-2.0 API as replacement for + * [certificate]. + */ + @Deprecated("Use certificate instead.", replaceWith = ReplaceWith("certificate")) + val pgpPublicKeyRing: PGPPublicKeyRing = certificate.pgpPublicKeyRing +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthority.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthority.kt index 093c2325..95e23bc7 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthority.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthority.kt @@ -5,14 +5,15 @@ package org.pgpainless.authentication import java.util.* +import org.bouncycastle.bcpg.KeyIdentifier import org.pgpainless.key.OpenPgpFingerprint /** * Interface for a CA that can authenticate trust-worthy certificates. Such a CA might be a fixed * list of trustworthy certificates, or a dynamic implementation like the Web-of-Trust. * - * @see PGPainless-WOT - * @see OpenPGP Web of Trust + * @see [PGPainless-WOT](https://github.com/pgpainless/pgpainless-wot) + * @see [OpenPGP Web of Trust](https://sequoia-pgp.gitlab.io/sequoia-wot/) */ interface CertificateAuthority { @@ -36,7 +37,30 @@ interface CertificateAuthority { email: Boolean, referenceTime: Date, targetAmount: Int - ): CertificateAuthenticity + ): CertificateAuthenticity? = + authenticateBinding(fingerprint.keyIdentifier, userId, email, referenceTime, targetAmount) + + /** + * Determine the authenticity of the binding between the given cert identifier and the userId. + * In other words, determine, how much evidence can be gathered, that the certificate with the + * given fingerprint really belongs to the user with the given userId. + * + * @param certIdentifier identifier of the certificate + * @param userId userId + * @param email if true, the userId will be treated as an email address and all user-IDs + * containing the email address will be matched. + * @param referenceTime reference time at which the binding shall be evaluated + * @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly + * authenticated, 60 = partially authenticated...) + * @return information about the authenticity of the binding + */ + fun authenticateBinding( + certIdentifier: KeyIdentifier, + userId: CharSequence, + email: Boolean, + referenceTime: Date, + targetAmount: Int + ): CertificateAuthenticity? /** * Lookup certificates, which carry a trustworthy binding to the given userId. @@ -50,7 +74,7 @@ interface CertificateAuthority { * @return list of identified bindings */ fun lookupByUserId( - userId: String, + userId: CharSequence, email: Boolean, referenceTime: Date, targetAmount: Int @@ -70,5 +94,22 @@ interface CertificateAuthority { fingerprint: OpenPgpFingerprint, referenceTime: Date, targetAmount: Int + ): List = + identifyByFingerprint(fingerprint.keyIdentifier, referenceTime, targetAmount) + + /** + * Identify trustworthy bindings for a certificate. The result is a list of authenticatable + * userIds on the certificate. + * + * @param certIdentifier identifier of the certificate + * @param referenceTime reference time for trust calculations + * @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly + * authenticated, 60 = partially authenticated...) + * @return list of identified bindings + */ + fun identifyByFingerprint( + certIdentifier: KeyIdentifier, + referenceTime: Date, + targetAmount: Int ): List } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/PolicyAdapter.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/PolicyAdapter.kt new file mode 100644 index 00000000..64e7cc41 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/PolicyAdapter.kt @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle + +import java.util.Date +import org.bouncycastle.openpgp.api.OpenPGPPolicy +import org.bouncycastle.openpgp.api.OpenPGPPolicy.OpenPGPNotationRegistry +import org.pgpainless.policy.Policy + +/** Adapter class that adapts a PGPainless [Policy] object to Bouncy Castles [OpenPGPPolicy]. */ +class PolicyAdapter(val policy: Policy) : OpenPGPPolicy { + + /** + * Determine, whether the hash algorithm of a document signature is acceptable. + * + * @param algorithmId hash algorithm ID + * @param signatureCreationTime optional signature creation time + * @return boolean indicating whether the hash algorithm is acceptable + */ + override fun isAcceptableDocumentSignatureHashAlgorithm( + algorithmId: Int, + signatureCreationTime: Date? + ): Boolean { + return if (signatureCreationTime == null) + policy.dataSignatureHashAlgorithmPolicy.isAcceptable(algorithmId) + else + policy.dataSignatureHashAlgorithmPolicy.isAcceptable(algorithmId, signatureCreationTime) + } + + /** + * Determine, whether the hash algorithm of a revocation signature is acceptable. + * + * @param algorithmId hash algorithm ID + * @param revocationCreationTime optional revocation signature creation time + * @return boolean indicating whether the hash algorithm is acceptable + */ + override fun isAcceptableRevocationSignatureHashAlgorithm( + algorithmId: Int, + revocationCreationTime: Date? + ): Boolean { + return if (revocationCreationTime == null) + policy.revocationSignatureHashAlgorithmPolicy.isAcceptable(algorithmId) + else + policy.revocationSignatureHashAlgorithmPolicy.isAcceptable( + algorithmId, revocationCreationTime) + } + + /** + * Determine, whether the hash algorithm of a certification signature is acceptable. + * + * @param algorithmId hash algorithm ID + * @param certificationCreationTime optional certification signature creation time + * @return boolean indicating whether the hash algorithm is acceptable + */ + override fun isAcceptableCertificationSignatureHashAlgorithm( + algorithmId: Int, + certificationCreationTime: Date? + ): Boolean { + return if (certificationCreationTime == null) + policy.certificationSignatureHashAlgorithmPolicy.isAcceptable(algorithmId) + else + policy.certificationSignatureHashAlgorithmPolicy.isAcceptable( + algorithmId, certificationCreationTime) + } + + /** + * Return the default hash algorithm for certification signatures. This is used as fallback if + * not suitable hash algorithm can be negotiated. + * + * @return default certification signature hash algorithm + */ + override fun getDefaultCertificationSignatureHashAlgorithm(): Int { + return policy.certificationSignatureHashAlgorithmPolicy.defaultHashAlgorithm.algorithmId + } + + /** + * Return the default hash algorithm for document signatures. This is used as fallback if not + * suitable hash algorithm can be negotiated. + * + * @return default document signature hash algorithm + */ + override fun getDefaultDocumentSignatureHashAlgorithm(): Int { + return policy.dataSignatureHashAlgorithmPolicy.defaultHashAlgorithm.algorithmId + } + + /** + * Determine, whether the given symmetric encryption algorithm is acceptable. + * + * @param algorithmId symmetric encryption algorithm ID + * @return boolean indicating, whether the encryption algorithm is acceptable + */ + override fun isAcceptableSymmetricKeyAlgorithm(algorithmId: Int): Boolean { + return policy.messageEncryptionAlgorithmPolicy.symmetricAlgorithmPolicy.isAcceptable( + algorithmId) + } + /** + * Return the default symmetric encryption algorithm. This algorithm is used as fallback to + * encrypt messages if no suitable symmetric encryption algorithm can be negotiated. + * + * @return default symmetric encryption algorithm + */ + override fun getDefaultSymmetricKeyAlgorithm(): Int { + return policy.messageEncryptionAlgorithmPolicy.symmetricAlgorithmPolicy + .defaultSymmetricKeyAlgorithm + .algorithmId + } + + /** + * Determine, whether the [bitStrength] of an asymmetric public key of the given algorithm is + * strong enough. + * + * @param algorithmId public key algorithm ID + * @param bitStrength strength of the key in bits + * @return boolean indicating whether the bit strength is sufficient + */ + override fun isAcceptablePublicKeyStrength(algorithmId: Int, bitStrength: Int): Boolean { + return policy.publicKeyAlgorithmPolicy.isAcceptable(algorithmId, bitStrength) + } + + /** + * Adapt PGPainless' [org.pgpainless.util.NotationRegistry] to Bouncy Castles + * [OpenPGPNotationRegistry]. + * + * @return adapted [OpenPGPNotationRegistry] + */ + override fun getNotationRegistry(): OpenPGPNotationRegistry { + return object : OpenPGPNotationRegistry() { + + /** Determine, whether the given [notationName] is known by the registry. */ + override fun isNotationKnown(notationName: String?): Boolean { + return notationName?.let { policy.notationRegistry.isKnownNotation(it) } ?: false + } + + /** + * Add a known notation name to the registry. + * + * @param notationName notation name + */ + override fun addKnownNotation(notationName: String?) { + notationName?.let { policy.notationRegistry.addKnownNotation(it) } + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPCertificateExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPCertificateExtensions.kt new file mode 100644 index 00000000..04053547 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPCertificateExtensions.kt @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import java.io.OutputStream +import org.bouncycastle.bcpg.ArmoredOutputStream +import org.bouncycastle.bcpg.PacketFormat +import org.bouncycastle.openpgp.PGPOnePassSignature +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey +import org.pgpainless.algorithm.OpenPGPKeyVersion + +/** + * Return the [OpenPGPComponentKey] that issued the given [PGPOnePassSignature]. + * + * @param ops one pass signature + */ +fun OpenPGPCertificate.getSigningKeyFor(ops: PGPOnePassSignature): OpenPGPComponentKey? = + this.getKey(ops.keyIdentifier) + +/** Return the [OpenPGPKeyVersion] of the certificates primary key. */ +fun OpenPGPCertificate.getKeyVersion(): OpenPGPKeyVersion = primaryKey.getKeyVersion() + +/** Return the [OpenPGPKeyVersion] of the component key. */ +fun OpenPGPComponentKey.getKeyVersion(): OpenPGPKeyVersion = OpenPGPKeyVersion.from(this.version) + +/** + * ASCII-armor-encode the certificate into the given [OutputStream]. + * + * @param outputStream output stream + * @param format packet length encoding format, defaults to [PacketFormat.ROUNDTRIP] + */ +fun OpenPGPCertificate.asciiArmor( + outputStream: OutputStream, + format: PacketFormat = PacketFormat.ROUNDTRIP +) { + outputStream.write(toAsciiArmoredString(format).encodeToByteArray()) +} + +/** + * ASCII-armor-encode the certificate into the given [OutputStream]. + * + * @param outputStream output stream + * @param format packet length encoding format, defaults to [PacketFormat.ROUNDTRIP] + * @param armorBuilder builder for the ASCII armored output stream + */ +fun OpenPGPCertificate.asciiArmor( + outputStream: OutputStream, + format: PacketFormat, + armorBuilder: ArmoredOutputStream.Builder +) { + outputStream.write(toAsciiArmoredString(format, armorBuilder).encodeToByteArray()) +} + +fun OpenPGPCertificate.encode( + outputStream: OutputStream, + format: PacketFormat = PacketFormat.ROUNDTRIP +) { + outputStream.write(getEncoded(format)) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPImplementationExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPImplementationExtensions.kt new file mode 100644 index 00000000..a546c1a1 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPImplementationExtensions.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import org.bouncycastle.bcpg.HashAlgorithmTags +import org.bouncycastle.openpgp.api.EncryptedDataPacketType +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism +import org.bouncycastle.openpgp.api.OpenPGPImplementation +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder +import org.bouncycastle.openpgp.operator.PGPDigestCalculator + +/** + * Return a [PGPDigestCalculator] that is based on [HashAlgorithmTags.SHA1], used for key checksum + * calculations. + */ +fun OpenPGPImplementation.checksumCalculator(): PGPDigestCalculator { + return pgpDigestCalculatorProvider().get(HashAlgorithmTags.SHA1) +} + +/** + * Return a [PGPDataEncryptorBuilder] for the given [MessageEncryptionMechanism]. + * + * @param mechanism + * @return data encryptor builder + */ +fun OpenPGPImplementation.pgpDataEncryptorBuilder( + mechanism: MessageEncryptionMechanism +): PGPDataEncryptorBuilder { + require(mechanism.isEncrypted) { "Cannot create PGPDataEncryptorBuilder for NULL algorithm." } + return pgpDataEncryptorBuilder(mechanism.symmetricKeyAlgorithm).also { + when (mechanism.mode!!) { + EncryptedDataPacketType.SED -> it.setWithIntegrityPacket(false) + EncryptedDataPacketType.SEIPDv1 -> it.setWithIntegrityPacket(true) + EncryptedDataPacketType.SEIPDv2 -> { + it.setWithAEAD(mechanism.aeadAlgorithm, mechanism.symmetricKeyAlgorithm) + it.setUseV6AEAD() + } + EncryptedDataPacketType.LIBREPGP_OED -> { + it.setWithAEAD(mechanism.aeadAlgorithm, mechanism.symmetricKeyAlgorithm) + it.setUseV5AEAD() + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPKeyExtensions.kt new file mode 100644 index 00000000..cbdf97ef --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPKeyExtensions.kt @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPPrivateKey +import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPSecretKey +import org.pgpainless.util.Passphrase + +/** + * Return the [OpenPGPSecretKey] that can be used to decrypt the given [PGPPublicKeyEncryptedData]. + * + * @param pkesk public-key encrypted session-key packet + * @return secret key or null if no matching secret key was found + */ +fun OpenPGPKey.getSecretKeyFor(pkesk: PGPPublicKeyEncryptedData): OpenPGPSecretKey? = + this.getSecretKey(pkesk.keyIdentifier) + +/** + * Unlock the [OpenPGPSecretKey], returning the unlocked [OpenPGPPrivateKey]. + * + * @param passphrase passphrase to unlock the key + * @return unlocked [OpenPGPPrivateKey] + */ +fun OpenPGPSecretKey.unlock(passphrase: Passphrase): OpenPGPPrivateKey = + this.unlock(passphrase.getChars()) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPKeyGeneratorExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPKeyGeneratorExtensions.kt new file mode 100644 index 00000000..c085c08d --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPKeyGeneratorExtensions.kt @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites +import org.bouncycastle.openpgp.api.OpenPGPKeyGenerator +import org.pgpainless.algorithm.AEADCipherMode +import org.pgpainless.algorithm.AlgorithmSuite +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.Feature +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm + +/** + * Apply different algorithm preferences (features, symmetric-key-, hash-, compression- and AEAD + * algorithm preferences to the [OpenPGPKeyGenerator] for key generation. The preferences will be + * set on preference-signatures on the generated keys. + * + * @param algorithms algorithm suite + * @return this + */ +fun OpenPGPKeyGenerator.setAlgorithmSuite(algorithms: AlgorithmSuite): OpenPGPKeyGenerator { + setDefaultFeatures(true, algorithms.features) + setDefaultSymmetricKeyPreferences(true, algorithms.symmetricKeyAlgorithms) + setDefaultHashAlgorithmPreferences(true, algorithms.hashAlgorithms) + setDefaultCompressionAlgorithmPreferences(true, algorithms.compressionAlgorithms) + setDefaultAeadAlgorithmPreferences(false, algorithms.aeadAlgorithms) + return this +} + +fun OpenPGPKeyGenerator.setDefaultFeatures( + critical: Boolean = true, + features: Set? +): OpenPGPKeyGenerator { + this.setDefaultFeatures { + val b = features?.let { f -> Feature.toBitmask(*f.toTypedArray()) } ?: 0 + it.apply { setFeature(critical, b) } + } + return this +} + +/** + * Define [SymmetricKeyAlgorithms][SymmetricKeyAlgorithm] that will be applied as symmetric key + * algorithm preferences to preference-signatures on freshly generated keys. + * + * @param critical whether to mark the preference subpacket as critical + * @param symmetricKeyAlgorithms ordered set of preferred symmetric key algorithms + * @return this + */ +fun OpenPGPKeyGenerator.setDefaultSymmetricKeyPreferences( + critical: Boolean = true, + symmetricKeyAlgorithms: Set? +): OpenPGPKeyGenerator = apply { + symmetricKeyAlgorithms?.let { algorithms -> + this.setDefaultSymmetricKeyPreferences { + val algorithmIds = algorithms.map { a -> a.algorithmId }.toIntArray() + it.apply { setPreferredSymmetricAlgorithms(critical, algorithmIds) } + } + } +} + +/** + * Define [HashAlgorithms][HashAlgorithm] that will be applied as hash algorithm preferences to + * preference-signatures on freshly generated keys. + * + * @param critical whether to mark the preference subpacket as critical + * @param hashAlgorithms ordered set of preferred hash algorithms + * @return this + */ +fun OpenPGPKeyGenerator.setDefaultHashAlgorithmPreferences( + critical: Boolean = true, + hashAlgorithms: Set? +): OpenPGPKeyGenerator = apply { + hashAlgorithms?.let { algorithms -> + this.setDefaultHashAlgorithmPreferences { + val algorithmIds = algorithms.map { a -> a.algorithmId }.toIntArray() + it.apply { setPreferredHashAlgorithms(critical, algorithmIds) } + } + } +} + +/** + * Define [CompressionAlgorithms][CompressionAlgorithm] that will be applied as compression + * algorithm preferences to preference-signatures on freshly generated keys. + * + * @param critical whether to mark the preference subpacket as critical + * @param compressionAlgorithms ordered set of preferred compression algorithms + * @return this + */ +fun OpenPGPKeyGenerator.setDefaultCompressionAlgorithmPreferences( + critical: Boolean = true, + compressionAlgorithms: Set? +): OpenPGPKeyGenerator = apply { + compressionAlgorithms?.let { algorithms -> + this.setDefaultCompressionAlgorithmPreferences { + val algorithmIds = algorithms.map { a -> a.algorithmId }.toIntArray() + it.apply { setPreferredCompressionAlgorithms(critical, algorithmIds) } + } + } +} + +/** + * Define [AEADCipherModes][AEADCipherMode] that will be applied as AEAD algorithm preferences to + * preference signatures on freshly generated keys. + * + * @param critical whether to mark the preferences subpacket as critical + * @param aeadAlgorithms ordered set of AEAD preferences + * @return this + */ +fun OpenPGPKeyGenerator.setDefaultAeadAlgorithmPreferences( + critical: Boolean = false, + aeadAlgorithms: Set? +): OpenPGPKeyGenerator = apply { + aeadAlgorithms?.let { algorithms -> + this.setDefaultAeadAlgorithmPreferences { + val builder = PreferredAEADCiphersuites.builder(critical) + for (ciphermode: AEADCipherMode in algorithms) { + builder.addCombination( + ciphermode.ciphermode.algorithmId, ciphermode.aeadAlgorithm.algorithmId) + } + it.apply { setPreferredAEADCiphersuites(builder) } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensions.kt index 7126db66..adde4dc6 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensions.kt @@ -4,19 +4,45 @@ package org.pgpainless.bouncycastle.extensions -import openpgp.openPgpKeyId +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPOnePassSignature import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey +import org.bouncycastle.openpgp.api.OpenPGPImplementation import org.pgpainless.PGPainless import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.SubkeyIdentifier -/** Return true, if this [PGPKeyRing] contains the subkey identified by the [SubkeyIdentifier]. */ +/** + * Return true, if this [PGPKeyRing] contains the subkey identified by the [SubkeyIdentifier]. + * + * @param subkeyIdentifier subkey identifier + * @return true if the [PGPKeyRing] contains the [SubkeyIdentifier] + */ fun PGPKeyRing.matches(subkeyIdentifier: SubkeyIdentifier): Boolean = - this.publicKey.keyID == subkeyIdentifier.primaryKeyId && - this.getPublicKey(subkeyIdentifier.subkeyId) != null + this.publicKey.keyIdentifier.matchesExplicit(subkeyIdentifier.certificateIdentifier) && + this.getPublicKey(subkeyIdentifier.componentKeyIdentifier) != null + +/** + * Return true, if this [PGPKeyRing] contains the given [componentKey]. + * + * @param componentKey component key + * @return true if the [PGPKeyRing] contains the [componentKey] + */ +fun PGPKeyRing.matches(componentKey: OpenPGPComponentKey): Boolean = + this.matches(SubkeyIdentifier(componentKey)) + +/** + * Return true, if the [PGPKeyRing] contains a public key with the given [keyIdentifier]. + * + * @param keyIdentifier KeyIdentifier + * @return true if key with the given key-ID is present, false otherwise + */ +fun PGPKeyRing.hasPublicKey(keyIdentifier: KeyIdentifier): Boolean = + this.getPublicKey(keyIdentifier) != null /** * Return true, if the [PGPKeyRing] contains a public key with the given key-ID. @@ -24,7 +50,8 @@ fun PGPKeyRing.matches(subkeyIdentifier: SubkeyIdentifier): Boolean = * @param keyId keyId * @return true if key with the given key-ID is present, false otherwise */ -fun PGPKeyRing.hasPublicKey(keyId: Long): Boolean = this.getPublicKey(keyId) != null +@Deprecated("Pass in a KeyIdentifier instead.") +fun PGPKeyRing.hasPublicKey(keyId: Long): Boolean = hasPublicKey(KeyIdentifier(keyId)) /** * Return true, if the [PGPKeyRing] contains a public key with the given fingerprint. @@ -33,7 +60,7 @@ fun PGPKeyRing.hasPublicKey(keyId: Long): Boolean = this.getPublicKey(keyId) != * @return true if key with the given fingerprint is present, false otherwise */ fun PGPKeyRing.hasPublicKey(fingerprint: OpenPgpFingerprint): Boolean = - this.getPublicKey(fingerprint) != null + hasPublicKey(fingerprint.keyIdentifier) /** * Return the [PGPPublicKey] with the given [OpenPgpFingerprint] or null, if no such key is present. @@ -42,17 +69,41 @@ fun PGPKeyRing.hasPublicKey(fingerprint: OpenPgpFingerprint): Boolean = * @return public key */ fun PGPKeyRing.getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = - this.getPublicKey(fingerprint.bytes) + this.getPublicKey(fingerprint.keyIdentifier) -fun PGPKeyRing.requirePublicKey(keyId: Long): PGPPublicKey = - getPublicKey(keyId) - ?: throw NoSuchElementException( - "OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.") +/** + * Return the [PGPPublicKey] with the given [keyIdentifier], or throw a [NoSuchElementException] if + * no matching public key was found. + * + * @param keyIdentifier key identifier + * @return public key + * @throws NoSuchElementException if no matching public key was found + */ +fun PGPKeyRing.requirePublicKey(keyIdentifier: KeyIdentifier): PGPPublicKey = + getPublicKey(keyIdentifier) + ?: throw NoSuchElementException("OpenPGP key does not contain key with id $keyIdentifier.") +/** + * Return the [PGPPublicKey] with the given key-id, or throw a [NoSuchElementException] if no + * matching public key was found. + * + * @param keyId key id + * @return public key + * @throws NoSuchElementException if no matching public key was found + */ +@Deprecated("Pass in a KeyIdentifier instead.") +fun PGPKeyRing.requirePublicKey(keyId: Long): PGPPublicKey = requirePublicKey(KeyIdentifier(keyId)) + +/** + * Return the [PGPPublicKey] with the given [fingerprint], or throw a [NoSuchElementException] if no + * matching public key was found. + * + * @param fingerprint key fingerprint + * @return public key + * @throws NoSuchElementException if no matching public key was found + */ fun PGPKeyRing.requirePublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey = - getPublicKey(fingerprint) - ?: throw NoSuchElementException( - "OpenPGP key does not contain key with fingerprint $fingerprint.") + requirePublicKey(fingerprint.keyIdentifier) /** * Return the [PGPPublicKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. If @@ -60,11 +111,12 @@ fun PGPKeyRing.requirePublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey = * subpacket to identify the [PGPPublicKey] via its key-ID. */ fun PGPKeyRing.getPublicKeyFor(signature: PGPSignature): PGPPublicKey? = - signature.fingerprint?.let { this.getPublicKey(it) } ?: this.getPublicKey(signature.keyID) + signature.fingerprint?.let { this.getPublicKey(it.keyIdentifier) } + ?: this.getPublicKey(signature.keyID) /** Return the [PGPPublicKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */ fun PGPKeyRing.getPublicKeyFor(onePassSignature: PGPOnePassSignature): PGPPublicKey? = - this.getPublicKey(onePassSignature.keyID) + this.getPublicKey(onePassSignature.keyIdentifier) /** Return the [OpenPgpFingerprint] of this OpenPGP key. */ val PGPKeyRing.openPgpFingerprint: OpenPgpFingerprint @@ -72,3 +124,22 @@ val PGPKeyRing.openPgpFingerprint: OpenPgpFingerprint /** Return this OpenPGP key as an ASCII armored String. */ fun PGPKeyRing.toAsciiArmor(): String = PGPainless.asciiArmor(this) + +/** + * Convert the given [PGPKeyRing] into an [OpenPGPCertificate]. + * + * @return certificate + */ +@Deprecated("Use toOpenPGPCertificate(implementation) instead.") +fun PGPKeyRing.toOpenPGPCertificate(): OpenPGPCertificate = + toOpenPGPCertificate(PGPainless.getInstance().implementation) + +/** + * Convert the given [PGPKeyRing] into an [OpenPGPCertificate] using the given + * [OpenPGPImplementation]. + * + * @param implementation OpenPGP implementation + * @return certificate + */ +fun PGPKeyRing.toOpenPGPCertificate(implementation: OpenPGPImplementation): OpenPGPCertificate = + OpenPGPCertificate(this, implementation) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensions.kt index d267fa83..c669a4fa 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensions.kt @@ -4,10 +4,10 @@ package org.pgpainless.bouncycastle.extensions -import org.bouncycastle.asn1.gnu.GNUObjectIdentifiers import org.bouncycastle.bcpg.ECDHPublicBCPGKey import org.bouncycastle.bcpg.ECDSAPublicBCPGKey import org.bouncycastle.bcpg.EdDSAPublicBCPGKey +import org.bouncycastle.internal.asn1.gnu.GNUObjectIdentifiers import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil import org.bouncycastle.openpgp.PGPPublicKey import org.pgpainless.algorithm.PublicKeyAlgorithm diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt index 99c562e6..44570f17 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt @@ -4,8 +4,16 @@ package org.pgpainless.bouncycastle.extensions -import openpgp.openPgpKeyId -import org.bouncycastle.openpgp.* +import org.bouncycastle.bcpg.KeyIdentifier +import org.bouncycastle.openpgp.PGPOnePassSignature +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.api.OpenPGPImplementation +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.pgpainless.PGPainless import org.pgpainless.key.OpenPgpFingerprint /** OpenPGP certificate containing the public keys of this OpenPGP key. */ @@ -18,7 +26,17 @@ val PGPSecretKeyRing.certificate: PGPPublicKeyRing * @param keyId keyId of the secret key * @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise */ -fun PGPSecretKeyRing.hasSecretKey(keyId: Long): Boolean = this.getSecretKey(keyId) != null +@Deprecated("Pass in a KeyIdentifier instead.") +fun PGPSecretKeyRing.hasSecretKey(keyId: Long): Boolean = hasSecretKey(KeyIdentifier(keyId)) + +/** + * Return true, if the [PGPSecretKeyRing] contains a [PGPSecretKey] with the given [keyIdentifier]. + * + * @param keyIdentifier identifier of the secret key + * @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise + */ +fun PGPSecretKeyRing.hasSecretKey(keyIdentifier: KeyIdentifier): Boolean = + this.getSecretKey(keyIdentifier) != null /** * Return true, if the [PGPSecretKeyRing] contains a [PGPSecretKey] with the given fingerprint. @@ -27,7 +45,7 @@ fun PGPSecretKeyRing.hasSecretKey(keyId: Long): Boolean = this.getSecretKey(keyI * @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise */ fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean = - this.getSecretKey(fingerprint) != null + hasSecretKey(fingerprint.keyIdentifier) /** * Return the [PGPSecretKey] with the given [OpenPgpFingerprint]. @@ -36,7 +54,7 @@ fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean = * @return the secret key or null */ fun PGPSecretKeyRing.getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? = - this.getSecretKey(fingerprint.bytes) + this.getSecretKey(fingerprint.keyIdentifier) /** * Return the [PGPSecretKey] with the given key-ID. @@ -44,10 +62,20 @@ fun PGPSecretKeyRing.getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey * @throws NoSuchElementException if the OpenPGP key doesn't contain a secret key with the given * key-ID */ +@Deprecated("Pass in a KeyIdentifier instead.") fun PGPSecretKeyRing.requireSecretKey(keyId: Long): PGPSecretKey = - getSecretKey(keyId) + requireSecretKey(KeyIdentifier(keyId)) + +/** + * Return the [PGPSecretKey] with the given [keyIdentifier]. + * + * @throws NoSuchElementException if the OpenPGP key doesn't contain a secret key with the given + * keyIdentifier + */ +fun PGPSecretKeyRing.requireSecretKey(keyIdentifier: KeyIdentifier): PGPSecretKey = + getSecretKey(keyIdentifier) ?: throw NoSuchElementException( - "OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.") + "OpenPGP key does not contain key with id ${keyIdentifier}.") /** * Return the [PGPSecretKey] with the given fingerprint. @@ -56,9 +84,7 @@ fun PGPSecretKeyRing.requireSecretKey(keyId: Long): PGPSecretKey = * fingerprint */ fun PGPSecretKeyRing.requireSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey = - getSecretKey(fingerprint) - ?: throw NoSuchElementException( - "OpenPGP key does not contain key with fingerprint $fingerprint.") + requireSecretKey(fingerprint.keyIdentifier) /** * Return the [PGPSecretKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. If @@ -70,10 +96,32 @@ fun PGPSecretKeyRing.getSecretKeyFor(signature: PGPSignature): PGPSecretKey? = /** Return the [PGPSecretKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */ fun PGPSecretKeyRing.getSecretKeyFor(onePassSignature: PGPOnePassSignature): PGPSecretKey? = - this.getSecretKey(onePassSignature.keyID) + this.getSecretKey(onePassSignature.keyIdentifier) +/** + * Return the [PGPSecretKey] that can be used to decrypt the given [PGPPublicKeyEncryptedData] + * packet. + * + * @param pkesk public-key encrypted session-key packet + * @return secret-key or null if no matching secret key was found + */ fun PGPSecretKeyRing.getSecretKeyFor(pkesk: PGPPublicKeyEncryptedData): PGPSecretKey? = - when (pkesk.version) { - 3 -> this.getSecretKey(pkesk.keyID) - else -> throw NotImplementedError("Version 6 PKESKs are not yet supported.") - } + this.getSecretKey(pkesk.keyIdentifier) + +/** + * Convert the [PGPSecretKeyRing] into an [OpenPGPKey]. + * + * @return key + */ +@Deprecated("Use toOpenPGPKey(implementation) instead.") +fun PGPSecretKeyRing.toOpenPGPKey(): OpenPGPKey = + toOpenPGPKey(PGPainless.getInstance().implementation) + +/** + * Convert the [PGPSecretKeyRing] into an [OpenPGPKey] using the given [OpenPGPImplementation]. + * + * @param implementation openpgp implementation + * @return key + */ +fun PGPSecretKeyRing.toOpenPGPKey(implementation: OpenPGPImplementation): OpenPGPKey = + OpenPGPKey(this, implementation) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt index 1393883c..8ffd81a1 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt @@ -5,13 +5,16 @@ package org.pgpainless.bouncycastle.extensions import java.util.* +import openpgp.formatUTC import openpgp.plusSeconds +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.algorithm.RevocationState import org.pgpainless.algorithm.SignatureType +import org.pgpainless.exception.SignatureValidationException import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.util.RevocationAttributes.Reason import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil @@ -54,25 +57,31 @@ val PGPSignature.issuerKeyId: Long } } -/** Return true, if the signature was likely issued by a key with the given fingerprint. */ -fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint): Boolean = - this.fingerprint?.let { it.keyId == fingerprint.keyId } ?: (keyID == fingerprint.keyId) - /** * Return true, if the signature was likely issued by a key with the given fingerprint. * - * @param fingerprint fingerprint bytes + * @param fingerprint fingerprint of the key + * @return true if signature was likely issued by the key */ -@Deprecated("Discouraged in favor of method taking an OpenPgpFingerprint.") -fun PGPSignature.wasIssuedBy(fingerprint: ByteArray): Boolean = - try { - wasIssuedBy(OpenPgpFingerprint.parseFromBinary(fingerprint)) - } catch (e: IllegalArgumentException) { - // Unknown fingerprint length / format - false - } +fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint): Boolean = + wasIssuedBy(fingerprint.keyIdentifier) -fun PGPSignature.wasIssuedBy(key: PGPPublicKey): Boolean = wasIssuedBy(OpenPgpFingerprint.of(key)) +/** + * Return true, if the signature was likely issued by the given key. + * + * @param key key + * @return true if signature was likely issued by the key + */ +fun PGPSignature.wasIssuedBy(key: PGPPublicKey): Boolean = wasIssuedBy(key.keyIdentifier) + +/** + * Return true, if the signature was likely issued by a key with the given identifier. + * + * @param keyIdentifier key identifier + * @return true if signature was likely issued by the key + */ +fun PGPSignature.wasIssuedBy(keyIdentifier: KeyIdentifier): Boolean = + KeyIdentifier.matches(this.keyIdentifiers, keyIdentifier, true) /** Return true, if this signature is a hard revocation. */ val PGPSignature.isHardRevocation @@ -90,19 +99,57 @@ val PGPSignature.isHardRevocation else -> false // Not a revocation } +/** + * Assert that the signatures creation time falls into the period between [notBefore] and + * [notAfter]. + * + * @param notBefore lower bound. If null, do not check the lower bound + * @param notAfter upper bound. If null, do not check the upper bound + */ +fun PGPSignature.assertCreatedInBounds(notBefore: Date?, notAfter: Date?) { + if (notBefore != null && creationTime < notBefore) { + throw SignatureValidationException( + "Signature was made before the earliest allowed signature creation time." + + " Created: ${creationTime.formatUTC()}," + + " earliest allowed: ${notBefore.formatUTC()}") + } + if (notAfter != null && creationTime > notAfter) { + throw SignatureValidationException( + "Signature was made after the latest allowed signature creation time." + + " Created: ${creationTime.formatUTC()}," + + " latest allowed: ${notAfter.formatUTC()}") + } +} + +/** + * Deduce a [RevocationState] from the signature. Non-revocation signatures result in + * [RevocationState.notRevoked]. Hard revocations result in [RevocationState.hardRevoked], while + * soft revocations return [RevocationState.softRevoked] + * + * @return revocation state + */ fun PGPSignature?.toRevocationState() = if (this == null) RevocationState.notRevoked() else if (isHardRevocation) RevocationState.hardRevoked() else RevocationState.softRevoked(creationTime) +/** The signatures issuer fingerprint as [OpenPgpFingerprint]. */ val PGPSignature.fingerprint: OpenPgpFingerprint? get() = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this) +/** The signatures [PublicKeyAlgorithm]. */ val PGPSignature.publicKeyAlgorithm: PublicKeyAlgorithm get() = PublicKeyAlgorithm.requireFromId(keyAlgorithm) +/** The signatures [HashAlgorithm]. */ val PGPSignature.signatureHashAlgorithm: HashAlgorithm get() = HashAlgorithm.requireFromId(hashAlgorithm) +/** + * Return true if the signature has the given [SignatureType]. + * + * @param type signature type + * @return true if the signature type matches the signatures type + */ fun PGPSignature.isOfType(type: SignatureType): Boolean = SignatureType.fromCode(signatureType) == type diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PreferredAEADCipherSuitesExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PreferredAEADCipherSuitesExtensions.kt new file mode 100644 index 00000000..0d7f8539 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PreferredAEADCipherSuitesExtensions.kt @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites +import org.pgpainless.algorithm.AEADCipherMode + +/** Convert the [PreferredAEADCiphersuites] packet into a [Set] of [AEADCipherMode]. */ +fun PreferredAEADCiphersuites?.toAEADCipherModes(): Set { + return this?.algorithms?.asSequence()?.map { AEADCipherMode(it) }?.toSet() ?: setOf() +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PreferredAlgorithmsExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PreferredAlgorithmsExtensions.kt new file mode 100644 index 00000000..1fadd60b --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PreferredAlgorithmsExtensions.kt @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import org.bouncycastle.bcpg.sig.PreferredAlgorithms +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm + +/** Convert the [PreferredAlgorithms] packet into a [Set] of [HashAlgorithm] preferences. */ +fun PreferredAlgorithms?.toHashAlgorithms(): Set { + return this?.preferences?.asSequence()?.map { HashAlgorithm.requireFromId(it) }?.toSet() + ?: setOf() +} + +/** Convert the [PreferredAlgorithms] packet into a [Set] of [SymmetricKeyAlgorithm] preferences. */ +fun PreferredAlgorithms?.toSymmetricKeyAlgorithms(): Set { + return this?.preferences?.asSequence()?.map { SymmetricKeyAlgorithm.requireFromId(it) }?.toSet() + ?: setOf() +} + +/** Convert the [PreferredAlgorithms] packet into a [Set] of [CompressionAlgorithm] preferences. */ +fun PreferredAlgorithms?.toCompressionAlgorithms(): Set { + return this?.preferences?.asSequence()?.map { CompressionAlgorithm.requireFromId(it) }?.toSet() + ?: setOf() +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt index 9bafa6da..1da0a42d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt @@ -46,6 +46,7 @@ class CachingBcPublicKeyDataDecryptorFactory( return decryptorFactory.createDataDecryptor(p0, p1) } + @Deprecated("Deprecated in Java") override fun recoverSessionData( keyAlgorithm: Int, secKeyData: Array, diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt index 39a4e8e4..d8433f25 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt @@ -7,9 +7,14 @@ package org.pgpainless.decryption_verification import java.io.IOException import java.io.InputStream import java.util.* +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.api.OpenPGPKeyMaterialProvider.OpenPGPCertificateProvider +import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory -import org.pgpainless.bouncycastle.extensions.getPublicKeyFor +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 @@ -19,7 +24,7 @@ import org.pgpainless.util.Passphrase import org.pgpainless.util.SessionKey /** Options for decryption and signature verification. */ -class ConsumerOptions { +class ConsumerOptions(private val api: PGPainless) { private var ignoreMDCErrors = false var isDisableAsciiArmorCRC = false @@ -27,17 +32,18 @@ class ConsumerOptions { private var verifyNotBefore: Date? = null private var verifyNotAfter: Date? = Date() - private val certificates = CertificateSource() + private val certificates = CertificateSource(api) private val detachedSignatures = mutableSetOf() - private var missingCertificateCallback: MissingPublicKeyCallback? = null + private var missingCertificateCallback: OpenPGPCertificateProvider? = null private var sessionKey: SessionKey? = null private val customDecryptorFactories = mutableMapOf() - private val decryptionKeys = mutableMapOf() + private val decryptionKeys = mutableMapOf() private val decryptionPassphrases = mutableSetOf() private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE private var multiPassStrategy: MultiPassStrategy = InMemoryMultiPassStrategy() + private var allowDecryptionWithNonEncryptionKey: Boolean = false /** * Consider signatures on the message made before the given timestamp invalid. Null means no @@ -65,14 +71,26 @@ class ConsumerOptions { fun getVerifyNotAfter() = verifyNotAfter + fun addVerificationCert(verificationCert: OpenPGPCertificate): ConsumerOptions = apply { + this.certificates.addCertificate(verificationCert) + } + + fun addVerificationCerts(verificationCerts: Collection): ConsumerOptions = + apply { + for (cert in verificationCerts) { + addVerificationCert(cert) + } + } + /** * Add a certificate (public key ring) for signature verification. * * @param verificationCert certificate for signature verification * @return options */ + @Deprecated("Pass OpenPGPCertificate instead.") fun addVerificationCert(verificationCert: PGPPublicKeyRing): ConsumerOptions = apply { - this.certificates.addCertificate(verificationCert) + this.certificates.addCertificate(api.toCertificate(verificationCert)) } /** @@ -81,10 +99,11 @@ class ConsumerOptions { * @param verificationCerts certificates for signature verification * @return options */ + @Deprecated("Use of methods taking PGPPublicKeyRingCollections is discouraged.") fun addVerificationCerts(verificationCerts: PGPPublicKeyRingCollection): ConsumerOptions = apply { for (cert in verificationCerts) { - addVerificationCert(cert) + addVerificationCert(api.toCertificate(cert)) } } @@ -117,6 +136,14 @@ class ConsumerOptions { } } + fun addVerificationOfDetachedSignature(signature: OpenPGPDocumentSignature): ConsumerOptions = + apply { + if (signature.issuerCertificate != null) { + addVerificationCert(signature.issuerCertificate) + } + addVerificationOfDetachedSignature(signature.signature) + } + /** * Add a detached signature for the signature verification process. * @@ -137,9 +164,10 @@ class ConsumerOptions { * @param callback callback * @return options */ - fun setMissingCertificateCallback(callback: MissingPublicKeyCallback): ConsumerOptions = apply { - this.missingCertificateCallback = callback - } + fun setMissingCertificateCallback(callback: OpenPGPCertificateProvider): ConsumerOptions = + apply { + this.missingCertificateCallback = callback + } /** * Attempt decryption using a session key. @@ -155,52 +183,45 @@ class ConsumerOptions { fun getSessionKey() = sessionKey + @JvmOverloads + fun addDecryptionKey( + key: OpenPGPKey, + protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys() + ) = apply { decryptionKeys[key] = protector } + /** * Add a key for message decryption. If the key is encrypted, the [SecretKeyRingProtector] is * used to decrypt it when needed. * * @param key key - * @param keyRingProtector protector for the secret key + * @param protector protector for the secret key * @return options */ @JvmOverloads + @Deprecated("Pass OpenPGPKey instead.") fun addDecryptionKey( key: PGPSecretKeyRing, protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys() - ) = apply { decryptionKeys[key] = protector } + ) = addDecryptionKey(api.toKey(key), protector) /** * Add the keys in the provided key collection for message decryption. * * @param keys key collection - * @param keyRingProtector protector for encrypted secret keys + * @param protector protector for encrypted secret keys * @return options */ @JvmOverloads + @Deprecated("Pass OpenPGPKey instances instead.") fun addDecryptionKeys( keys: PGPSecretKeyRingCollection, protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys() ) = apply { for (key in keys) { - addDecryptionKey(key, protector) + addDecryptionKey(api.toKey(key), protector) } } - /** - * Add a passphrase for message decryption. This passphrase will be used to try to decrypt - * messages which were symmetrically encrypted for a passphrase. - * - * See - * [Symmetrically Encrypted Data Packet](https://datatracker.ietf.org/doc/html/rfc4880#section-5.7) - * - * @param passphrase passphrase - * @return options - */ - @Deprecated( - "Deprecated in favor of addMessagePassphrase", - ReplaceWith("addMessagePassphrase(passphrase)")) - fun addDecryptionPassphrase(passphrase: Passphrase) = addMessagePassphrase(passphrase) - /** * Add a passphrase for message decryption. This passphrase will be used to try to decrypt * messages which were symmetrically encrypted for a passphrase. @@ -240,21 +261,21 @@ class ConsumerOptions { * * @return decryption keys */ - fun getDecryptionKeys() = decryptionKeys.keys.toSet() + fun getDecryptionKeys(): Set = decryptionKeys.keys.toSet() /** * Return the set of available message decryption passphrases. * * @return decryption passphrases */ - fun getDecryptionPassphrases() = decryptionPassphrases.toSet() + fun getDecryptionPassphrases(): Set = decryptionPassphrases.toSet() /** * Return an object holding available certificates for signature verification. * * @return certificate source */ - fun getCertificateSource() = certificates + fun getCertificateSource(): CertificateSource = certificates /** * Return the callback that gets called when a certificate for signature verification is @@ -262,7 +283,7 @@ class ConsumerOptions { * * @return missing public key callback */ - fun getMissingCertificateCallback() = missingCertificateCallback + fun getMissingCertificateCallback(): OpenPGPCertificateProvider? = missingCertificateCallback /** * Return the [SecretKeyRingProtector] for the given [PGPSecretKeyRing]. @@ -270,7 +291,7 @@ class ConsumerOptions { * @param decryptionKeyRing secret key * @return protector for that particular secret key */ - fun getSecretKeyProtector(decryptionKeyRing: PGPSecretKeyRing): SecretKeyRingProtector? { + fun getSecretKeyProtector(decryptionKeyRing: OpenPGPKey): SecretKeyRingProtector? { return decryptionKeys[decryptionKeyRing] } @@ -306,7 +327,15 @@ class ConsumerOptions { this.ignoreMDCErrors = ignoreMDCErrors } - fun isIgnoreMDCErrors() = ignoreMDCErrors + fun isIgnoreMDCErrors(): Boolean = ignoreMDCErrors + + fun setAllowDecryptionWithMissingKeyFlags(): ConsumerOptions = apply { + allowDecryptionWithNonEncryptionKey = true + } + + fun getAllowDecryptionWithNonEncryptionKey(): Boolean { + return allowDecryptionWithNonEncryptionKey + } /** * Force PGPainless to handle the data provided by the [InputStream] as non-OpenPGP data. This @@ -322,7 +351,7 @@ class ConsumerOptions { * * @return true if non-OpenPGP data is forced */ - fun isForceNonOpenPgpData() = forceNonOpenPgpData + fun isForceNonOpenPgpData(): Boolean = forceNonOpenPgpData /** * Specify the [MissingKeyPassphraseStrategy]. This strategy defines, how missing passphrases @@ -377,15 +406,25 @@ class ConsumerOptions { * Source for OpenPGP certificates. When verifying signatures on a message, this object holds * available signer certificates. */ - class CertificateSource { - private val explicitCertificates: MutableSet = mutableSetOf() + class CertificateSource(private val api: PGPainless) { + private val explicitCertificates: MutableSet = mutableSetOf() /** * Add a certificate as verification cert explicitly. * * @param certificate certificate */ + @Deprecated("Pass in an OpenPGPCertificate instead.") fun addCertificate(certificate: PGPPublicKeyRing) { + explicitCertificates.add(api.toCertificate(certificate)) + } + + /** + * Add a certificate as explicitly provided verification cert. + * + * @param certificate explicit verification cert + */ + fun addCertificate(certificate: OpenPGPCertificate) { explicitCertificates.add(certificate) } @@ -394,7 +433,7 @@ class ConsumerOptions { * * @return explicitly set verification certs */ - fun getExplicitCertificates(): Set { + fun getExplicitCertificates(): Set { return explicitCertificates.toSet() } @@ -406,15 +445,31 @@ class ConsumerOptions { * @param keyId key id * @return certificate */ - fun getCertificate(keyId: Long): PGPPublicKeyRing? { - return explicitCertificates.firstOrNull { it.getPublicKey(keyId) != null } + @Deprecated("Pass in a KeyIdentifier instead.") + fun getCertificate(keyId: Long): OpenPGPCertificate? { + return getCertificate(KeyIdentifier(keyId)) } - fun getCertificate(signature: PGPSignature): PGPPublicKeyRing? = - explicitCertificates.firstOrNull { it.getPublicKeyFor(signature) != null } + /** + * Return a certificate which contains a component key for the given [identifier]. This + * method first checks all explicitly provided verification certs and if no cert is found it + * consults the certificate stores. + * + * @param identifier key identifier + * @return certificate or null if no match is found + */ + fun getCertificate(identifier: KeyIdentifier): OpenPGPCertificate? { + return explicitCertificates.firstOrNull { it.getKey(identifier) != null } + } + + /** Find a certificate containing the issuer component key for the given [signature]. */ + fun getCertificate(signature: PGPSignature): OpenPGPCertificate? = + explicitCertificates.firstOrNull { it.getSigningKeyFor(signature) != null } } companion object { - @JvmStatic fun get() = ConsumerOptions() + @JvmOverloads + @JvmStatic + fun get(api: PGPainless = PGPainless.getInstance()) = ConsumerOptions(api) } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt index d1d4f8b2..147fa62a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt @@ -5,22 +5,24 @@ package org.pgpainless.decryption_verification import java.io.InputStream +import org.pgpainless.PGPainless /** * Builder class that takes an [InputStream] of ciphertext (or plaintext signed data) and combines * it with a configured [ConsumerOptions] object to form a [DecryptionStream] which can be used to * decrypt an OpenPGP message or verify signatures. */ -class DecryptionBuilder : DecryptionBuilderInterface { +class DecryptionBuilder(private val api: PGPainless) : DecryptionBuilderInterface { override fun onInputStream(inputStream: InputStream): DecryptionBuilderInterface.DecryptWith { - return DecryptWithImpl(inputStream) + return DecryptWithImpl(inputStream, api) } - class DecryptWithImpl(val inputStream: InputStream) : DecryptionBuilderInterface.DecryptWith { + class DecryptWithImpl(val inputStream: InputStream, val api: PGPainless) : + DecryptionBuilderInterface.DecryptWith { override fun withOptions(consumerOptions: ConsumerOptions): DecryptionStream { - return OpenPgpMessageInputStream.create(inputStream, consumerOptions) + return OpenPgpMessageInputStream.create(inputStream, consumerOptions, api) } } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt index 50ef3e02..27f53bc6 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt @@ -6,6 +6,7 @@ package org.pgpainless.decryption_verification import kotlin.jvm.Throws import org.bouncycastle.bcpg.AEADEncDataPacket +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPSessionKey @@ -33,12 +34,36 @@ class HardwareSecurity { * @return decrypted session key * @throws HardwareSecurityException exception */ + @Deprecated("Pass in a KeyIdentifier instead of a Long keyId.") @Throws(HardwareSecurityException::class) fun decryptSessionKey( keyId: Long, keyAlgorithm: Int, sessionKeyData: ByteArray, pkeskVersion: Int + ): ByteArray = + decryptSessionKey(KeyIdentifier(keyId), keyAlgorithm, sessionKeyData, pkeskVersion) + + /** + * 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 [HardwareSecurityException] is + * thrown. + * + * @param keyIdentifier identifier of the encryption component key + * @param keyAlgorithm algorithm + * @param sessionKeyData encrypted session key + * @param pkeskVersion version of the Public-Key-Encrypted-Session-Key packet (3 or 6) + * @return decrypted session key + * @throws HardwareSecurityException exception + */ + @Throws(HardwareSecurityException::class) + fun decryptSessionKey( + keyIdentifier: KeyIdentifier, + keyAlgorithm: Int, + sessionKeyData: ByteArray, + pkeskVersion: Int ): ByteArray } @@ -77,6 +102,7 @@ class HardwareSecurity { return factory.createDataDecryptor(seipd, sessionKey) } + @Deprecated("Deprecated in Java") override fun recoverSessionData( keyAlgorithm: Int, secKeyData: Array, @@ -84,7 +110,7 @@ class HardwareSecurity { ): ByteArray { return try { callback.decryptSessionKey( - subkeyIdentifier.subkeyId, keyAlgorithm, secKeyData[0], pkeskVersion) + subkeyIdentifier.keyIdentifier, keyAlgorithm, secKeyData[0], pkeskVersion) } catch (e: HardwareSecurityException) { throw PGPException("Hardware-backed decryption failed.", e) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt index 4618882c..3b215669 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt @@ -9,8 +9,6 @@ import java.io.InputStream import org.bouncycastle.openpgp.PGPEncryptedData import org.bouncycastle.openpgp.PGPException import org.pgpainless.exception.ModificationDetectionException -import org.slf4j.Logger -import org.slf4j.LoggerFactory class IntegrityProtectedInputStream( private val inputStream: InputStream, @@ -30,15 +28,9 @@ class IntegrityProtectedInputStream( if (encryptedData.isIntegrityProtected && !options.isIgnoreMDCErrors()) { try { if (!encryptedData.verify()) throw ModificationDetectionException() - LOGGER.debug("Integrity Protection check passed.") } catch (e: PGPException) { throw IOException("Data appears to not be integrity protected.", e) } } } - - companion object { - @JvmStatic - val LOGGER: Logger = LoggerFactory.getLogger(IntegrityProtectedInputStream::class.java) - } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt index acfcba51..0a2fb971 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt @@ -6,104 +6,103 @@ package org.pgpainless.decryption_verification import java.io.IOException import java.io.InputStream +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.* -import org.pgpainless.implementation.ImplementationFactory +import org.bouncycastle.openpgp.api.OpenPGPImplementation +import org.pgpainless.PGPainless import org.pgpainless.util.ArmorUtils /** * Inspect an OpenPGP message to determine IDs of its encryption keys or whether it is passphrase * protected. */ -class MessageInspector { +class MessageInspector(val api: PGPainless = PGPainless.getInstance()) { /** * Info about an OpenPGP message. * - * @param keyIds List of recipient key ids for whom the message is encrypted. + * @param keyIdentifiers List of recipient [KeyIdentifiers][KeyIdentifier] for whom the message + * is encrypted. * @param isPassphraseEncrypted true, if the message is encrypted for a passphrase * @param isSignedOnly true, if the message is not encrypted, but signed using OnePassSignatures */ data class EncryptionInfo( - val keyIds: List, + val keyIdentifiers: List, val isPassphraseEncrypted: Boolean, val isSignedOnly: Boolean ) { val isEncrypted: Boolean get() = isPassphraseEncrypted || keyIds.isNotEmpty() + + val keyIds: List = keyIdentifiers.map { it.keyId } } - companion object { + /** + * Parses parts of the provided OpenPGP message in order to determine which keys were used to + * encrypt it. + * + * @param message OpenPGP message + * @return encryption info + * @throws PGPException in case the message is broken + * @throws IOException in case of an IO error + */ + @Throws(PGPException::class, IOException::class) + fun determineEncryptionInfoForMessage(message: String): EncryptionInfo = + determineEncryptionInfoForMessage(message.byteInputStream()) - /** - * Parses parts of the provided OpenPGP message in order to determine which keys were used - * to encrypt it. - * - * @param message OpenPGP message - * @return encryption info - * @throws PGPException in case the message is broken - * @throws IOException in case of an IO error - */ - @JvmStatic - @Throws(PGPException::class, IOException::class) - fun determineEncryptionInfoForMessage(message: String): EncryptionInfo = - determineEncryptionInfoForMessage(message.byteInputStream()) + /** + * Parses parts of the provided OpenPGP message in order to determine which keys were used to + * encrypt it. Note: This method does not rewind the passed in Stream, so you might need to take + * care of that yourselves. + * + * @param inputStream openpgp message + * @return encryption information + * @throws IOException in case of an IO error + * @throws PGPException if the message is broken + */ + @Throws(PGPException::class, IOException::class) + fun determineEncryptionInfoForMessage(inputStream: InputStream): EncryptionInfo { + return processMessage(ArmorUtils.getDecoderStream(inputStream)) + } - /** - * Parses parts of the provided OpenPGP message in order to determine which keys were used - * to encrypt it. Note: This method does not rewind the passed in Stream, so you might need - * to take care of that yourselves. - * - * @param inputStream openpgp message - * @return encryption information - * @throws IOException in case of an IO error - * @throws PGPException if the message is broken - */ - @JvmStatic - @Throws(PGPException::class, IOException::class) - fun determineEncryptionInfoForMessage(inputStream: InputStream): EncryptionInfo { - return processMessage(ArmorUtils.getDecoderStream(inputStream)) - } + @Throws(PGPException::class, IOException::class) + private fun processMessage(inputStream: InputStream): EncryptionInfo { + var objectFactory = api.implementation.pgpObjectFactory(inputStream) - @JvmStatic - @Throws(PGPException::class, IOException::class) - private fun processMessage(inputStream: InputStream): EncryptionInfo { - var objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(inputStream) - - var n: Any? - while (objectFactory.nextObject().also { n = it } != null) { - when (val next = n!!) { - is PGPOnePassSignatureList -> { - if (!next.isEmpty) { - return EncryptionInfo( - listOf(), isPassphraseEncrypted = false, isSignedOnly = true) - } - } - is PGPEncryptedDataList -> { - var isPassphraseEncrypted = false - val keyIds = mutableListOf() - for (encryptedData in next) { - if (encryptedData is PGPPublicKeyEncryptedData) { - keyIds.add(encryptedData.keyID) - } else if (encryptedData is PGPPBEEncryptedData) { - isPassphraseEncrypted = true - } - } - // Data is encrypted, we cannot go deeper - return EncryptionInfo(keyIds, isPassphraseEncrypted, false) - } - is PGPCompressedData -> { - objectFactory = - ImplementationFactory.getInstance() - .getPGPObjectFactory(PGPUtil.getDecoderStream(next.dataStream)) - continue - } - is PGPLiteralData -> { - break + var n: Any? + while (objectFactory.nextObject().also { n = it } != null) { + when (val next = n!!) { + is PGPOnePassSignatureList -> { + if (!next.isEmpty) { + return EncryptionInfo( + listOf(), isPassphraseEncrypted = false, isSignedOnly = true) } } + is PGPEncryptedDataList -> { + var isPassphraseEncrypted = false + val keyIdentifiers = mutableListOf() + for (encryptedData in next) { + if (encryptedData is PGPPublicKeyEncryptedData) { + keyIdentifiers.add(encryptedData.keyIdentifier) + } else if (encryptedData is PGPPBEEncryptedData) { + isPassphraseEncrypted = true + } + } + // Data is encrypted, we cannot go deeper + return EncryptionInfo(keyIdentifiers, isPassphraseEncrypted, false) + } + is PGPCompressedData -> { + objectFactory = + OpenPGPImplementation.getInstance() + .pgpObjectFactory(PGPUtil.getDecoderStream(next.dataStream)) + continue + } + is PGPLiteralData -> { + break + } } - return EncryptionInfo(listOf(), isPassphraseEncrypted = false, isSignedOnly = false) } + return EncryptionInfo(listOf(), isPassphraseEncrypted = false, isSignedOnly = false) } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt index f7238391..4e0260c1 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt @@ -6,8 +6,11 @@ package org.pgpainless.decryption_verification import java.util.* import javax.annotation.Nonnull +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPLiteralData +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism +import org.bouncycastle.openpgp.api.OpenPGPCertificate import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.algorithm.SymmetricKeyAlgorithm @@ -21,35 +24,67 @@ import org.pgpainless.util.SessionKey /** View for extracting metadata about a [Message]. */ class MessageMetadata(val message: Message) { - // ################################################################################################################ - // ### Encryption - // ### - // ################################################################################################################ + // ########################################################################################################## + // Encryption + // ########################################################################################################## /** * The [SymmetricKeyAlgorithm] of the outermost encrypted data packet, or null if message is * unencrypted. */ + @Deprecated( + "Deprecated in favor of encryptionMechanism", + replaceWith = ReplaceWith("encryptionMechanism")) val encryptionAlgorithm: SymmetricKeyAlgorithm? get() = encryptionAlgorithms.let { if (it.hasNext()) it.next() else null } + /** + * The [MessageEncryptionMechanism] of the outermost encrypted data packet, or null if the + * message is unencrypted. + */ + val encryptionMechanism: MessageEncryptionMechanism? + get() = encryptionMechanisms.let { if (it.hasNext()) it.next() else null } + /** * [Iterator] of each [SymmetricKeyAlgorithm] 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. */ + @Deprecated( + "Deprecated in favor of encryptionMechanisms", + replaceWith = ReplaceWith("encryptionMechanisms")) val encryptionAlgorithms: Iterator get() = encryptionLayers.asSequence().map { it.algorithm }.iterator() + /** + * [Iterator] of each [MessageEncryptionMechanism] encountered in the message. The first item + * returned by the iterator is the encryption mechanism 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. + */ + val encryptionMechanisms: Iterator + get() = encryptionLayers.asSequence().map { it.mechanism }.iterator() + + /** Return true, if the message is encrypted, false otherwise. */ val isEncrypted: Boolean get() = - if (encryptionAlgorithm == null) false - else encryptionAlgorithm != SymmetricKeyAlgorithm.NULL + if (encryptionMechanism == null) false + else + encryptionMechanism!!.symmetricKeyAlgorithm != + SymmetricKeyAlgorithm.NULL.algorithmId - fun isEncryptedFor(keys: PGPKeyRing): Boolean { + /** Return true, if the message was encrypted for the given [OpenPGPCertificate]. */ + fun isEncryptedFor(cert: OpenPGPCertificate): Boolean { return encryptionLayers.asSequence().any { - it.recipients.any { keyId -> keys.getPublicKey(keyId) != null } + it.recipients.any { identifier -> cert.getKey(identifier) != null } + } + } + + /** Return true, if the message was encrypted for the given [PGPKeyRing]. */ + fun isEncryptedFor(cert: PGPKeyRing): Boolean { + return encryptionLayers.asSequence().any { + it.recipients.any { keyId -> cert.getPublicKey(keyId) != null } } } @@ -78,17 +113,25 @@ class MessageMetadata(val message: Message) { get() = encryptionLayers.asSequence().mapNotNull { it.decryptionKey }.firstOrNull() /** List containing all recipient keyIDs. */ + @Deprecated( + "Use of key-ids is discouraged in favor of KeyIdentifiers", + replaceWith = ReplaceWith("recipientKeyIdentifiers")) val recipientKeyIds: List + get() = recipientKeyIdentifiers.map { it.keyId }.toList() + + /** List containing all recipient [KeyIdentifiers][KeyIdentifier]. */ + val recipientKeyIdentifiers: List get() = encryptionLayers .asSequence() .map { it.recipients.toMutableList() } - .reduce { all, keyIds -> - all.addAll(keyIds) + .reduce { all, keyIdentifiers -> + all.addAll(keyIdentifiers) all } .toList() + /** [Iterator] of all [EncryptedData] layers of the message. */ val encryptionLayers: Iterator get() = object : LayerIterator(message) { @@ -97,10 +140,9 @@ class MessageMetadata(val message: Message) { override fun getProperty(last: Layer) = last as EncryptedData } - // ################################################################################################################ - // ### Compression - // ### - // ################################################################################################################ + // ########################################################################################################## + // Compression + // ########################################################################################################## /** * [CompressionAlgorithm] of the outermost compressed data packet, or null, if the message does @@ -118,6 +160,7 @@ class MessageMetadata(val message: Message) { val compressionAlgorithms: Iterator get() = compressionLayers.asSequence().map { it.algorithm }.iterator() + /** [Iterator] of all [CompressedData] layers of the message. */ val compressionLayers: Iterator get() = object : LayerIterator(message) { @@ -126,10 +169,9 @@ class MessageMetadata(val message: Message) { override fun getProperty(last: Layer) = last as CompressedData } - // ################################################################################################################ - // ### Signatures - // ### - // ################################################################################################################ + // ########################################################################################################## + // Signatures + // ########################################################################################################## val isUsingCleartextSignatureFramework: Boolean get() = message.cleartextSigned @@ -253,7 +295,8 @@ class MessageMetadata(val message: Message) { email, it.signature.creationTime, targetAmount) - .authenticated + ?.authenticated + ?: false } } @@ -270,6 +313,9 @@ class MessageMetadata(val message: Message) { fun isVerifiedSignedBy(keys: PGPKeyRing) = verifiedSignatures.any { keys.matches(it.signingKey) } + fun isVerifiedSignedBy(cert: OpenPGPCertificate) = + verifiedSignatures.any { cert.pgpKeyRing.matches(it.signingKey) } + fun isVerifiedDetachedSignedBy(fingerprint: OpenPgpFingerprint) = verifiedDetachedSignatures.any { it.signingKey.matches(fingerprint) } @@ -282,18 +328,16 @@ class MessageMetadata(val message: Message) { fun isVerifiedInlineSignedBy(keys: PGPKeyRing) = verifiedInlineSignatures.any { keys.matches(it.signingKey) } - // ################################################################################################################ - // ### Literal Data - // ### - // ################################################################################################################ + // ########################################################################################################## + // Literal Data + // ########################################################################################################## /** * 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. * - * @see RFC4880 §5.9. Literal Data - * Packet + * @see [RFC4880 §5.9. Literal Data Packet](https://www.rfc-editor.org/rfc/rfc4880#section-5.9) */ val filename: String? = findLiteralData()?.fileName @@ -310,8 +354,7 @@ class MessageMetadata(val message: Message) { * the modification date of a decrypted file, but since this field is not necessarily part of * the signed data, its use is discouraged. * - * @see RFC4880 §5.9. Literal Data - * Packet + * @see [RFC4880 §5.9. Literal Data Packet](https://www.rfc-editor.org/rfc/rfc4880#section-5.9) */ val modificationDate: Date? = findLiteralData()?.modificationDate @@ -320,8 +363,7 @@ class MessageMetadata(val message: Message) { * binary data, ...) the data has. Since this field is not necessarily part of the signed data * of a message, its usage is discouraged. * - * @see RFC4880 §5.9. Literal Data - * Packet + * @see [RFC4880 §5.9. Literal Data Packet](https://www.rfc-editor.org/rfc/rfc4880#section-5.9) */ val literalDataEncoding: StreamEncoding? = findLiteralData()?.format @@ -349,10 +391,9 @@ class MessageMetadata(val message: Message) { return nested as LiteralData } - // ################################################################################################################ - // ### Message Structure - // ### - // ################################################################################################################ + // ########################################################################################################## + // Message Structure + // ########################################################################################################## interface Packet @@ -415,8 +456,8 @@ class MessageMetadata(val message: Message) { * Outermost OpenPGP Message structure. * * @param cleartextSigned whether the message is using the Cleartext Signature Framework - * @see RFC4880 §7. Cleartext - * Signature Framework + * @see + * [RFC4880 §7. Cleartext Signature Framework](https://www.rfc-editor.org/rfc/rfc4880#section-7) */ class Message(var cleartextSigned: Boolean = false) : Layer(0) { fun setCleartextSigned() = apply { cleartextSigned = true } @@ -455,18 +496,24 @@ class MessageMetadata(val message: Message) { /** * Encrypted Data. * - * @param algorithm symmetric key algorithm used to encrypt the packet. + * @param mechanism mechanism used to encrypt the packet. * @param depth nesting depth at which this packet was encountered. */ - class EncryptedData(val algorithm: SymmetricKeyAlgorithm, depth: Int) : Layer(depth), Nested { + class EncryptedData(val mechanism: MessageEncryptionMechanism, depth: Int) : + Layer(depth), Nested { /** [SessionKey] used to decrypt the packet. */ var sessionKey: SessionKey? = null /** List of all recipient key ids to which the packet was encrypted for. */ - val recipients: List = mutableListOf() + val recipients: List = mutableListOf() - fun addRecipients(keyIds: List) = apply { (recipients as MutableList).addAll(keyIds) } + val algorithm: SymmetricKeyAlgorithm = + SymmetricKeyAlgorithm.requireFromId(mechanism.symmetricKeyAlgorithm) + + fun addRecipients(keyIds: List) = apply { + (recipients as MutableList).addAll(keyIds) + } /** * Identifier of the subkey that was used to decrypt the packet (in case of a public key diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt deleted file mode 100644 index eb81847f..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification - -import org.bouncycastle.openpgp.PGPPublicKeyRing - -fun interface MissingPublicKeyCallback { - - /** - * This method gets called if we encounter a signature made by a key which was not provided for - * signature verification. If you cannot provide the requested key, it is safe to return null - * here. PGPainless will then continue verification with the next signature. - * - * Note: The key-id might belong to a subkey, so be aware that when looking up the - * [PGPPublicKeyRing], 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. - * - * @param keyId ID of the missing signing (sub)key - * @return keyring containing the key or null - * @see RFC - */ - fun onMissingPublicKeyEncountered(keyId: Long): PGPPublicKeyRing? -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPGPAnimalSnifferInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPGPAnimalSnifferInputStream.kt new file mode 100644 index 00000000..8eb7f44c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPGPAnimalSnifferInputStream.kt @@ -0,0 +1,321 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import java.io.BufferedInputStream +import java.io.ByteArrayInputStream +import java.io.InputStream +import org.bouncycastle.bcpg.AEADEncDataPacket +import org.bouncycastle.bcpg.BCPGInputStream +import org.bouncycastle.bcpg.CompressedDataPacket +import org.bouncycastle.bcpg.LiteralDataPacket +import org.bouncycastle.bcpg.MarkerPacket +import org.bouncycastle.bcpg.OnePassSignaturePacket +import org.bouncycastle.bcpg.PacketFormat +import org.bouncycastle.bcpg.PacketTags.AEAD_ENC_DATA +import org.bouncycastle.bcpg.PacketTags.COMPRESSED_DATA +import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_1 +import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_2 +import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_3 +import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_4 +import org.bouncycastle.bcpg.PacketTags.LITERAL_DATA +import org.bouncycastle.bcpg.PacketTags.MARKER +import org.bouncycastle.bcpg.PacketTags.MOD_DETECTION_CODE +import org.bouncycastle.bcpg.PacketTags.ONE_PASS_SIGNATURE +import org.bouncycastle.bcpg.PacketTags.PADDING +import org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY +import org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY_ENC_SESSION +import org.bouncycastle.bcpg.PacketTags.PUBLIC_SUBKEY +import org.bouncycastle.bcpg.PacketTags.RESERVED +import org.bouncycastle.bcpg.PacketTags.SECRET_KEY +import org.bouncycastle.bcpg.PacketTags.SECRET_SUBKEY +import org.bouncycastle.bcpg.PacketTags.SIGNATURE +import org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC +import org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC_SESSION +import org.bouncycastle.bcpg.PacketTags.SYM_ENC_INTEGRITY_PRO +import org.bouncycastle.bcpg.PacketTags.TRUST +import org.bouncycastle.bcpg.PacketTags.USER_ATTRIBUTE +import org.bouncycastle.bcpg.PacketTags.USER_ID +import org.bouncycastle.bcpg.PublicKeyEncSessionPacket +import org.bouncycastle.bcpg.PublicKeyPacket +import org.bouncycastle.bcpg.SecretKeyPacket +import org.bouncycastle.bcpg.SignaturePacket +import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket +import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket +import org.bouncycastle.util.Arrays +import org.pgpainless.algorithm.AEADAlgorithm +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.algorithm.SymmetricKeyAlgorithm + +/** + * InputStream used to determine the nature of potential OpenPGP data. + * + * @param input underlying input stream + * @param check whether to perform the costly checking inside the constructor + */ +class OpenPGPAnimalSnifferInputStream(input: InputStream, check: Boolean) : + BufferedInputStream(input) { + + private val buffer: ByteArray + private val bufferLen: Int + + private var containsArmorHeader: Boolean = false + private var containsOpenPgpPackets: Boolean = false + private var resemblesMessage: Boolean = false + + init { + mark(MAX_BUFFER_SIZE) + buffer = ByteArray(MAX_BUFFER_SIZE) + bufferLen = read(buffer) + reset() + + if (check) { + inspectBuffer() + } + } + + constructor(input: InputStream) : this(input, true) + + /** Return true, if the underlying data is ASCII armored. */ + val isAsciiArmored: Boolean + get() = containsArmorHeader + + /** + * Return true, if the data is possibly binary OpenPGP. The criterion for this are less strict + * than for [resemblesMessage], 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 + */ + val isBinaryOpenPgp: Boolean + get() = 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 a [PGPEncryptedData], + * [PGPCompressedData], [PGPOnePassSignature] or [PGPLiteralData] packet. The plausibility of + * these data packets is checked as far as possible. + * + * @return true if likely OpenPGP message + */ + val isLikelyOpenPgpMessage: Boolean + get() = resemblesMessage + + /** Return true, if the underlying data is non-OpenPGP data. */ + val isNonOpenPgp: Boolean + get() = !isAsciiArmored && !isBinaryOpenPgp + + /** Costly perform a plausibility check of the first encountered OpenPGP packet. */ + fun inspectBuffer() { + if (checkForAsciiArmor()) { + return + } + + checkForBinaryOpenPgp() + } + + private fun checkForAsciiArmor(): Boolean { + if (startsWithIgnoringWhitespace(buffer, ARMOR_HEADER, bufferLen)) { + containsArmorHeader = true + return true + } + 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 fun checkForBinaryOpenPgp() { + if (bufferLen == -1) { + // empty data + return + } + + val bufferIn = ByteArrayInputStream(buffer, 0, bufferLen) + val pIn = BCPGInputStream(bufferIn) + try { + nonExhaustiveParseAndCheckPlausibility(pIn) + } catch (e: Exception) { + return + } + } + + private fun nonExhaustiveParseAndCheckPlausibility(packetIn: BCPGInputStream) { + val packet = packetIn.readPacket() + when (packet.packetTag) { + PUBLIC_KEY_ENC_SESSION -> { + packet as PublicKeyEncSessionPacket + if (PublicKeyAlgorithm.fromId(packet.algorithm) == null) { + return + } + } + SIGNATURE -> { + packet as SignaturePacket + if (SignatureType.fromCode(packet.signatureType) == null) { + return + } + if (PublicKeyAlgorithm.fromId(packet.keyAlgorithm) == null) { + return + } + if (HashAlgorithm.fromId(packet.hashAlgorithm) == null) { + return + } + } + ONE_PASS_SIGNATURE -> { + packet as OnePassSignaturePacket + if (SignatureType.fromCode(packet.signatureType) == null) { + return + } + if (PublicKeyAlgorithm.fromId(packet.keyAlgorithm) == null) { + return + } + if (HashAlgorithm.fromId(packet.hashAlgorithm) == null) { + return + } + } + SYMMETRIC_KEY_ENC_SESSION -> { + packet as SymmetricKeyEncSessionPacket + if (SymmetricKeyAlgorithm.fromId(packet.encAlgorithm) == null) { + return + } + } + SECRET_KEY -> { + packet as SecretKeyPacket + val publicKey = packet.publicKeyPacket + if (PublicKeyAlgorithm.fromId(publicKey.algorithm) == null) { + return + } + if (publicKey.version !in 3..6) { + return + } + } + PUBLIC_KEY -> { + packet as PublicKeyPacket + if (PublicKeyAlgorithm.fromId(packet.algorithm) == null) { + return + } + if (packet.version !in 3..6) { + return + } + } + COMPRESSED_DATA -> { + packet as CompressedDataPacket + if (CompressionAlgorithm.fromId(packet.algorithm) == null) { + return + } + } + SYMMETRIC_KEY_ENC -> { + // Not much we can check here + } + MARKER -> { + packet as MarkerPacket + if (!Arrays.areEqual( + packet.getEncoded(PacketFormat.CURRENT), + byteArrayOf(0xca.toByte(), 0x03, 0x50, 0x47, 0x50), + )) { + return + } + } + LITERAL_DATA -> { + packet as LiteralDataPacket + if (packet.format.toChar() !in charArrayOf('b', 'u', 't', 'l', '1', 'm')) { + return + } + } + SYM_ENC_INTEGRITY_PRO -> { + packet as SymmetricEncIntegrityPacket + if (packet.version !in + intArrayOf( + SymmetricEncIntegrityPacket.VERSION_1, + SymmetricEncIntegrityPacket.VERSION_2)) { + return + } + + if (packet.version == SymmetricEncIntegrityPacket.VERSION_2) { + if (SymmetricKeyAlgorithm.fromId(packet.cipherAlgorithm) == null) { + return + } + if (AEADAlgorithm.fromId(packet.aeadAlgorithm) == null) { + return + } + } + } + AEAD_ENC_DATA -> { + packet as AEADEncDataPacket + if (SymmetricKeyAlgorithm.fromId(packet.algorithm.toInt()) == null) { + return + } + } + RESERVED, // this Packet Type ID MUST NOT be used + PUBLIC_SUBKEY, // Never found at the start of a stream + SECRET_SUBKEY, // Never found at the start of a stream + TRUST, // Never found at the start of a stream + MOD_DETECTION_CODE, // At the end of SED data - Never found at the start of a stream + USER_ID, // Never found at the start of a stream + USER_ATTRIBUTE, // Never found at the start of a stream + PADDING, // At the end of messages (optionally padded message) or certificates + EXPERIMENTAL_1, // experimental + EXPERIMENTAL_2, // experimental + EXPERIMENTAL_3, // experimental + EXPERIMENTAL_4 -> { // experimental + containsOpenPgpPackets = true + resemblesMessage = false + return + } + else -> return + } + + containsOpenPgpPackets = true + if (packet.packetTag != SYMMETRIC_KEY_ENC) { + resemblesMessage = true + } + } + + private fun startsWithIgnoringWhitespace( + bytes: ByteArray, + subSequence: CharSequence, + bufferLen: Int + ): Boolean { + if (bufferLen == -1) { + return false + } + + for (i in 0 until bufferLen) { + // Working on bytes is not trivial with unicode data, but its good enough here + if (Character.isWhitespace(bytes[i].toInt())) { + continue + } + + if ((i + subSequence.length) > bytes.size) { + return false + } + + for (j in subSequence.indices) { + if (bytes[i + j].toInt().toChar() != subSequence[j]) { + return false + } + } + return true + } + return false + } + + companion object { + const val ARMOR_HEADER = "-----BEGIN PGP " + const val MAX_BUFFER_SIZE = 8192 * 2 + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index bd24b245..8425181a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -11,22 +11,32 @@ import java.io.OutputStream import java.util.zip.Inflater import java.util.zip.InflaterInputStream import openpgp.openPgpKeyId +import org.bouncycastle.bcpg.AEADEncDataPacket import org.bouncycastle.bcpg.BCPGInputStream import org.bouncycastle.bcpg.CompressionAlgorithmTags +import org.bouncycastle.bcpg.KeyIdentifier +import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket import org.bouncycastle.bcpg.UnsupportedPacketVersionException import org.bouncycastle.openpgp.PGPCompressedData import org.bouncycastle.openpgp.PGPEncryptedData import org.bouncycastle.openpgp.PGPEncryptedDataList import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPKeyPair import org.bouncycastle.openpgp.PGPOnePassSignature 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.PGPSignatureException +import org.bouncycastle.openpgp.api.EncryptedDataPacketType +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPPrivateKey +import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPSecretKey +import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature +import org.bouncycastle.openpgp.api.exception.MalformedOpenPGPSignatureException import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory import org.bouncycastle.util.io.TeeInputStream @@ -35,10 +45,10 @@ import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.OpenPgpPacket import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.algorithm.SymmetricKeyAlgorithm -import org.pgpainless.bouncycastle.extensions.getPublicKeyFor +import org.pgpainless.bouncycastle.extensions.assertCreatedInBounds import org.pgpainless.bouncycastle.extensions.getSecretKeyFor +import org.pgpainless.bouncycastle.extensions.getSigningKeyFor import org.pgpainless.bouncycastle.extensions.issuerKeyId -import org.pgpainless.bouncycastle.extensions.unlock import org.pgpainless.decryption_verification.MessageMetadata.CompressedData import org.pgpainless.decryption_verification.MessageMetadata.EncryptedData import org.pgpainless.decryption_verification.MessageMetadata.Layer @@ -55,14 +65,10 @@ 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 +import org.pgpainless.exception.WrongPassphraseException import org.pgpainless.key.SubkeyIdentifier -import org.pgpainless.key.util.KeyRingUtils -import org.pgpainless.policy.Policy -import org.pgpainless.signature.consumer.CertificateValidator +import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey import org.pgpainless.signature.consumer.OnePassSignatureCheck -import org.pgpainless.signature.consumer.SignatureCheck -import org.pgpainless.signature.consumer.SignatureValidator import org.pgpainless.util.ArmoredInputStreamFactory import org.pgpainless.util.SessionKey import org.slf4j.LoggerFactory @@ -72,10 +78,10 @@ class OpenPgpMessageInputStream( inputStream: InputStream, private val options: ConsumerOptions, private val layerMetadata: Layer, - private val policy: Policy + private val api: PGPainless ) : DecryptionStream() { - private val signatures: Signatures = Signatures(options) + private val signatures: Signatures = Signatures(options, api) private var packetInputStream: TeeBCPGInputStream? = null private var nestedInputStream: InputStream? = null private val syntaxVerifier = PDA() @@ -129,8 +135,8 @@ class OpenPgpMessageInputStream( inputStream: InputStream, options: ConsumerOptions, metadata: Layer, - policy: Policy - ) : this(Type.standard, inputStream, options, metadata, policy) + api: PGPainless + ) : this(Type.standard, inputStream, options, metadata, api) private fun consumePackets() { val pIn = packetInputStream ?: return @@ -176,7 +182,7 @@ class OpenPgpMessageInputStream( } OpenPgpPacket.PADDING -> { LOGGER.debug("Skipping Padding Packet") - pIn.readPacket() + pIn.readPadding() } OpenPgpPacket.SK, OpenPgpPacket.PK, @@ -186,10 +192,6 @@ class OpenPgpMessageInputStream( OpenPgpPacket.UID, OpenPgpPacket.UATTR -> throw MalformedOpenPgpMessageException("Illegal Packet in Stream: $packet") - OpenPgpPacket.PADDING -> { - LOGGER.debug("Padding packet") - pIn.readPadding() - } OpenPgpPacket.EXP_1, OpenPgpPacket.EXP_2, OpenPgpPacket.EXP_3, @@ -230,7 +232,7 @@ class OpenPgpMessageInputStream( LOGGER.debug( "Compressed Data Packet (${compressionLayer.algorithm}) at depth ${layerMetadata.depth} encountered.") nestedInputStream = - OpenPgpMessageInputStream(decompress(compressedData), options, compressionLayer, policy) + OpenPgpMessageInputStream(decompress(compressedData), options, compressionLayer, api) } private fun decompress(compressedData: PGPCompressedData): InputStream { @@ -311,7 +313,7 @@ class OpenPgpMessageInputStream( signatures .leaveNesting() // TODO: Only leave nesting if all OPSs of the nesting layer are // dealt with - signatures.addCorrespondingOnePassSignature(signature, layerMetadata, policy) + signatures.addCorrespondingOnePassSignature(signature, layerMetadata) } else { LOGGER.debug( "Prepended Signature Packet by key ${keyId.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") @@ -320,20 +322,38 @@ class OpenPgpMessageInputStream( } private fun processEncryptedData(): Boolean { - LOGGER.debug( - "Symmetrically Encrypted Data Packet at depth ${layerMetadata.depth} encountered.") + // TODO: Replace by dedicated encryption packet type input symbols syntaxVerifier.next(InputSymbol.ENCRYPTED_DATA) + val encDataList = packetInputStream!!.readEncryptedDataList() - if (!encDataList.isIntegrityProtected && !encDataList.get(0).isAEAD) { - LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected.") - if (!options.isIgnoreMDCErrors()) { - throw MessageNotIntegrityProtectedException() + val esks = ESKsAndData(encDataList) + + when (EncryptedDataPacketType.of(encDataList)!!) { + EncryptedDataPacketType.SEIPDv2 -> + LOGGER.debug( + "Symmetrically Encrypted Integrity Protected Data Packet version 2 at depth " + + "${layerMetadata.depth} encountered.") + EncryptedDataPacketType.SEIPDv1 -> + LOGGER.debug( + "Symmetrically Encrypted Integrity Protected Data Packet version 1 at depth " + + "${layerMetadata.depth} encountered.") + EncryptedDataPacketType.LIBREPGP_OED -> + LOGGER.debug( + "LibrePGP OCB-Encrypted Data Packet at depth " + + "${layerMetadata.depth} encountered.") + EncryptedDataPacketType.SED -> { + LOGGER.debug( + "(Deprecated) Symmetrically Encrypted Data Packet at depth " + + "${layerMetadata.depth} encountered.") + LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected.") + if (!options.isIgnoreMDCErrors()) { + throw MessageNotIntegrityProtectedException() + } } } - val esks = SortedESKs(encDataList) LOGGER.debug( - "Symmetrically Encrypted Integrity-Protected Data has ${esks.skesks.size} SKESK(s) and" + + "Encrypted 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.") @@ -343,7 +363,7 @@ class OpenPgpMessageInputStream( esks.pkesks .filter { // find matching PKESK - it.keyID == key.subkeyId + it.keyIdentifier == key.keyIdentifier } .forEach { // attempt decryption @@ -359,9 +379,9 @@ class OpenPgpMessageInputStream( LOGGER.debug("Attempt decryption with provided session key.") throwIfUnacceptable(sk.algorithm) - val decryptorFactory = - ImplementationFactory.getInstance().getSessionKeyDataDecryptorFactory(sk) - val layer = EncryptedData(sk.algorithm, layerMetadata.depth + 1) + val pgpSk = PGPSessionKey(sk.algorithm.algorithmId, sk.key) + val decryptorFactory = api.implementation.sessionKeyDataDecryptorFactory(pgpSk) + val layer = esks.toEncryptedData(sk, layerMetadata.depth + 1) val skEncData = encDataList.extractSessionKeyEncryptedData() try { val decrypted = skEncData.getDataStream(decryptorFactory) @@ -369,7 +389,7 @@ class OpenPgpMessageInputStream( val integrityProtected = IntegrityProtectedInputStream(decrypted, skEncData, options) nestedInputStream = - OpenPgpMessageInputStream(integrityProtected, options, layer, policy) + OpenPgpMessageInputStream(integrityProtected, options, layer, api) LOGGER.debug("Successfully decrypted data using provided session key") return true } catch (e: PGPException) { @@ -392,7 +412,7 @@ class OpenPgpMessageInputStream( } val decryptorFactory = - ImplementationFactory.getInstance().getPBEDataDecryptorFactory(passphrase) + api.implementation.pbeDataDecryptorFactory(passphrase.getChars()) if (decryptSKESKAndStream(esks, skesk, decryptorFactory)) { return true } @@ -400,30 +420,45 @@ class OpenPgpMessageInputStream( } val postponedDueToMissingPassphrase = - mutableListOf>() + mutableListOf>() // try (known) secret keys esks.pkesks.forEach { pkesk -> - LOGGER.debug("Encountered PKESK for recipient ${pkesk.keyID.openPgpKeyId()}") + LOGGER.debug("Encountered PKESK for recipient ${pkesk.keyIdentifier}") val decryptionKeyCandidates = getDecryptionKeys(pkesk) for (decryptionKeys in decryptionKeyCandidates) { val secretKey = decryptionKeys.getSecretKeyFor(pkesk)!! - val decryptionKeyId = SubkeyIdentifier(decryptionKeys, secretKey.keyID) - if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + if (!secretKey.isEncryptionKey && + !options.getAllowDecryptionWithNonEncryptionKey()) { + LOGGER.debug( + "Message is encrypted for ${secretKey.keyIdentifier}, but the key is not encryption capable.") + continue + } + if (hasUnsupportedS2KSpecifier(secretKey)) { continue } - LOGGER.debug("Attempt decryption using secret key $decryptionKeyId") + LOGGER.debug("Attempt decryption using secret key ${decryptionKeys.keyIdentifier}") val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue - if (!protector.hasPassphraseFor(secretKey.keyID)) { + if (!protector.hasPassphraseFor(secretKey.keyIdentifier)) { LOGGER.debug( - "Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") + "Missing passphrase for key ${decryptionKeys.keyIdentifier}. Postponing decryption until all other keys were tried.") postponedDueToMissingPassphrase.add(secretKey to pkesk) continue } - val privateKey = secretKey.unlock(protector) - if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { + val privateKey = + try { + unlockSecretKey(secretKey, protector) + } catch (e: PGPException) { + throw WrongPassphraseException(secretKey.keyIdentifier, e) + } + if (decryptWithPrivateKey( + esks, + privateKey.keyPair, + SubkeyIdentifier( + secretKey.openPGPKey.pgpSecretKeyRing, secretKey.keyIdentifier), + pkesk)) { return true } } @@ -431,24 +466,24 @@ class OpenPgpMessageInputStream( // try anonymous secret keys for (pkesk in esks.anonPkesks) { - for ((decryptionKeys, secretKey) in findPotentialDecryptionKeys(pkesk)) { - val decryptionKeyId = SubkeyIdentifier(decryptionKeys, secretKey.keyID) - if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + for (decryptionKey in findPotentialDecryptionKeys(pkesk)) { + if (hasUnsupportedS2KSpecifier(decryptionKey)) { continue } - LOGGER.debug("Attempt decryption of anonymous PKESK with key $decryptionKeyId.") - val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue + LOGGER.debug("Attempt decryption of anonymous PKESK with key $decryptionKey.") + val protector = options.getSecretKeyProtector(decryptionKey.openPGPKey) ?: continue - if (!protector.hasPassphraseFor(secretKey.keyID)) { + if (!protector.hasPassphraseFor(decryptionKey.keyIdentifier)) { LOGGER.debug( - "Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") - postponedDueToMissingPassphrase.add(secretKey to pkesk) + "Missing passphrase for key ${decryptionKey.keyIdentifier}. Postponing decryption until all other keys were tried.") + postponedDueToMissingPassphrase.add(decryptionKey to pkesk) continue } - val privateKey = secretKey.unlock(protector) - if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { + val privateKey = decryptionKey.unlock(protector) + if (decryptWithPrivateKey( + esks, privateKey.keyPair, SubkeyIdentifier(decryptionKey), pkesk)) { return true } } @@ -458,23 +493,28 @@ class OpenPgpMessageInputStream( MissingKeyPassphraseStrategy.THROW_EXCEPTION) { // Non-interactive mode: Throw an exception with all locked decryption keys postponedDueToMissingPassphrase - .map { SubkeyIdentifier(getDecryptionKey(it.first.keyID)!!, it.first.keyID) } + .map { SubkeyIdentifier(it.first) } .also { if (it.isNotEmpty()) throw MissingPassphraseException(it.toSet()) } } else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { for ((secretKey, pkesk) in postponedDueToMissingPassphrase) { - val keyId = secretKey.keyID + val keyId = secretKey.keyIdentifier val decryptionKeys = getDecryptionKey(pkesk)!! - val decryptionKeyId = SubkeyIdentifier(decryptionKeys, keyId) - if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + val decryptionKeyId = SubkeyIdentifier(decryptionKeys.pgpSecretKeyRing, keyId) + if (hasUnsupportedS2KSpecifier(secretKey)) { continue } LOGGER.debug( "Attempt decryption with key $decryptionKeyId while interactively requesting its passphrase.") val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue - val privateKey = secretKey.unlock(protector) - if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { + val privateKey: OpenPGPPrivateKey = + try { + unlockSecretKey(secretKey, protector) + } catch (e: PGPException) { + throw WrongPassphraseException(secretKey.keyIdentifier, e) + } + if (decryptWithPrivateKey(esks, privateKey.keyPair, decryptionKeyId, pkesk)) { return true } } @@ -488,25 +528,22 @@ class OpenPgpMessageInputStream( } private fun decryptWithPrivateKey( - esks: SortedESKs, - privateKey: PGPPrivateKey, + esks: ESKsAndData, + privateKey: PGPKeyPair, decryptionKeyId: SubkeyIdentifier, pkesk: PGPPublicKeyEncryptedData ): Boolean { val decryptorFactory = - ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey) + api.implementation.publicKeyDataDecryptorFactory(privateKey.privateKey) return decryptPKESKAndStream(esks, decryptionKeyId, decryptorFactory, pkesk) } - private fun hasUnsupportedS2KSpecifier( - secretKey: PGPSecretKey, - decryptionKeyId: SubkeyIdentifier - ): Boolean { - val s2k = secretKey.s2K + private fun hasUnsupportedS2KSpecifier(secretKey: OpenPGPSecretKey): Boolean { + val s2k = secretKey.pgpSecretKey.s2K if (s2k != null) { if (s2k.type in 100..110) { LOGGER.debug( - "Skipping PKESK because key $decryptionKeyId has unsupported private S2K specifier ${s2k.type}") + "Skipping PKESK because key ${secretKey.keyIdentifier} has unsupported private S2K specifier ${s2k.type}") return true } } @@ -514,7 +551,7 @@ class OpenPgpMessageInputStream( } private fun decryptSKESKAndStream( - esks: SortedESKs, + esks: ESKsAndData, skesk: PGPPBEEncryptedData, decryptorFactory: PBEDataDecryptorFactory ): Boolean { @@ -522,13 +559,13 @@ class OpenPgpMessageInputStream( val decrypted = skesk.getDataStream(decryptorFactory) val sessionKey = SessionKey(skesk.getSessionKey(decryptorFactory)) throwIfUnacceptable(sessionKey.algorithm) - val encryptedData = EncryptedData(sessionKey.algorithm, layerMetadata.depth + 1) + val encryptedData = esks.toEncryptedData(sessionKey, layerMetadata.depth + 1) encryptedData.sessionKey = sessionKey - encryptedData.addRecipients(esks.pkesks.map { it.keyID }) + encryptedData.addRecipients(esks.pkesks.map { it.keyIdentifier }) LOGGER.debug("Successfully decrypted data with passphrase") val integrityProtected = IntegrityProtectedInputStream(decrypted, skesk, options) nestedInputStream = - OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) + OpenPgpMessageInputStream(integrityProtected, options, encryptedData, api) return true } catch (e: UnacceptableAlgorithmException) { throw e @@ -540,7 +577,7 @@ class OpenPgpMessageInputStream( } private fun decryptPKESKAndStream( - esks: SortedESKs, + esks: ESKsAndData, decryptionKeyId: SubkeyIdentifier, decryptorFactory: PublicKeyDataDecryptorFactory, pkesk: PGPPublicKeyEncryptedData @@ -550,18 +587,14 @@ class OpenPgpMessageInputStream( val sessionKey = SessionKey(pkesk.getSessionKey(decryptorFactory)) throwIfUnacceptable(sessionKey.algorithm) - val encryptedData = - EncryptedData( - SymmetricKeyAlgorithm.requireFromId( - pkesk.getSymmetricAlgorithm(decryptorFactory)), - layerMetadata.depth + 1) + val encryptedData = esks.toEncryptedData(sessionKey, layerMetadata.depth) encryptedData.decryptionKey = decryptionKeyId encryptedData.sessionKey = sessionKey - encryptedData.addRecipients(esks.pkesks.plus(esks.anonPkesks).map { it.keyID }) + encryptedData.addRecipients(esks.pkesks.plus(esks.anonPkesks).map { it.keyIdentifier }) LOGGER.debug("Successfully decrypted data with key $decryptionKeyId") val integrityProtected = IntegrityProtectedInputStream(decrypted, pkesk, options) nestedInputStream = - OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) + OpenPgpMessageInputStream(integrityProtected, options, encryptedData, api) return true } catch (e: UnacceptableAlgorithmException) { throw e @@ -599,7 +632,7 @@ class OpenPgpMessageInputStream( throw RuntimeException(e) } } - signatures.finish(layerMetadata, policy) + signatures.finish(layerMetadata) } return r } @@ -626,7 +659,7 @@ class OpenPgpMessageInputStream( throw RuntimeException(e) } } - signatures.finish(layerMetadata, policy) + signatures.finish(layerMetadata) } return r } @@ -672,44 +705,33 @@ class OpenPgpMessageInputStream( return MessageMetadata((layerMetadata as Message)) } - private fun getDecryptionKey(keyId: Long): PGPSecretKeyRing? = + private fun getDecryptionKey(pkesk: PGPPublicKeyEncryptedData): OpenPGPKey? = options.getDecryptionKeys().firstOrNull { - it.any { k -> k.keyID == keyId } - .and(PGPainless.inspectKeyRing(it).decryptionSubkeys.any { k -> k.keyID == keyId }) - } - - private fun getDecryptionKey(pkesk: PGPPublicKeyEncryptedData): PGPSecretKeyRing? = - options.getDecryptionKeys().firstOrNull { - it.getSecretKeyFor(pkesk) != null && - PGPainless.inspectKeyRing(it).decryptionSubkeys.any { subkey -> - when (pkesk.version) { - 3 -> pkesk.keyID == subkey.keyID - else -> throw NotImplementedError("Version 6 PKESK not yet supported.") - } + it.pgpSecretKeyRing.getSecretKeyFor(pkesk) != null && + api.inspect(it).decryptionSubkeys.any { subkey -> + pkesk.keyIdentifier.matchesExplicit(subkey.keyIdentifier) } } - private fun getDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List = + private fun getDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List = options.getDecryptionKeys().filter { - it.getSecretKeyFor(pkesk) != null && - PGPainless.inspectKeyRing(it).decryptionSubkeys.any { subkey -> - when (pkesk.version) { - 3 -> pkesk.keyID == subkey.keyID - else -> throw NotImplementedError("Version 6 PKESK not yet supported.") - } + it.pgpSecretKeyRing.getSecretKeyFor(pkesk) != null && + api.inspect(it).decryptionSubkeys.any { subkey -> + pkesk.keyIdentifier.matchesExplicit(subkey.keyIdentifier) } } private fun findPotentialDecryptionKeys( pkesk: PGPPublicKeyEncryptedData - ): List> { + ): List { val algorithm = pkesk.algorithm - val candidates = mutableListOf>() + val candidates = mutableListOf() options.getDecryptionKeys().forEach { - val info = PGPainless.inspectKeyRing(it) + val info = api.inspect(it) for (key in info.decryptionSubkeys) { - if (key.algorithm == algorithm && info.isSecretKeyAvailable(key.keyID)) { - candidates.add(it to it.getSecretKey(key.keyID)) + if (key.pgpPublicKey.algorithm == algorithm && + info.isSecretKeyAvailable(key.keyIdentifier)) { + candidates.add(it.getSecretKey(key.keyIdentifier)) } } } @@ -717,7 +739,8 @@ class OpenPgpMessageInputStream( } private fun isAcceptable(algorithm: SymmetricKeyAlgorithm): Boolean = - policy.symmetricKeyDecryptionAlgorithmPolicy.isAcceptable(algorithm) + api.algorithmPolicy.messageDecryptionAlgorithmPolicy.symmetricAlgorithmPolicy.isAcceptable( + algorithm) private fun throwIfUnacceptable(algorithm: SymmetricKeyAlgorithm) { if (!isAcceptable(algorithm)) { @@ -726,7 +749,32 @@ class OpenPgpMessageInputStream( } } - private class SortedESKs(esks: PGPEncryptedDataList) { + private class ESKsAndData(private val esks: PGPEncryptedDataList) { + fun toEncryptedData(sk: SessionKey, depth: Int): EncryptedData { + return when (EncryptedDataPacketType.of(esks)!!) { + EncryptedDataPacketType.SED -> + EncryptedData( + MessageEncryptionMechanism.legacyEncryptedNonIntegrityProtected( + sk.algorithm.algorithmId), + depth) + EncryptedDataPacketType.SEIPDv1 -> + EncryptedData( + MessageEncryptionMechanism.integrityProtected(sk.algorithm.algorithmId), + depth) + EncryptedDataPacketType.SEIPDv2 -> { + val seipd2 = esks.encryptedData as SymmetricEncIntegrityPacket + EncryptedData( + MessageEncryptionMechanism.aead( + seipd2.cipherAlgorithm, seipd2.aeadAlgorithm), + depth) + } + EncryptedDataPacketType.LIBREPGP_OED -> { + val oed = esks.encryptedData as AEADEncDataPacket + EncryptedData(MessageEncryptionMechanism.librePgp(oed.algorithm.toInt()), depth) + } + }.also { it.sessionKey = sk } + } + val skesks: List val pkesks: List val anonPkesks: List @@ -735,14 +783,15 @@ class OpenPgpMessageInputStream( skesks = mutableListOf() pkesks = mutableListOf() anonPkesks = mutableListOf() + for (esk in esks) { if (esk is PGPPBEEncryptedData) { skesks.add(esk) } else if (esk is PGPPublicKeyEncryptedData) { - if (esk.keyID != 0L) { - pkesks.add(esk) - } else { + if (esk.keyIdentifier.isWildcard) { anonPkesks.add(esk) + } else { + pkesks.add(esk) } } else { throw IllegalArgumentException("Unknown ESK class type ${esk.javaClass}") @@ -754,9 +803,9 @@ class OpenPgpMessageInputStream( get() = skesks.plus(pkesks).plus(anonPkesks) } - private class Signatures(val options: ConsumerOptions) : OutputStream() { - val detachedSignatures = mutableListOf() - val prependedSignatures = mutableListOf() + private class Signatures(val options: ConsumerOptions, val api: PGPainless) : OutputStream() { + val detachedSignatures = mutableListOf() + val prependedSignatures = mutableListOf() val onePassSignatures = mutableListOf() val opsUpdateStack = ArrayDeque>() var literalOPS = mutableListOf() @@ -775,47 +824,49 @@ class OpenPgpMessageInputStream( fun addDetachedSignature(signature: PGPSignature) { val check = initializeSignature(signature) val keyId = signature.issuerKeyId - if (check != null) { + if (check.issuer != null) { detachedSignatures.add(check) } else { LOGGER.debug( "No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") detachedSignaturesWithMissingCert.add( SignatureVerification.Failure( - signature, null, SignatureValidationException("Missing verification key."))) + check, SignatureValidationException("Missing verification key."))) } } fun addPrependedSignature(signature: PGPSignature) { val check = initializeSignature(signature) val keyId = signature.issuerKeyId - if (check != null) { + if (check.issuer != null) { prependedSignatures.add(check) } else { LOGGER.debug( "No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") prependedSignaturesWithMissingCert.add( SignatureVerification.Failure( - signature, null, SignatureValidationException("Missing verification key"))) + check, SignatureValidationException("Missing verification key"))) } } - fun initializeSignature(signature: PGPSignature): SignatureCheck? { - val certificate = findCertificate(signature) ?: return null - val publicKey = certificate.getPublicKeyFor(signature) ?: return null - val verifierKey = SubkeyIdentifier(certificate, publicKey.keyID) - initialize(signature, publicKey) - return SignatureCheck(signature, certificate, verifierKey) + fun initializeSignature(signature: PGPSignature): OpenPGPDocumentSignature { + val certificate = + findCertificate(signature) ?: return OpenPGPDocumentSignature(signature, null) + val publicKey = + certificate.getSigningKeyFor(signature) + ?: return OpenPGPDocumentSignature(signature, null) + initialize(signature, publicKey.pgpPublicKey) + return OpenPGPDocumentSignature(signature, publicKey) } fun addOnePassSignature(signature: PGPOnePassSignature) { val certificate = findCertificate(signature) if (certificate != null) { - val publicKey = certificate.getPublicKeyFor(signature) + val publicKey = certificate.getSigningKeyFor(signature) if (publicKey != null) { val ops = OnePassSignatureCheck(signature, certificate) - initialize(signature, publicKey) + initialize(signature, publicKey.pgpPublicKey) onePassSignatures.add(ops) literalOPS.add(ops) } @@ -825,15 +876,11 @@ class OpenPgpMessageInputStream( } } - fun addCorrespondingOnePassSignature( - signature: PGPSignature, - layer: Layer, - policy: Policy - ) { + fun addCorrespondingOnePassSignature(signature: PGPSignature, layer: Layer) { var found = false - val keyId = signature.issuerKeyId - for ((i, check) in onePassSignatures.withIndex().reversed()) { - if (check.onePassSignature.keyID != keyId) { + for (check in onePassSignatures.reversed()) { + if (!KeyIdentifier.matches( + signature.keyIdentifiers, check.onePassSignature.keyIdentifier, true)) { continue } found = true @@ -841,34 +888,42 @@ class OpenPgpMessageInputStream( if (check.signature != null) { continue } - check.signature = signature - val verification = - SignatureVerification( - signature, - SubkeyIdentifier(check.verificationKeys, check.onePassSignature.keyID)) + + val documentSignature = + OpenPGPDocumentSignature( + signature, check.verificationKeys.getSigningKeyFor(signature)) + val verification = SignatureVerification(documentSignature) try { - SignatureValidator.signatureWasCreatedInBounds( - options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(signature) - CertificateValidator.validateCertificateAndVerifyOnePassSignature(check, policy) - LOGGER.debug("Acceptable signature by key ${verification.signingKey}") - layer.addVerifiedOnePassSignature(verification) + signature.assertCreatedInBounds( + options.getVerifyNotBefore(), options.getVerifyNotAfter()) + if (documentSignature.verify(check.onePassSignature) && + documentSignature.isValid(api.implementation.policy())) { + layer.addVerifiedOnePassSignature(verification) + } else { + throw SignatureValidationException("Incorrect OnePassSignature.") + } + } catch (e: MalformedOpenPGPSignatureException) { + throw SignatureValidationException("Malformed OnePassSignature.", e) } catch (e: SignatureValidationException) { - LOGGER.debug("Rejected signature by key ${verification.signingKey}", e) layer.addRejectedOnePassSignature( SignatureVerification.Failure(verification, e)) + } catch (e: PGPSignatureException) { + layer.addRejectedOnePassSignature( + SignatureVerification.Failure( + verification, SignatureValidationException(e.message, e))) } break } if (!found) { LOGGER.debug( - "No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") + "No suitable certificate for verification of signature by key ${signature.issuerKeyId.openPgpKeyId()} found.") inbandSignaturesWithMissingCert.add( SignatureVerification.Failure( - signature, null, SignatureValidationException("Missing verification key."))) + OpenPGPDocumentSignature(signature, null), + SignatureValidationException("Missing verification key."))) } } @@ -884,7 +939,7 @@ class OpenPgpMessageInputStream( opsUpdateStack.removeFirst() } - private fun findCertificate(signature: PGPSignature): PGPPublicKeyRing? { + private fun findCertificate(signature: PGPSignature): OpenPGPCertificate? { val cert = options.getCertificateSource().getCertificate(signature) if (cert != null) { return cert @@ -893,21 +948,19 @@ class OpenPgpMessageInputStream( if (options.getMissingCertificateCallback() != null) { return options .getMissingCertificateCallback()!! - .onMissingPublicKeyEncountered(signature.keyID) + .provide(signature.keyIdentifiers.first()) } return null // TODO: Missing cert for sig } - private fun findCertificate(signature: PGPOnePassSignature): PGPPublicKeyRing? { - val cert = options.getCertificateSource().getCertificate(signature.keyID) + private fun findCertificate(signature: PGPOnePassSignature): OpenPGPCertificate? { + val cert = options.getCertificateSource().getCertificate(signature.keyIdentifier) if (cert != null) { return cert } if (options.getMissingCertificateCallback() != null) { - return options - .getMissingCertificateCallback()!! - .onMissingPublicKeyEncountered(signature.keyID) + return options.getMissingCertificateCallback()!!.provide(signature.keyIdentifier) } return null // TODO: Missing cert for sig } @@ -956,40 +1009,39 @@ class OpenPgpMessageInputStream( } } - fun finish(layer: Layer, policy: Policy) { + fun finish(layer: Layer) { for (detached in detachedSignatures) { - val verification = - SignatureVerification(detached.signature, detached.signingKeyIdentifier) + val verification = SignatureVerification(detached) try { - SignatureValidator.signatureWasCreatedInBounds( - options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(detached.signature) - CertificateValidator.validateCertificateAndVerifyInitializedSignature( - detached.signature, - KeyRingUtils.publicKeys(detached.signingKeyRing), - policy) - LOGGER.debug("Acceptable signature by key ${verification.signingKey}") - layer.addVerifiedDetachedSignature(verification) + detached.signature.assertCreatedInBounds( + options.getVerifyNotBefore(), options.getVerifyNotAfter()) + if (!detached.verify()) { + throw SignatureValidationException("Incorrect detached signature.") + } else if (!detached.isValid(api.implementation.policy())) { + throw SignatureValidationException("Detached signature is not valid.") + } else { + layer.addVerifiedDetachedSignature(verification) + } + } catch (e: MalformedOpenPGPSignatureException) { + throw SignatureValidationException("Malformed detached signature.", e) } catch (e: SignatureValidationException) { - LOGGER.debug("Rejected signature by key ${verification.signingKey}", e) layer.addRejectedDetachedSignature( SignatureVerification.Failure(verification, e)) } } for (prepended in prependedSignatures) { - val verification = - SignatureVerification(prepended.signature, prepended.signingKeyIdentifier) + val verification = SignatureVerification(prepended) try { - SignatureValidator.signatureWasCreatedInBounds( - options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(prepended.signature) - CertificateValidator.validateCertificateAndVerifyInitializedSignature( - prepended.signature, - KeyRingUtils.publicKeys(prepended.signingKeyRing), - policy) - LOGGER.debug("Acceptable signature by key ${verification.signingKey}") - layer.addVerifiedPrependedSignature(verification) + prepended.signature.assertCreatedInBounds( + options.getVerifyNotBefore(), options.getVerifyNotAfter()) + if (prepended.verify() && prepended.isValid(api.implementation.policy())) { + layer.addVerifiedPrependedSignature(verification) + } else { + throw SignatureValidationException("Incorrect prepended signature.") + } + } catch (e: MalformedOpenPGPSignatureException) { + throw SignatureValidationException("Malformed prepended signature.", e) } catch (e: SignatureValidationException) { LOGGER.debug("Rejected signature by key ${verification.signingKey}", e) layer.addRejectedPrependedSignature( @@ -1029,27 +1081,21 @@ class OpenPgpMessageInputStream( } } - companion object { - @JvmStatic - private fun initialize(signature: PGPSignature, publicKey: PGPPublicKey) { - val verifierProvider = - ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider - try { - signature.init(verifierProvider, publicKey) - } catch (e: PGPException) { - throw RuntimeException(e) - } + private fun initialize(signature: PGPSignature, publicKey: PGPPublicKey) { + val verifierProvider = api.implementation.pgpContentVerifierBuilderProvider() + try { + signature.init(verifierProvider, publicKey) + } catch (e: PGPException) { + throw RuntimeException(e) } + } - @JvmStatic - private fun initialize(ops: PGPOnePassSignature, publicKey: PGPPublicKey) { - val verifierProvider = - ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider - try { - ops.init(verifierProvider, publicKey) - } catch (e: PGPException) { - throw RuntimeException(e) - } + private fun initialize(ops: PGPOnePassSignature, publicKey: PGPPublicKey) { + val verifierProvider = api.implementation.pgpContentVerifierBuilderProvider() + try { + ops.init(verifierProvider, publicKey) + } catch (e: PGPException) { + throw RuntimeException(e) } } } @@ -1059,32 +1105,27 @@ class OpenPgpMessageInputStream( private val LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream::class.java) @JvmStatic - fun create(inputStream: InputStream, options: ConsumerOptions) = - create(inputStream, options, PGPainless.getPolicy()) - - @JvmStatic - fun create(inputStream: InputStream, options: ConsumerOptions, policy: Policy) = - create(inputStream, options, Message(), policy) + fun create(inputStream: InputStream, options: ConsumerOptions, api: PGPainless) = + create(inputStream, options, Message(), api) @JvmStatic internal fun create( inputStream: InputStream, options: ConsumerOptions, metadata: Layer, - policy: Policy + api: PGPainless ): OpenPgpMessageInputStream { - val openPgpIn = OpenPgpInputStream(inputStream) + val openPgpIn = OpenPGPAnimalSnifferInputStream(inputStream) openPgpIn.reset() if (openPgpIn.isNonOpenPgp || options.isForceNonOpenPgpData()) { return OpenPgpMessageInputStream( - Type.non_openpgp, openPgpIn, options, metadata, policy) + Type.non_openpgp, openPgpIn, options, metadata, api) } if (openPgpIn.isBinaryOpenPgp) { // Simply consume OpenPGP message - return OpenPgpMessageInputStream( - Type.standard, openPgpIn, options, metadata, policy) + return OpenPgpMessageInputStream(Type.standard, openPgpIn, options, metadata, api) } return if (openPgpIn.isAsciiArmored) { @@ -1092,10 +1133,10 @@ class OpenPgpMessageInputStream( if (armorIn.isClearText) { (metadata as Message).setCleartextSigned() OpenPgpMessageInputStream( - Type.cleartext_signed, armorIn, options, metadata, policy) + Type.cleartext_signed, armorIn, options, metadata, api) } else { // Simply consume dearmored OpenPGP message - OpenPgpMessageInputStream(Type.standard, armorIn, options, metadata, policy) + OpenPgpMessageInputStream(Type.standard, armorIn, options, metadata, api) } } else { throw AssertionError("Cannot deduce type of data.") diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt index 3e00fbb2..ed6ead40 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt @@ -5,22 +5,23 @@ package org.pgpainless.decryption_verification import org.bouncycastle.openpgp.PGPSignature -import org.pgpainless.decryption_verification.SignatureVerification.Failure +import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature import org.pgpainless.exception.SignatureValidationException import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.signature.SignatureUtils /** - * Tuple of a signature and an identifier of its corresponding verification key. Semantic meaning of - * the signature verification (success, failure) is merely given by context. E.g. - * [MessageMetadata.getVerifiedInlineSignatures] contains verified verifications, while the class - * [Failure] contains failed verifications. + * An evaluated document signature. * - * @param signature PGPSignature object - * @param signingKey [SubkeyIdentifier] of the (sub-) key that is used for signature verification. - * Note, that this might be null, e.g. in case of a [Failure] due to missing verification key. + * @param documentSignature OpenPGPDocumentSignature object */ -data class SignatureVerification(val signature: PGPSignature, val signingKey: SubkeyIdentifier) { +data class SignatureVerification(val documentSignature: OpenPGPDocumentSignature) { + + /** Underlying [PGPSignature]. */ + val signature: PGPSignature = documentSignature.signature + + /** [SubkeyIdentifier] of the component key that created the signature. */ + val signingKey: SubkeyIdentifier = SubkeyIdentifier(documentSignature.issuer) override fun toString(): String { return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)};" + @@ -31,20 +32,27 @@ data class SignatureVerification(val signature: PGPSignature, val signingKey: Su * Tuple object of a [SignatureVerification] and the corresponding * [SignatureValidationException] that caused the verification to fail. * - * @param signatureVerification verification (tuple of [PGPSignature] and corresponding - * [SubkeyIdentifier]) + * @param documentSignature signature that could not be verified * @param validationException exception that caused the verification to fail */ data class Failure( - val signature: PGPSignature, - val signingKey: SubkeyIdentifier?, + val documentSignature: OpenPGPDocumentSignature, val validationException: SignatureValidationException ) { + /** Underlying [PGPSignature]. */ + val signature: PGPSignature = documentSignature.signature + + /** + * [SubkeyIdentifier] of the component key that created the signature. Note: In case of a + * missing verification key, this might be null. + */ + val signingKey: SubkeyIdentifier? = documentSignature.issuer?.let { SubkeyIdentifier(it) } + constructor( verification: SignatureVerification, validationException: SignatureValidationException - ) : this(verification.signature, verification.signingKey, validationException) + ) : this(verification.documentSignature, validationException) override fun toString(): String { return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)}; Key: ${signingKey?.toString() ?: "null"}; Failure: ${validationException.message}" diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt index 78614a96..bf277743 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt @@ -8,9 +8,9 @@ import java.io.* import kotlin.jvm.Throws import org.bouncycastle.bcpg.ArmoredInputStream import org.bouncycastle.openpgp.PGPSignatureList +import org.bouncycastle.openpgp.api.OpenPGPImplementation import org.bouncycastle.util.Strings import org.pgpainless.exception.WrongConsumingMethodException -import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.util.ArmoredInputStreamFactory /** @@ -72,7 +72,7 @@ class ClearsignedMessageUtil { } } - val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(input) + val objectFactory = OpenPGPImplementation.getInstance().pgpObjectFactory(input) val next = objectFactory.nextObject() ?: PGPSignatureList(arrayOf()) return next as PGPSignatureList } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt index 47aed2be..8de2726b 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt @@ -6,14 +6,13 @@ package org.pgpainless.encryption_signing import java.security.MessageDigest import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPPrivateKey -import org.bouncycastle.openpgp.PGPSecretKeyRing -import org.bouncycastle.openpgp.PGPSignature import org.bouncycastle.openpgp.PGPSignatureGenerator +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature import org.pgpainless.PGPainless import org.pgpainless.algorithm.SignatureType -import org.pgpainless.bouncycastle.extensions.unlock import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.protection.UnlockSecretKey class BcHashContextSigner { @@ -22,14 +21,17 @@ class BcHashContextSigner { fun signHashContext( hashContext: MessageDigest, signatureType: SignatureType, - secretKey: PGPSecretKeyRing, + secretKey: OpenPGPKey, protector: SecretKeyRingProtector - ): PGPSignature { - val info = PGPainless.inspectKeyRing(secretKey) + ): OpenPGPDocumentSignature { + val info = PGPainless.getInstance().inspect(secretKey) return info.signingSubkeys - .mapNotNull { info.getSecretKey(it.keyID) } + .mapNotNull { info.getSecretKey(it.keyIdentifier) } .firstOrNull() - ?.let { signHashContext(hashContext, signatureType, it.unlock(protector)) } + ?.let { + signHashContext( + hashContext, signatureType, UnlockSecretKey.unlockSecretKey(it, protector)) + } ?: throw PGPException("Key does not contain suitable signing subkey.") } @@ -45,11 +47,13 @@ class BcHashContextSigner { internal fun signHashContext( hashContext: MessageDigest, signatureType: SignatureType, - privateKey: PGPPrivateKey - ): PGPSignature { - return PGPSignatureGenerator(BcPGPHashContextContentSignerBuilder(hashContext)) - .apply { init(signatureType.code, privateKey) } + privateKey: OpenPGPKey.OpenPGPPrivateKey + ): OpenPGPDocumentSignature { + return PGPSignatureGenerator( + BcPGPHashContextContentSignerBuilder(hashContext), privateKey.keyPair.publicKey) + .apply { init(signatureType.code, privateKey.keyPair.privateKey) } .generate() + .let { OpenPGPDocumentSignature(it, privateKey.publicKey) } } } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt index 6b4713d6..3f86aab5 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt @@ -5,69 +5,25 @@ package org.pgpainless.encryption_signing import java.io.OutputStream -import org.pgpainless.PGPainless.Companion.getPolicy -import org.pgpainless.algorithm.CompressionAlgorithm -import org.pgpainless.algorithm.SymmetricKeyAlgorithm -import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity +import org.pgpainless.PGPainless import org.pgpainless.util.NullOutputStream -import org.slf4j.Logger -import org.slf4j.LoggerFactory -class EncryptionBuilder : EncryptionBuilderInterface { +class EncryptionBuilder(private val api: PGPainless) : EncryptionBuilderInterface { override fun onOutputStream( outputStream: OutputStream ): EncryptionBuilderInterface.WithOptions { - return WithOptionsImpl(outputStream) + return WithOptionsImpl(outputStream, api) } override fun discardOutput(): EncryptionBuilderInterface.WithOptions { return onOutputStream(NullOutputStream()) } - class WithOptionsImpl(val outputStream: OutputStream) : EncryptionBuilderInterface.WithOptions { + class WithOptionsImpl(val outputStream: OutputStream, private val api: PGPainless) : + EncryptionBuilderInterface.WithOptions { override fun withOptions(options: ProducerOptions): EncryptionStream { - return EncryptionStream(outputStream, options) - } - } - - companion object { - - @JvmStatic val LOGGER: Logger = LoggerFactory.getLogger(EncryptionBuilder::class.java) - - /** - * Negotiate the [SymmetricKeyAlgorithm] used for message encryption. - * - * @param encryptionOptions encryption options - * @return negotiated symmetric key algorithm - */ - @JvmStatic - fun negotiateSymmetricEncryptionAlgorithm( - encryptionOptions: EncryptionOptions - ): SymmetricKeyAlgorithm { - val preferences = - encryptionOptions.keyViews.values - .map { it.preferredSymmetricKeyAlgorithms } - .toList() - val algorithm = - byPopularity() - .negotiate( - getPolicy().symmetricKeyEncryptionAlgorithmPolicy, - encryptionOptions.encryptionAlgorithmOverride, - preferences) - LOGGER.debug( - "Negotiation resulted in {} being the symmetric encryption algorithm of choice.", - algorithm) - return algorithm - } - - @JvmStatic - fun negotiateCompressionAlgorithm(producerOptions: ProducerOptions): CompressionAlgorithm { - val compressionAlgorithmOverride = producerOptions.compressionAlgorithmOverride - return compressionAlgorithmOverride - ?: getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm() - - // TODO: Negotiation + return EncryptionStream(outputStream, options, api) } } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt index f261b85e..ee2c5b18 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt @@ -5,55 +5,69 @@ package org.pgpainless.encryption_signing import java.util.* -import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey +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.algorithm.negotiation.EncryptionMechanismNegotiator +import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity import org.pgpainless.authentication.CertificateAuthority import org.pgpainless.encryption_signing.EncryptionOptions.EncryptionKeySelector -import org.pgpainless.exception.KeyException -import org.pgpainless.exception.KeyException.* -import org.pgpainless.implementation.ImplementationFactory -import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.exception.KeyException.ExpiredKeyException +import org.pgpainless.exception.KeyException.UnacceptableEncryptionKeyException +import org.pgpainless.exception.KeyException.UnacceptableSelfSignatureException import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.info.KeyAccessor import org.pgpainless.key.info.KeyRingInfo import org.pgpainless.util.Passphrase -class EncryptionOptions(private val purpose: EncryptionPurpose) { +class EncryptionOptions(private val purpose: EncryptionPurpose, private val api: PGPainless) { + + var encryptionMechanismNegotiator: EncryptionMechanismNegotiator = + EncryptionMechanismNegotiator.modificationDetectionOrBetter(byPopularity()) private val _encryptionMethods: MutableSet = mutableSetOf() - private val _encryptionKeyIdentifiers: MutableSet = mutableSetOf() + private val keysAndAccessors: MutableMap = mutableMapOf() private val _keyRingInfo: MutableMap = mutableMapOf() - private val _keyViews: MutableMap = mutableMapOf() private val encryptionKeySelector: EncryptionKeySelector = encryptToAllCapableSubkeys() private var allowEncryptionWithMissingKeyFlags = false private var evaluationDate = Date() - private var _encryptionAlgorithmOverride: SymmetricKeyAlgorithm? = null + private var _encryptionMechanismOverride: MessageEncryptionMechanism? = null val encryptionMethods get() = _encryptionMethods.toSet() val encryptionKeyIdentifiers - get() = _encryptionKeyIdentifiers.toSet() + get() = keysAndAccessors.keys.map { SubkeyIdentifier(it) } - val keyRingInfo - get() = _keyRingInfo.toMap() - - val keyViews - get() = _keyViews.toMap() + val encryptionKeys + get() = keysAndAccessors.keys.toSet() + @Deprecated( + "Deprecated in favor of encryptionMechanismOverride", + replaceWith = ReplaceWith("encryptionMechanismOverride")) + // TODO: Remove in 2.1 val encryptionAlgorithmOverride - get() = _encryptionAlgorithmOverride + get() = + _encryptionMechanismOverride?.let { + SymmetricKeyAlgorithm.requireFromId(it.symmetricKeyAlgorithm) + } - constructor() : this(EncryptionPurpose.ANY) + val encryptionMechanismOverride + get() = _encryptionMechanismOverride + + constructor(api: PGPainless) : this(EncryptionPurpose.ANY, api) /** - * Factory method to create an [EncryptionOptions] object which will encrypt for keys which - * carry the flag [org.pgpainless.algorithm.KeyFlag.ENCRYPT_COMMS]. + * Set the evaluation date for certificate evaluation. * - * @return encryption options + * @param evaluationDate reference time + * @return this */ fun setEvaluationDate(evaluationDate: Date) = apply { this.evaluationDate = evaluationDate } @@ -89,11 +103,14 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) { /** * Add all key rings in the provided [Iterable] (e.g. - * [org.bouncycastle.openpgp.PGPPublicKeyRingCollection]) as recipients. + * [org.bouncycastle.openpgp.PGPPublicKeyRingCollection]) as recipients. Note: This method is + * deprecated. Instead, repeatedly call [addRecipient], passing in individual + * [OpenPGPCertificate] instances. * * @param keys keys * @return this */ + @Deprecated("Repeatedly pass OpenPGPCertificate instances instead.") fun addRecipients(keys: Iterable) = apply { keys.toList().let { require(it.isNotEmpty()) { "Set of recipient keys cannot be empty." } @@ -104,12 +121,15 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) { /** * Add all key rings in the provided [Iterable] (e.g. * [org.bouncycastle.openpgp.PGPPublicKeyRingCollection]) as recipients. Per key ring, the - * selector is applied to select one or more encryption subkeys. + * selector is applied to select one or more encryption subkeys. Note: This method is + * deprecated. Instead, repeatedly call [addRecipient], passing in individual + * [OpenPGPCertificate] instances. * * @param keys keys * @param selector encryption key selector * @return this */ + @Deprecated("Repeatedly pass OpenPGPCertificate instances instead.") fun addRecipients(keys: Iterable, selector: EncryptionKeySelector) = apply { keys.toList().let { require(it.isNotEmpty()) { "Set of recipient keys cannot be empty." } @@ -117,71 +137,175 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) { } } + /** + * Encrypt the message to the recipients [OpenPGPCertificate]. + * + * @param cert recipient certificate + * @return this + */ + fun addRecipient(cert: OpenPGPCertificate) = addRecipient(cert, encryptionKeySelector) + /** * Add a recipient by providing a key. * * @param key key ring * @return this */ + @Deprecated( + "Pass in OpenPGPCertificate instead.", + replaceWith = + ReplaceWith("addRecipient(key.toOpenPGPCertificate(), encryptionKeySelector)")) fun addRecipient(key: PGPPublicKeyRing) = addRecipient(key, encryptionKeySelector) + /** + * Encrypt the message for the given recipients [OpenPGPCertificate], sourcing algorithm + * preferences by inspecting the binding signature on the passed [userId]. + * + * @param cert recipient certificate + * @param userId recipient user-id + * @return this + */ + fun addRecipient(cert: OpenPGPCertificate, userId: CharSequence) = + addRecipient(cert, userId, encryptionKeySelector) + /** * Add a recipient by providing a key and recipient user-id. The user-id is used to determine - * the recipients preferences (algorithms etc.). + * the recipients preferences (algorithms etc.). Note: This method is deprecated. Replace the + * [PGPPublicKeyRing] instance with an [OpenPGPCertificate]. * * @param key key ring * @param userId user id * @return this */ + @Deprecated( + "Pass in OpenPGPCertificate instead.", + replaceWith = ReplaceWith("addRecipient(key.toOpenPGPCertificate(), userId)")) fun addRecipient(key: PGPPublicKeyRing, userId: CharSequence) = addRecipient(key, userId, encryptionKeySelector) + /** + * Encrypt the message for the given recipients [OpenPGPCertificate], sourcing algorithm + * preferences by inspecting the binding signature on the given [userId] and filtering the + * recipient subkeys through the given [EncryptionKeySelector]. + * + * @param cert recipient certificate + * @param userId user-id for sourcing algorithm preferences + * @param encryptionKeySelector decides which subkeys to encrypt for + * @return this + */ fun addRecipient( - key: PGPPublicKeyRing, + cert: OpenPGPCertificate, userId: CharSequence, encryptionKeySelector: EncryptionKeySelector ) = apply { - val info = KeyRingInfo(key, evaluationDate) + val info = api.inspect(cert, evaluationDate) val subkeys = encryptionKeySelector.selectEncryptionSubkeys( info.getEncryptionSubkeys(userId, purpose)) if (subkeys.isEmpty()) { - throw KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)) + throw UnacceptableEncryptionKeyException(cert) } for (subkey in subkeys) { - val keyId = SubkeyIdentifier(key, subkey.keyID) + val keyId = SubkeyIdentifier(subkey) _keyRingInfo[keyId] = info - _keyViews[keyId] = KeyAccessor.ViaUserId(info, keyId, userId.toString()) - addRecipientKey(key, subkey, false) + val accessor = KeyAccessor.ViaUserId(subkey, cert.getUserId(userId.toString())) + addRecipientKey(subkey, accessor, false) } } - fun addRecipient(key: PGPPublicKeyRing, encryptionKeySelector: EncryptionKeySelector) = apply { - addAsRecipient(key, encryptionKeySelector, false) - } + /** + * Encrypt the message for the given recipients public key, sourcing algorithm preferences by + * inspecting the binding signature on the given [userId] and filtering the recipient subkeys + * through the given [EncryptionKeySelector]. + * + * @param key recipient public key + * @param userId user-id for sourcing algorithm preferences + * @param encryptionKeySelector decides which subkeys to encrypt for + * @return this + */ + @Deprecated( + "Pass in OpenPGPCertificate instead.", + replaceWith = + ReplaceWith("addRecipient(key.toOpenPGPCertificate(), userId, encryptionKeySelector)")) + fun addRecipient( + key: PGPPublicKeyRing, + userId: CharSequence, + encryptionKeySelector: EncryptionKeySelector + ) = addRecipient(api.toCertificate(key), userId, encryptionKeySelector) + /** + * Encrypt the message for the given recipients [OpenPGPCertificate], filtering encryption + * subkeys through the given [EncryptionKeySelector]. + * + * @param cert recipient certificate + * @param encryptionKeySelector decides, which subkeys to encrypt for + * @return this + */ + fun addRecipient(cert: OpenPGPCertificate, encryptionKeySelector: EncryptionKeySelector) = + addAsRecipient(cert, encryptionKeySelector, false) + + /** + * Encrypt the message for the given recipients public key, filtering encryption subkeys through + * the given [EncryptionKeySelector]. + * + * @param key recipient public key + * @param encryptionKeySelector decides, which subkeys to encrypt for + * @return this + */ + @Deprecated( + "Pass in OpenPGPCertificate instead.", + replaceWith = + ReplaceWith("addRecipient(key.toOpenPGPCertificate(), encryptionKeySelector)")) + fun addRecipient(key: PGPPublicKeyRing, encryptionKeySelector: EncryptionKeySelector) = + addRecipient(api.toCertificate(key), encryptionKeySelector) + + /** + * Encrypt the message for the recipients [OpenPGPCertificate], keeping the recipient anonymous + * by setting a wildcard key-id / fingerprint. + * + * @param cert recipient certificate + * @param selector decides, which subkeys to encrypt for + * @return this + */ @JvmOverloads + fun addHiddenRecipient( + cert: OpenPGPCertificate, + selector: EncryptionKeySelector = encryptionKeySelector + ) = addAsRecipient(cert, selector, true) + + /** + * Encrypt the message for the recipients public key, keeping the recipient anonymous by setting + * a wildcard key-id / fingerprint. + * + * @param key recipient public key + * @param selector decides, which subkeys to encrypt for + * @return this + */ + @JvmOverloads + @Deprecated( + "Pass in an OpenPGPCertificate instead.", + replaceWith = ReplaceWith("addHiddenRecipient(key.toOpenPGPCertificate(), selector)")) fun addHiddenRecipient( key: PGPPublicKeyRing, selector: EncryptionKeySelector = encryptionKeySelector - ) = apply { addAsRecipient(key, selector, true) } + ) = addHiddenRecipient(api.toCertificate(key), selector) private fun addAsRecipient( - key: PGPPublicKeyRing, + cert: OpenPGPCertificate, selector: EncryptionKeySelector, wildcardKeyId: Boolean ) = apply { - val info = KeyRingInfo(key, evaluationDate) + val info = api.inspect(cert, evaluationDate) val primaryKeyExpiration = try { info.primaryKeyExpirationDate } catch (e: NoSuchElementException) { - throw UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key)) + throw UnacceptableSelfSignatureException(cert) } if (primaryKeyExpiration != null && primaryKeyExpiration < evaluationDate) { - throw ExpiredKeyException(OpenPgpFingerprint.of(key), primaryKeyExpiration) + throw ExpiredKeyException(cert, primaryKeyExpiration) } var encryptionSubkeys = selector.selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose)) @@ -193,31 +317,31 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) { if (encryptionSubkeys.isEmpty() && allowEncryptionWithMissingKeyFlags) { encryptionSubkeys = info.validSubkeys - .filter { it.isEncryptionKey } - .filter { info.getKeyFlagsOf(it.keyID).isEmpty() } + .filter { it.pgpPublicKey.isEncryptionKey } + .filter { info.getKeyFlagsOf(it.keyIdentifier).isEmpty() } } if (encryptionSubkeys.isEmpty()) { - throw UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)) + throw UnacceptableEncryptionKeyException(cert) } for (subkey in encryptionSubkeys) { - val keyId = SubkeyIdentifier(key, subkey.keyID) + val keyId = SubkeyIdentifier(subkey) _keyRingInfo[keyId] = info - _keyViews[keyId] = KeyAccessor.ViaKeyId(info, keyId) - addRecipientKey(key, subkey, wildcardKeyId) + val accessor = KeyAccessor.ViaKeyIdentifier(subkey) + addRecipientKey(subkey, accessor, wildcardKeyId) } } private fun addRecipientKey( - certificate: PGPPublicKeyRing, - key: PGPPublicKey, - wildcardKeyId: Boolean + key: OpenPGPComponentKey, + accessor: KeyAccessor, + wildcardRecipient: Boolean ) { - _encryptionKeyIdentifiers.add(SubkeyIdentifier(certificate, key.keyID)) + keysAndAccessors[key] = accessor addEncryptionMethod( - ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(key).also { - it.setUseWildcardKeyID(wildcardKeyId) + api.implementation.publicKeyKeyEncryptionMethodGenerator(key.pgpPublicKey).also { + it.setUseWildcardRecipient(wildcardRecipient) }) } @@ -241,7 +365,7 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) { fun addMessagePassphrase(passphrase: Passphrase) = apply { require(!passphrase.isEmpty) { "Passphrase MUST NOT be empty." } addEncryptionMethod( - ImplementationFactory.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase)) + api.implementation.pbeKeyEncryptionMethodGenerator(passphrase.getChars())) } /** @@ -270,11 +394,27 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) { * @param encryptionAlgorithm encryption algorithm override * @return this */ + @Deprecated( + "Deprecated in favor of overrideEncryptionMechanism", + replaceWith = + ReplaceWith( + "overrideEncryptionMechanism(MessageEncryptionMechanism.integrityProtected(encryptionAlgorithm.algorithmId))")) + // TODO: Remove in 2.1 fun overrideEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply { require(encryptionAlgorithm != SymmetricKeyAlgorithm.NULL) { "Encryption algorithm override cannot be NULL." } - _encryptionAlgorithmOverride = encryptionAlgorithm + overrideEncryptionMechanism( + MessageEncryptionMechanism.integrityProtected(encryptionAlgorithm.algorithmId)) + } + + fun overrideEncryptionMechanism(encryptionMechanism: MessageEncryptionMechanism) = apply { + require( + api.algorithmPolicy.messageEncryptionAlgorithmPolicy.isAcceptable( + encryptionMechanism)) { + "Provided symmetric encryption algorithm is not acceptable." + } + _encryptionMechanismOverride = encryptionMechanism } /** @@ -291,16 +431,50 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) { fun hasEncryptionMethod() = _encryptionMethods.isNotEmpty() + fun usesOnlyPasswordBasedEncryption() = + _encryptionMethods.all { it is PBEKeyEncryptionMethodGenerator } + + internal fun negotiateEncryptionMechanism(): MessageEncryptionMechanism { + if (encryptionMechanismOverride != null) { + return encryptionMechanismOverride!! + } + + val features = keysAndAccessors.values.map { it.features }.toList() + val aeadAlgorithms = keysAndAccessors.values.map { it.preferredAEADCipherSuites }.toList() + val symmetricKeyAlgorithms = + keysAndAccessors.values.map { it.preferredSymmetricKeyAlgorithms }.toList() + + val mechanism = + encryptionMechanismNegotiator.negotiate( + api.algorithmPolicy, + encryptionMechanismOverride, + features, + aeadAlgorithms, + symmetricKeyAlgorithms) + + return mechanism + } + fun interface EncryptionKeySelector { - fun selectEncryptionSubkeys(encryptionCapableKeys: List): List + fun selectEncryptionSubkeys( + encryptionCapableKeys: List + ): List } companion object { - @JvmStatic fun get() = EncryptionOptions() + @JvmOverloads + @JvmStatic + fun get(api: PGPainless = PGPainless.getInstance()) = EncryptionOptions(api) - @JvmStatic fun encryptCommunications() = EncryptionOptions(EncryptionPurpose.COMMUNICATIONS) + @JvmOverloads + @JvmStatic + fun encryptCommunications(api: PGPainless = PGPainless.getInstance()) = + EncryptionOptions(EncryptionPurpose.COMMUNICATIONS, api) - @JvmStatic fun encryptDataAtRest() = EncryptionOptions(EncryptionPurpose.STORAGE) + @JvmOverloads + @JvmStatic + fun encryptDataAtRest(api: PGPainless = PGPainless.getInstance()) = + EncryptionOptions(EncryptionPurpose.STORAGE, api) /** * Only encrypt to the first valid encryption capable subkey we stumble upon. diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt index 4f6e6978..410deba1 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt @@ -8,23 +8,45 @@ import java.util.* import org.bouncycastle.openpgp.PGPLiteralData import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.bouncycastle.extensions.matches import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.util.MultiMap +import org.pgpainless.util.SessionKey data class EncryptionResult( - val encryptionAlgorithm: SymmetricKeyAlgorithm, + val encryptionMechanism: MessageEncryptionMechanism, + val sessionKey: SessionKey?, val compressionAlgorithm: CompressionAlgorithm, - val detachedSignatures: MultiMap, + val detachedDocumentSignatures: OpenPGPSignatureSet, val recipients: Set, val fileName: String, val modificationDate: Date, val fileEncoding: StreamEncoding ) { + @Deprecated( + "Use encryptionMechanism instead.", replaceWith = ReplaceWith("encryptionMechanism")) + val encryptionAlgorithm: SymmetricKeyAlgorithm? + get() = SymmetricKeyAlgorithm.fromId(encryptionMechanism.symmetricKeyAlgorithm) + + @Deprecated( + "Use detachedSignatures instead", replaceWith = ReplaceWith("detachedDocumentSignatures")) + // TODO: Remove in 2.1 + val detachedSignatures: MultiMap + get() { + val map = MultiMap() + detachedDocumentSignatures.signatures + .map { SubkeyIdentifier(it.issuer) to it.signature } + .forEach { map.put(it.first, it.second) } + return map + } + /** * Return true, if the message is marked as for-your-eyes-only. This is typically done by * setting the filename "_CONSOLE". @@ -34,6 +56,9 @@ data class EncryptionResult( val isForYourEyesOnly: Boolean get() = PGPLiteralData.CONSOLE == fileName + fun isEncryptedFor(certificate: OpenPGPCertificate) = + recipients.any { certificate.getKey(it.keyIdentifier) != null } + /** * Returns true, if the message was encrypted for at least one subkey of the given certificate. * @@ -52,17 +77,19 @@ data class EncryptionResult( } class Builder { - var _encryptionAlgorithm: SymmetricKeyAlgorithm? = null + var _encryptionMechanism: MessageEncryptionMechanism = + MessageEncryptionMechanism.unencrypted() var _compressionAlgorithm: CompressionAlgorithm? = null - val detachedSignatures: MultiMap = MultiMap() + val detachedSignatures: MutableList = mutableListOf() val recipients: Set = mutableSetOf() private var _fileName = "" private var _modificationDate = Date(0) private var _encoding = StreamEncoding.BINARY + private var _sessionKey: SessionKey? = null - fun setEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply { - _encryptionAlgorithm = encryptionAlgorithm + fun setEncryptionMechanism(mechanism: MessageEncryptionMechanism): Builder = apply { + _encryptionMechanism = mechanism } fun setCompressionAlgorithm(compressionAlgorithm: CompressionAlgorithm) = apply { @@ -81,19 +108,20 @@ data class EncryptionResult( (recipients as MutableSet).add(recipient) } - fun addDetachedSignature( - signingSubkeyIdentifier: SubkeyIdentifier, - detachedSignature: PGPSignature - ) = apply { detachedSignatures.put(signingSubkeyIdentifier, detachedSignature) } + fun setSessionKey(sessionKey: SessionKey) = apply { _sessionKey = sessionKey } + + fun addDetachedSignature(signature: OpenPGPDocumentSignature): Builder = apply { + detachedSignatures.add(signature) + } fun build(): EncryptionResult { - checkNotNull(_encryptionAlgorithm) { "Encryption algorithm not set." } checkNotNull(_compressionAlgorithm) { "Compression algorithm not set." } return EncryptionResult( - _encryptionAlgorithm!!, + _encryptionMechanism, + _sessionKey, _compressionAlgorithm!!, - detachedSignatures, + OpenPGPSignatureSet(detachedSignatures), recipients, _fileName, _modificationDate, diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt index f2617c34..12d8a115 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt @@ -13,12 +13,14 @@ import org.bouncycastle.openpgp.PGPCompressedDataGenerator import org.bouncycastle.openpgp.PGPEncryptedDataGenerator import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPLiteralDataGenerator +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism +import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature +import org.pgpainless.PGPainless import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.StreamEncoding -import org.pgpainless.algorithm.SymmetricKeyAlgorithm -import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.bouncycastle.extensions.pgpDataEncryptorBuilder import org.pgpainless.util.ArmoredOutputStreamFactory -import org.slf4j.LoggerFactory +import org.pgpainless.util.SessionKey // 1 << 8 causes wrong partial body length encoding // 1 << 9 fixes this. @@ -29,14 +31,15 @@ const val BUFFER_SIZE = 1 shl 9 * 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. + * This class was originally based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. * - * @see Source + * @see + * [PGPEncryptingStream](https://github.com/neuhalje/bouncy-gpg/blob/master/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStream.java) */ class EncryptionStream( private var outermostStream: OutputStream, private val options: ProducerOptions, + private val api: PGPainless ) : OutputStream() { private val resultBuilder: EncryptionResult.Builder = EncryptionResult.builder() @@ -62,12 +65,10 @@ class EncryptionStream( private fun prepareArmor() { if (!options.isAsciiArmor) { - LOGGER.debug("Output will be unarmored.") return } outermostStream = BufferedOutputStream(outermostStream) - LOGGER.debug("Wrap encryption output in ASCII armor.") armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream, options).also { outermostStream = it } } @@ -75,45 +76,43 @@ class EncryptionStream( @Throws(IOException::class, PGPException::class) private fun prepareEncryption() { if (options.encryptionOptions == null) { - // No encryption options -> no encryption - resultBuilder.setEncryptionAlgorithm(SymmetricKeyAlgorithm.NULL) return } require(options.encryptionOptions.encryptionMethods.isNotEmpty()) { "If EncryptionOptions are provided, at least one encryption method MUST be provided as well." } - EncryptionBuilder.negotiateSymmetricEncryptionAlgorithm(options.encryptionOptions).let { - resultBuilder.setEncryptionAlgorithm(it) - LOGGER.debug("Encrypt message using symmetric algorithm $it.") - val encryptedDataGenerator = - PGPEncryptedDataGenerator( - ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(it).apply { - setWithIntegrityPacket(true) - }) - options.encryptionOptions.encryptionMethods.forEach { m -> - encryptedDataGenerator.addMethod(m) - } - options.encryptionOptions.encryptionKeyIdentifiers.forEach { r -> - resultBuilder.addRecipient(r) - } + val mechanism: MessageEncryptionMechanism = + options.encryptionOptions.negotiateEncryptionMechanism() + resultBuilder.setEncryptionMechanism(mechanism) + val encryptedDataGenerator = + PGPEncryptedDataGenerator(api.implementation.pgpDataEncryptorBuilder(mechanism)) - publicKeyEncryptedStream = - encryptedDataGenerator.open(outermostStream, ByteArray(BUFFER_SIZE)).also { stream - -> - outermostStream = stream - } + options.encryptionOptions.encryptionMethods.forEach { m -> + encryptedDataGenerator.addMethod(m) } + options.encryptionOptions.encryptionKeyIdentifiers.forEach { r -> + resultBuilder.addRecipient(r) + } + encryptedDataGenerator.setSessionKeyExtractionCallback { pgpSessionKey -> + if (pgpSessionKey != null) { + resultBuilder.setSessionKey(SessionKey(pgpSessionKey)) + } + } + + publicKeyEncryptedStream = + encryptedDataGenerator.open(outermostStream, ByteArray(BUFFER_SIZE)).also { stream -> + outermostStream = stream + } } @Throws(IOException::class) private fun prepareCompression() { - EncryptionBuilder.negotiateCompressionAlgorithm(options).let { + options.negotiateCompressionAlgorithm(api.algorithmPolicy).let { resultBuilder.setCompressionAlgorithm(it) compressedDataGenerator = PGPCompressedDataGenerator(it.algorithmId) if (it == CompressionAlgorithm.UNCOMPRESSED) return - LOGGER.debug("Compress using $it.") basicCompressionStream = BCPGOutputStream(compressedDataGenerator!!.open(outermostStream)).also { stream -> outermostStream = stream @@ -249,8 +248,9 @@ class EncryptionStream( options.signingOptions.signingMethods.entries.reversed().forEach { (key, method) -> method.signatureGenerator.generate().let { sig -> + val documentSignature = OpenPGPDocumentSignature(sig, key.publicKey) if (method.isDetached) { - resultBuilder.addDetachedSignature(key, sig) + resultBuilder.addDetachedSignature(documentSignature) } if (!method.isDetached || options.isCleartextSigned) { sig.encode(signatureLayerStream) @@ -266,8 +266,4 @@ class EncryptionStream( val isClosed get() = closed - - companion object { - @JvmStatic private val LOGGER = LoggerFactory.getLogger(EncryptionStream::class.java) - } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/OpenPGPSignatureSet.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/OpenPGPSignatureSet.kt new file mode 100644 index 00000000..c93fef37 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/OpenPGPSignatureSet.kt @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPSignature + +class OpenPGPSignatureSet(val signatures: List) : Iterable { + + fun getSignaturesBy(cert: OpenPGPCertificate): List = + signatures.filter { sig -> sig.signature.keyIdentifiers.any { cert.getKey(it) != null } } + + fun getSignaturesBy(componentKey: OpenPGPCertificate.OpenPGPComponentKey): List = + signatures.filter { sig -> + sig.signature.keyIdentifiers.any { componentKey.keyIdentifier.matchesExplicit(it) } + } + + override fun iterator(): Iterator { + return signatures.iterator() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt index e77ef2e3..900d7fe6 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt @@ -6,16 +6,17 @@ package org.pgpainless.encryption_signing import java.util.* import org.bouncycastle.openpgp.PGPLiteralData -import org.pgpainless.PGPainless import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.StreamEncoding +import org.pgpainless.algorithm.negotiation.CompressionAlgorithmNegotiator +import org.pgpainless.policy.Policy -class ProducerOptions -private constructor( +class ProducerOptions( val encryptionOptions: EncryptionOptions?, val signingOptions: SigningOptions? ) { - + var compressionAlgorithmNegotiator: CompressionAlgorithmNegotiator = + CompressionAlgorithmNegotiator.staticNegotiation() private var _fileName: String = "" private var _modificationDate: Date = PGPLiteralData.NOW private var encodingField: StreamEncoding = StreamEncoding.BINARY @@ -24,8 +25,8 @@ private constructor( private var _hideArmorHeaders = false var isDisableAsciiArmorCRC = false - private var _compressionAlgorithmOverride: CompressionAlgorithm = - PGPainless.getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm + private var _compressionAlgorithmOverride: CompressionAlgorithm? = null + private var asciiArmor = true private var _comment: String? = null private var _version: String? = null @@ -104,6 +105,13 @@ private constructor( */ fun hasVersion() = version != null + /** + * Configure the resulting OpenPGP message to make use of the Cleartext Signature Framework + * (CSF). A CSF message MUST be signed using detached signatures only and MUST NOT be encrypted. + * + * @see + * [RFC9580: OpenPGP - Cleartext Signature Framework](https://www.rfc-editor.org/rfc/rfc9580.html#name-cleartext-signature-framewo) + */ fun setCleartextSigned() = apply { require(signingOptions != null) { "Signing Options cannot be null if cleartext signing is enabled." @@ -174,8 +182,8 @@ private constructor( * * @param encoding encoding * @return this - * @see RFC4880 §5.9. - * Literal Data Packet + * @see + * [RFC4880 §5.9. Literal Data Packet](https://datatracker.ietf.org/doc/html/rfc4880#section-5.9) * @deprecated options other than the default value of [StreamEncoding.BINARY] are discouraged. */ @Deprecated("Options other than BINARY are discouraged.") @@ -212,7 +220,7 @@ private constructor( _compressionAlgorithmOverride = compressionAlgorithm } - val compressionAlgorithmOverride: CompressionAlgorithm + val compressionAlgorithmOverride: CompressionAlgorithm? get() = _compressionAlgorithmOverride val isHideArmorHeaders: Boolean @@ -230,6 +238,11 @@ private constructor( _hideArmorHeaders = hideArmorHeaders } + internal fun negotiateCompressionAlgorithm(policy: Policy): CompressionAlgorithm { + return compressionAlgorithmNegotiator.negotiate( + policy, compressionAlgorithmOverride, setOf()) + } + companion object { /** * Sign and encrypt some data. @@ -239,8 +252,10 @@ private constructor( * @return builder */ @JvmStatic - fun signAndEncrypt(encryptionOptions: EncryptionOptions, signingOptions: SigningOptions) = - ProducerOptions(encryptionOptions, signingOptions) + fun signAndEncrypt( + encryptionOptions: EncryptionOptions, + signingOptions: SigningOptions + ): ProducerOptions = ProducerOptions(encryptionOptions, signingOptions) /** * Sign some data without encryption. @@ -248,7 +263,9 @@ private constructor( * @param signingOptions signing options * @return builder */ - @JvmStatic fun sign(signingOptions: SigningOptions) = ProducerOptions(null, signingOptions) + @JvmStatic + fun sign(signingOptions: SigningOptions): ProducerOptions = + ProducerOptions(null, signingOptions) /** * Encrypt some data without signing. @@ -257,13 +274,14 @@ private constructor( * @return builder */ @JvmStatic - fun encrypt(encryptionOptions: EncryptionOptions) = ProducerOptions(encryptionOptions, null) + fun encrypt(encryptionOptions: EncryptionOptions): ProducerOptions = + ProducerOptions(encryptionOptions, null) /** * Only wrap the data in an OpenPGP packet. No encryption or signing will be applied. * * @return builder */ - @JvmStatic fun noEncryptionNoSigning() = ProducerOptions(null, null) + @JvmStatic fun noEncryptionNoSigning(): ProducerOptions = ProducerOptions(null, null) } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt index e0fe2972..ce0774e8 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt @@ -5,28 +5,32 @@ package org.pgpainless.encryption_signing import java.util.* +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.* -import org.pgpainless.PGPainless.Companion.getPolicy -import org.pgpainless.PGPainless.Companion.inspectKeyRing +import org.bouncycastle.openpgp.api.OpenPGPImplementation +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPPrivateKey +import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPSecretKey +import org.pgpainless.PGPainless import org.pgpainless.algorithm.DocumentSignatureType import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.PublicKeyAlgorithm.Companion.requireFromId +import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator.Companion.negotiateSignatureHashAlgorithm -import org.pgpainless.bouncycastle.extensions.unlock import org.pgpainless.exception.KeyException import org.pgpainless.exception.KeyException.* -import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.OpenPgpFingerprint.Companion.of -import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey import org.pgpainless.policy.Policy import org.pgpainless.signature.subpackets.BaseSignatureSubpackets.Callback import org.pgpainless.signature.subpackets.SignatureSubpackets import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper -class SigningOptions { - - val signingMethods: Map = mutableMapOf() +class SigningOptions(private val api: PGPainless) { + var hashAlgorithmNegotiator: HashAlgorithmNegotiator = + negotiateSignatureHashAlgorithm(api.algorithmPolicy) + val signingMethods: Map = mutableMapOf() private var _hashAlgorithmOverride: HashAlgorithm? = null private var _evaluationDate: Date = Date() @@ -46,6 +50,7 @@ class SigningOptions { _hashAlgorithmOverride = hashAlgorithmOverride } + /** Evaluation date for signing keys. */ val evaluationDate: Date get() = _evaluationDate @@ -61,17 +66,34 @@ class 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 + * @param signingKey OpenPGPKey containing the signing (sub-)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 */ @Throws(KeyException::class, PGPException::class) + fun addSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: OpenPGPKey + ): SigningOptions = apply { + addInlineSignature( + signingKeyProtector, signingKey, null, DocumentSignatureType.BINARY_DOCUMENT) + } + + /** + * 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 KeyException if something is wrong with the key + * @throws PGPException if the key cannot be unlocked or a signing method cannot be created + */ + @Deprecated("Pass an OpenPGPKey instead.") + @Throws(KeyException::class, PGPException::class) + // TODO: Remove in 2.1 fun addSignature(signingKeyProtector: SecretKeyRingProtector, signingKey: PGPSecretKeyRing) = - apply { - addInlineSignature( - signingKeyProtector, signingKey, null, DocumentSignatureType.BINARY_DOCUMENT) - } + addSignature(signingKeyProtector, api.toKey(signingKey)) /** * Add inline signatures with all secret key rings in the provided secret key ring collection. @@ -85,6 +107,8 @@ class SigningOptions { * created */ @Throws(KeyException::class, PGPException::class) + @Deprecated("Repeatedly call addInlineSignature(), passing an OpenPGPKey instead.") + // TODO: Remove in 2.1 fun addInlineSignatures( signingKeyProtector: SecretKeyRingProtector, signingKeys: Iterable, @@ -93,6 +117,23 @@ class SigningOptions { signingKeys.forEach { addInlineSignature(signingKeyProtector, it, null, signatureType) } } + /** + * Add inline signatures with the provided [signingKey]. + * + * @param signingKeyProtector decryptor to unlock the signing secret keys + * @param signingKey OpenPGP key + * @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 + */ + fun addInlineSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: OpenPGPKey, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT + ): SigningOptions = addInlineSignature(signingKeyProtector, signingKey, null, signatureType) + /** * 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. @@ -105,11 +146,13 @@ class SigningOptions { * @throws PGPException if the key cannot be unlocked or the signing method cannot be created */ @Throws(KeyException::class, PGPException::class) + @Deprecated("Pass an OpenPGPKey instead.") + // TODO: Remove in 2.1 fun addInlineSignature( signingKeyProtector: SecretKeyRingProtector, signingKey: PGPSecretKeyRing, signatureType: DocumentSignatureType - ) = apply { addInlineSignature(signingKeyProtector, signingKey, null, signatureType) } + ) = addInlineSignature(signingKeyProtector, api.toKey(signingKey), signatureType) /** * Add an inline-signature. Inline signatures are being embedded into the message itself and can @@ -128,16 +171,15 @@ class SigningOptions { * @throws KeyException if the key is invalid * @throws PGPException if the key cannot be unlocked or the signing method cannot be created */ - @Throws(KeyException::class, PGPException::class) @JvmOverloads fun addInlineSignature( signingKeyProtector: SecretKeyRingProtector, - signingKey: PGPSecretKeyRing, + signingKey: OpenPGPKey, userId: CharSequence? = null, signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, subpacketsCallback: Callback? = null ) = apply { - val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) + val keyRingInfo = api.inspect(signingKey, evaluationDate) if (userId != null && !keyRingInfo.isUserIdValid(userId)) { throw UnboundUserIdException( of(signingKey), @@ -148,23 +190,89 @@ class SigningOptions { val signingPubKeys = keyRingInfo.signingSubkeys if (signingPubKeys.isEmpty()) { - throw UnacceptableSigningKeyException(of(signingKey)) + throw UnacceptableSigningKeyException(signingKey) } for (signingPubKey in signingPubKeys) { - val signingSecKey: PGPSecretKey = - signingKey.getSecretKey(signingPubKey.keyID) - ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) - val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector) + val signingSecKey: OpenPGPSecretKey = + signingKey.getSecretKey(signingPubKey) + ?: throw MissingSecretKeyException(signingPubKey) + val signingPrivKey: OpenPGPPrivateKey = + unlockSecretKey(signingSecKey, signingKeyProtector) val hashAlgorithms = if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) - else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) - val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) + else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyIdentifier) + val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms) addSigningMethod( - signingKey, signingSubkey, hashAlgorithm, signatureType, false, subpacketsCallback) + signingPrivKey, hashAlgorithm, signatureType, false, subpacketsCallback) } } + /** + * 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 signingKeyProtector decryptor to unlock the signing secret key + * @param signingKey signing key + * @param userId user-id of the signer + * @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 PGPException if the key cannot be unlocked or the signing method cannot be created + */ + @Deprecated("Pass an OpenPGPKey instead.") + @Throws(KeyException::class, PGPException::class) + @JvmOverloads + // TODO: Remove in 2.1 + fun addInlineSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + userId: CharSequence? = null, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketsCallback: Callback? = null + ) = + addInlineSignature( + signingKeyProtector, api.toKey(signingKey), userId, signatureType, subpacketsCallback) + + /** + * Create an inline signature using the given signing component key (e.g. a specific subkey). + * + * @param signingKeyProtector decryptor to unlock the secret key + * @param signingKey signing component key + * @param signatureType signature type + * @param subpacketsCallback callback to modify the signatures subpackets + * @return builder + * @throws PGPException if the secret key cannot be unlocked or if no signing method can be + * created. + */ + fun addInlineSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: OpenPGPSecretKey, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketsCallback: Callback? = null + ): SigningOptions = apply { + val openPGPKey = signingKey.openPGPKey + val keyRingInfo = api.inspect(openPGPKey, evaluationDate) + val signingPubKeys = keyRingInfo.signingSubkeys + if (signingPubKeys.isEmpty()) { + throw UnacceptableSigningKeyException(openPGPKey) + } + + if (!signingPubKeys.any { it.keyIdentifier.matchesExplicit(signingKey.keyIdentifier) }) { + throw MissingSecretKeyException(signingKey) + } + + val signingPrivKey = unlockSecretKey(signingKey, signingKeyProtector) + val hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingKey.keyIdentifier) + val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms) + addSigningMethod(signingPrivKey, hashAlgorithm, signatureType, false, subpacketsCallback) + } + /** * Create an inline signature using the signing key with the given keyId. * @@ -183,35 +291,23 @@ class SigningOptions { */ @Throws(KeyException::class, PGPException::class) @JvmOverloads + @Deprecated("Pass in an OpenPGPSecretKey instead.") + // TODO: Remove in 2.1 fun addInlineSignature( signingKeyProtector: SecretKeyRingProtector, signingKey: PGPSecretKeyRing, keyId: Long, signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, subpacketsCallback: Callback? = null - ) = apply { - val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) - val signingPubKeys = keyRingInfo.signingSubkeys - if (signingPubKeys.isEmpty()) { - throw UnacceptableSigningKeyException(of(signingKey)) - } - - for (signingPubKey in signingPubKeys) { - if (signingPubKey.keyID != keyId) { - continue - } - - val signingSecKey = - signingKey.getSecretKey(signingPubKey.keyID) - ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) - val signingSubkey = signingSecKey.unlock(signingKeyProtector) - val hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) - val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) - addSigningMethod( - signingKey, signingSubkey, hashAlgorithm, signatureType, false, subpacketsCallback) - return this - } - throw MissingSecretKeyException(of(signingKey), keyId) + ): SigningOptions { + val key = api.toKey(signingKey) + val subkeyIdentifier = KeyIdentifier(keyId) + return addInlineSignature( + signingKeyProtector, + key.getSecretKey(subkeyIdentifier) + ?: throw MissingSecretKeyException(of(signingKey), subkeyIdentifier), + signatureType, + subpacketsCallback) } /** @@ -226,6 +322,8 @@ class SigningOptions { * method cannot be created */ @Throws(KeyException::class, PGPException::class) + @Deprecated("Repeatedly call addDetachedSignature(), passing an OpenPGPKey instead.") + // TODO: Remove in 2.1 fun addDetachedSignatures( signingKeyProtector: SecretKeyRingProtector, signingKeys: Iterable, @@ -234,6 +332,12 @@ class SigningOptions { signingKeys.forEach { addDetachedSignature(signingKeyProtector, it, null, signatureType) } } + fun addDetachedSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: OpenPGPKey, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT + ): SigningOptions = addDetachedSignature(signingKeyProtector, signingKey, null, signatureType) + /** * Create a detached signature. Detached signatures are not being added into the PGP message * itself. Instead, they can be distributed separately to the message. Detached signatures are @@ -247,7 +351,9 @@ class SigningOptions { * @throws PGPException if the key cannot be validated or unlocked, or if no signature method * can be created */ + @Deprecated("Pass an OpenPGPKey instead.") @Throws(KeyException::class, PGPException::class) + // TODO: Remove in 2.1 fun addDetachedSignature( signingKeyProtector: SecretKeyRingProtector, signingKey: PGPSecretKeyRing, @@ -273,15 +379,14 @@ class SigningOptions { * can be created */ @JvmOverloads - @Throws(KeyException::class, PGPException::class) fun addDetachedSignature( signingKeyProtector: SecretKeyRingProtector, - signingKey: PGPSecretKeyRing, - userId: String? = null, + signingKey: OpenPGPKey, + userId: CharSequence? = null, signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, subpacketCallback: Callback? = null - ) = apply { - val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) + ): SigningOptions = apply { + val keyRingInfo = api.inspect(signingKey, evaluationDate) if (userId != null && !keyRingInfo.isUserIdValid(userId)) { throw UnboundUserIdException( of(signingKey), @@ -292,23 +397,80 @@ class SigningOptions { val signingPubKeys = keyRingInfo.signingSubkeys if (signingPubKeys.isEmpty()) { - throw UnacceptableSigningKeyException(of(signingKey)) + throw UnacceptableSigningKeyException(signingKey) } for (signingPubKey in signingPubKeys) { - val signingSecKey: PGPSecretKey = - signingKey.getSecretKey(signingPubKey.keyID) - ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) - val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector) - val hashAlgorithms = - if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) - else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) - val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) - addSigningMethod( - signingKey, signingSubkey, hashAlgorithm, signatureType, true, subpacketCallback) + val signingSecKey: OpenPGPSecretKey = + signingKey.getSecretKey(signingPubKey.keyIdentifier) + ?: throw MissingSecretKeyException(signingPubKey) + addDetachedSignature( + signingKeyProtector, signingSecKey, userId, signatureType, subpacketCallback) } } + /** + * Create a detached signature. 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 signingKeyProtector decryptor to unlock the secret signing key + * @param signingKey signing key + * @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 + * @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 + */ + @Deprecated("Pass an OpenPGPKey instead.") + @JvmOverloads + @Throws(KeyException::class, PGPException::class) + // TODO: Remove in 2.1 + fun addDetachedSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + userId: String? = null, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketCallback: Callback? = null + ) = + addDetachedSignature( + signingKeyProtector, api.toKey(signingKey), userId, signatureType, subpacketCallback) + + /** + * Create a detached signature. 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 creates a signature using the provided [signingKey], taking into consideration + * the preferences found on the binding signature of the given [userId]. + * + * @param signingKeyProtector protector to unlock the signing key + * @param signingKey OpenPGP key for signing + * @param userId user-id to determine algorithm preferences + * @param signatureType document signature type + * @param subpacketCallback callback to change the subpackets of the signature + * @return this + */ + fun addDetachedSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: OpenPGPSecretKey, + userId: CharSequence? = null, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketCallback: Callback? = null + ): SigningOptions = apply { + val keyRingInfo = api.inspect(signingKey.openPGPKey, evaluationDate) + val signingPrivKey: OpenPGPPrivateKey = signingKey.unlock(signingKeyProtector) + val hashAlgorithms = + if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) + else keyRingInfo.getPreferredHashAlgorithms(signingKey.keyIdentifier) + val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms) + addSigningMethod(signingPrivKey, hashAlgorithm, signatureType, true, subpacketCallback) + } + /** * Create a detached signature using the signing key with the given keyId. * @@ -327,63 +489,45 @@ class SigningOptions { */ @Throws(KeyException::class, PGPException::class) @JvmOverloads + @Deprecated("Pass an OpenPGPSecretKey instead.") + // TODO: Remove in 2.1 fun addDetachedSignature( signingKeyProtector: SecretKeyRingProtector, signingKey: PGPSecretKeyRing, keyId: Long, signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, subpacketsCallback: Callback? = null - ) = apply { - val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) - - val signingPubKeys = keyRingInfo.signingSubkeys - if (signingPubKeys.isEmpty()) { - throw UnacceptableSigningKeyException(of(signingKey)) - } - - for (signingPubKey in signingPubKeys) { - if (signingPubKey.keyID == keyId) { - val signingSecKey: PGPSecretKey = - signingKey.getSecretKey(signingPubKey.keyID) - ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) - val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector) - val hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) - val hashAlgorithm: HashAlgorithm = - negotiateHashAlgorithm(hashAlgorithms, getPolicy()) - addSigningMethod( - signingKey, - signingSubkey, - hashAlgorithm, - signatureType, - true, - subpacketsCallback) - return this - } - } - - throw MissingSecretKeyException(of(signingKey), keyId) + ): SigningOptions { + val key = api.toKey(signingKey) + val signingKeyIdentifier = KeyIdentifier(keyId) + return addDetachedSignature( + signingKeyProtector, + key.getSecretKey(signingKeyIdentifier) + ?: throw MissingSecretKeyException(of(key), signingKeyIdentifier), + null, + signatureType, + subpacketsCallback) } private fun addSigningMethod( - signingKey: PGPSecretKeyRing, - signingSubkey: PGPPrivateKey, + signingKey: OpenPGPPrivateKey, hashAlgorithm: HashAlgorithm, signatureType: DocumentSignatureType, detached: Boolean, subpacketCallback: Callback? = null ) { - val signingKeyIdentifier = SubkeyIdentifier(signingKey, signingSubkey.keyID) - val signingSecretKey: PGPSecretKey = signingKey.getSecretKey(signingSubkey.keyID) + val signingSecretKey: PGPSecretKey = signingKey.secretKey.pgpSecretKey val publicKeyAlgorithm = requireFromId(signingSecretKey.publicKey.algorithm) val bitStrength = signingSecretKey.publicKey.bitStrength - if (!getPolicy().publicKeyAlgorithmPolicy.isAcceptable(publicKeyAlgorithm, bitStrength)) { + if (!api.algorithmPolicy.publicKeyAlgorithmPolicy.isAcceptable( + publicKeyAlgorithm, bitStrength)) { throw UnacceptableSigningKeyException( PublicKeyAlgorithmPolicyException( - of(signingKey), signingSecretKey.keyID, publicKeyAlgorithm, bitStrength)) + signingKey.secretKey, publicKeyAlgorithm, bitStrength)) } val generator: PGPSignatureGenerator = - createSignatureGenerator(signingSubkey, hashAlgorithm, signatureType) + createSignatureGenerator(signingKey.keyPair, hashAlgorithm, signatureType) // Subpackets val hashedSubpackets = @@ -399,7 +543,7 @@ class SigningOptions { val signingMethod = if (detached) SigningMethod.detachedSignature(generator, hashAlgorithm) else SigningMethod.inlineSignature(generator, hashAlgorithm) - (signingMethods as MutableMap)[signingKeyIdentifier] = signingMethod + (signingMethods as MutableMap)[signingKey] = signingMethod } /** @@ -414,32 +558,29 @@ class SigningOptions { * @param policy policy * @return selected hash algorithm */ - private fun negotiateHashAlgorithm( - preferences: Set, - policy: Policy - ): HashAlgorithm { - return _hashAlgorithmOverride - ?: negotiateSignatureHashAlgorithm(policy).negotiateHashAlgorithm(preferences) + private fun negotiateHashAlgorithm(preferences: Set?): HashAlgorithm { + return _hashAlgorithmOverride ?: hashAlgorithmNegotiator.negotiateHashAlgorithm(preferences) } @Throws(PGPException::class) private fun createSignatureGenerator( - privateKey: PGPPrivateKey, + signingKey: PGPKeyPair, hashAlgorithm: HashAlgorithm, signatureType: DocumentSignatureType ): PGPSignatureGenerator { - return ImplementationFactory.getInstance() - .getPGPContentSignerBuilder( - privateKey.publicKeyPacket.algorithm, hashAlgorithm.algorithmId) + return OpenPGPImplementation.getInstance() + .pgpContentSignerBuilder(signingKey.publicKey.algorithm, hashAlgorithm.algorithmId) .let { csb -> - PGPSignatureGenerator(csb).also { - it.init(signatureType.signatureType.code, privateKey) + PGPSignatureGenerator(csb, signingKey.publicKey).also { + it.init(signatureType.signatureType.code, signingKey.privateKey) } } } companion object { - @JvmStatic fun get() = SigningOptions() + @JvmOverloads + @JvmStatic + fun get(api: PGPainless = PGPainless.getInstance()) = SigningOptions(api) } /** A method of signing. */ diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/exception/KeyException.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/KeyException.kt new file mode 100644 index 00000000..584de2ad --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/KeyException.kt @@ -0,0 +1,212 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception + +import java.util.* +import javax.annotation.Nonnull +import org.bouncycastle.bcpg.KeyIdentifier +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.OpenPgpFingerprint.Companion.of +import org.pgpainless.util.DateUtil.Companion.formatUTCDate + +abstract class KeyException : RuntimeException { + + val fingerprint: OpenPgpFingerprint + + protected constructor(message: String, fingerprint: OpenPgpFingerprint) : super(message) { + this.fingerprint = fingerprint + } + + protected constructor( + message: String, + fingerprint: OpenPgpFingerprint, + underlying: Throwable + ) : super(message, underlying) { + this.fingerprint = fingerprint + } + + class ExpiredKeyException(fingerprint: OpenPgpFingerprint, expirationDate: Date) : + KeyException( + "Key $fingerprint is expired. Expiration date: ${formatUTCDate(expirationDate)}", + fingerprint, + ) { + + constructor(cert: OpenPGPCertificate, expirationDate: Date) : this(of(cert), expirationDate) + + constructor( + componentKey: OpenPGPComponentKey, + expirationDate: Date + ) : this(of(componentKey), expirationDate) + } + + class RevokedKeyException : KeyException { + constructor( + fingerprint: OpenPgpFingerprint + ) : super( + "Key $fingerprint appears to be revoked.", + fingerprint, + ) + + constructor( + componentKey: OpenPGPComponentKey + ) : super( + "Subkey ${componentKey.keyIdentifier} appears to be revoked.", + of(componentKey), + ) + + constructor( + cert: OpenPGPCertificate + ) : super( + "Key or certificate ${cert.keyIdentifier} appears to be revoked.", + of(cert), + ) + } + + class UnacceptableEncryptionKeyException : KeyException { + constructor(cert: OpenPGPCertificate) : this(of(cert)) + + constructor( + subkey: OpenPGPComponentKey + ) : super( + "Subkey ${subkey.keyIdentifier} is not an acceptable encryption key.", + of(subkey), + ) + + constructor( + fingerprint: OpenPgpFingerprint + ) : super("Key $fingerprint has no acceptable encryption key.", fingerprint) + + constructor( + reason: PublicKeyAlgorithmPolicyException + ) : super( + "Key ${reason.fingerprint} has no acceptable encryption key.", + reason.fingerprint, + reason) + } + + class UnacceptableSigningKeyException : KeyException { + constructor(cert: OpenPGPCertificate) : this(of(cert)) + + constructor(subkey: OpenPGPComponentKey) : this(of(subkey)) + + constructor( + fingerprint: OpenPgpFingerprint + ) : super("Key $fingerprint has no acceptable signing key.", fingerprint) + + constructor( + reason: KeyException.PublicKeyAlgorithmPolicyException + ) : super( + "Key ${reason.fingerprint} has no acceptable signing key.", reason.fingerprint, reason) + } + + class UnacceptableThirdPartyCertificationKeyException(fingerprint: OpenPgpFingerprint) : + KeyException("Key $fingerprint has no acceptable certification key.", fingerprint) {} + + class UnacceptableSelfSignatureException : KeyException { + constructor(cert: OpenPGPCertificate) : this(of(cert)) + + constructor( + fingerprint: OpenPgpFingerprint + ) : super( + "Key $fingerprint does not have a valid/acceptable signature to derive an expiration date from.", + fingerprint, + ) + } + + class MissingSecretKeyException : KeyException { + val missingSecretKeyIdentifier: KeyIdentifier + + constructor( + publicKey: OpenPGPComponentKey + ) : this( + of(publicKey.certificate), + publicKey.keyIdentifier, + ) + + constructor( + fingerprint: OpenPgpFingerprint, + keyIdentifier: KeyIdentifier + ) : super( + "Key $fingerprint does not contain a secret key for public key $keyIdentifier", + fingerprint, + ) { + missingSecretKeyIdentifier = keyIdentifier + } + + @Deprecated("Pass in a KeyIdentifier instead.") + constructor( + fingerprint: OpenPgpFingerprint, + keyId: Long + ) : this(fingerprint, KeyIdentifier(keyId)) + } + + class PublicKeyAlgorithmPolicyException : KeyException { + val violatingSubkeyId: KeyIdentifier + + constructor( + subkey: OpenPGPComponentKey, + algorithm: PublicKeyAlgorithm, + bitSize: Int + ) : super( + """Subkey ${subkey.keyIdentifier} of key ${subkey.certificate.keyIdentifier} is violating the Public Key Algorithm Policy: +$algorithm of size $bitSize is not acceptable.""", + of(subkey), + ) { + this.violatingSubkeyId = subkey.keyIdentifier + } + + constructor( + fingerprint: OpenPgpFingerprint, + keyId: Long, + algorithm: PublicKeyAlgorithm, + bitSize: Int + ) : super( + """Subkey ${java.lang.Long.toHexString(keyId)} of key $fingerprint is violating the Public Key Algorithm Policy: +$algorithm of size $bitSize is not acceptable.""", + fingerprint, + ) { + this.violatingSubkeyId = KeyIdentifier(keyId) + } + } + + class UnboundUserIdException( + fingerprint: OpenPgpFingerprint, + userId: String, + userIdSignature: PGPSignature?, + userIdRevocation: PGPSignature? + ) : + KeyException( + errorMessage( + fingerprint, + userId, + userIdSignature, + userIdRevocation, + ), + fingerprint, + ) { + + companion object { + private fun errorMessage( + @Nonnull fingerprint: OpenPgpFingerprint, + @Nonnull userId: String, + userIdSignature: PGPSignature?, + userIdRevocation: PGPSignature? + ): String { + val 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/kotlin/org/pgpainless/exception/KeyIntegrityException.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/KeyIntegrityException.kt new file mode 100644 index 00000000..a4444020 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/KeyIntegrityException.kt @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception + +import java.lang.AssertionError + +/** + * 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. + */ +class KeyIntegrityException : AssertionError("Key Integrity Exception") diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/exception/MalformedOpenPgpMessageException.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/MalformedOpenPgpMessageException.kt new file mode 100644 index 00000000..23d9ffdc --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/MalformedOpenPgpMessageException.kt @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception + +import org.pgpainless.decryption_verification.syntax_check.InputSymbol +import org.pgpainless.decryption_verification.syntax_check.StackSymbol +import org.pgpainless.decryption_verification.syntax_check.State + +/** + * 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](https://www.rfc-editor.org/rfc/rfc4880#section-11.3) + */ +class MalformedOpenPgpMessageException : RuntimeException { + constructor(message: String) : super(message) + + constructor(message: String, e: MalformedOpenPgpMessageException) : super(message, e) + + constructor( + state: State, + input: InputSymbol, + stackItem: StackSymbol? + ) : this( + "There is no legal transition from state '$state' for input '$input' when '${stackItem ?: "null"}' is on top of the stack.", + ) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/exception/MessageNotIntegrityProtectedException.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/MessageNotIntegrityProtectedException.kt new file mode 100644 index 00000000..df3b8eb8 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/MessageNotIntegrityProtectedException.kt @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception + +import org.bouncycastle.openpgp.PGPException + +class MessageNotIntegrityProtectedException : + PGPException( + "Message is encrypted using a 'Symmetrically Encrypted Data' (SED) packet, which enables certain types of attacks. " + + "A 'Symmetrically Encrypted Integrity Protected' (SEIP) packet should be used instead.", + ) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/exception/MissingDecryptionMethodException.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/MissingDecryptionMethodException.kt new file mode 100644 index 00000000..1f1237f2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/MissingDecryptionMethodException.kt @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception + +import org.bouncycastle.openpgp.PGPException + +/** + * Exception that is thrown when decryption fails due to a missing decryption key or decryption + * passphrase. This can happen when the user does not provide the right set of keys / the right + * password when decrypting a message. + */ +class MissingDecryptionMethodException(message: String) : PGPException(message) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/exception/MissingPassphraseException.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/MissingPassphraseException.kt new file mode 100644 index 00000000..ee404a99 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/MissingPassphraseException.kt @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception + +import java.util.* +import org.bouncycastle.openpgp.PGPException +import org.pgpainless.key.SubkeyIdentifier + +class MissingPassphraseException(keyIds: Set) : + PGPException( + "Missing passphrase encountered for keys ${keyIds.toTypedArray().contentToString()}") { + val keyIds: Set = Collections.unmodifiableSet(keyIds) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/exception/ModificationDetectionException.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/ModificationDetectionException.kt new file mode 100644 index 00000000..09796f85 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/ModificationDetectionException.kt @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception + +import java.io.IOException + +/** Exception that gets thrown when the verification of a modification detection code failed. */ +class ModificationDetectionException : IOException() diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/exception/SignatureValidationException.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/SignatureValidationException.kt new file mode 100644 index 00000000..6cc6b32e --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/SignatureValidationException.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception + +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.SignatureType + +class SignatureValidationException : PGPException { + + constructor(message: String?) : super(message) + + constructor(message: String?, underlying: Exception) : super(message, underlying) + + constructor( + message: String, + rejections: Map + ) : super("$message: ${exceptionMapToString(rejections)}") + + companion object { + @JvmStatic + private fun exceptionMapToString(rejections: Map): String = + buildString { + append(rejections.size).append(" rejected signatures:\n") + for (signature in rejections.keys) { + append(sigTypeToString(signature.signatureType)) + .append(' ') + .append(signature.creationTime) + .append(": ") + .append(rejections[signature]!!.message) + .append('\n') + } + } + + @JvmStatic + private fun sigTypeToString(type: Int): String = + SignatureType.fromCode(type)?.toString() + ?: "0x${java.lang.Long.toHexString(type.toLong())}" + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/exception/UnacceptableAlgorithmException.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/UnacceptableAlgorithmException.kt new file mode 100644 index 00000000..b2783cd6 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/UnacceptableAlgorithmException.kt @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception + +import org.bouncycastle.openpgp.PGPException + +/** Exception that gets thrown if unacceptable algorithms are encountered. */ +class UnacceptableAlgorithmException(message: String) : PGPException(message) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/exception/WrongConsumingMethodException.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/WrongConsumingMethodException.kt new file mode 100644 index 00000000..e1fe0842 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/WrongConsumingMethodException.kt @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception + +import org.bouncycastle.openpgp.PGPException + +class WrongConsumingMethodException(message: String) : PGPException(message) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/exception/WrongPassphraseException.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/WrongPassphraseException.kt new file mode 100644 index 00000000..4ac7cf68 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/WrongPassphraseException.kt @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception + +import org.bouncycastle.bcpg.KeyIdentifier +import org.bouncycastle.openpgp.PGPException + +class WrongPassphraseException : PGPException { + + constructor(message: String) : super(message) + + constructor(message: String, cause: PGPException) : super(message, cause) + + @Deprecated("Pass in a KeyIdentifier instead.") + constructor(keyId: Long, cause: PGPException) : this(KeyIdentifier(keyId), cause) + + constructor( + keyIdentifier: KeyIdentifier, + cause: PGPException + ) : this("Wrong passphrase provided for key $keyIdentifier", cause) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt deleted file mode 100644 index dcf594ea..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.implementation - -import java.io.InputStream -import java.security.KeyPair -import java.util.* -import org.bouncycastle.crypto.AsymmetricCipherKeyPair -import org.bouncycastle.openpgp.* -import org.bouncycastle.openpgp.bc.BcPGPObjectFactory -import org.bouncycastle.openpgp.operator.* -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator -import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory -import org.bouncycastle.openpgp.operator.bc.BcPBEKeyEncryptionMethodGenerator -import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder -import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyEncryptorBuilder -import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder -import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider -import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder -import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider -import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter -import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator -import org.bouncycastle.openpgp.operator.bc.BcSessionKeyDataDecryptorFactory -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair -import org.pgpainless.algorithm.HashAlgorithm -import org.pgpainless.algorithm.PublicKeyAlgorithm -import org.pgpainless.algorithm.SymmetricKeyAlgorithm -import org.pgpainless.util.Passphrase - -class BcImplementationFactory : ImplementationFactory() { - override val pgpDigestCalculatorProvider: BcPGPDigestCalculatorProvider = - BcPGPDigestCalculatorProvider() - override val pgpContentVerifierBuilderProvider: BcPGPContentVerifierBuilderProvider = - BcPGPContentVerifierBuilderProvider() - override val keyFingerprintCalculator: BcKeyFingerprintCalculator = BcKeyFingerprintCalculator() - - override fun getPBESecretKeyEncryptor( - symmetricKeyAlgorithm: SymmetricKeyAlgorithm, - digestCalculator: PGPDigestCalculator, - passphrase: Passphrase - ): PBESecretKeyEncryptor = - BcPBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.algorithmId, digestCalculator) - .build(passphrase.getChars()) - - override fun getPBESecretKeyEncryptor( - encryptionAlgorithm: SymmetricKeyAlgorithm, - hashAlgorithm: HashAlgorithm, - s2kCount: Int, - passphrase: Passphrase - ): PBESecretKeyEncryptor = - BcPBESecretKeyEncryptorBuilder( - encryptionAlgorithm.algorithmId, getPGPDigestCalculator(hashAlgorithm), s2kCount) - .build(passphrase.getChars()) - - override fun getPBESecretKeyDecryptor(passphrase: Passphrase): PBESecretKeyDecryptor = - BcPBESecretKeyDecryptorBuilder(pgpDigestCalculatorProvider).build(passphrase.getChars()) - - override fun getPGPContentSignerBuilder( - keyAlgorithm: Int, - hashAlgorithm: Int - ): PGPContentSignerBuilder = BcPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) - - override fun getPBEDataDecryptorFactory(passphrase: Passphrase): PBEDataDecryptorFactory = - BcPBEDataDecryptorFactory(passphrase.getChars(), pgpDigestCalculatorProvider) - - override fun getPublicKeyDataDecryptorFactory( - privateKey: PGPPrivateKey - ): PublicKeyDataDecryptorFactory = BcPublicKeyDataDecryptorFactory(privateKey) - - override fun getSessionKeyDataDecryptorFactory( - sessionKey: PGPSessionKey - ): SessionKeyDataDecryptorFactory = BcSessionKeyDataDecryptorFactory(sessionKey) - - override fun getPublicKeyKeyEncryptionMethodGenerator( - key: PGPPublicKey - ): PublicKeyKeyEncryptionMethodGenerator = BcPublicKeyKeyEncryptionMethodGenerator(key) - - override fun getPBEKeyEncryptionMethodGenerator( - passphrase: Passphrase - ): PBEKeyEncryptionMethodGenerator = BcPBEKeyEncryptionMethodGenerator(passphrase.getChars()) - - override fun getPGPDataEncryptorBuilder(symmetricKeyAlgorithm: Int): PGPDataEncryptorBuilder = - BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm) - - override fun getPGPKeyPair( - publicKeyAlgorithm: PublicKeyAlgorithm, - keyPair: KeyPair, - creationDate: Date - ): PGPKeyPair = - BcPGPKeyPair( - publicKeyAlgorithm.algorithmId, - jceToBcKeyPair(publicKeyAlgorithm, keyPair, creationDate), - creationDate) - - override fun getPGPObjectFactory(inputStream: InputStream): PGPObjectFactory = - BcPGPObjectFactory(inputStream) - - private fun jceToBcKeyPair( - publicKeyAlgorithm: PublicKeyAlgorithm, - keyPair: KeyPair, - creationDate: Date - ): AsymmetricCipherKeyPair = - BcPGPKeyConverter().let { converter -> - JcaPGPKeyPair(publicKeyAlgorithm.algorithmId, keyPair, creationDate).let { pair -> - AsymmetricCipherKeyPair( - converter.getPublicKey(pair.publicKey), - converter.getPrivateKey(pair.privateKey)) - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt deleted file mode 100644 index 58478379..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.implementation - -import java.io.InputStream -import java.security.KeyPair -import java.util.* -import org.bouncycastle.openpgp.* -import org.bouncycastle.openpgp.operator.* -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 - -abstract class ImplementationFactory { - - companion object { - @JvmStatic private var instance: ImplementationFactory = BcImplementationFactory() - - @JvmStatic fun getInstance() = instance - - @JvmStatic - fun setFactoryImplementation(implementation: ImplementationFactory) = apply { - instance = implementation - } - } - - abstract val pgpDigestCalculatorProvider: PGPDigestCalculatorProvider - abstract val pgpContentVerifierBuilderProvider: PGPContentVerifierBuilderProvider - abstract val keyFingerprintCalculator: KeyFingerPrintCalculator - - val v4FingerprintCalculator: PGPDigestCalculator - get() = getPGPDigestCalculator(HashAlgorithm.SHA1) - - @Throws(PGPException::class) - abstract fun getPBESecretKeyEncryptor( - symmetricKeyAlgorithm: SymmetricKeyAlgorithm, - digestCalculator: PGPDigestCalculator, - passphrase: Passphrase - ): PBESecretKeyEncryptor - - @Throws(PGPException::class) - abstract fun getPBESecretKeyDecryptor(passphrase: Passphrase): PBESecretKeyDecryptor - - @Throws(PGPException::class) - abstract fun getPBESecretKeyEncryptor( - encryptionAlgorithm: SymmetricKeyAlgorithm, - hashAlgorithm: HashAlgorithm, - s2kCount: Int, - passphrase: Passphrase - ): PBESecretKeyEncryptor - - fun getPGPDigestCalculator(hashAlgorithm: HashAlgorithm): PGPDigestCalculator = - getPGPDigestCalculator(hashAlgorithm.algorithmId) - - fun getPGPDigestCalculator(hashAlgorithm: Int): PGPDigestCalculator = - pgpDigestCalculatorProvider.get(hashAlgorithm) - - fun getPGPContentSignerBuilder( - keyAlgorithm: PublicKeyAlgorithm, - hashAlgorithm: HashAlgorithm - ): PGPContentSignerBuilder = - getPGPContentSignerBuilder(keyAlgorithm.algorithmId, hashAlgorithm.algorithmId) - - abstract fun getPGPContentSignerBuilder( - keyAlgorithm: Int, - hashAlgorithm: Int - ): PGPContentSignerBuilder - - @Throws(PGPException::class) - abstract fun getPBEDataDecryptorFactory(passphrase: Passphrase): PBEDataDecryptorFactory - - abstract fun getPublicKeyDataDecryptorFactory( - privateKey: PGPPrivateKey - ): PublicKeyDataDecryptorFactory - - fun getSessionKeyDataDecryptorFactory(sessionKey: SessionKey): SessionKeyDataDecryptorFactory = - getSessionKeyDataDecryptorFactory( - PGPSessionKey(sessionKey.algorithm.algorithmId, sessionKey.key)) - - abstract fun getSessionKeyDataDecryptorFactory( - sessionKey: PGPSessionKey - ): SessionKeyDataDecryptorFactory - - abstract fun getPublicKeyKeyEncryptionMethodGenerator( - key: PGPPublicKey - ): PublicKeyKeyEncryptionMethodGenerator - - abstract fun getPBEKeyEncryptionMethodGenerator( - passphrase: Passphrase - ): PBEKeyEncryptionMethodGenerator - - fun getPGPDataEncryptorBuilder( - symmetricKeyAlgorithm: SymmetricKeyAlgorithm - ): PGPDataEncryptorBuilder = getPGPDataEncryptorBuilder(symmetricKeyAlgorithm.algorithmId) - - abstract fun getPGPDataEncryptorBuilder(symmetricKeyAlgorithm: Int): PGPDataEncryptorBuilder - - @Throws(PGPException::class) - abstract fun getPGPKeyPair( - publicKeyAlgorithm: PublicKeyAlgorithm, - keyPair: KeyPair, - creationDate: Date - ): PGPKeyPair - - fun getPGPObjectFactory(bytes: ByteArray): PGPObjectFactory = - getPGPObjectFactory(bytes.inputStream()) - - abstract fun getPGPObjectFactory(inputStream: InputStream): PGPObjectFactory - - override fun toString(): String { - return javaClass.simpleName - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt deleted file mode 100644 index 865f1e0d..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.implementation - -import java.io.InputStream -import java.security.KeyPair -import java.util.* -import org.bouncycastle.openpgp.* -import org.bouncycastle.openpgp.operator.* -import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair -import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder -import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator -import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder -import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder -import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder -import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder -import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator -import org.bouncycastle.openpgp.operator.jcajce.JceSessionKeyDataDecryptorFactoryBuilder -import org.pgpainless.algorithm.HashAlgorithm -import org.pgpainless.algorithm.PublicKeyAlgorithm -import org.pgpainless.algorithm.SymmetricKeyAlgorithm -import org.pgpainless.provider.ProviderFactory -import org.pgpainless.util.Passphrase - -class JceImplementationFactory : ImplementationFactory() { - override val pgpDigestCalculatorProvider: PGPDigestCalculatorProvider = - JcaPGPDigestCalculatorProviderBuilder().setProvider(ProviderFactory.provider).build() - override val pgpContentVerifierBuilderProvider: PGPContentVerifierBuilderProvider = - JcaPGPContentVerifierBuilderProvider().setProvider(ProviderFactory.provider) - override val keyFingerprintCalculator: KeyFingerPrintCalculator = - JcaKeyFingerprintCalculator().setProvider(ProviderFactory.provider) - - override fun getPBESecretKeyEncryptor( - symmetricKeyAlgorithm: SymmetricKeyAlgorithm, - digestCalculator: PGPDigestCalculator, - passphrase: Passphrase - ): PBESecretKeyEncryptor = - JcePBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.algorithmId, digestCalculator) - .setProvider(ProviderFactory.provider) - .build(passphrase.getChars()) - - override fun getPBESecretKeyEncryptor( - encryptionAlgorithm: SymmetricKeyAlgorithm, - hashAlgorithm: HashAlgorithm, - s2kCount: Int, - passphrase: Passphrase - ): PBESecretKeyEncryptor = - JcePBESecretKeyEncryptorBuilder( - encryptionAlgorithm.algorithmId, getPGPDigestCalculator(hashAlgorithm), s2kCount) - .setProvider(ProviderFactory.provider) - .build(passphrase.getChars()) - - override fun getPBESecretKeyDecryptor(passphrase: Passphrase): PBESecretKeyDecryptor = - JcePBESecretKeyDecryptorBuilder(pgpDigestCalculatorProvider) - .setProvider(ProviderFactory.provider) - .build(passphrase.getChars()) - - override fun getPGPContentSignerBuilder( - keyAlgorithm: Int, - hashAlgorithm: Int - ): PGPContentSignerBuilder = - JcaPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) - .setProvider(ProviderFactory.provider) - - override fun getPBEDataDecryptorFactory(passphrase: Passphrase): PBEDataDecryptorFactory = - JcePBEDataDecryptorFactoryBuilder(pgpDigestCalculatorProvider) - .setProvider(ProviderFactory.provider) - .build(passphrase.getChars()) - - override fun getPublicKeyDataDecryptorFactory( - privateKey: PGPPrivateKey - ): PublicKeyDataDecryptorFactory = - JcePublicKeyDataDecryptorFactoryBuilder() - .setProvider(ProviderFactory.provider) - .build(privateKey) - - override fun getSessionKeyDataDecryptorFactory( - sessionKey: PGPSessionKey - ): SessionKeyDataDecryptorFactory = - JceSessionKeyDataDecryptorFactoryBuilder() - .setProvider(ProviderFactory.provider) - .build(sessionKey) - - override fun getPublicKeyKeyEncryptionMethodGenerator( - key: PGPPublicKey - ): PublicKeyKeyEncryptionMethodGenerator = - JcePublicKeyKeyEncryptionMethodGenerator(key).setProvider(ProviderFactory.provider) - - override fun getPBEKeyEncryptionMethodGenerator( - passphrase: Passphrase - ): PBEKeyEncryptionMethodGenerator = - JcePBEKeyEncryptionMethodGenerator(passphrase.getChars()) - .setProvider(ProviderFactory.provider) - - override fun getPGPDataEncryptorBuilder(symmetricKeyAlgorithm: Int): PGPDataEncryptorBuilder = - JcePGPDataEncryptorBuilder(symmetricKeyAlgorithm).setProvider(ProviderFactory.provider) - - override fun getPGPKeyPair( - publicKeyAlgorithm: PublicKeyAlgorithm, - keyPair: KeyPair, - creationDate: Date - ): PGPKeyPair = JcaPGPKeyPair(publicKeyAlgorithm.algorithmId, keyPair, creationDate) - - override fun getPGPObjectFactory(inputStream: InputStream): PGPObjectFactory = - PGPObjectFactory(inputStream, keyFingerprintCalculator) -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt index 9a2f1f7b..7508ca77 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt @@ -5,9 +5,13 @@ package org.pgpainless.key import java.nio.charset.Charset +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey +import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPPrivateKey import org.bouncycastle.util.encoders.Hex /** Abstract super class of different version OpenPGP fingerprints. */ @@ -28,8 +32,8 @@ abstract class OpenPgpFingerprint : CharSequence, Comparable * fingerprint, but we don't care, since V3 is deprecated. * * @return key id - * @see RFC-4880 §12.2: Key IDs and - * Fingerprints + * @see + * [RFC-4880 §12.2: Key IDs and Fingerprints](https://tools.ietf.org/html/rfc4880#section-12.2) */ abstract val keyId: Long @@ -55,10 +59,12 @@ abstract class OpenPgpFingerprint : CharSequence, Comparable constructor(keys: PGPKeyRing) : this(keys.publicKey) + abstract val keyIdentifier: KeyIdentifier + /** * Check, whether the fingerprint consists of 40 valid hexadecimal characters. * - * @param fp fingerprint to check. + * @param fingerprint fingerprint to check. * @return true if fingerprint is valid. */ protected abstract fun isValid(fingerprint: String): Boolean @@ -121,11 +127,19 @@ abstract class OpenPgpFingerprint : CharSequence, Comparable * Return the fingerprint of the primary key of the given key ring. This method * automatically matches key versions to fingerprint implementations. * - * @param ring key ring + * @param keys key ring * @return fingerprint */ @JvmStatic fun of(keys: PGPKeyRing): OpenPgpFingerprint = of(keys.publicKey) + /** Return the [OpenPgpFingerprint] of the primary key of the given [OpenPGPCertificate]. */ + @JvmStatic fun of(cert: OpenPGPCertificate): OpenPgpFingerprint = of(cert.pgpPublicKeyRing) + + /** Return the [OpenPgpFingerprint] of the given [OpenPGPComponentKey]. */ + @JvmStatic fun of(key: OpenPGPComponentKey): OpenPgpFingerprint = of(key.pgpPublicKey) + + @JvmStatic fun of(key: OpenPGPPrivateKey): OpenPgpFingerprint = of(key.secretKey) + /** * Try to parse an [OpenPgpFingerprint] from the given fingerprint string. If the trimmed * fingerprint without whitespace is 64 characters long, it is either a v5 or v6 diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt index e02f0ae7..720d97f8 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt @@ -5,13 +5,13 @@ package org.pgpainless.key import java.net.URI -import java.nio.Buffer -import java.nio.ByteBuffer -import java.nio.charset.Charset +import org.bouncycastle.bcpg.FingerprintUtil +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSecretKey -import org.bouncycastle.util.encoders.Hex +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey class OpenPgpV4Fingerprint : OpenPgpFingerprint { @@ -19,6 +19,10 @@ class OpenPgpV4Fingerprint : OpenPgpFingerprint { constructor(bytes: ByteArray) : super(bytes) + constructor(key: OpenPGPCertificate) : super(key.fingerprint) + + constructor(key: OpenPGPComponentKey) : super(key.pgpPublicKey) + constructor(key: PGPPublicKey) : super(key) constructor(key: PGPSecretKey) : super(key) @@ -27,17 +31,9 @@ class OpenPgpV4Fingerprint : OpenPgpFingerprint { override fun getVersion() = 4 - override val keyId: Long - get() { - val bytes = Hex.decode(toString().toByteArray(Charset.forName("UTF-8"))) - val buf = ByteBuffer.wrap(bytes) + override val keyId: Long = FingerprintUtil.keyIdFromV4Fingerprint(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 - (buf as Buffer).position(12) // 20 - 8 bytes = offset 12 - return buf.getLong() - } + override val keyIdentifier: KeyIdentifier = KeyIdentifier(bytes) override fun isValid(fingerprint: String): Boolean { return fingerprint.matches("^[0-9A-F]{40}$".toRegex()) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV5Fingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV5Fingerprint.kt index 7bc36cc9..5864bcd9 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV5Fingerprint.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV5Fingerprint.kt @@ -4,6 +4,8 @@ package org.pgpainless.key +import org.bouncycastle.bcpg.FingerprintUtil +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSecretKey @@ -21,7 +23,11 @@ class OpenPgpV5Fingerprint : _64DigitFingerprint { constructor(bytes: ByteArray) : super(bytes) + override val keyId: Long = FingerprintUtil.keyIdFromLibrePgpFingerprint(bytes) + override fun getVersion(): Int { return 5 } + + override val keyIdentifier: KeyIdentifier = KeyIdentifier(bytes) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt index 2aec7976..5dfee653 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt @@ -4,50 +4,137 @@ package org.pgpainless.key -import openpgp.openPgpKeyId +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey +import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPPrivateKey /** - * Tuple class used to identify a subkey by fingerprints of the primary key of the subkeys key ring, - * as well as the subkeys fingerprint. + * Tuple class used to identify a subkey (component key) by fingerprints of the certificate, as well + * as the component keys fingerprint. */ class SubkeyIdentifier( - val primaryKeyFingerprint: OpenPgpFingerprint, - val subkeyFingerprint: OpenPgpFingerprint + /** Fingerprint of the certificate. */ + val certificateFingerprint: OpenPgpFingerprint, + /** Fingerprint of the target component key. */ + val componentKeyFingerprint: OpenPgpFingerprint ) { - constructor(fingerprint: OpenPgpFingerprint) : this(fingerprint, fingerprint) - - constructor(keys: PGPKeyRing) : this(keys.publicKey) - - constructor(key: PGPPublicKey) : this(OpenPgpFingerprint.of(key)) - + /** + * Constructor for a [SubkeyIdentifier] pointing to the primary key identified by the + * [certificateFingerprint]. + * + * @param certificateFingerprint primary key fingerprint + */ constructor( - keys: PGPKeyRing, - keyId: Long + certificateFingerprint: OpenPgpFingerprint + ) : this(certificateFingerprint, certificateFingerprint) + + /** + * Constructor for a [SubkeyIdentifier] pointing to the primary key of the given [PGPKeyRing]. + * + * @param certificate certificate + */ + constructor(certificate: PGPKeyRing) : this(certificate.publicKey) + + /** + * Constructor for a [SubkeyIdentifier] pointing to the given [primaryKey]. + * + * @param primaryKey primary key + */ + constructor(primaryKey: PGPPublicKey) : this(OpenPgpFingerprint.of(primaryKey)) + + /** + * Constructor for a [SubkeyIdentifier] pointing to a component key (identified by + * [componentKeyId]) from the given [certificate]. + */ + @Deprecated("Pass in a KeyIdentifier instead of a keyId.") + constructor( + certificate: PGPKeyRing, + componentKeyId: Long + ) : this(certificate, KeyIdentifier(componentKeyId)) + + /** + * Constructor for a [SubkeyIdentifier] pointing to the given [componentKey]. + * + * @param componentKey component key + */ + constructor( + componentKey: OpenPGPComponentKey + ) : this(OpenPgpFingerprint.of(componentKey.certificate), OpenPgpFingerprint.of(componentKey)) + + /** Constructor for a [SubkeyIdentifier] pointing to the given [componentKey]. */ + constructor(componentKey: OpenPGPPrivateKey) : this(componentKey.secretKey) + + /** + * Constructor for a [SubkeyIdentifier] pointing to a component key (identified by the + * [componentKeyFingerprint]) of the given [certificate]. + * + * @param certificate certificate + * @param componentKeyFingerprint fingerprint of the component key + */ + constructor( + certificate: PGPKeyRing, + componentKeyFingerprint: OpenPgpFingerprint + ) : this(OpenPgpFingerprint.of(certificate), componentKeyFingerprint) + + /** + * Constructor for a [SubkeyIdentifier] pointing to a component key (identified by the + * [componentKeyIdentifier]) of the given [certificate]. + * + * @param certificate certificate + * @param componentKeyIdentifier identifier of the component key + */ + constructor( + certificate: PGPKeyRing, + componentKeyIdentifier: KeyIdentifier ) : this( - OpenPgpFingerprint.of(keys.publicKey), + OpenPgpFingerprint.of(certificate), OpenPgpFingerprint.of( - keys.getPublicKey(keyId) + certificate.getPublicKey(componentKeyIdentifier) ?: throw NoSuchElementException( - "OpenPGP key does not contain subkey ${keyId.openPgpKeyId()}"))) + "OpenPGP key does not contain subkey $componentKeyIdentifier"))) - constructor( - keys: PGPKeyRing, - subkeyFingerprint: OpenPgpFingerprint - ) : this(OpenPgpFingerprint.of(keys), subkeyFingerprint) + @Deprecated( + "Use certificateFingerprint instead.", replaceWith = ReplaceWith("certificateFingerprint")) + val primaryKeyFingerprint: OpenPgpFingerprint = certificateFingerprint - val keyId = subkeyFingerprint.keyId - val fingerprint = subkeyFingerprint + @Deprecated( + "Use componentKeyFingerprint instead.", + replaceWith = ReplaceWith("componentKeyFingerprint")) + val subkeyFingerprint: OpenPgpFingerprint = componentKeyFingerprint - val subkeyId = subkeyFingerprint.keyId - val primaryKeyId = primaryKeyFingerprint.keyId + /** [KeyIdentifier] of the component key. */ + val keyIdentifier = componentKeyFingerprint.keyIdentifier - val isPrimaryKey = primaryKeyId == subkeyId + /** [KeyIdentifier] of the component key. */ + val componentKeyIdentifier = keyIdentifier + /** [KeyIdentifier] of the primary key of the certificate the component key belongs to. */ + val certificateIdentifier = certificateFingerprint.keyIdentifier + + /** Key-ID of the component key. */ + @Deprecated("Use of key-ids is discouraged.") val keyId = keyIdentifier.keyId + + /** Fingerprint of the component key. */ + val fingerprint = componentKeyFingerprint + + /** Key-ID of the component key. */ + @Deprecated("Use of key-ids is discouraged.") val subkeyId = componentKeyIdentifier.keyId + + /** Key-ID of the primary key of the certificate the component key belongs to. */ + @Deprecated("Use of key-ids is discouraged.") val primaryKeyId = certificateIdentifier.keyId + + /** True, if the component key is the primary key. */ + val isPrimaryKey = certificateIdentifier.matchesExplicit(componentKeyIdentifier) + + /** + * Return true, if the provided [fingerprint] matches either the [certificateFingerprint] or + * [componentKeyFingerprint]. + */ fun matches(fingerprint: OpenPgpFingerprint) = - primaryKeyFingerprint == fingerprint || subkeyFingerprint == fingerprint + certificateFingerprint == fingerprint || componentKeyFingerprint == fingerprint override fun equals(other: Any?): Boolean { if (other == null) { @@ -60,13 +147,13 @@ class SubkeyIdentifier( return false } - return primaryKeyFingerprint == other.primaryKeyFingerprint && - subkeyFingerprint == other.subkeyFingerprint + return certificateFingerprint == other.certificateFingerprint && + componentKeyFingerprint == other.componentKeyFingerprint } override fun hashCode(): Int { - return primaryKeyFingerprint.hashCode() + 31 * subkeyFingerprint.hashCode() + return certificateFingerprint.hashCode() + 31 * componentKeyFingerprint.hashCode() } - override fun toString(): String = "$subkeyFingerprint $primaryKeyFingerprint" + override fun toString(): String = "$componentKeyFingerprint $certificateFingerprint" } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt index a34dd880..465787ea 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt @@ -4,13 +4,11 @@ package org.pgpainless.key -import java.nio.Buffer -import java.nio.ByteBuffer -import java.nio.charset.Charset +import org.bouncycastle.bcpg.FingerprintUtil +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSecretKey -import org.bouncycastle.util.encoders.Hex /** * This class represents a hex encoded, upper case OpenPGP v5 or v6 fingerprint. Since both @@ -34,23 +32,14 @@ open class _64DigitFingerprint : OpenPgpFingerprint { constructor(keys: PGPKeyRing) : super(keys) - override val keyId: Long - get() { - val bytes = Hex.decode(fingerprint.toByteArray(Charset.forName("UTF-8"))) - val 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 - (buf as Buffer).position(0) - - return buf.getLong() - } + override val keyId: Long = FingerprintUtil.keyIdFromV6Fingerprint(bytes) override fun getVersion(): Int { return -1 // might be v5 or v6 } + override val keyIdentifier: KeyIdentifier = KeyIdentifier(bytes) + override fun isValid(fingerprint: String): Boolean { return fingerprint.matches(("^[0-9A-F]{64}$".toRegex())) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt index 9499355c..ef6d03b8 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt @@ -7,12 +7,15 @@ package org.pgpainless.key.certification import java.util.* import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPPublicKeyRing -import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.openpgp.PGPSecretKeyRing import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.api.OpenPGPSignature import org.pgpainless.PGPainless import org.pgpainless.algorithm.CertificationType import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.SignatureType import org.pgpainless.algorithm.Trustworthiness import org.pgpainless.exception.KeyException import org.pgpainless.exception.KeyException.ExpiredKeyException @@ -20,9 +23,11 @@ import org.pgpainless.exception.KeyException.MissingSecretKeyException import org.pgpainless.exception.KeyException.RevokedKeyException import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.signature.builder.RevocationSignatureBuilder import org.pgpainless.signature.builder.ThirdPartyCertificationSignatureBuilder import org.pgpainless.signature.builder.ThirdPartyDirectKeySignatureBuilder import org.pgpainless.signature.subpackets.CertificationSubpackets +import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets /** * API for creating certifications and delegations (Signatures) on keys. This API can be used to @@ -32,7 +37,7 @@ import org.pgpainless.signature.subpackets.CertificationSubpackets * 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. */ -class CertifyCertificate { +class CertifyCertificate(private val api: PGPainless) { /** * Create a certification over a User-Id. By default, this method will use @@ -42,22 +47,50 @@ class CertifyCertificate { * @param certificate certificate * @return API */ + @JvmOverloads + fun certifyUserId( + userId: CharSequence, + certificate: OpenPGPCertificate, + certificationType: CertificationType = CertificationType.GENERIC + ): CertificationOnUserId = CertificationOnUserId(userId, certificate, certificationType, api) + + /** + * Create a certification over a User-Id. By default, this method will use + * [CertificationType.GENERIC] to create the signature. + * + * @param userId user-id to certify + * @param certificate certificate + * @return API + */ + @Deprecated( + "Pass in an OpenPGPCertificate instead.", replaceWith = ReplaceWith("certifyUserId")) fun userIdOnCertificate(userId: String, certificate: PGPPublicKeyRing): CertificationOnUserId = userIdOnCertificate(userId, certificate, CertificationType.GENERIC) /** * Create a certification of the given [CertificationType] over a User-Id. * - * @param userid user-id to certify + * @param userId user-id to certify * @param certificate certificate * @param certificationType type of signature * @return API */ + @Deprecated("Pass in an OpenPGPCertificate instead of PGPPublicKeyRing.") fun userIdOnCertificate( userId: String, certificate: PGPPublicKeyRing, certificationType: CertificationType - ) = CertificationOnUserId(userId, certificate, certificationType) + ) = CertificationOnUserId(userId, certificate, certificationType, api) + + /** + * Create a certification revocation signature for the given [userId] on the given + * [certificate]. + * + * @param userId userid to revoke + * @param certificate certificate carrying the userid + */ + fun revokeCertifiedUserId(userId: CharSequence, certificate: OpenPGPCertificate) = + RevocationOnUserId(userId, certificate, api) /** * Create a delegation (direct key signature) over a certificate. This can be used to mark a @@ -67,6 +100,19 @@ class CertifyCertificate { * @param certificate certificate * @return API */ + @JvmOverloads + fun delegateTrust(certificate: OpenPGPCertificate, trustworthiness: Trustworthiness? = null) = + DelegationOnCertificate(certificate, trustworthiness, api) + + /** + * Create a delegation (direct key signature) over a certificate. This can be used to mark a + * certificate as a trusted introducer (see [certificate] method with [Trustworthiness] + * argument). + * + * @param certificate certificate + * @return API + */ + @Deprecated("Pass in an OpenPGPCertificate instead of PGPPublicKeyRing.") fun certificate(certificate: PGPPublicKeyRing): DelegationOnCertificate = certificate(certificate, null) @@ -79,15 +125,45 @@ class CertifyCertificate { * @param trustworthiness trustworthiness of the certificate * @return API */ + @Deprecated("Pass in an OpenPGPCertificate instead of PGPPublicKeyRing.") fun certificate(certificate: PGPPublicKeyRing, trustworthiness: Trustworthiness?) = - DelegationOnCertificate(certificate, trustworthiness) + DelegationOnCertificate(certificate, trustworthiness, api) + + /** + * Create a key revocation signature, revoking a delegation over the given [certificate]. + * + * @param certificate certificate to revoke the delegation to + */ + fun revokeDelegatedTrust(certificate: OpenPGPCertificate): RevocationOnCertificate = + RevocationOnCertificate(certificate, api) class CertificationOnUserId( - val userId: String, - val certificate: PGPPublicKeyRing, - val certificationType: CertificationType + private val userId: CharSequence, + private val certificate: OpenPGPCertificate, + private val certificationType: CertificationType, + private val api: PGPainless ) { + @Deprecated("Use primary constructor instead.") + constructor( + userId: String, + certificate: PGPPublicKeyRing, + certificationType: CertificationType, + api: PGPainless + ) : this(userId, api.toCertificate(certificate), certificationType, api) + + fun withKey( + key: OpenPGPKey, + protector: SecretKeyRingProtector + ): CertificationOnUserIdWithSubpackets { + val secretKey = getCertifyingSecretKey(key, api) + val sigBuilder = + ThirdPartyCertificationSignatureBuilder( + certificationType.asSignatureType(), secretKey, protector, api) + + return CertificationOnUserIdWithSubpackets(certificate, userId, sigBuilder, api) + } + /** * Create the certification using the given key. * @@ -96,26 +172,28 @@ class CertifyCertificate { * @return API * @throws PGPException in case of an OpenPGP related error */ + @Deprecated("Pass in an OpenPGPKey instead of a PGPSecretKeyRing.") fun withKey( certificationKey: PGPSecretKeyRing, protector: SecretKeyRingProtector - ): CertificationOnUserIdWithSubpackets { - - val secretKey = getCertifyingSecretKey(certificationKey) - val sigBuilder = - ThirdPartyCertificationSignatureBuilder( - certificationType.asSignatureType(), secretKey, protector) - - return CertificationOnUserIdWithSubpackets(certificate, userId, sigBuilder) - } + ): CertificationOnUserIdWithSubpackets = withKey(api.toKey(certificationKey), protector) } class CertificationOnUserIdWithSubpackets( - val certificate: PGPPublicKeyRing, - val userId: String, - val sigBuilder: ThirdPartyCertificationSignatureBuilder + private val certificate: OpenPGPCertificate, + private val userId: CharSequence, + private val sigBuilder: ThirdPartyCertificationSignatureBuilder, + private val api: PGPainless ) { + @Deprecated("Pass in an OpenPGPCertificate instead of a PGPPublicKeyRing.") + constructor( + certificate: PGPPublicKeyRing, + userId: String, + sigBuilder: ThirdPartyCertificationSignatureBuilder, + api: PGPainless + ) : this(api.toCertificate(certificate), userId, sigBuilder, api) + /** * Apply the given signature subpackets and build the certification. * @@ -139,16 +217,97 @@ class CertifyCertificate { fun build(): CertificationResult { val signature = sigBuilder.build(certificate, userId) val certifiedCertificate = - KeyRingUtils.injectCertification(certificate, userId, signature) + api.toCertificate( + KeyRingUtils.injectCertification( + certificate.pgpPublicKeyRing, userId, signature.signature)) + + return CertificationResult(certifiedCertificate, signature) + } + } + + class RevocationOnUserId( + private val userId: CharSequence, + private val certificate: OpenPGPCertificate, + private val api: PGPainless + ) { + + fun withKey( + key: OpenPGPKey, + protector: SecretKeyRingProtector + ): RevocationOnUserIdWithSubpackets { + val secretKey = getCertifyingSecretKey(key, api) + val sigBuilder = + RevocationSignatureBuilder( + SignatureType.CERTIFICATION_REVOCATION, secretKey, protector, api) + + return RevocationOnUserIdWithSubpackets(certificate, userId, sigBuilder, api) + } + } + + class RevocationOnUserIdWithSubpackets( + private val certificate: OpenPGPCertificate, + private val userId: CharSequence, + private val sigBuilder: RevocationSignatureBuilder, + private val api: PGPainless + ) { + + /** + * Apply the given signature subpackets and build the revocation signature. + * + * @param subpacketCallback callback to modify the revocation signatures subpackets + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun buildWithSubpackets( + subpacketCallback: RevocationSignatureSubpackets.Callback + ): CertificationResult { + sigBuilder.applyCallback(subpacketCallback) + return build() + } + + /** + * Build the revocation signature. + * + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun build(): CertificationResult { + val signature = sigBuilder.build(certificate.primaryKey, userId) + val certifiedCertificate = + api.toCertificate( + KeyRingUtils.injectCertification( + certificate.pgpPublicKeyRing, userId, signature.signature)) + return CertificationResult(certifiedCertificate, signature) } } class DelegationOnCertificate( - val certificate: PGPPublicKeyRing, - val trustworthiness: Trustworthiness? + private val certificate: OpenPGPCertificate, + private val trustworthiness: Trustworthiness?, + private val api: PGPainless ) { + @Deprecated("Pass in an OpenPGPCertificate instead of PGPPublicKeyRing.") + constructor( + certificate: PGPPublicKeyRing, + trustworthiness: Trustworthiness?, + api: PGPainless + ) : this(api.toCertificate(certificate), trustworthiness, api) + + fun withKey( + key: OpenPGPKey, + protector: SecretKeyRingProtector + ): DelegationOnCertificateWithSubpackets { + val secretKey = getCertifyingSecretKey(key, api) + val sigBuilder = ThirdPartyDirectKeySignatureBuilder(secretKey, protector, api) + if (trustworthiness != null) { + sigBuilder.hashedSubpackets.setTrust( + true, trustworthiness.depth, trustworthiness.amount) + } + return DelegationOnCertificateWithSubpackets(certificate, sigBuilder, api) + } + /** * Build the delegation using the given certification key. * @@ -157,25 +316,26 @@ class CertifyCertificate { * @return API * @throws PGPException in case of an OpenPGP related error */ + @Deprecated("Pass in an OpenPGPKey instead of PGPSecretKeyRing.") fun withKey( certificationKey: PGPSecretKeyRing, protector: SecretKeyRingProtector - ): DelegationOnCertificateWithSubpackets { - val secretKey = getCertifyingSecretKey(certificationKey) - val sigBuilder = ThirdPartyDirectKeySignatureBuilder(secretKey, protector) - if (trustworthiness != null) { - sigBuilder.hashedSubpackets.setTrust( - true, trustworthiness.depth, trustworthiness.amount) - } - return DelegationOnCertificateWithSubpackets(certificate, sigBuilder) - } + ): DelegationOnCertificateWithSubpackets = withKey(api.toKey(certificationKey), protector) } class DelegationOnCertificateWithSubpackets( - val certificate: PGPPublicKeyRing, - val sigBuilder: ThirdPartyDirectKeySignatureBuilder + private val certificate: OpenPGPCertificate, + private val sigBuilder: ThirdPartyDirectKeySignatureBuilder, + private val api: PGPainless ) { + @Deprecated("Pass in an OpenPGPCertificate instead of a PGPPublicKeyRing.") + constructor( + certificate: PGPPublicKeyRing, + sigBuilder: ThirdPartyDirectKeySignatureBuilder, + api: PGPainless + ) : this(api.toCertificate(certificate), sigBuilder, api) + /** * Apply the given signature subpackets and build the delegation signature. * @@ -197,14 +357,73 @@ class CertifyCertificate { * @throws PGPException in case of an OpenPGP related error */ fun build(): CertificationResult { - val delegatedKey = certificate.publicKey + val delegatedKey = certificate.primaryKey val delegation = sigBuilder.build(delegatedKey) val delegatedCertificate = - KeyRingUtils.injectCertification(certificate, delegatedKey, delegation) + api.toCertificate( + KeyRingUtils.injectCertification( + certificate.pgpPublicKeyRing, + delegatedKey.pgpPublicKey, + delegation.signature)) return CertificationResult(delegatedCertificate, delegation) } } + class RevocationOnCertificate( + private val certificate: OpenPGPCertificate, + private val api: PGPainless + ) { + + fun withKey( + key: OpenPGPKey, + protector: SecretKeyRingProtector + ): RevocationOnCertificateWithSubpackets { + val secretKey = getCertifyingSecretKey(key, api) + val sigBuilder = + RevocationSignatureBuilder(SignatureType.KEY_REVOCATION, secretKey, protector, api) + return RevocationOnCertificateWithSubpackets(certificate, sigBuilder, api) + } + } + + class RevocationOnCertificateWithSubpackets( + private val certificate: OpenPGPCertificate, + private val sigBuilder: RevocationSignatureBuilder, + private val api: PGPainless + ) { + + /** + * Apply the given signature subpackets and build the delegation revocation signature. + * + * @param subpacketsCallback callback to modify the revocations subpackets + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun buildWithSubpackets( + subpacketsCallback: RevocationSignatureSubpackets.Callback + ): CertificationResult { + sigBuilder.applyCallback(subpacketsCallback) + return build() + } + + /** + * Build the delegation revocation signature. + * + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun build(): CertificationResult { + val revokedKey = certificate.primaryKey + val revocation = sigBuilder.build(revokedKey) + val revokedCertificate = + api.toCertificate( + KeyRingUtils.injectCertification( + certificate.pgpPublicKeyRing, + revokedKey.pgpPublicKey, + revocation.signature)) + return CertificationResult(revokedCertificate, revocation) + } + } + /** * Result of a certification operation. * @@ -212,20 +431,26 @@ class CertifyCertificate { * @param certification the newly created signature */ data class CertificationResult( - val certifiedCertificate: PGPPublicKeyRing, - val certification: PGPSignature - ) + val certifiedCertificate: OpenPGPCertificate, + val certification: OpenPGPSignature + ) { + val publicKeyRing: PGPPublicKeyRing = certifiedCertificate.pgpPublicKeyRing + val pgpSignature: PGPSignature = certification.signature + } companion object { @JvmStatic - private fun getCertifyingSecretKey(certificationKey: PGPSecretKeyRing): PGPSecretKey { + private fun getCertifyingSecretKey( + certificationKey: OpenPGPKey, + api: PGPainless + ): OpenPGPKey.OpenPGPSecretKey { val now = Date() - val info = PGPainless.inspectKeyRing(certificationKey, now) + val info = api.inspect(certificationKey, now) val fingerprint = info.fingerprint val certificationPubKey = info.getPublicKey(fingerprint) requireNotNull(certificationPubKey) { "Primary key cannot be null." } - if (!info.isKeyValidlyBound(certificationPubKey.keyID)) { + if (!info.isKeyValidlyBound(certificationPubKey.keyIdentifier)) { throw RevokedKeyException(fingerprint) } @@ -238,8 +463,8 @@ class CertifyCertificate { throw ExpiredKeyException(fingerprint, expirationDate) } - return certificationKey.getSecretKey(certificationPubKey.keyID) - ?: throw MissingSecretKeyException(fingerprint, certificationPubKey.keyID) + return certificationKey.getSecretKey(certificationPubKey.keyIdentifier) + ?: throw MissingSecretKeyException(fingerprint, certificationPubKey.keyIdentifier) } } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt index f69d4a08..ce3a2ddd 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt @@ -6,7 +6,7 @@ package org.pgpainless.key.collection import java.io.InputStream import org.bouncycastle.openpgp.* -import org.pgpainless.implementation.ImplementationFactory +import org.bouncycastle.openpgp.api.OpenPGPImplementation import org.pgpainless.util.ArmorUtils /** @@ -55,8 +55,8 @@ class PGPKeyRingCollection( val certificates = mutableListOf() // Double getDecoderStream because of #96 val objectFactory = - ImplementationFactory.getInstance() - .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) + OpenPGPImplementation.getInstance() + .pgpObjectFactory(ArmorUtils.getDecoderStream(inputStream)) for (obj in objectFactory) { if (obj == null) { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt index 05adf7d9..b7038307 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt @@ -5,59 +5,62 @@ package org.pgpainless.key.generation import java.io.IOException -import java.security.KeyPairGenerator import java.util.* import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.api.OpenPGPImplementation +import org.bouncycastle.openpgp.api.OpenPGPKey import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder -import org.bouncycastle.openpgp.operator.PGPDigestCalculator import org.bouncycastle.util.Strings import org.pgpainless.PGPainless +import org.pgpainless.algorithm.AlgorithmSuite import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.OpenPGPKeyVersion import org.pgpainless.algorithm.SignatureType +import org.pgpainless.bouncycastle.extensions.checksumCalculator import org.pgpainless.bouncycastle.extensions.unlock -import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.policy.Policy -import org.pgpainless.provider.ProviderFactory import org.pgpainless.signature.subpackets.SelfSignatureSubpackets import org.pgpainless.signature.subpackets.SignatureSubpackets import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper import org.pgpainless.util.Passphrase -class KeyRingBuilder : KeyRingBuilderInterface { +class KeyRingBuilder(private val version: OpenPGPKeyVersion, private val api: PGPainless) : + KeyRingBuilderInterface { private var primaryKeySpec: KeySpec? = null private val subKeySpecs = mutableListOf() private val userIds = mutableMapOf() private var passphrase = Passphrase.emptyPassphrase() private var expirationDate: Date? = Date(System.currentTimeMillis() + (5 * MILLIS_IN_YEAR)) + private var algorithmSuite: AlgorithmSuite = api.algorithmPolicy.keyGenerationAlgorithmSuite + + override fun withPreferences(preferences: AlgorithmSuite): KeyRingBuilder = apply { + algorithmSuite = preferences + } override fun setPrimaryKey(keySpec: KeySpec): KeyRingBuilder = apply { - verifyKeySpecCompliesToPolicy(keySpec, PGPainless.getPolicy()) + verifyKeySpecCompliesToPolicy(keySpec, api.algorithmPolicy) verifyPrimaryKeyCanCertify(keySpec) this.primaryKeySpec = keySpec } override fun addSubkey(keySpec: KeySpec): KeyRingBuilder = apply { - verifyKeySpecCompliesToPolicy(keySpec, PGPainless.getPolicy()) + verifyKeySpecCompliesToPolicy(keySpec, api.algorithmPolicy) subKeySpecs.add(keySpec) } override fun addUserId(userId: CharSequence): KeyRingBuilder = apply { - userIds[userId.toString().trim()] = null + userIds[userId.toString()] = null } override fun addUserId(userId: ByteArray): KeyRingBuilder = addUserId(Strings.fromUTF8ByteArray(userId)) override fun setExpirationDate(expirationDate: Date?): KeyRingBuilder = apply { - if (expirationDate == null) { - this.expirationDate = null - return@apply - } this.expirationDate = - expirationDate.let { + expirationDate?.let { require(Date() < expirationDate) { "Expiration date must be in the future." } expirationDate } @@ -83,50 +86,61 @@ class KeyRingBuilder : KeyRingBuilderInterface { private fun keyIsCertificationCapable(keySpec: KeySpec) = keySpec.keyType.canCertify - override fun build(): PGPSecretKeyRing { - val keyFingerprintCalculator = ImplementationFactory.getInstance().v4FingerprintCalculator - val secretKeyEncryptor = buildSecretKeyEncryptor(keyFingerprintCalculator) + override fun build(): OpenPGPKey { + val checksumCalculator = api.implementation.checksumCalculator() + + // generate primary key + requireNotNull(primaryKeySpec) { "Primary Key spec required." } + val certKey = generateKeyPair(primaryKeySpec!!, version, api.implementation) + + val secretKeyEncryptor = buildSecretKeyEncryptor(certKey.publicKey) val secretKeyDecryptor = buildSecretKeyDecryptor() passphrase.clear() // Passphrase was used above, so we can get rid of it - // generate primary key - requireNotNull(primaryKeySpec) { "Primary Key spec required." } - val certKey = generateKeyPair(primaryKeySpec!!) val signer = buildContentSigner(certKey) - val signatureGenerator = PGPSignatureGenerator(signer) + val signatureGenerator = PGPSignatureGenerator(signer, certKey.publicKey) - val hashedSubPacketGenerator = primaryKeySpec!!.subpacketGenerator - hashedSubPacketGenerator.setIssuerFingerprintAndKeyId(certKey.publicKey) - expirationDate?.let { hashedSubPacketGenerator.setKeyExpirationTime(certKey.publicKey, it) } + val hashedSignatureSubpackets: SignatureSubpackets = + SignatureSubpackets.createHashedSubpackets(certKey.publicKey).apply { + setKeyFlags(primaryKeySpec!!.keyFlags) + (primaryKeySpec!!.preferredHashAlgorithmsOverride ?: algorithmSuite.hashAlgorithms) + ?.let { setPreferredHashAlgorithms(it) } + (primaryKeySpec!!.preferredCompressionAlgorithmsOverride + ?: algorithmSuite.compressionAlgorithms) + ?.let { setPreferredCompressionAlgorithms(it) } + (primaryKeySpec!!.preferredSymmetricAlgorithmsOverride + ?: algorithmSuite.symmetricKeyAlgorithms) + ?.let { setPreferredSymmetricKeyAlgorithms(it) } + (primaryKeySpec!!.preferredAEADAlgorithmsOverride ?: algorithmSuite.aeadAlgorithms) + ?.let { setPreferredAEADCiphersuites(it) } + (primaryKeySpec!!.featuresOverride ?: algorithmSuite.features)?.let { + setFeatures(*it.toTypedArray()) + } + } + + expirationDate?.let { + hashedSignatureSubpackets.setKeyExpirationTime(certKey.publicKey, it) + } if (userIds.isNotEmpty()) { - hashedSubPacketGenerator.setPrimaryUserId() + hashedSignatureSubpackets.setPrimaryUserId() } - val generator = PGPSignatureSubpacketGenerator() - SignatureSubpacketsHelper.applyTo(hashedSubPacketGenerator, generator) - val hashedSubPackets = generator.generate() + val hashedSubPackets = hashedSignatureSubpackets.subpacketsGenerator.generate() val ringGenerator = if (userIds.isEmpty()) { PGPKeyRingGenerator( + certKey, checksumCalculator, hashedSubPackets, null, signer, secretKeyEncryptor) + } else { + PGPKeyRingGenerator( + SignatureType.POSITIVE_CERTIFICATION.code, certKey, - keyFingerprintCalculator, + userIds.keys.first(), + checksumCalculator, hashedSubPackets, null, signer, secretKeyEncryptor) - } else { - userIds.keys.first().let { primaryUserId -> - PGPKeyRingGenerator( - SignatureType.POSITIVE_CERTIFICATION.code, - certKey, - primaryUserId, - keyFingerprintCalculator, - hashedSubPackets, - null, - signer, - secretKeyEncryptor) - } } addSubKeys(certKey, ringGenerator) @@ -148,7 +162,7 @@ class KeyRingBuilder : KeyRingBuilderInterface { val callback = additionalUserId.value val subpackets = if (callback == null) { - hashedSubPacketGenerator.also { it.setPrimaryUserId(null) } + hashedSignatureSubpackets.also { it.setPrimaryUserId(null) } } else { SignatureSubpackets.createHashedSubpackets(primaryPubKey).also { callback.modifyHashedSubpackets(it) @@ -165,33 +179,46 @@ class KeyRingBuilder : KeyRingBuilderInterface { // Reassemble secret key ring with modified primary key val primarySecretKey = - PGPSecretKey( - privateKey, primaryPubKey, keyFingerprintCalculator, true, secretKeyEncryptor) + PGPSecretKey(privateKey, primaryPubKey, checksumCalculator, true, secretKeyEncryptor) val secretKeyList = mutableListOf(primarySecretKey) while (secretKeys.hasNext()) { secretKeyList.add(secretKeys.next()) } - return PGPSecretKeyRing(secretKeyList) + val pgpSecretKeyRing = PGPSecretKeyRing(secretKeyList) + return OpenPGPKey(pgpSecretKeyRing, api.implementation) } private fun addSubKeys(primaryKey: PGPKeyPair, ringGenerator: PGPKeyRingGenerator) { for (subKeySpec in subKeySpecs) { - val subKey = generateKeyPair(subKeySpec) - if (subKeySpec.isInheritedSubPackets) { - ringGenerator.addSubKey(subKey) - } else { - var hashedSubpackets = subKeySpec.subpackets - try { - hashedSubpackets = - addPrimaryKeyBindingSignatureIfNecessary( - primaryKey, subKey, hashedSubpackets) - } catch (e: IOException) { - throw PGPException( - "Exception while adding primary key binding signature to signing subkey.", - e) + val subKey = generateKeyPair(subKeySpec, version, api.implementation) + val hashedSignatureSubpackets: SignatureSubpackets = + SignatureSubpackets.createHashedSubpackets(subKey.publicKey).apply { + setKeyFlags(subKeySpec.keyFlags) + subKeySpec.preferredHashAlgorithmsOverride?.let { + setPreferredHashAlgorithms(it) + } + subKeySpec.preferredCompressionAlgorithmsOverride?.let { + setPreferredCompressionAlgorithms(it) + } + subKeySpec.preferredSymmetricAlgorithmsOverride?.let { + setPreferredSymmetricKeyAlgorithms(it) + } + subKeySpec.preferredAEADAlgorithmsOverride?.let { + setPreferredAEADCiphersuites(it) + } + subKeySpec.featuresOverride?.let { setFeatures(*it.toTypedArray()) } } - ringGenerator.addSubKey(subKey, hashedSubpackets, null) + + var hashedSubpackets: PGPSignatureSubpacketVector = + hashedSignatureSubpackets.subpacketsGenerator.generate() + try { + hashedSubpackets = + addPrimaryKeyBindingSignatureIfNecessary(primaryKey, subKey, hashedSubpackets) + } catch (e: IOException) { + throw PGPException( + "Exception while adding primary key binding signature to signing subkey.", e) } + ringGenerator.addSubKey(subKey, hashedSubpackets, null) } } @@ -206,7 +233,8 @@ class KeyRingBuilder : KeyRingBuilderInterface { return hashedSubpackets } - val bindingSignatureGenerator = PGPSignatureGenerator(buildContentSigner(subKey)) + val bindingSignatureGenerator = + PGPSignatureGenerator(buildContentSigner(subKey), subKey.publicKey) bindingSignatureGenerator.init(SignatureType.PRIMARYKEY_BINDING.code, subKey.privateKey) val primaryKeyBindingSig = bindingSignatureGenerator.generateCertification(primaryKey.publicKey, subKey.publicKey) @@ -217,30 +245,34 @@ class KeyRingBuilder : KeyRingBuilderInterface { private fun buildContentSigner(certKey: PGPKeyPair): PGPContentSignerBuilder { val hashAlgorithm = - PGPainless.getPolicy().certificationSignatureHashAlgorithmPolicy.defaultHashAlgorithm - return ImplementationFactory.getInstance() - .getPGPContentSignerBuilder(certKey.publicKey.algorithm, hashAlgorithm.algorithmId) + api.algorithmPolicy.certificationSignatureHashAlgorithmPolicy.defaultHashAlgorithm + return api.implementation.pgpContentSignerBuilder( + certKey.publicKey.algorithm, hashAlgorithm.algorithmId) } private fun buildSecretKeyEncryptor( - keyFingerprintCalculator: PGPDigestCalculator + publicKey: PGPPublicKey, ): PBESecretKeyEncryptor? { - val keyEncryptionAlgorithm = - PGPainless.getPolicy() - .symmetricKeyEncryptionAlgorithmPolicy - .defaultSymmetricKeyAlgorithm check(passphrase.isValid) { "Passphrase was cleared." } + val protectionSettings = api.algorithmPolicy.keyProtectionSettings return if (passphrase.isEmpty) null else - ImplementationFactory.getInstance() - .getPBESecretKeyEncryptor( - keyEncryptionAlgorithm, keyFingerprintCalculator, passphrase) + api.implementation + .pbeSecretKeyEncryptorFactory( + protectionSettings.aead, + protectionSettings.encryptionAlgorithm.algorithmId, + protectionSettings.s2kCount) + .build(passphrase.getChars(), publicKey.publicKeyPacket) } private fun buildSecretKeyDecryptor(): PBESecretKeyDecryptor? { check(passphrase.isValid) { "Passphrase was cleared." } return if (passphrase.isEmpty) null - else ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase) + else + api.implementation + .pbeSecretKeyDecryptorBuilderProvider() + .provide() + .build(passphrase.getChars()) } companion object { @@ -250,19 +282,14 @@ class KeyRingBuilder : KeyRingBuilderInterface { @JvmOverloads fun generateKeyPair( spec: KeySpec, + version: OpenPGPKeyVersion, + implementation: OpenPGPImplementation = PGPainless.getInstance().implementation, creationTime: Date = spec.keyCreationDate ?: Date() ): PGPKeyPair { - spec.keyType.let { type -> - // Create raw Key Pair - val keyPair = - KeyPairGenerator.getInstance(type.name, ProviderFactory.provider) - .also { it.initialize(type.algorithmSpec) } - .generateKeyPair() + val gen = + implementation.pgpKeyPairGeneratorProvider().get(version.numeric, creationTime) - // Form PGP Key Pair - return ImplementationFactory.getInstance() - .getPGPKeyPair(type.algorithm, keyPair, creationTime) - } + return spec.keyType.generateKeyPair(gen) } } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt index ecc818b6..3bf95996 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt @@ -8,11 +8,14 @@ import java.security.InvalidAlgorithmParameterException import java.security.NoSuchAlgorithmException import java.util.* import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.pgpainless.algorithm.AlgorithmSuite import org.pgpainless.util.Passphrase interface KeyRingBuilderInterface> { + fun withPreferences(preferences: AlgorithmSuite): B + fun setPrimaryKey(keySpec: KeySpec): B fun setPrimaryKey(builder: KeySpecBuilder): B = setPrimaryKey(builder.build()) @@ -33,5 +36,5 @@ interface KeyRingBuilderInterface> { NoSuchAlgorithmException::class, PGPException::class, InvalidAlgorithmParameterException::class) - fun build(): PGPSecretKeyRing + fun build(): OpenPGPKey } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt index 82743661..122b8e31 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt @@ -4,9 +4,11 @@ package org.pgpainless.key.generation -import org.bouncycastle.openpgp.PGPSecretKeyRing -import org.pgpainless.PGPainless.Companion.buildKeyRing +import java.util.* +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.pgpainless.PGPainless import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.OpenPGPKeyVersion import org.pgpainless.key.generation.KeySpec.Companion.getBuilder import org.pgpainless.key.generation.type.KeyType import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve @@ -14,7 +16,11 @@ import org.pgpainless.key.generation.type.rsa.RsaLength import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec import org.pgpainless.util.Passphrase -class KeyRingTemplates { +class KeyRingTemplates( + private val version: OpenPGPKeyVersion, + private val creationTime: Date = Date(), + private val api: PGPainless = PGPainless.getInstance() +) { /** * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, a @@ -23,20 +29,25 @@ class KeyRingTemplates { * @param userId userId or null * @param length length of the RSA keys * @param passphrase passphrase to encrypt the key with. Can be empty for an unencrytped key. - * @return key + * @return [OpenPGPKey] */ @JvmOverloads fun rsaKeyRing( userId: CharSequence?, length: RsaLength, passphrase: Passphrase = Passphrase.emptyPassphrase() - ): PGPSecretKeyRing = - buildKeyRing() + ): OpenPGPKey = + api.buildKey(version) .apply { - setPrimaryKey(getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER)) - addSubkey(getBuilder(KeyType.RSA(length), KeyFlag.SIGN_DATA)) + setPrimaryKey( + getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER) + .setKeyCreationDate(creationTime)) addSubkey( - getBuilder(KeyType.RSA(length), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + getBuilder(KeyType.RSA(length), KeyFlag.SIGN_DATA) + .setKeyCreationDate(creationTime)) + addSubkey( + getBuilder(KeyType.RSA(length), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE) + .setKeyCreationDate(creationTime)) setPassphrase(passphrase) if (userId != null) { addUserId(userId) @@ -52,9 +63,9 @@ class KeyRingTemplates { * @param length length of the RSA keys * @param password passphrase to encrypt the key with. Can be null or blank for unencrypted * keys. - * @return key + * @return [OpenPGPKey] */ - fun rsaKeyRing(userId: CharSequence?, length: RsaLength, password: String?): PGPSecretKeyRing = + fun rsaKeyRing(userId: CharSequence?, length: RsaLength, password: String?): OpenPGPKey = password.let { if (it.isNullOrBlank()) { rsaKeyRing(userId, length, Passphrase.emptyPassphrase()) @@ -69,23 +80,24 @@ class KeyRingTemplates { * * @param userId user id. * @param length length in bits. - * @param password Password of the key. Can be empty for unencrypted keys. - * @return [PGPSecretKeyRing] containing the KeyPair. + * @param passphrase Password of the key. Can be empty for unencrypted keys. + * @return [OpenPGPKey] */ @JvmOverloads fun simpleRsaKeyRing( userId: CharSequence?, length: RsaLength, passphrase: Passphrase = Passphrase.emptyPassphrase() - ): PGPSecretKeyRing = - buildKeyRing() + ): OpenPGPKey = + api.buildKey(version) .apply { setPrimaryKey( getBuilder( - KeyType.RSA(length), - KeyFlag.CERTIFY_OTHER, - KeyFlag.SIGN_DATA, - KeyFlag.ENCRYPT_COMMS)) + KeyType.RSA(length), + KeyFlag.CERTIFY_OTHER, + KeyFlag.SIGN_DATA, + KeyFlag.ENCRYPT_COMMS) + .setKeyCreationDate(creationTime)) setPassphrase(passphrase) if (userId != null) { addUserId(userId.toString()) @@ -100,7 +112,7 @@ class KeyRingTemplates { * @param userId user id. * @param length length in bits. * @param password Password of the key. Can be null or blank for unencrypted keys. - * @return [PGPSecretKeyRing] containing the KeyPair. + * @return [OpenPGPKey] */ fun simpleRsaKeyRing(userId: CharSequence?, length: RsaLength, password: String?) = password.let { @@ -118,31 +130,34 @@ class KeyRingTemplates { * * @param userId user-id * @param passphrase Password of the private key. Can be empty for an unencrypted key. - * @return [PGPSecretKeyRing] containing the key pairs. + * @return [OpenPGPKey] */ @JvmOverloads fun simpleEcKeyRing( userId: CharSequence?, passphrase: Passphrase = Passphrase.emptyPassphrase() - ): PGPSecretKeyRing = - buildKeyRing() + ): OpenPGPKey { + val signingKeyType = + if (version == OpenPGPKeyVersion.v6) KeyType.Ed25519() + else KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519) + val encryptionKeyType = + if (version == OpenPGPKeyVersion.v6) KeyType.X25519() + else KeyType.XDH_LEGACY(XDHLegacySpec._X25519) + return api.buildKey(version) .apply { setPrimaryKey( - getBuilder( - KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), - KeyFlag.CERTIFY_OTHER, - KeyFlag.SIGN_DATA)) + getBuilder(signingKeyType, KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) + .setKeyCreationDate(creationTime)) addSubkey( - getBuilder( - KeyType.XDH_LEGACY(XDHLegacySpec._X25519), - KeyFlag.ENCRYPT_STORAGE, - KeyFlag.ENCRYPT_COMMS)) + getBuilder(encryptionKeyType, KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS) + .setKeyCreationDate(creationTime)) setPassphrase(passphrase) if (userId != null) { addUserId(userId.toString()) } } .build() + } /** * Creates a key ring consisting of an ed25519 EdDSA primary key and a X25519 XDH subkey. The @@ -150,10 +165,10 @@ class KeyRingTemplates { * used for encryption and decryption of messages. * * @param userId user-id - * @param passphrase Password of the private key. Can be null or blank for an unencrypted key. - * @return [PGPSecretKeyRing] containing the key pairs. + * @param password Password of the private key. Can be null or blank for an unencrypted key. + * @return [OpenPGPKey] */ - fun simpleEcKeyRing(userId: CharSequence?, password: String?): PGPSecretKeyRing = + fun simpleEcKeyRing(userId: CharSequence?, password: String?): OpenPGPKey = password.let { if (it.isNullOrBlank()) { simpleEcKeyRing(userId, Passphrase.emptyPassphrase()) @@ -168,31 +183,36 @@ class KeyRingTemplates { * * @param userId primary user id * @param passphrase passphrase for the private key. Can be empty for an unencrypted key. - * @return key ring + * @return [OpenPGPKey] */ @JvmOverloads fun modernKeyRing( userId: CharSequence?, passphrase: Passphrase = Passphrase.emptyPassphrase() - ): PGPSecretKeyRing = - buildKeyRing() + ): OpenPGPKey { + val signingKeyType = + if (version == OpenPGPKeyVersion.v6) KeyType.Ed25519() + else KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519) + val encryptionKeyType = + if (version == OpenPGPKeyVersion.v6) KeyType.X25519() + else KeyType.XDH_LEGACY(XDHLegacySpec._X25519) + return api.buildKey(version) .apply { setPrimaryKey( - getBuilder( - KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + getBuilder(signingKeyType, KeyFlag.CERTIFY_OTHER) + .setKeyCreationDate(creationTime)) addSubkey( - getBuilder( - KeyType.XDH_LEGACY(XDHLegacySpec._X25519), - KeyFlag.ENCRYPT_COMMS, - KeyFlag.ENCRYPT_STORAGE)) + getBuilder(encryptionKeyType, KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE) + .setKeyCreationDate(creationTime)) addSubkey( - getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) + getBuilder(signingKeyType, KeyFlag.SIGN_DATA).setKeyCreationDate(creationTime)) setPassphrase(passphrase) if (userId != null) { addUserId(userId) } } .build() + } /** * Generate a modern PGP key ring consisting of an ed25519 EdDSA primary key which is used to @@ -200,9 +220,9 @@ class KeyRingTemplates { * * @param userId primary user id * @param password passphrase for the private key. Can be null or blank for an unencrypted key. - * @return key ring + * @return [OpenPGPKey] */ - fun modernKeyRing(userId: CharSequence?, password: String?): PGPSecretKeyRing = + fun modernKeyRing(userId: CharSequence?, password: String?): OpenPGPKey = password.let { if (it.isNullOrBlank()) { modernKeyRing(userId, Passphrase.emptyPassphrase()) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt index f616a7f2..66f5f9e0 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt @@ -5,22 +5,25 @@ package org.pgpainless.key.generation import java.util.* -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector +import org.pgpainless.algorithm.AEADCipherMode +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.Feature +import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.key.generation.type.KeyType -import org.pgpainless.signature.subpackets.SignatureSubpackets -import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper data class KeySpec( val keyType: KeyType, - val subpacketGenerator: SignatureSubpackets, - val isInheritedSubPackets: Boolean, + val keyFlags: List, + val preferredCompressionAlgorithmsOverride: Set?, + val preferredHashAlgorithmsOverride: Set?, + val preferredSymmetricAlgorithmsOverride: Set?, + val preferredAEADAlgorithmsOverride: Set?, + val featuresOverride: Set?, val keyCreationDate: Date? ) { - val subpackets: PGPSignatureSubpacketVector - get() = SignatureSubpacketsHelper.toVector(subpacketGenerator) - companion object { @JvmStatic fun getBuilder(type: KeyType, vararg flags: KeyFlag) = KeySpecBuilder(type, *flags) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt index 0e7f9aae..59d9efe7 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt @@ -5,26 +5,20 @@ package org.pgpainless.key.generation import java.util.* -import org.pgpainless.PGPainless import org.pgpainless.algorithm.* import org.pgpainless.key.generation.type.KeyType -import org.pgpainless.signature.subpackets.SelfSignatureSubpackets -import org.pgpainless.signature.subpackets.SignatureSubpackets import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil -class KeySpecBuilder -constructor( +class KeySpecBuilder( private val type: KeyType, private val keyFlags: List, ) : KeySpecBuilderInterface { - private val hashedSubpackets: SelfSignatureSubpackets = SignatureSubpackets() - private val algorithmSuite: AlgorithmSuite = PGPainless.getPolicy().keyGenerationAlgorithmSuite - private var preferredCompressionAlgorithms: Set = - algorithmSuite.compressionAlgorithms - private var preferredHashAlgorithms: Set = algorithmSuite.hashAlgorithms - private var preferredSymmetricAlgorithms: Set = - algorithmSuite.symmetricKeyAlgorithms + private var preferredCompressionAlgorithms: Set? = null + private var preferredHashAlgorithms: Set? = null + private var preferredSymmetricAlgorithms: Set? = null + private var preferredAEADAlgorithms: Set? = null + private var features: Set? = null private var keyCreationDate: Date? = null constructor(type: KeyType, vararg keyFlags: KeyFlag) : this(type, listOf(*keyFlags)) @@ -35,11 +29,13 @@ constructor( override fun overridePreferredCompressionAlgorithms( vararg algorithms: CompressionAlgorithm - ): KeySpecBuilder = apply { this.preferredCompressionAlgorithms = algorithms.toSet() } + ): KeySpecBuilder = apply { + this.preferredCompressionAlgorithms = if (algorithms.isEmpty()) null else algorithms.toSet() + } override fun overridePreferredHashAlgorithms(vararg algorithms: HashAlgorithm): KeySpecBuilder = apply { - this.preferredHashAlgorithms = algorithms.toSet() + this.preferredHashAlgorithms = if (algorithms.isEmpty()) null else algorithms.toSet() } override fun overridePreferredSymmetricKeyAlgorithms( @@ -48,7 +44,17 @@ constructor( require(!algorithms.contains(SymmetricKeyAlgorithm.NULL)) { "NULL (unencrypted) is an invalid symmetric key algorithm preference." } - this.preferredSymmetricAlgorithms = algorithms.toSet() + this.preferredSymmetricAlgorithms = if (algorithms.isEmpty()) null else algorithms.toSet() + } + + override fun overridePreferredAEADAlgorithms( + vararg algorithms: AEADCipherMode + ): KeySpecBuilder = apply { + this.preferredAEADAlgorithms = if (algorithms.isEmpty()) null else algorithms.toSet() + } + + override fun overrideFeatures(vararg features: Feature): KeySpecBuilder = apply { + this.features = if (features.isEmpty()) null else features.toSet() } override fun setKeyCreationDate(creationDate: Date): KeySpecBuilder = apply { @@ -56,14 +62,14 @@ constructor( } override fun build(): KeySpec { - return hashedSubpackets - .apply { - setKeyFlags(keyFlags) - setPreferredCompressionAlgorithms(preferredCompressionAlgorithms) - setPreferredHashAlgorithms(preferredHashAlgorithms) - setPreferredSymmetricKeyAlgorithms(preferredSymmetricAlgorithms) - setFeatures(Feature.MODIFICATION_DETECTION) - } - .let { KeySpec(type, hashedSubpackets as SignatureSubpackets, false, keyCreationDate) } + return KeySpec( + type, + keyFlags, + preferredCompressionAlgorithms, + preferredHashAlgorithms, + preferredSymmetricAlgorithms, + preferredAEADAlgorithms, + features, + keyCreationDate) } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt index 7fb767e4..956a70b0 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt @@ -5,7 +5,9 @@ package org.pgpainless.key.generation import java.util.* +import org.pgpainless.algorithm.AEADCipherMode import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.Feature import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.SymmetricKeyAlgorithm @@ -21,6 +23,10 @@ interface KeySpecBuilderInterface { vararg algorithms: SymmetricKeyAlgorithm ): KeySpecBuilder + fun overridePreferredAEADAlgorithms(vararg algorithms: AEADCipherMode): KeySpecBuilder + + fun overrideFeatures(vararg features: Feature): KeySpecBuilder + fun setKeyCreationDate(creationDate: Date): KeySpecBuilder fun build(): KeySpec diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt index c7691f46..2fa6d0c4 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt @@ -4,9 +4,14 @@ package org.pgpainless.key.generation.type -import java.security.spec.AlgorithmParameterSpec +import org.bouncycastle.openpgp.PGPKeyPair +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.ecc.Ed25519 +import org.pgpainless.key.generation.type.ecc.Ed448 import org.pgpainless.key.generation.type.ecc.EllipticCurve +import org.pgpainless.key.generation.type.ecc.X25519 +import org.pgpainless.key.generation.type.ecc.X448 import org.pgpainless.key.generation.type.ecc.ecdh.ECDH import org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacy @@ -40,13 +45,6 @@ interface KeyType { */ val bitStrength: Int - /** - * Return an implementation of [AlgorithmParameterSpec] that can be used to generate the key. - * - * @return algorithm parameter spec - */ - val algorithmSpec: AlgorithmParameterSpec - /** * Return true if the key that is generated from this type is able to carry the SIGN_DATA key * flag. See [org.pgpainless.algorithm.KeyFlag.SIGN_DATA]. @@ -92,6 +90,8 @@ interface KeyType { val canEncryptStorage: Boolean @JvmName("canEncryptStorage") get() = algorithm.encryptionCapable + fun generateKeyPair(generator: PGPKeyPairGenerator): PGPKeyPair + companion object { @JvmStatic fun RSA(length: RsaLength): RSA = RSA.withLength(length) @@ -103,5 +103,13 @@ interface KeyType { fun EDDSA_LEGACY(curve: EdDSALegacyCurve): EdDSALegacy = EdDSALegacy.fromCurve(curve) @JvmStatic fun XDH_LEGACY(curve: XDHLegacySpec): XDHLegacy = XDHLegacy.fromSpec(curve) + + @JvmStatic fun X25519(): X25519 = org.pgpainless.key.generation.type.ecc.X25519() + + @JvmStatic fun X448(): X448 = org.pgpainless.key.generation.type.ecc.X448() + + @JvmStatic fun Ed25519(): Ed25519 = org.pgpainless.key.generation.type.ecc.Ed25519() + + @JvmStatic fun Ed448(): Ed448 = org.pgpainless.key.generation.type.ecc.Ed448() } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/Ed25519.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/Ed25519.kt new file mode 100644 index 00000000..e20d0dd7 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/Ed25519.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.ecc + +import org.bouncycastle.openpgp.PGPKeyPair +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType + +class Ed25519 : KeyType { + override val name: String = "Ed25519" + override val algorithm: PublicKeyAlgorithm = PublicKeyAlgorithm.ED25519 + override val bitStrength: Int = 256 + + override fun generateKeyPair(generator: PGPKeyPairGenerator): PGPKeyPair { + return generator.generateEd25519KeyPair() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/Ed448.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/Ed448.kt new file mode 100644 index 00000000..67392917 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/Ed448.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.ecc + +import org.bouncycastle.openpgp.PGPKeyPair +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType + +class Ed448 : KeyType { + override val name: String = "Ed448" + override val algorithm: PublicKeyAlgorithm = PublicKeyAlgorithm.ED448 + override val bitStrength: Int = 456 + + override fun generateKeyPair(generator: PGPKeyPairGenerator): PGPKeyPair { + return generator.generateEd448KeyPair() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/X25519.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/X25519.kt new file mode 100644 index 00000000..22e5da8c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/X25519.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.ecc + +import org.bouncycastle.openpgp.PGPKeyPair +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType + +class X25519 : KeyType { + override val name: String = "X25519" + override val algorithm: PublicKeyAlgorithm = PublicKeyAlgorithm.X25519 + override val bitStrength: Int = 256 + + override fun generateKeyPair(generator: PGPKeyPairGenerator): PGPKeyPair { + return generator.generateX25519KeyPair() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/X448.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/X448.kt new file mode 100644 index 00000000..3dc56f4e --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/X448.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.ecc + +import org.bouncycastle.openpgp.PGPKeyPair +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType + +class X448 : KeyType { + override val name: String = "X448" + override val algorithm: PublicKeyAlgorithm = PublicKeyAlgorithm.X448 + override val bitStrength: Int = 448 + + override fun generateKeyPair(generator: PGPKeyPairGenerator): PGPKeyPair { + return generator.generateX448KeyPair() + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt index 04e196e0..650604dc 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt @@ -4,7 +4,9 @@ package org.pgpainless.key.generation.type.ecc.ecdh -import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec +import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil +import org.bouncycastle.openpgp.PGPKeyPair +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.key.generation.type.KeyType import org.pgpainless.key.generation.type.ecc.EllipticCurve @@ -13,7 +15,10 @@ class ECDH private constructor(val curve: EllipticCurve) : KeyType { override val name = "ECDH" override val algorithm = PublicKeyAlgorithm.ECDH override val bitStrength = curve.bitStrength - override val algorithmSpec = ECNamedCurveGenParameterSpec(curve.curveName) + + override fun generateKeyPair(generator: PGPKeyPairGenerator): PGPKeyPair { + return ECUtil.getNamedCurveOid(curve.curveName).let { generator.generateECDHKeyPair(it) } + } companion object { @JvmStatic fun fromCurve(curve: EllipticCurve) = ECDH(curve) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt index 1784b49d..49e917cd 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt @@ -4,7 +4,9 @@ package org.pgpainless.key.generation.type.ecc.ecdsa -import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec +import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil +import org.bouncycastle.openpgp.PGPKeyPair +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.key.generation.type.KeyType import org.pgpainless.key.generation.type.ecc.EllipticCurve @@ -13,7 +15,10 @@ class ECDSA private constructor(val curve: EllipticCurve) : KeyType { override val name = "ECDSA" override val algorithm = PublicKeyAlgorithm.ECDSA override val bitStrength = curve.bitStrength - override val algorithmSpec = ECNamedCurveGenParameterSpec(curve.curveName) + + override fun generateKeyPair(generator: PGPKeyPairGenerator): PGPKeyPair { + return ECUtil.getNamedCurveOid(curve.curveName).let { generator.generateECDSAKeyPair(it) } + } companion object { @JvmStatic fun fromCurve(curve: EllipticCurve) = ECDSA(curve) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacy.kt index e177de68..8ed48619 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacy.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacy.kt @@ -4,7 +4,8 @@ package org.pgpainless.key.generation.type.eddsa_legacy -import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec +import org.bouncycastle.openpgp.PGPKeyPair +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.key.generation.type.KeyType @@ -12,7 +13,10 @@ class EdDSALegacy private constructor(val curve: EdDSALegacyCurve) : KeyType { override val name = "EdDSA" override val algorithm = PublicKeyAlgorithm.EDDSA_LEGACY override val bitStrength = curve.bitStrength - override val algorithmSpec = ECNamedCurveGenParameterSpec(curve.curveName) + + override fun generateKeyPair(generator: PGPKeyPairGenerator): PGPKeyPair { + return generator.generateLegacyEd25519KeyPair() + } companion object { @JvmStatic fun fromCurve(curve: EdDSALegacyCurve) = EdDSALegacy(curve) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt deleted file mode 100644 index d925fc3d..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.elgamal - -import org.bouncycastle.jce.spec.ElGamalParameterSpec -import org.pgpainless.algorithm.PublicKeyAlgorithm -import org.pgpainless.key.generation.type.KeyType - -/** - * ElGamal encryption only key type. - * - * @deprecated the use of ElGamal is not recommended anymore. - */ -@Deprecated("The use of ElGamal is not recommended anymore.") -class ElGamal private constructor(length: ElGamalLength) : KeyType { - - override val name = "ElGamal" - override val algorithm = PublicKeyAlgorithm.ELGAMAL_ENCRYPT - override val bitStrength = length.length - override val algorithmSpec = ElGamalParameterSpec(length.p, length.g) - - companion object { - @JvmStatic fun withLength(length: ElGamalLength) = ElGamal(length) - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt deleted file mode 100644 index 2d29b88d..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.elgamal - -import java.math.BigInteger -import org.pgpainless.key.generation.type.KeyLength - -/** - * The following primes are taken from RFC-3526. - * - * @see RFC-3526: More Modular Exponential (MODP) - * Diffie-Hellman groups for Internet Key Exchange (IKE) - * @deprecated the use of ElGamal keys is no longer recommended. - */ -@Deprecated("The use of ElGamal keys is no longer recommended.") -enum class ElGamalLength(override val length: Int, p: String, g: String) : KeyLength { - - /** prime: 2^1536 - 2^1472 - 1 + 2^64 * { [2^1406 pi] + 741804 }. generator: 2 */ - _1536( - 1536, - "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF", - "2"), - - /** prime: 2^2048 - 2^1984 - 1 + 2^64 * { [2^1918 pi] + 124476 }. generator: 2 */ - _2048( - 2048, - "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF", - "2"), - - /** prime: 2^3072 - 2^3008 - 1 + 2^64 * { [2^2942 pi] + 1690314 }. generator: 2 */ - _3072( - 3072, - "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF", - "2"), - - /** prime: 2^4096 - 2^4032 - 1 + 2^64 * { [2^3966 pi] + 240904 }. generator: 2 */ - _4096( - 4096, - "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF", - "2"), - - /** prime: 2^6144 - 2^6080 - 1 + 2^64 * { [2^6014 pi] + 929484 }. generator: 2 */ - _6144( - 6144, - "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DCC4024FFFFFFFFFFFFFFFF", - "2"), - - /** prime: 2^8192 - 2^8128 - 1 + 2^64 * { [2^8062 pi] + 4743158 }. generator: 2 */ - _8192( - 8192, - "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD922222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC50846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E7160C980DD98EDD3DFFFFFFFFFFFFFFFFF", - "2"); - - val p: BigInteger - val g: BigInteger - - init { - this.p = BigInteger(p, 16) - this.g = BigInteger(g, 16) - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt index 39ddbbbb..c73d6293 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt @@ -4,7 +4,8 @@ package org.pgpainless.key.generation.type.rsa -import java.security.spec.RSAKeyGenParameterSpec +import org.bouncycastle.openpgp.PGPKeyPair +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.key.generation.type.KeyType @@ -14,7 +15,10 @@ class RSA private constructor(length: RsaLength) : KeyType { override val name = "RSA" override val algorithm = PublicKeyAlgorithm.RSA_GENERAL override val bitStrength = length.length - override val algorithmSpec = RSAKeyGenParameterSpec(length.length, RSAKeyGenParameterSpec.F4) + + override fun generateKeyPair(generator: PGPKeyPairGenerator): PGPKeyPair { + return generator.generateRsaKeyPair(bitStrength) + } companion object { @JvmStatic fun withLength(length: RsaLength) = RSA(length) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacy.kt index 4f0408bc..288603fa 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacy.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacy.kt @@ -4,7 +4,8 @@ package org.pgpainless.key.generation.type.xdh_legacy -import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec +import org.bouncycastle.openpgp.PGPKeyPair +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.key.generation.type.KeyType @@ -12,7 +13,10 @@ class XDHLegacy private constructor(spec: XDHLegacySpec) : KeyType { override val name = "XDH" override val algorithm = PublicKeyAlgorithm.ECDH override val bitStrength = spec.bitStrength - override val algorithmSpec = ECNamedCurveGenParameterSpec(spec.algorithmName) + + override fun generateKeyPair(generator: PGPKeyPairGenerator): PGPKeyPair { + return generator.generateLegacyX25519KeyPair() + } companion object { @JvmStatic fun fromSpec(spec: XDHLegacySpec) = XDHLegacy(spec) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt index 935c4f48..3e1bc8c2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt @@ -4,86 +4,71 @@ package org.pgpainless.key.info -import org.bouncycastle.openpgp.PGPSignature +import java.util.* +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPCertificateComponent +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPIdentityComponent +import org.pgpainless.algorithm.AEADCipherMode import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.Feature import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.SymmetricKeyAlgorithm -import org.pgpainless.key.SubkeyIdentifier -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil +import org.pgpainless.bouncycastle.extensions.toCompressionAlgorithms +import org.pgpainless.bouncycastle.extensions.toHashAlgorithms +import org.pgpainless.bouncycastle.extensions.toSymmetricKeyAlgorithms -abstract class KeyAccessor(protected val info: KeyRingInfo, protected val key: SubkeyIdentifier) { +abstract class KeyAccessor( + protected val key: OpenPGPComponentKey, + private val referenceTime: Date +) { - /** - * Depending on the way we address the key (key-id or user-id), return the respective - * [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. - * - * @return signature - */ - abstract val signatureWithPreferences: PGPSignature + abstract val component: OpenPGPCertificateComponent - /** Preferred symmetric key encryption algorithms. */ val preferredSymmetricKeyAlgorithms: Set get() = - SignatureSubpacketsUtil.parsePreferredSymmetricKeyAlgorithms(signatureWithPreferences) + component.getSymmetricCipherPreferences(referenceTime)?.toSymmetricKeyAlgorithms() + ?: setOf() - /** Preferred hash algorithms. */ val preferredHashAlgorithms: Set - get() = SignatureSubpacketsUtil.parsePreferredHashAlgorithms(signatureWithPreferences) + get() = component.getHashAlgorithmPreferences(referenceTime)?.toHashAlgorithms() ?: setOf() - /** Preferred compression algorithms. */ val preferredCompressionAlgorithms: Set get() = - SignatureSubpacketsUtil.parsePreferredCompressionAlgorithms(signatureWithPreferences) + component.getCompressionAlgorithmPreferences(referenceTime)?.toCompressionAlgorithms() + ?: setOf() + + val preferredAEADCipherSuites: Set + get() = + component + .getAEADCipherSuitePreferences(referenceTime) + ?.rawAlgorithms + ?.map { AEADCipherMode(it) } + ?.toSet() + ?: setOf() + + val features: Set + get() = + Feature.fromBitmask(component.getFeatures(referenceTime)?.features?.toInt() ?: 0) + .toSet() /** * Address the key via a user-id (e.g. `Alice `). In this case we are * sourcing preferred algorithms from the user-id certification first. */ - class ViaUserId(info: KeyRingInfo, key: SubkeyIdentifier, private val userId: CharSequence) : - KeyAccessor(info, key) { - override val signatureWithPreferences: PGPSignature - get() = - checkNotNull(info.getLatestUserIdCertification(userId.toString())) { - "No valid user-id certification signature found for '$userId'." - } + class ViaUserId( + key: OpenPGPComponentKey, + userId: OpenPGPIdentityComponent, + referenceTime: Date = Date() + ) : KeyAccessor(key, referenceTime) { + override val component: OpenPGPCertificateComponent = userId } /** * Address the key via key-id. In this case we are sourcing preferred algorithms from the keys * direct-key signature first. */ - class ViaKeyId(info: KeyRingInfo, key: SubkeyIdentifier) : KeyAccessor(info, key) { - override val signatureWithPreferences: PGPSignature - get() { - // If the key is located by Key ID, the algorithm of the primary User ID of the key - // provides the - // preferred symmetric algorithm. - info.primaryUserId?.let { userId -> - info.getLatestUserIdCertification(userId).let { if (it != null) return it } - } - - return info.getCurrentSubkeyBindingSignature(key.subkeyId) - ?: throw NoSuchElementException( - "Key does not carry acceptable self-signature signature.") - } - } - - class SubKey(info: KeyRingInfo, key: SubkeyIdentifier) : KeyAccessor(info, key) { - override val signatureWithPreferences: PGPSignature - get() = - checkNotNull( - if (key.isPrimaryKey) { - info.latestDirectKeySelfSignature - ?: info.primaryUserId?.let { info.getLatestUserIdCertification(it) } - } else { - info.getCurrentSubkeyBindingSignature(key.subkeyId) - }) { - "No valid signature found." - } + class ViaKeyIdentifier(key: OpenPGPComponentKey, referenceTime: Date = Date()) : + KeyAccessor(key, referenceTime) { + override val component: OpenPGPCertificateComponent = key } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt deleted file mode 100644 index 75a35140..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub , 2021 Flowcrypt a.s. -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.info - -import org.bouncycastle.openpgp.PGPPublicKey -import org.bouncycastle.openpgp.PGPSecretKey -import org.pgpainless.bouncycastle.extensions.getCurveName -import org.pgpainless.bouncycastle.extensions.hasDummyS2K -import org.pgpainless.bouncycastle.extensions.isDecrypted -import org.pgpainless.bouncycastle.extensions.isEncrypted - -@Deprecated("Deprecated in favor of extension functions to PGPSecretKey and PGPPublicKey.") -class KeyInfo private constructor(val secretKey: PGPSecretKey?, val publicKey: PGPPublicKey) { - - constructor(secretKey: PGPSecretKey) : this(secretKey, secretKey.publicKey) - - constructor(publicKey: PGPPublicKey) : this(null, publicKey) - - /** - * Return the name of the elliptic curve used by this key, or throw an - * [IllegalArgumentException] if the key is not based on elliptic curves, or on an unknown - * curve. - */ - @Deprecated( - "Deprecated in favor of calling getCurveName() on the PGPPublicKey itself.", - ReplaceWith("publicKey.getCurveName()")) - val curveName: String - get() = publicKey.getCurveName() - - /** - * Return true, if the secret key is encrypted. This method returns false, if the secret key is - * null. - */ - @Deprecated( - "Deprecated in favor of calling isEncrypted() on the PGPSecretKey itself.", - ReplaceWith("secretKey.isEncrypted()")) - val isEncrypted: Boolean - get() = secretKey?.isEncrypted() ?: false - - /** - * Return true, if the secret key is decrypted. This method returns true, if the secret key is - * null. - */ - @Deprecated( - "Deprecated in favor of calling isDecrypted() on the PGPSecretKey itself.", - ReplaceWith("secretKey.isDecrypted()")) - val isDecrypted: Boolean - get() = secretKey?.isDecrypted() ?: true - - /** - * Return true, if the secret key is using the GNU_DUMMY_S2K s2k type. This method returns - * false, if the secret key is null. - */ - @Deprecated( - "Deprecated in favor of calling hasDummyS2K() on the PGPSecretKey itself.", - ReplaceWith("secretKey.hasDummyS2K()")) - val hasDummyS2K: Boolean - @JvmName("hasDummyS2K") get() = secretKey?.hasDummyS2K() ?: false - - companion object { - @JvmStatic - @Deprecated( - "Deprecated in favor of calling isEncrypted() on the PGPSecretKey itself.", - ReplaceWith("secretKey.isEncrypted()")) - fun isEncrypted(secretKey: PGPSecretKey?) = secretKey.isEncrypted() - - @JvmStatic - @Deprecated( - "Deprecated in favor of calling isDecrypted() on the PGPSecretKey itself.", - ReplaceWith("secretKey.isDecrypted()")) - fun isDecrypted(secretKey: PGPSecretKey?) = secretKey.isDecrypted() - - @JvmStatic - @Deprecated( - "Deprecated in favor of calling hasDummyS2K() on the PGPSecretKey itself.", - ReplaceWith("secretKey.hasDummyS2K()")) - fun hasDummyS2K(secretKey: PGPSecretKey?) = secretKey.hasDummyS2K() - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt index ce4fbe56..d324b953 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt @@ -5,8 +5,13 @@ package org.pgpainless.key.info import java.util.* -import openpgp.openPgpKeyId +import kotlin.NoSuchElementException +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPSecretKey import org.pgpainless.PGPainless import org.pgpainless.algorithm.* import org.pgpainless.bouncycastle.extensions.* @@ -14,44 +19,65 @@ import org.pgpainless.exception.KeyException.UnboundUserIdException import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.util.KeyRingUtils -import org.pgpainless.policy.Policy -import org.pgpainless.signature.consumer.SignaturePicker import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil.Companion.getKeyExpirationTimeAsDate import org.pgpainless.util.DateUtil import org.slf4j.LoggerFactory class KeyRingInfo( - val keys: PGPKeyRing, - val policy: Policy = PGPainless.getPolicy(), - val referenceDate: Date = Date() + val keys: OpenPGPCertificate, + private val api: PGPainless = PGPainless.getInstance(), + private val referenceDate: Date = Date() ) { + constructor( + keys: PGPKeyRing, + api: PGPainless = PGPainless.getInstance(), + referenceDate: Date = Date() + ) : this( + if (keys is PGPSecretKeyRing) OpenPGPKey(keys, api.implementation) + else OpenPGPCertificate(keys, api.implementation), + api, + referenceDate) + @JvmOverloads constructor( keys: PGPKeyRing, referenceDate: Date = Date() - ) : this(keys, PGPainless.getPolicy(), referenceDate) + ) : this(keys, PGPainless.getInstance(), referenceDate) - private val signatures: Signatures = Signatures(keys, referenceDate, policy) + /** Primary [OpenPGPCertificate.OpenPGPPrimaryKey]. */ + val primaryKey: OpenPGPCertificate.OpenPGPPrimaryKey = keys.primaryKey - /** Primary [PGPPublicKey]. */ - val publicKey: PGPPublicKey = KeyRingUtils.requirePrimaryPublicKeyFrom(keys) + /** Primary [OpenPGPCertificate.OpenPGPPrimaryKey]. */ + @Deprecated("Use primaryKey instead.", replaceWith = ReplaceWith("primaryKey")) + val publicKey: OpenPGPCertificate.OpenPGPPrimaryKey = primaryKey /** Primary key ID. */ - val keyId: Long = publicKey.keyID + val keyIdentifier: KeyIdentifier = primaryKey.keyIdentifier + + @Deprecated( + "Use of raw key-ids is deprecated in favor of key-identifiers", + replaceWith = ReplaceWith("keyIdentifier")) + val keyId: Long = keyIdentifier.keyId /** Primary key fingerprint. */ - val fingerprint: OpenPgpFingerprint = OpenPgpFingerprint.of(keys) + val fingerprint: OpenPgpFingerprint = OpenPgpFingerprint.of(primaryKey.pgpPublicKey) /** All User-IDs (valid, expired, revoked). */ - val userIds: List = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey) + val userIds: List = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(primaryKey.pgpPublicKey) /** Primary User-ID. */ - val primaryUserId = findPrimaryUserId() + val primaryUserId: String? = keys.getPrimaryUserId(referenceDate)?.userId /** Revocation State. */ - val revocationState = signatures.primaryKeyRevocation.toRevocationState() + val revocationState: RevocationState = + primaryKey.getLatestSelfSignature(referenceDate)?.let { + if (!it.isRevocation) RevocationState.notRevoked() + else if (it.isHardRevocation) RevocationState.hardRevoked() + else RevocationState.softRevoked(it.creationTime) + } + ?: RevocationState.notRevoked() /** * Return the date on which the primary key was revoked, or null if it has not yet been revoked. * @@ -61,47 +87,38 @@ class KeyRingInfo( if (revocationState.isSoftRevocation()) revocationState.date else null /** - * Primary [PGPSecretKey] of this key ring or null if the key ring is not a [PGPSecretKeyRing]. + * Primary [OpenPGPSecretKey] of this key ring or null if the key ring is not a [OpenPGPKey]. */ - val secretKey: PGPSecretKey? = - when (keys) { - is PGPSecretKeyRing -> keys.secretKey!! - else -> null - } + val secretKey: OpenPGPSecretKey? = + if (keys.isSecretKey) { + (keys as OpenPGPKey).primarySecretKey + } else null /** OpenPGP key version. */ - val version: Int = publicKey.version + val version: OpenPGPKeyVersion = keys.getKeyVersion() /** - * Return all [PGPPublicKeys][PGPPublicKey] of this key ring. The first key in the list being - * the primary key. Note that the list is unmodifiable. + * Return all [public component keys][OpenPGPComponentKey] of this key ring. The first key in + * the list being the primary key. Note that the list is unmodifiable. * * @return list of public keys */ - val publicKeys: List = keys.publicKeys.asSequence().toList() + val publicKeys: List = keys.keys - /** All secret keys. If the key ring is a [PGPPublicKeyRing], then return an empty list. */ - val secretKeys: List = - when (keys) { - is PGPSecretKeyRing -> keys.secretKeys.asSequence().toList() - else -> listOf() - } + /** All secret keys. If the key ring is not an [OpenPGPKey], then return an empty list. */ + val secretKeys: List = + if (keys.isSecretKey) { + (keys as OpenPGPKey).secretKeys.values.toList() + } else listOf() - /** List of valid public subkeys. */ - val validSubkeys: List = - keys.publicKeys.asSequence().filter { isKeyValidlyBound(it.keyID) }.toList() + /** List of valid public component keys. */ + val validSubkeys: List = keys.getValidKeys(referenceDate) /** List of valid user-IDs. */ - val validUserIds: List = userIds.filter { isUserIdBound(it) } + val validUserIds: List = keys.getValidUserIds(referenceDate).map { it.userId } /** List of valid and expired user-IDs. */ - val validAndExpiredUserIds: List = - userIds.filter { - val certification = signatures.userIdCertifications[it] ?: return@filter false - val revocation = signatures.userIdRevocations[it] ?: return@filter true - return@filter !revocation.isHardRevocation && - certification.creationTime > revocation.creationTime - } + val validAndExpiredUserIds: List = userIds /** List of email addresses that can be extracted from the user-IDs. */ val emailAddresses: List = @@ -116,44 +133,49 @@ class KeyRingInfo( } /** Newest direct-key self-signature on the primary key. */ - val latestDirectKeySelfSignature: PGPSignature? = signatures.primaryKeySelfSignature + val latestDirectKeySelfSignature: PGPSignature? = + primaryKey.getLatestDirectKeySelfSignature(referenceDate)?.signature /** Newest primary-key revocation self-signature. */ - val revocationSelfSignature: PGPSignature? = signatures.primaryKeyRevocation + val revocationSelfSignature: PGPSignature? = + primaryKey.getLatestKeyRevocationSelfSignature(referenceDate)?.signature /** Public-key encryption-algorithm of the primary key. */ - val algorithm: PublicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(publicKey.algorithm) + val algorithm: PublicKeyAlgorithm = + PublicKeyAlgorithm.requireFromId(primaryKey.pgpPublicKey.algorithm) /** Creation date of the primary key. */ - val creationDate: Date = publicKey.creationTime!! + val creationDate: Date = primaryKey.creationTime!! /** Latest date at which the key was modified (either by adding a subkey or self-signature). */ - val lastModified: Date = getMostRecentSignature()?.creationTime ?: getLatestKeyCreationDate() + val lastModified: Date = keys.lastModificationDate - /** True, if the underlying keyring is a [PGPSecretKeyRing]. */ - val isSecretKey: Boolean = keys is PGPSecretKeyRing + /** True, if the underlying key is a [OpenPGPKey]. */ + val isSecretKey: Boolean = keys.isSecretKey /** True, if there are no encrypted secret keys. */ val isFullyDecrypted: Boolean = - !isSecretKey || secretKeys.all { it.hasDummyS2K() || it.isDecrypted() } + !isSecretKey || + secretKeys.all { it.pgpSecretKey.hasDummyS2K() || it.pgpSecretKey.isDecrypted() } /** True, if there are only encrypted secret keys. */ val isFullyEncrypted: Boolean = - isSecretKey && secretKeys.none { !it.hasDummyS2K() && it.isDecrypted() } + isSecretKey && + secretKeys.none { !it.pgpSecretKey.hasDummyS2K() && it.pgpSecretKey.isDecrypted() } /** List of public keys, whose secret key counterparts can be used to decrypt messages. */ - val decryptionSubkeys: List = - keys.publicKeys + val decryptionSubkeys: List = + keys.keys .asSequence() .filter { - if (it.keyID != keyId) { - if (signatures.subkeyBindings[it.keyID] == null) { - LOGGER.debug("Subkey ${it.keyID.openPgpKeyId()} has no binding signature.") + if (!it.keyIdentifier.matchesExplicit(keyIdentifier)) { + if (it.getLatestSelfSignature(referenceDate) == null) { + LOGGER.debug("Subkey ${it.keyIdentifier} has no binding signature.") return@filter false } } - if (!it.isEncryptionKey) { - LOGGER.debug("(Sub-?)Key ${it.keyID.openPgpKeyId()} is not encryption-capable.") + if (!it.pgpPublicKey.isEncryptionKey) { + LOGGER.debug("(Sub-?)Key ${it.keyIdentifier} is not encryption-capable.") return@filter false } return@filter true @@ -164,12 +186,16 @@ class KeyRingInfo( val primaryKeyExpirationDate: Date? get() { val directKeyExpirationDate: Date? = - latestDirectKeySelfSignature?.let { getKeyExpirationTimeAsDate(it, publicKey) } + latestDirectKeySelfSignature?.let { + getKeyExpirationTimeAsDate(it, primaryKey.pgpPublicKey) + } val possiblyExpiredPrimaryUserId = getPossiblyExpiredPrimaryUserId() val primaryUserIdCertification = possiblyExpiredPrimaryUserId?.let { getLatestUserIdCertification(it) } val userIdExpirationDate: Date? = - primaryUserIdCertification?.let { getKeyExpirationTimeAsDate(it, publicKey) } + primaryUserIdCertification?.let { + getKeyExpirationTimeAsDate(it, primaryKey.pgpPublicKey) + } if (latestDirectKeySelfSignature == null && primaryUserIdCertification == null) { throw NoSuchElementException( @@ -186,11 +212,11 @@ class KeyRingInfo( } /** List of all subkeys that can be used to sign a message. */ - val signingSubkeys: List = - validSubkeys.filter { getKeyFlagsOf(it.keyID).contains(KeyFlag.SIGN_DATA) } + val signingSubkeys: List = keys.getSigningKeys(referenceDate) /** Whether the key is usable for encryption. */ - val isUsableForEncryption: Boolean = isUsableForEncryption(EncryptionPurpose.ANY) + val isUsableForEncryption: Boolean = + keys.getComponentKeysWithFlag(referenceDate, EncryptionPurpose.ANY.code).isNotEmpty() /** * Whether the key is capable of signing messages. This field is also true, if the key contains @@ -199,31 +225,45 @@ class KeyRingInfo( * * To check for keys that are actually usable to sign messages, use [isUsableForSigning]. */ - val isSigningCapable: Boolean = isKeyValidlyBound(keyId) && signingSubkeys.isNotEmpty() + val isSigningCapable: Boolean = isKeyValidlyBound(keyIdentifier) && signingSubkeys.isNotEmpty() /** Whether the key is actually usable to sign messages. */ val isUsableForSigning: Boolean = - isSigningCapable && signingSubkeys.any { isSecretKeyAvailable(it.keyID) } + isSigningCapable && signingSubkeys.any { isSecretKeyAvailable(it.keyIdentifier) } + + /** + * True, if the [OpenPGPCertificate] can be used to certify other + * [OpenPGPCertificates][OpenPGPCertificate]. + */ + val isUsableForThirdPartyCertification: Boolean = + isKeyValidlyBound(keyIdentifier) && + getKeyFlagsOf(keyIdentifier).contains(KeyFlag.CERTIFY_OTHER) /** [HashAlgorithm] preferences of the primary user-ID or if absent, of the primary key. */ - val preferredHashAlgorithms: Set + val preferredHashAlgorithms: Set? get() = primaryUserId?.let { getPreferredHashAlgorithms(it) } - ?: getPreferredHashAlgorithms(keyId) + ?: getPreferredHashAlgorithms(keyIdentifier) /** * [SymmetricKeyAlgorithm] preferences of the primary user-ID or if absent of the primary key. */ - val preferredSymmetricKeyAlgorithms: Set + val preferredSymmetricKeyAlgorithms: Set? get() = primaryUserId?.let { getPreferredSymmetricKeyAlgorithms(it) } - ?: getPreferredSymmetricKeyAlgorithms(keyId) + ?: getPreferredSymmetricKeyAlgorithms(keyIdentifier) /** [CompressionAlgorithm] preferences of the primary user-ID or if absent, the primary key. */ - val preferredCompressionAlgorithms: Set + val preferredCompressionAlgorithms: Set? get() = primaryUserId?.let { getPreferredCompressionAlgorithms(it) } - ?: getPreferredCompressionAlgorithms(keyId) + ?: getPreferredCompressionAlgorithms(keyIdentifier) + + /** [AEADCipherMode] preferences of the primary user-id, or if absent, the primary key. */ + val preferredAEADCipherSuites: Set? + get() = + primaryUserId?.let { getPreferredAEADCipherSuites(it) } + ?: getPreferredAEADCipherSuites(keyIdentifier) /** * Return the expiration date of the subkey with the provided fingerprint. @@ -232,7 +272,24 @@ class KeyRingInfo( * @return expiration date or null */ fun getSubkeyExpirationDate(fingerprint: OpenPgpFingerprint): Date? { - return getSubkeyExpirationDate(fingerprint.keyId) + return getSubkeyExpirationDate(fingerprint.keyIdentifier) + } + + /** + * Return the expiration date of the [OpenPGPComponentKey] with the provided [keyIdentifier]. + * + * @param keyIdentifier subkey KeyIdentifier + * @return expiration date + */ + fun getSubkeyExpirationDate(keyIdentifier: KeyIdentifier): Date? { + if (primaryKey.keyIdentifier.matchesExplicit(keyIdentifier)) return primaryKeyExpirationDate + val subkey = + getPublicKey(keyIdentifier) + ?: throw NoSuchElementException("No subkey with key-ID ${keyIdentifier} found.") + val bindingSig = + getCurrentSubkeyBindingSignature(keyIdentifier) + ?: throw AssertionError("Subkey has no valid binding signature.") + return bindingSig.getKeyExpirationDate(subkey.creationTime) } /** @@ -241,16 +298,9 @@ class KeyRingInfo( * @param keyId subkey keyId * @return expiration date */ + @Deprecated("Pass in a KeyIdentifer instead.") fun getSubkeyExpirationDate(keyId: Long): Date? { - if (publicKey.keyID == keyId) return primaryKeyExpirationDate - val subkey = - getPublicKey(keyId) - ?: throw NoSuchElementException( - "No subkey with key-ID ${keyId.openPgpKeyId()} found.") - val bindingSig = - getCurrentSubkeyBindingSignature(keyId) - ?: throw AssertionError("Subkey has no valid binding signature.") - return bindingSig.getKeyExpirationDate(subkey.creationTime) + return getSubkeyExpirationDate(KeyIdentifier(keyId)) } /** @@ -265,7 +315,7 @@ class KeyRingInfo( } val primaryKeyExpiration = primaryKeyExpirationDate - val keysWithFlag: List = getKeysWithKeyFlag(use) + val keysWithFlag: List = getKeysWithKeyFlag(use) if (keysWithFlag.isEmpty()) throw NoSuchElementException("No key with the required key flag found.") @@ -273,7 +323,9 @@ class KeyRingInfo( val latestSubkeyExpiration = keysWithFlag .map { key -> - getSubkeyExpirationDate(key.keyID).also { if (it == null) nonExpiring = true } + getSubkeyExpirationDate(key.keyIdentifier).also { + if (it == null) nonExpiring = true + } } .filterNotNull() .maxByOrNull { it } @@ -290,7 +342,11 @@ class KeyRingInfo( * @return true, if the given user-ID is hard-revoked. */ fun isHardRevoked(userId: CharSequence): Boolean { - return signatures.userIdRevocations[userId]?.isHardRevocation ?: false + return keys + .getUserId(userId.toString()) + ?.getLatestSelfSignature(referenceDate) + ?.isHardRevocation + ?: false } /** @@ -299,8 +355,8 @@ class KeyRingInfo( * @param flag flag * @return keys with flag */ - fun getKeysWithKeyFlag(flag: KeyFlag): List = - publicKeys.filter { getKeyFlagsOf(it.keyID).contains(flag) } + fun getKeysWithKeyFlag(flag: KeyFlag): List = + publicKeys.filter { getKeyFlagsOf(it.keyIdentifier).contains(flag) } /** * Return a list of all subkeys which can be used to encrypt a message for the given user-ID. @@ -310,10 +366,10 @@ class KeyRingInfo( fun getEncryptionSubkeys( userId: CharSequence?, purpose: EncryptionPurpose - ): List { + ): List { if (userId != null && !isUserIdValid(userId)) { throw UnboundUserIdException( - OpenPgpFingerprint.of(keys), + OpenPgpFingerprint.of(primaryKey.pgpPublicKey), userId.toString(), getLatestUserIdCertification(userId), getUserIdRevocation(userId)) @@ -326,7 +382,7 @@ class KeyRingInfo( * * @return subkeys which can be used for encryption */ - fun getEncryptionSubkeys(purpose: EncryptionPurpose): List { + fun getEncryptionSubkeys(purpose: EncryptionPurpose): List { primaryKeyExpirationDate?.let { if (it < referenceDate) { LOGGER.debug( @@ -335,29 +391,29 @@ class KeyRingInfo( } } - return keys.publicKeys + return keys.keys .asSequence() .filter { - if (!isKeyValidlyBound(it.keyID)) { - LOGGER.debug("(Sub?)-Key ${it.keyID.openPgpKeyId()} is not validly bound.") + if (!isKeyValidlyBound(it.keyIdentifier)) { + LOGGER.debug("(Sub?)-Key ${it.keyIdentifier} is not validly bound.") return@filter false } - getSubkeyExpirationDate(it.keyID)?.let { exp -> + getSubkeyExpirationDate(it.keyIdentifier)?.let { exp -> if (exp < referenceDate) { LOGGER.debug( - "(Sub?)-Key ${it.keyID.openPgpKeyId()} is expired on ${DateUtil.formatUTCDate(exp)}.") + "(Sub?)-Key ${it.keyIdentifier} is expired on ${DateUtil.formatUTCDate(exp)}.") return@filter false } } - if (!it.isEncryptionKey) { + if (!it.pgpPublicKey.isEncryptionKey) { LOGGER.debug( - "(Sub?)-Key ${it.keyID.openPgpKeyId()} algorithm is not capable of encryption.") + "(Sub?)-Key ${it.keyIdentifier} algorithm is not capable of encryption.") return@filter false } - val keyFlags = getKeyFlagsOf(it.keyID) + val keyFlags = getKeyFlagsOf(it.keyIdentifier) when (purpose) { EncryptionPurpose.COMMUNICATIONS -> return@filter keyFlags.contains(KeyFlag.ENCRYPT_COMMS) @@ -378,7 +434,7 @@ class KeyRingInfo( * encryption-purpose. */ fun isUsableForEncryption(purpose: EncryptionPurpose): Boolean { - return isKeyValidlyBound(keyId) && getEncryptionSubkeys(purpose).isNotEmpty() + return isKeyValidlyBound(keyIdentifier) && getEncryptionSubkeys(purpose).isNotEmpty() } /** @@ -396,20 +452,14 @@ class KeyRingInfo( /** Return the most-recently created self-signature on the key. */ private fun getMostRecentSignature(): PGPSignature? = - setOfNotNull(latestDirectKeySelfSignature, revocationSelfSignature) - .asSequence() - .plus(signatures.userIdCertifications.values) - .plus(signatures.userIdRevocations.values) - .plus(signatures.subkeyBindings.values) - .plus(signatures.subkeyRevocations.values) - .maxByOrNull { it.creationTime } + keys.components.map { it.latestSelfSignature }.maxByOrNull { it.creationTime }?.signature /** * Return the creation time of the latest added subkey. * * @return latest key creation time */ fun getLatestKeyCreationDate(): Date = - validSubkeys.maxByOrNull { creationDate }?.creationTime + keys.getValidKeys(referenceDate).maxByOrNull { it.creationTime }?.creationTime ?: throw AssertionError("Apparently there is no validly bound key in this key ring.") /** @@ -418,7 +468,7 @@ class KeyRingInfo( * @return latest self-certification for the given user-ID. */ fun getLatestUserIdCertification(userId: CharSequence): PGPSignature? = - signatures.userIdCertifications[userId] + keys.getUserId(userId.toString())?.getCertification(referenceDate)?.signature /** * Return the latest revocation self-signature for the given user-ID @@ -426,32 +476,54 @@ class KeyRingInfo( * @return latest user-ID revocation for the given user-ID */ fun getUserIdRevocation(userId: CharSequence): PGPSignature? = - signatures.userIdRevocations[userId] + keys.getUserId(userId.toString())?.getRevocation(referenceDate)?.signature + + /** + * Return the current binding signature for the subkey with the given [keyIdentifier]. + * + * @param keyIdentifier subkey identifier + * @return current subkey binding signature + */ + fun getCurrentSubkeyBindingSignature(keyIdentifier: KeyIdentifier): PGPSignature? = + keys.getKey(keyIdentifier)?.getCertification(referenceDate)?.signature /** * Return the current binding signature for the subkey with the given key-ID. * + * @param keyId key-ID * @return current subkey binding signature */ + @Deprecated("Pass in a KeyIdentifier instead.") fun getCurrentSubkeyBindingSignature(keyId: Long): PGPSignature? = - signatures.subkeyBindings[keyId] + getCurrentSubkeyBindingSignature(KeyIdentifier(keyId)) + + /** + * Return the current revocation signature for the subkey with the given [keyIdentifier]. + * + * @param keyIdentifier subkey identifier + * @return current subkey revocation signature + */ + fun getSubkeyRevocationSignature(keyIdentifier: KeyIdentifier): PGPSignature? = + keys.getKey(keyIdentifier)?.getRevocation(referenceDate)?.signature /** * Return the current revocation signature for the subkey with the given key-ID. * * @return current subkey revocation signature */ + @Deprecated("Pass in a KeyIdentifier instead.") fun getSubkeyRevocationSignature(keyId: Long): PGPSignature? = - signatures.subkeyRevocations[keyId] + getSubkeyRevocationSignature(KeyIdentifier(keyId)) /** - * Return a list of [KeyFlags][KeyFlag] that apply to the subkey with the provided key id. + * Return a list of [KeyFlags][KeyFlag] that apply to the subkey with the provided + * [keyIdentifier]. * - * @param keyId key-id + * @param keyIdentifier keyIdentifier * @return list of key flags */ - fun getKeyFlagsOf(keyId: Long): List = - if (keyId == publicKey.keyID) { + fun getKeyFlagsOf(keyIdentifier: KeyIdentifier): List = + if (primaryKey.keyIdentifier.matchesExplicit(keyIdentifier)) { latestDirectKeySelfSignature?.let { sig -> SignatureSubpacketsUtil.parseKeyFlags(sig)?.let { flags -> return flags @@ -466,7 +538,7 @@ class KeyRingInfo( } listOf() } else { - getCurrentSubkeyBindingSignature(keyId)?.let { + getCurrentSubkeyBindingSignature(keyIdentifier)?.let { SignatureSubpacketsUtil.parseKeyFlags(it)?.let { flags -> return flags } @@ -474,6 +546,15 @@ class KeyRingInfo( listOf() } + /** + * Return a list of [KeyFlags][KeyFlag] that apply to the subkey with the provided key id. + * + * @param keyId key-id + * @return list of key flags + */ + @Deprecated("Pass in a KeyIdentifier instead.") + fun getKeyFlagsOf(keyId: Long): List = getKeyFlagsOf(KeyIdentifier(keyId)) + /** * Return a list of [KeyFlags][KeyFlag] that apply to the given user-id. * @@ -491,13 +572,35 @@ class KeyRingInfo( "While user-id '$userId' was reported as valid, there appears to be no certification for it.") } + /** + * Return the [OpenPGPComponentKey] with the given [keyIdentifier] from this + * [OpenPGPCertificate] or [OpenPGPKey]. + * + * @param keyIdentifier keyIdentifier + * @return public component key or null + */ + fun getPublicKey(keyIdentifier: KeyIdentifier): OpenPGPComponentKey? = + keys.getKey(keyIdentifier) + /** * Return the public key with the given key id from the provided key ring. * * @param keyId key id * @return public key or null */ - fun getPublicKey(keyId: Long): PGPPublicKey? = keys.getPublicKey(keyId) + @Deprecated("Pass in a KeyIdentifier instead.") + fun getPublicKey(keyId: Long): OpenPGPComponentKey? = getPublicKey(KeyIdentifier(keyId)) + + /** + * Return the [OpenPGPSecretKey] component with the given [keyIdentifier]. + * + * @param keyIdentifier keyIdentifier + * @return secret key or null + */ + fun getSecretKey(keyIdentifier: KeyIdentifier): OpenPGPSecretKey? = + if (keys.isSecretKey) { + (keys as OpenPGPKey).getSecretKey(keyIdentifier) + } else null /** * Return the secret key with the given key id. @@ -505,34 +608,36 @@ class KeyRingInfo( * @param keyId key id * @return secret key or null */ - fun getSecretKey(keyId: Long): PGPSecretKey? = - when (keys) { - is PGPSecretKeyRing -> keys.getSecretKey(keyId) - else -> null - } + @Deprecated("Pass in a KeyIdentifier instead.") + fun getSecretKey(keyId: Long): OpenPGPSecretKey? = getSecretKey(KeyIdentifier(keyId)) /** - * Return true, if the secret-key with the given key-ID is available (i.e. not moved to a - * smart-card). + * Return true, if the secret-key with the given [keyIdentifier] is available (i.e. part of the + * certificate AND not moved to a smart-card). * * @return availability of the secret key */ - fun isSecretKeyAvailable(keyId: Long): Boolean { - return getSecretKey(keyId)?.let { - return if (it.s2K == null) true // Unencrypted key - else it.s2K.type !in 100..110 // Secret key on smart-card + fun isSecretKeyAvailable(keyIdentifier: KeyIdentifier): Boolean { + return getSecretKey(keyIdentifier)?.let { + return if (it.pgpSecretKey.s2K == null) true // Unencrypted key + else it.pgpSecretKey.s2K.type !in 100..110 // Secret key on smart-card } ?: false // Missing secret key } + @Deprecated("Pass in a KeyIdentifier instead.") + fun isSecretKeyAvailable(keyId: Long): Boolean { + return isSecretKeyAvailable(KeyIdentifier(keyId)) + } + /** * Return the public key with the given fingerprint. * * @param fingerprint fingerprint * @return public key or null */ - fun getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = - keys.getPublicKey(fingerprint.keyId) + fun getPublicKey(fingerprint: OpenPgpFingerprint): OpenPGPComponentKey? = + keys.getKey(fingerprint.keyIdentifier) /** * Return the secret key with the given fingerprint. @@ -540,11 +645,8 @@ class KeyRingInfo( * @param fingerprint fingerprint * @return secret key or null */ - fun getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? = - when (keys) { - is PGPSecretKeyRing -> keys.getSecretKey(fingerprint.keyId) - else -> null - } + fun getSecretKey(fingerprint: OpenPgpFingerprint): OpenPGPSecretKey? = + getSecretKey(fingerprint.keyIdentifier) /** * Return the public key matching the given [SubkeyIdentifier]. @@ -553,9 +655,11 @@ class KeyRingInfo( * @throws IllegalArgumentException if the identifier's primary key does not match the primary * key of the key. */ - fun getPublicKey(identifier: SubkeyIdentifier): PGPPublicKey? { - require(identifier.primaryKeyId == publicKey.keyID) { "Mismatching primary key ID." } - return getPublicKey(identifier.subkeyId) + fun getPublicKey(identifier: SubkeyIdentifier): OpenPGPComponentKey? { + require(primaryKey.keyIdentifier.matchesExplicit(identifier.keyIdentifier)) { + "Mismatching primary key ID." + } + return getPublicKey(identifier.componentKeyIdentifier) } /** @@ -565,16 +669,23 @@ class KeyRingInfo( * @throws IllegalArgumentException if the identifier's primary key does not match the primary * key of the key. */ - fun getSecretKey(identifier: SubkeyIdentifier): PGPSecretKey? = - when (keys) { - is PGPSecretKeyRing -> { - require(identifier.primaryKeyId == publicKey.keyID) { - "Mismatching primary key ID." - } - keys.getSecretKey(identifier.subkeyId) - } - else -> null + fun getSecretKey(identifier: SubkeyIdentifier): OpenPGPComponentKey? { + require(primaryKey.keyIdentifier.matchesExplicit(identifier.keyIdentifier)) { + "Mismatching primary key ID." } + return getSecretKey(identifier.componentKeyIdentifier) + } + + /** + * Return true if the [OpenPGPComponentKey] with the given [keyIdentifier] is bound to the + * [OpenPGPCertificate] properly. + * + * @param keyIdentifier identifier of the component key + * @return true if key is bound validly + */ + fun isKeyValidlyBound(keyIdentifier: KeyIdentifier): Boolean { + return keys.getKey(keyIdentifier)?.isBoundAt(referenceDate) ?: false + } /** * Return true if the public key with the given key id is bound to the key ring properly. @@ -582,141 +693,187 @@ class KeyRingInfo( * @param keyId key id * @return true if key is bound validly */ - fun isKeyValidlyBound(keyId: Long): Boolean { - val publicKey = keys.getPublicKey(keyId) ?: return false - - // Primary key -> Check Primary Key Revocation - if (publicKey.keyID == this.publicKey.keyID) { - return if (signatures.primaryKeyRevocation != null && - signatures.primaryKeyRevocation.isHardRevocation) { - false - } else signatures.primaryKeyRevocation == null - } - - // Else Subkey -> Check Subkey Revocation - val binding = signatures.subkeyBindings[keyId] - val revocation = signatures.subkeyRevocations[keyId] - - // No valid binding - if (binding == null || binding.isExpired(referenceDate)) { - return false - } - - // Revocation - return if (revocation != null) { - if (revocation.isHardRevocation) { - // Subkey is hard revoked - false - } else { - // Key is soft-revoked, not yet re-bound - (revocation.isExpired(referenceDate) || - !revocation.creationTime.after(binding.creationTime)) - } - } else true - } + @Deprecated("Pass in a KeyIdentifier instead.") + fun isKeyValidlyBound(keyId: Long): Boolean = isKeyValidlyBound(KeyIdentifier(keyId)) /** * Return the current primary user-id of the key ring. * *

- * Note: If no user-id is marked as primary key using a [PrimaryUserID] packet, this method - * returns the first user-id on the key, otherwise null. + * Note: If no user-id is marked as primary key using a + * [org.bouncycastle.bcpg.sig.PrimaryUserID] packet, this method returns the first user-id on + * the key, otherwise null. * * @return primary user-id or null */ private fun findPrimaryUserId(): String? { - if (userIds.isEmpty()) { - return null - } - - return signatures.userIdCertifications - .filter { (_, certification) -> certification.hashedSubPackets.isPrimaryUserID } - .entries - .maxByOrNull { (_, certification) -> certification.creationTime } - ?.key - ?: signatures.userIdCertifications.keys.firstOrNull() + return keys.primaryKey.getExplicitOrImplicitPrimaryUserId(referenceDate)?.userId } - /** Return true, if the primary user-ID, as well as the given user-ID are valid and bound. */ - fun isUserIdValid(userId: CharSequence) = - if (primaryUserId == null) { - false - } else { - isUserIdBound(primaryUserId) && - (if (userId == primaryUserId) true else isUserIdBound(userId)) - } + /** + * Return true, if the primary user-ID, as well as the given user-ID are valid and bound. + * + * @param userId user-id + * @return true if the primary user-ID and the given user-ID are valid. + */ + fun isUserIdValid(userId: CharSequence): Boolean { + var valid = isUserIdBound(userId) + if (primaryUserId != null) valid = valid && isUserIdBound(primaryUserId) + valid = valid && isKeyValidlyBound(primaryKey.keyIdentifier) + return valid + } - /** Return true, if the given user-ID is validly bound. */ - fun isUserIdBound(userId: CharSequence) = - signatures.userIdCertifications[userId]?.let { sig -> - if (sig.isExpired(referenceDate)) { - // certification expired - return false - } - if (sig.hashedSubPackets.isPrimaryUserID) { - getKeyExpirationTimeAsDate(sig, publicKey)?.let { expirationDate -> - // key expired? - if (expirationDate < referenceDate) return false - } - } - signatures.userIdRevocations[userId]?.let { rev -> - if (rev.isHardRevocation) { - return false // hard revoked -> invalid - } - sig.creationTime > rev.creationTime // re-certification after soft revocation? - } - ?: true // certification, but no revocation - } - ?: false // no certification + /** + * Return true, if the given user-ID is validly bound. + * + * @param userId user-id + * @return true if the user-id is validly bound to the [OpenPGPCertificate] + */ + fun isUserIdBound(userId: CharSequence): Boolean = + keys.getUserId(userId.toString())?.isBoundAt(referenceDate) ?: false - /** [HashAlgorithm] preferences of the given user-ID. */ - fun getPreferredHashAlgorithms(userId: CharSequence): Set { - return getKeyAccessor(userId, keyId).preferredHashAlgorithms + /** + * Return the [HashAlgorithm] preferences of the given [userId]. + * + * @param userId user-id + * @return ordered set of preferred [HashAlgorithms][HashAlgorithm] (descending order) + */ + fun getPreferredHashAlgorithms(userId: CharSequence): Set? { + return (keys.getUserId(userId.toString()) + ?: throw NoSuchElementException("No user-id '$userId' found on this key.")) + .getHashAlgorithmPreferences(referenceDate) + ?.toHashAlgorithms() + } + + /** + * Return the [HashAlgorithm] preferences of the component key with the given [KeyIdentifier]. + * + * @param keyIdentifier identifier of a [OpenPGPComponentKey] + * @return ordered set of preferred [HashAlgorithms][HashAlgorithm] (descending order) + */ + fun getPreferredHashAlgorithms(keyIdentifier: KeyIdentifier): Set? { + return (keys.getKey(keyIdentifier) + ?: throw NoSuchElementException( + "No subkey with key-id $keyIdentifier found on this key.")) + .getHashAlgorithmPreferences(referenceDate) + ?.toHashAlgorithms() } /** [HashAlgorithm] preferences of the given key. */ - fun getPreferredHashAlgorithms(keyId: Long): Set { - return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)).preferredHashAlgorithms + @Deprecated("Pass KeyIdentifier instead.") + fun getPreferredHashAlgorithms(keyId: Long): Set? { + return getPreferredHashAlgorithms(KeyIdentifier(keyId)) } - /** [SymmetricKeyAlgorithm] preferences of the given user-ID. */ - fun getPreferredSymmetricKeyAlgorithms(userId: CharSequence): Set { - return getKeyAccessor(userId, keyId).preferredSymmetricKeyAlgorithms + /** + * Return the [SymmetricKeyAlgorithm] preferences of the given [userId]. + * + * @param userId user-id + * @return ordered set of preferred [SymmetricKeyAlgorithms][SymmetricKeyAlgorithm] (descending + * order) + */ + fun getPreferredSymmetricKeyAlgorithms(userId: CharSequence): Set? { + return (keys.getUserId(userId.toString()) + ?: throw NoSuchElementException("No user-id '$userId' found on this key.")) + .getSymmetricCipherPreferences(referenceDate) + ?.toSymmetricKeyAlgorithms() + } + + /** + * Return the [SymmetricKeyAlgorithm] preferences of the [OpenPGPComponentKey] with the given + * [keyIdentifier]. + * + * @param keyIdentifier identifier of the [OpenPGPComponentKey] + * @return ordered set of preferred [SymmetricKeyAlgorithms][SymmetricKeyAlgorithm] (descending + * order) + */ + fun getPreferredSymmetricKeyAlgorithms( + keyIdentifier: KeyIdentifier + ): Set? { + return (keys.getKey(keyIdentifier) + ?: throw NoSuchElementException( + "No subkey with key-id $keyIdentifier found on this key.")) + .getSymmetricCipherPreferences(referenceDate) + ?.toSymmetricKeyAlgorithms() } /** [SymmetricKeyAlgorithm] preferences of the given key. */ - fun getPreferredSymmetricKeyAlgorithms(keyId: Long): Set { - return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)) - .preferredSymmetricKeyAlgorithms + @Deprecated("Pass KeyIdentifier instead.") + fun getPreferredSymmetricKeyAlgorithms(keyId: Long): Set? { + return getPreferredSymmetricKeyAlgorithms(KeyIdentifier(keyId)) } - /** [CompressionAlgorithm] preferences of the given user-ID. */ - fun getPreferredCompressionAlgorithms(userId: CharSequence): Set { - return getKeyAccessor(userId, keyId).preferredCompressionAlgorithms + /** + * Return the [CompressionAlgorithm] preferences of the given [userId]. + * + * @param userId user-id + * @return ordered set of preferred [CompressionAlgorithms][CompressionAlgorithm] (descending + * order) + */ + fun getPreferredCompressionAlgorithms(userId: CharSequence): Set? { + return (keys.getUserId(userId.toString()) + ?: throw NoSuchElementException("No user-id '$userId' found on this key.")) + .getCompressionAlgorithmPreferences(referenceDate) + ?.toCompressionAlgorithms() + } + + /** + * Return the [CompressionAlgorithm] preferences of the [OpenPGPComponentKey] with the given + * [keyIdentifier]. + * + * @param keyIdentifier identifier of the [OpenPGPComponentKey] + * @return ordered set of preferred [CompressionAlgorithms][CompressionAlgorithm] (descending + * order) + */ + fun getPreferredCompressionAlgorithms( + keyIdentifier: KeyIdentifier + ): Set? { + return (keys.getKey(keyIdentifier) + ?: throw NoSuchElementException( + "No subkey with key-id $keyIdentifier found on this key.")) + .getCompressionAlgorithmPreferences(referenceDate) + ?.toCompressionAlgorithms() } /** [CompressionAlgorithm] preferences of the given key. */ - fun getPreferredCompressionAlgorithms(keyId: Long): Set { - return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)) - .preferredCompressionAlgorithms + @Deprecated("Pass in a KeyIdentifier instead.") + fun getPreferredCompressionAlgorithms(keyId: Long): Set? { + return getPreferredCompressionAlgorithms(KeyIdentifier(keyId)) } - val isUsableForThirdPartyCertification: Boolean = - isKeyValidlyBound(keyId) && getKeyFlagsOf(keyId).contains(KeyFlag.CERTIFY_OTHER) + /** + * Return the [AEADCipherMode] preferences of the given [userId]. + * + * @param userId user-ID + * @return ordered set of [AEADCipherModes][AEADCipherMode] (descending order, including + * implicitly supported AEAD modes) + */ + fun getPreferredAEADCipherSuites(userId: CharSequence): Set? { + return (keys.getUserId(userId.toString()) + ?: throw NoSuchElementException("No user-id '$userId' found on this key.")) + .getAEADCipherSuitePreferences(referenceDate) + ?.toAEADCipherModes() + } - private fun getKeyAccessor(userId: CharSequence?, keyId: Long): KeyAccessor { - if (getPublicKey(keyId) == null) { - throw NoSuchElementException( - "No subkey with key-id ${keyId.openPgpKeyId()} found on this key.") - } - if (userId != null && !userIds.contains(userId)) { - throw NoSuchElementException("No user-id '$userId' found on this key.") - } - return if (userId != null) { - KeyAccessor.ViaUserId(this, SubkeyIdentifier(keys, keyId), userId) - } else { - KeyAccessor.ViaKeyId(this, SubkeyIdentifier(keys, keyId)) - } + /** + * Return the [AEADCipherMode] preferences of the [OpenPGPComponentKey] with the given + * [keyIdentifier]. + * + * @param keyIdentifier component key identifier + * @return ordered set of [AEADCipherModes][AEADCipherMode] (descending order, including + * implicitly supported AEAD modes) + */ + fun getPreferredAEADCipherSuites(keyIdentifier: KeyIdentifier): Set? { + return (keys.getKey(keyIdentifier) + ?: throw NoSuchElementException( + "No subkey with key-id $keyIdentifier found on this key.")) + .getAEADCipherSuitePreferences(referenceDate) + ?.toAEADCipherModes() + } + + @Deprecated("Pass KeyIdentifier instead.") + fun getPreferredAEADCipherSuites(keyId: Long): Set? { + return getPreferredAEADCipherSuites(KeyIdentifier(keyId)) } companion object { @@ -733,34 +890,4 @@ class KeyRingInfo( @JvmStatic private val LOGGER = LoggerFactory.getLogger(KeyRingInfo::class.java) } - - private class Signatures(val keys: PGPKeyRing, val referenceDate: Date, val policy: Policy) { - val primaryKeyRevocation: PGPSignature? = - SignaturePicker.pickCurrentRevocationSelfSignature(keys, policy, referenceDate) - val primaryKeySelfSignature: PGPSignature? = - SignaturePicker.pickLatestDirectKeySignature(keys, policy, referenceDate) - val userIdRevocations = mutableMapOf() - val userIdCertifications = mutableMapOf() - val subkeyRevocations = mutableMapOf() - val subkeyBindings = mutableMapOf() - - init { - KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keys.publicKey).forEach { userId -> - SignaturePicker.pickCurrentUserIdRevocationSignature( - keys, userId, policy, referenceDate) - ?.let { userIdRevocations[userId] = it } - SignaturePicker.pickLatestUserIdCertificationSignature( - keys, userId, policy, referenceDate) - ?.let { userIdCertifications[userId] = it } - } - keys.publicKeys.asSequence().drop(1).forEach { subkey -> - SignaturePicker.pickCurrentSubkeyBindingRevocationSignature( - keys, subkey, policy, referenceDate) - ?.let { subkeyRevocations[subkey.keyID] = it } - SignaturePicker.pickLatestSubkeyBindingSignature( - keys, subkey, policy, referenceDate) - ?.let { subkeyBindings[subkey.keyID] = it } - } - } - } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/OpenPGPKeyUpdater.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/OpenPGPKeyUpdater.kt new file mode 100644 index 00000000..d0c5c524 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/OpenPGPKeyUpdater.kt @@ -0,0 +1,222 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification.secretkeyring + +import java.util.* +import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites.Combination +import org.bouncycastle.openpgp.api.KeyPairGeneratorCallback +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.api.OpenPGPKeyEditor +import org.bouncycastle.openpgp.api.SignatureParameters +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.Feature +import org.pgpainless.bouncycastle.PolicyAdapter +import org.pgpainless.bouncycastle.extensions.getKeyVersion +import org.pgpainless.bouncycastle.extensions.toAEADCipherModes +import org.pgpainless.bouncycastle.extensions.toCompressionAlgorithms +import org.pgpainless.bouncycastle.extensions.toHashAlgorithms +import org.pgpainless.bouncycastle.extensions.toSymmetricKeyAlgorithms +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.policy.Policy + +class OpenPGPKeyUpdater( + private var key: OpenPGPKey, + private val protector: SecretKeyRingProtector, + private val api: PGPainless = PGPainless.getInstance(), + private val policy: Policy = api.algorithmPolicy, + private val referenceTime: Date = Date() +) { + + init { + key = + OpenPGPKey( + key.pgpSecretKeyRing, api.implementation, PolicyAdapter(Policy.wildcardPolicy())) + } + + private val keyEditor = OpenPGPKeyEditor(key, protector) + + fun extendExpirationIfExpiresBefore( + expiresBeforeSeconds: Long, + newExpirationTimeSecondsFromNow: Long? = _5YEARS + ) = apply { + require(expiresBeforeSeconds > 0) { + "Time period to check expiration within MUST be positive." + } + require(newExpirationTimeSecondsFromNow == null || newExpirationTimeSecondsFromNow > 0) { + "New expiration period MUST be null or positive." + } + } + + fun replaceRejectedAlgorithmPreferencesAndFeatures(addNewAlgorithms: Boolean = false) = apply { + val features = key.primaryKey.getFeatures(referenceTime)?.features ?: 0 + val newFeatures = + Feature.fromBitmask(features.toInt()) + // Filter out unsupported features + .filter { policy.featurePolicy.isAcceptable(it) } + .toSet() + // Optionally add in new capabilities + .plus( + if (addNewAlgorithms) policy.keyGenerationAlgorithmSuite.features ?: listOf() + else listOf()) + .toTypedArray() + .let { Feature.toBitmask(*it) } + + // Hash Algs + val hashAlgs = key.primaryKey.hashAlgorithmPreferences.toHashAlgorithms() + val newHashAlgs = + hashAlgs + // Filter out unsupported hash algorithms + .filter { policy.dataSignatureHashAlgorithmPolicy.isAcceptable(it) } + // Optionally add in new hash algorithms + .plus( + if (addNewAlgorithms) + policy.keyGenerationAlgorithmSuite.hashAlgorithms ?: listOf() + else listOf()) + .toSet() + + // Sym Algs + val symAlgs = key.primaryKey.symmetricCipherPreferences.toSymmetricKeyAlgorithms() + val newSymAlgs = + symAlgs + .filter { + policy.messageEncryptionAlgorithmPolicy.symmetricAlgorithmPolicy.isAcceptable( + it) + } + .plus( + if (addNewAlgorithms) + policy.keyGenerationAlgorithmSuite.symmetricKeyAlgorithms ?: listOf() + else listOf()) + .toSet() + + // Comp Algs + val compAlgs = key.primaryKey.compressionAlgorithmPreferences.toCompressionAlgorithms() + val newCompAlgs = + compAlgs + .filter { policy.compressionAlgorithmPolicy.isAcceptable(it) } + .plus( + if (addNewAlgorithms) + policy.keyGenerationAlgorithmSuite.compressionAlgorithms ?: listOf() + else listOf()) + .toSet() + + // AEAD Prefs + val aeadAlgs = key.primaryKey.aeadCipherSuitePreferences.toAEADCipherModes() + val newAeadAlgs = + aeadAlgs + .filter { + policy.messageEncryptionAlgorithmPolicy.isAcceptable( + MessageEncryptionMechanism.aead( + it.ciphermode.algorithmId, it.aeadAlgorithm.algorithmId)) + } + .plus(policy.keyGenerationAlgorithmSuite.aeadAlgorithms ?: listOf()) + .toSet() + + if (features != newFeatures || + hashAlgs != newHashAlgs || + symAlgs != newSymAlgs || + compAlgs != newCompAlgs || + aeadAlgs != newAeadAlgs) { + keyEditor.addDirectKeySignature( + SignatureParameters.Callback.Util.modifyHashedSubpackets { sigGen -> + sigGen.apply { + setKeyFlags(key.primaryKey.keyFlags?.flags ?: 0) + setFeature(true, newFeatures) + setPreferredHashAlgorithms( + true, newHashAlgs.map { it.algorithmId }.toIntArray()) + setPreferredSymmetricAlgorithms( + true, newSymAlgs.map { it.algorithmId }.toIntArray()) + setPreferredCompressionAlgorithms( + true, newCompAlgs.map { it.algorithmId }.toIntArray()) + setPreferredAEADCiphersuites( + true, + newAeadAlgs + .map { + Combination( + it.ciphermode.algorithmId, it.aeadAlgorithm.algorithmId) + } + .toTypedArray()) + } + }) + } + } + + fun replaceWeakSubkeys( + revokeWeakKeys: Boolean = true, + signingKeysOnly: Boolean + ): OpenPGPKeyUpdater = apply { + replaceWeakSigningSubkeys(revokeWeakKeys) + if (!signingKeysOnly) { + replaceWeakEncryptionSubkeys(revokeWeakKeys) + } + } + + fun replaceWeakEncryptionSubkeys( + revokeWeakKeys: Boolean, + keyPairGeneratorCallback: KeyPairGeneratorCallback = + KeyPairGeneratorCallback.Util.encryptionKey() + ) { + val weakEncryptionKeys = + key.getEncryptionKeys(referenceTime).filterNot { + policy.publicKeyAlgorithmPolicy.isAcceptable( + it.algorithm, it.pgpPublicKey.bitStrength) + } + + if (weakEncryptionKeys.isNotEmpty()) { + keyEditor.addEncryptionSubkey(keyPairGeneratorCallback) + } + + if (revokeWeakKeys) { + weakEncryptionKeys + .filterNot { it.keyIdentifier.matches(key.primaryKey.keyIdentifier) } + .forEach { keyEditor.revokeComponentKey(it) } + } + } + + fun replaceWeakSigningSubkeys( + revokeWeakKeys: Boolean, + keyPairGenerator: PGPKeyPairGenerator = provideKeyPairGenerator(), + keyPairGeneratorCallback: KeyPairGeneratorCallback = + KeyPairGeneratorCallback.Util.signingKey() + ) { + val weakSigningKeys = + key.getSigningKeys(referenceTime).filterNot { + policy.publicKeyAlgorithmPolicy.isAcceptable( + it.algorithm, it.pgpPublicKey.bitStrength) + } + + if (weakSigningKeys.isNotEmpty()) { + keyEditor.addSigningSubkey(keyPairGeneratorCallback) + } + + if (revokeWeakKeys) { + weakSigningKeys + .filterNot { it.keyIdentifier.matches(key.primaryKey.keyIdentifier) } + .forEach { keyEditor.revokeComponentKey(it) } + } + + keyPairGeneratorCallback.generateFrom(keyPairGenerator) + } + + private fun provideKeyPairGenerator(): PGPKeyPairGenerator { + return api.implementation + .pgpKeyPairGeneratorProvider() + .get(key.primaryKey.getKeyVersion().numeric, referenceTime) + } + + fun finish(): OpenPGPKey { + return keyEditor.done() + } + + companion object { + const val SECOND: Long = 1000 + const val MINUTE: Long = 60 * SECOND + const val HOUR: Long = 60 * MINUTE + const val DAY: Long = 24 * HOUR + const val YEAR: Long = 365 * DAY + const val _5YEARS: Long = 5 * YEAR + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt index ec93c6d6..c98fd4bd 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -8,20 +8,27 @@ import java.util.* import java.util.function.Predicate import javax.annotation.Nonnull import kotlin.NoSuchElementException -import openpgp.openPgpKeyId +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.bcpg.sig.KeyExpirationTime import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPSubkey +import org.bouncycastle.openpgp.api.OpenPGPImplementation +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPSecretKey +import org.bouncycastle.openpgp.api.OpenPGPKeyEditor +import org.bouncycastle.openpgp.api.OpenPGPSignature +import org.bouncycastle.openpgp.api.SignatureParameters import org.pgpainless.PGPainless -import org.pgpainless.PGPainless.Companion.inspectKeyRing import org.pgpainless.algorithm.AlgorithmSuite -import org.pgpainless.algorithm.Feature import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.OpenPGPKeyVersion import org.pgpainless.algorithm.SignatureType import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator +import org.pgpainless.bouncycastle.extensions.checksumCalculator import org.pgpainless.bouncycastle.extensions.getKeyExpirationDate import org.pgpainless.bouncycastle.extensions.publicKeyAlgorithm -import org.pgpainless.bouncycastle.extensions.requirePublicKey -import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.generation.KeyRingBuilder import org.pgpainless.key.generation.KeySpec @@ -37,55 +44,69 @@ import org.pgpainless.util.Passphrase import org.pgpainless.util.selection.userid.SelectUserId class SecretKeyRingEditor( - var secretKeyRing: PGPSecretKeyRing, + var key: OpenPGPKey, + val api: PGPainless = PGPainless.getInstance(), override val referenceTime: Date = Date() ) : SecretKeyRingEditorInterface { + @JvmOverloads + constructor( + secretKeyRing: PGPSecretKeyRing, + api: PGPainless = PGPainless.getInstance(), + referenceTime: Date = Date() + ) : this(PGPainless.getInstance().toKey(secretKeyRing), api, referenceTime) + override fun addUserId( userId: CharSequence, callback: SelfSignatureSubpackets.Callback?, protector: SecretKeyRingProtector ): SecretKeyRingEditorInterface { - val sanitizedUserId = sanitizeUserId(userId).toString() - val primaryKey = secretKeyRing.secretKey - - val info = inspectKeyRing(secretKeyRing, referenceTime) + val info = api.inspect(key, referenceTime) require(!info.isHardRevoked(userId)) { "User-ID $userId is hard revoked and cannot be re-certified." } - val ( - hashAlgorithmPreferences, - symmetricKeyAlgorithmPreferences, - compressionAlgorithmPreferences) = - try { - Triple( - info.preferredHashAlgorithms, - info.preferredSymmetricKeyAlgorithms, - info.preferredCompressionAlgorithms) - } catch (e: IllegalStateException) { // missing user-id sig - val algorithmSuite = AlgorithmSuite.defaultAlgorithmSuite - Triple( - algorithmSuite.hashAlgorithms, - algorithmSuite.symmetricKeyAlgorithms, - algorithmSuite.compressionAlgorithms) - } + val hashAlgorithmPreferences = + info.preferredHashAlgorithms ?: AlgorithmSuite.defaultHashAlgorithms + val symmetricAlgorithmPreferences = + info.preferredSymmetricKeyAlgorithms ?: AlgorithmSuite.defaultSymmetricKeyAlgorithms + val compressionAlgorithmPreferences = + info.preferredCompressionAlgorithms ?: AlgorithmSuite.defaultCompressionAlgorithms + val aeadAlgorithmPreferences = + info.preferredAEADCipherSuites ?: AlgorithmSuite.defaultAEADAlgorithmSuites - val builder = - SelfSignatureBuilder(primaryKey, protector).apply { - hashedSubpackets.setSignatureCreationTime(referenceTime) - setSignatureType(SignatureType.POSITIVE_CERTIFICATION) - } - builder.hashedSubpackets.apply { - setKeyFlags(info.getKeyFlagsOf(primaryKey.keyID)) - setPreferredHashAlgorithms(hashAlgorithmPreferences) - setPreferredSymmetricKeyAlgorithms(symmetricKeyAlgorithmPreferences) - setPreferredCompressionAlgorithms(compressionAlgorithmPreferences) - setFeatures(Feature.MODIFICATION_DETECTION) - } - builder.applyCallback(callback) - secretKeyRing = - injectCertification(secretKeyRing, sanitizedUserId, builder.build(sanitizedUserId)) + key = + OpenPGPKeyEditor(key, protector) + .addUserId( + sanitizeUserId(userId).toString(), + object : SignatureParameters.Callback { + override fun apply(parameters: SignatureParameters): SignatureParameters { + return parameters + .setSignatureCreationTime(referenceTime) + .setHashedSubpacketsFunction { subpacketGenerator -> + val subpackets = SignatureSubpackets(subpacketGenerator) + subpackets.setAppropriateIssuerInfo(key.primaryKey.pgpPublicKey) + + subpackets.setKeyFlags(info.getKeyFlagsOf(key.keyIdentifier)) + subpackets.setPreferredHashAlgorithms(hashAlgorithmPreferences) + subpackets.setPreferredSymmetricKeyAlgorithms( + symmetricAlgorithmPreferences) + subpackets.setPreferredCompressionAlgorithms( + compressionAlgorithmPreferences) + subpackets.setPreferredAEADCiphersuites( + aeadAlgorithmPreferences) + + callback?.modifyHashedSubpackets(subpackets) + subpacketGenerator + } + .setUnhashedSubpacketsFunction { subpacketGenerator -> + callback?.modifyUnhashedSubpackets( + SignatureSubpackets(subpacketGenerator)) + subpacketGenerator + } + } + }) + .done() return this } @@ -94,8 +115,8 @@ class SecretKeyRingEditor( protector: SecretKeyRingProtector ): SecretKeyRingEditorInterface { val uid = sanitizeUserId(userId) - val primaryKey = secretKeyRing.publicKey - var info = inspectKeyRing(secretKeyRing, referenceTime) + val primaryKey = key.primaryKey.pgpPublicKey + var info = api.inspect(key, referenceTime) val primaryUserId = info.primaryUserId val signature = if (primaryUserId == null) info.latestDirectKeySelfSignature @@ -118,7 +139,7 @@ class SecretKeyRingEditor( protector) // unmark previous primary user-ids to be non-primary - info = inspectKeyRing(secretKeyRing, referenceTime) + info = api.inspect(key, referenceTime) info.validAndExpiredUserIds .filterNot { it == uid } .forEach { otherUserId -> @@ -189,7 +210,7 @@ class SecretKeyRingEditor( require(oldUID.isNotBlank()) { "Old user-ID cannot be empty." } require(newUID.isNotBlank()) { "New user-ID cannot be empty." } - val info = inspectKeyRing(secretKeyRing, referenceTime) + val info = api.inspect(key, referenceTime) if (!info.isUserIdValid(oldUID)) { throw NoSuchElementException( "Key does not carry user-ID '$oldUID', or it is not valid.") @@ -218,7 +239,6 @@ class SecretKeyRingEditor( } }, protector) - return revokeUserId(oldUID, protector) } @@ -230,8 +250,22 @@ class SecretKeyRingEditor( val callback = object : SelfSignatureSubpackets.Callback { override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { - SignatureSubpacketsHelper.applyFrom( - keySpec.subpackets, hashedSubpackets as SignatureSubpackets) + hashedSubpackets.apply { + setKeyFlags(keySpec.keyFlags) + keySpec.preferredHashAlgorithmsOverride?.let { + setPreferredHashAlgorithms(it) + } + keySpec.preferredCompressionAlgorithmsOverride?.let { + setPreferredCompressionAlgorithms(it) + } + keySpec.preferredSymmetricAlgorithmsOverride?.let { + setPreferredSymmetricKeyAlgorithms(it) + } + keySpec.preferredAEADAlgorithmsOverride?.let { + setPreferredAEADCiphersuites(it) + } + keySpec.featuresOverride?.let { setFeatures(*it.toTypedArray()) } + } hashedSubpackets.setSignatureCreationTime(referenceTime) } } @@ -244,10 +278,11 @@ class SecretKeyRingEditor( callback: SelfSignatureSubpackets.Callback?, protector: SecretKeyRingProtector ): SecretKeyRingEditorInterface { - val keyPair = KeyRingBuilder.generateKeyPair(keySpec, referenceTime) + val version = OpenPGPKeyVersion.from(key.primarySecretKey.version) + val keyPair = KeyRingBuilder.generateKeyPair(keySpec, version, api.implementation) val subkeyProtector = - PasswordBasedSecretKeyRingProtector.forKeyId(keyPair.keyID, subkeyPassphrase) - val keyFlags = KeyFlag.fromBitmask(keySpec.subpackets.keyFlags).toMutableList() + PasswordBasedSecretKeyRingProtector.forKeyId(keyPair.keyIdentifier, subkeyPassphrase) + val keyFlags = keySpec.keyFlags.toMutableList() return addSubKey( keyPair, callback, @@ -271,33 +306,41 @@ class SecretKeyRingEditor( val bitStrength = subkey.publicKey.bitStrength require( - PGPainless.getPolicy() - .publicKeyAlgorithmPolicy - .isAcceptable(subkeyAlgorithm, bitStrength)) { + api.algorithmPolicy.publicKeyAlgorithmPolicy.isAcceptable( + subkeyAlgorithm, bitStrength)) { "Public key algorithm policy violation: $subkeyAlgorithm with bit strength $bitStrength is not acceptable." } - val primaryKey = secretKeyRing.secretKey - val info = inspectKeyRing(secretKeyRing, referenceTime) + val primaryKey = key.primarySecretKey.pgpSecretKey + val info = api.inspect(key, referenceTime) val hashAlgorithm = - HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) + HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(api.algorithmPolicy) .negotiateHashAlgorithm(info.preferredHashAlgorithms) var secretSubkey = PGPSecretKey( subkey.privateKey, subkey.publicKey, - ImplementationFactory.getInstance().v4FingerprintCalculator, + OpenPGPImplementation.getInstance().checksumCalculator(), false, - subkeyProtector.getEncryptor(subkey.keyID)) + subkeyProtector.getEncryptor(subkey.publicKey)) + + val componentKey = + OpenPGPSecretKey( + OpenPGPSubkey(subkey.publicKey, key), + secretSubkey, + PGPainless.getInstance().implementation.pbeSecretKeyDecryptorBuilderProvider()) + val skBindingBuilder = - SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector, hashAlgorithm) + SubkeyBindingSignatureBuilder( + key.primarySecretKey, primaryKeyProtector, hashAlgorithm, api) skBindingBuilder.apply { hashedSubpackets.setSignatureCreationTime(referenceTime) hashedSubpackets.setKeyFlags(flags) if (subkeyAlgorithm.isSigningCapable()) { val pkBindingBuilder = - PrimaryKeyBindingSignatureBuilder(secretSubkey, subkeyProtector, hashAlgorithm) + PrimaryKeyBindingSignatureBuilder( + componentKey, subkeyProtector, hashAlgorithm, api) pkBindingBuilder.hashedSubpackets.setSignatureCreationTime(referenceTime) hashedSubpackets.addEmbeddedSignature(pkBindingBuilder.build(primaryKey.publicKey)) } @@ -306,7 +349,8 @@ class SecretKeyRingEditor( secretSubkey = KeyRingUtils.secretKeyPlusSignature( secretSubkey, skBindingBuilder.build(secretSubkey.publicKey)) - secretKeyRing = KeyRingUtils.keysPlusSecretKey(secretKeyRing, secretSubkey) + val secretKeyRing = KeyRingUtils.keysPlusSecretKey(key.pgpSecretKeyRing, secretSubkey) + key = api.toKey(secretKeyRing) return this } @@ -321,26 +365,33 @@ class SecretKeyRingEditor( protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback? ): SecretKeyRingEditorInterface { - return revokeSubKey(secretKeyRing.secretKey.keyID, protector, callback) + return revokeSubKey(key.keyIdentifier, protector, callback) } override fun revokeSubKey( - subkeyId: Long, + subkeyIdentifier: KeyIdentifier, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? ): SecretKeyRingEditorInterface { return revokeSubKey( - subkeyId, protector, callbackFromRevocationAttributes(revocationAttributes)) + subkeyIdentifier, protector, callbackFromRevocationAttributes(revocationAttributes)) } override fun revokeSubKey( - subkeyId: Long, + subkeyIdentifier: KeyIdentifier, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback? ): SecretKeyRingEditorInterface { - val revokeeSubKey = secretKeyRing.requirePublicKey(subkeyId) + var secretKeyRing = key.pgpSecretKeyRing + val revokeeSubKey = + key.getKey(subkeyIdentifier) + ?: throw NoSuchElementException( + "Certificate ${key.keyIdentifier} does not contain subkey $subkeyIdentifier") val subkeyRevocation = generateRevocation(protector, revokeeSubKey, callback) - secretKeyRing = injectCertification(secretKeyRing, revokeeSubKey, subkeyRevocation) + secretKeyRing = + injectCertification( + secretKeyRing, revokeeSubKey.pgpPublicKey, subkeyRevocation.signature) + key = api.toKey(secretKeyRing) return this } @@ -416,6 +467,7 @@ class SecretKeyRingEditor( expiration: Date?, protector: SecretKeyRingProtector ): SecretKeyRingEditorInterface { + var secretKeyRing = key.pgpSecretKeyRing require(secretKeyRing.secretKey.isMasterKey) { "OpenPGP key does not appear to contain a primary secret key." } @@ -427,11 +479,12 @@ class SecretKeyRingEditor( injectCertification( secretKeyRing, secretKeyRing.publicKey, - reissueDirectKeySignature(expiration, protector, prevDirectKeySig)) + reissueDirectKeySignature(expiration, protector, prevDirectKeySig).signature) } - val primaryUserId = - inspectKeyRing(secretKeyRing, referenceTime).getPossiblyExpiredPrimaryUserId() + val info = api.inspect(key, referenceTime) + + val primaryUserId = info.getPossiblyExpiredPrimaryUserId() if (primaryUserId != null) { val prevUserIdSig = getPreviousUserIdSignatures(primaryUserId) val userIdSig = @@ -439,7 +492,6 @@ class SecretKeyRingEditor( secretKeyRing = injectCertification(secretKeyRing, primaryUserId, userIdSig) } - val info = inspectKeyRing(secretKeyRing, referenceTime) for (userId in info.validUserIds) { if (userId == primaryUserId) { continue @@ -458,35 +510,40 @@ class SecretKeyRingEditor( } } + key = api.toKey(secretKeyRing) return this } override fun setExpirationDateOfSubkey( expiration: Date?, - keyId: Long, + keyId: KeyIdentifier, protector: SecretKeyRingProtector ): SecretKeyRingEditorInterface = apply { + var secretKeyRing = key.pgpSecretKeyRing + // is primary key - if (keyId == secretKeyRing.publicKey.keyID) { + if (keyId.matchesExplicit(key.keyIdentifier)) { return setExpirationDate(expiration, protector) } // is subkey val subkey = - secretKeyRing.getPublicKey(keyId) - ?: throw NoSuchElementException("No subkey with ID ${keyId.openPgpKeyId()} found.") + key.getKey(keyId) ?: throw NoSuchElementException("No subkey with ID $keyId found.") val prevBinding = - inspectKeyRing(secretKeyRing).getCurrentSubkeyBindingSignature(keyId) + api.inspect(key).getCurrentSubkeyBindingSignature(keyId) ?: throw NoSuchElementException( - "Previous subkey binding signaure for ${keyId.openPgpKeyId()} MUST NOT be null.") + "Previous subkey binding signature for $keyId MUST NOT be null.") val bindingSig = reissueSubkeyBindingSignature(subkey, expiration, protector, prevBinding) - secretKeyRing = injectCertification(secretKeyRing, subkey, bindingSig) + secretKeyRing = + injectCertification(secretKeyRing, subkey.pgpPublicKey, bindingSig.signature) + + key = api.toKey(secretKeyRing) } override fun createMinimalRevocationCertificate( protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? - ): PGPPublicKeyRing { + ): OpenPGPCertificate { // Check reason if (revocationAttributes != null) { require(RevocationAttributes.Reason.isKeyRevocation(revocationAttributes.reason)) { @@ -495,49 +552,47 @@ class SecretKeyRingEditor( } val revocation = createRevocation(protector, revocationAttributes) - var primaryKey = secretKeyRing.secretKey.publicKey + var primaryKey = key.primaryKey.pgpPublicKey primaryKey = KeyRingUtils.getStrippedDownPublicKey(primaryKey) - primaryKey = PGPPublicKey.addCertification(primaryKey, revocation) - return PGPPublicKeyRing(listOf(primaryKey)) + primaryKey = PGPPublicKey.addCertification(primaryKey, revocation.signature) + return api.toCertificate(PGPPublicKeyRing(listOf(primaryKey))) } override fun createRevocation( protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? - ): PGPSignature { + ): OpenPGPSignature { return generateRevocation( - protector, - secretKeyRing.publicKey, - callbackFromRevocationAttributes(revocationAttributes)) + protector, key.primaryKey, callbackFromRevocationAttributes(revocationAttributes)) } override fun createRevocation( - subkeyId: Long, + subkeyIdentifier: KeyIdentifier, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? - ): PGPSignature { + ): OpenPGPSignature { return generateRevocation( protector, - secretKeyRing.requirePublicKey(subkeyId), + key.getKey(subkeyIdentifier), callbackFromRevocationAttributes(revocationAttributes)) } override fun createRevocation( - subkeyId: Long, + subkeyIdentifier: KeyIdentifier, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback? - ): PGPSignature { - return generateRevocation(protector, secretKeyRing.requirePublicKey(subkeyId), callback) + ): OpenPGPSignature { + return generateRevocation(protector, key.getKey(subkeyIdentifier), callback) } override fun createRevocation( subkeyFingerprint: OpenPgpFingerprint, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? - ): PGPSignature { + ): OpenPGPSignature { return generateRevocation( protector, - secretKeyRing.requirePublicKey(subkeyFingerprint), + key.getKey(subkeyFingerprint.keyIdentifier), callbackFromRevocationAttributes(revocationAttributes)) } @@ -553,25 +608,26 @@ class SecretKeyRingEditor( } override fun changeSubKeyPassphraseFromOldPassphrase( - keyId: Long, + keyIdentifier: KeyIdentifier, oldPassphrase: Passphrase, oldProtectionSettings: KeyRingProtectionSettings ): SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { return WithKeyRingEncryptionSettingsImpl( this, - keyId, + keyIdentifier, CachingSecretKeyRingProtector( - mapOf(keyId to oldPassphrase), oldProtectionSettings, null)) + mapOf(keyIdentifier to oldPassphrase), oldProtectionSettings, null)) } - override fun done(): PGPSecretKeyRing { - return secretKeyRing + override fun done(): OpenPGPKey { + return key } private fun sanitizeUserId(userId: CharSequence): CharSequence = - // TODO: Further research how to sanitize user IDs. - // e.g. what about newlines? - userId.toString().trim() + // I'm not sure, what kind of sanitization is needed. + // Newlines are allowed, they just need to be escaped when emitted in an ASCII armor header + // Trailing/Leading whitespace is also fine. + userId.toString() private fun callbackFromRevocationAttributes(attributes: RevocationAttributes?) = object : RevocationSignatureSubpackets.Callback { @@ -584,15 +640,14 @@ class SecretKeyRingEditor( private fun generateRevocation( protector: SecretKeyRingProtector, - revokeeSubkey: PGPPublicKey, + revokeeSubkey: OpenPGPComponentKey, callback: RevocationSignatureSubpackets.Callback? - ): PGPSignature { - val primaryKey = secretKeyRing.secretKey + ): OpenPGPSignature { val signatureType = - if (revokeeSubkey.isMasterKey) SignatureType.KEY_REVOCATION + if (revokeeSubkey.isPrimaryKey) SignatureType.KEY_REVOCATION else SignatureType.SUBKEY_REVOCATION - return RevocationSignatureBuilder(signatureType, primaryKey, protector) + return RevocationSignatureBuilder(signatureType, key.primarySecretKey, protector, api) .apply { applyCallback(callback) } .build(revokeeSubkey) } @@ -603,25 +658,26 @@ class SecretKeyRingEditor( callback: RevocationSignatureSubpackets.Callback? ): SecretKeyRingEditorInterface { RevocationSignatureBuilder( - SignatureType.CERTIFICATION_REVOCATION, secretKeyRing.secretKey, protector) + SignatureType.CERTIFICATION_REVOCATION, key.primarySecretKey, protector, api) .apply { hashedSubpackets.setSignatureCreationTime(referenceTime) applyCallback(callback) } .let { - secretKeyRing = - injectCertification(secretKeyRing, userId, it.build(userId.toString())) + val secretKeyRing = + injectCertification(key.pgpSecretKeyRing, userId, it.build(userId.toString())) + key = api.toKey(secretKeyRing) } return this } private fun getPreviousDirectKeySignature(): PGPSignature? { - val info = inspectKeyRing(secretKeyRing, referenceTime) + val info = api.inspect(key, referenceTime) return info.latestDirectKeySelfSignature } private fun getPreviousUserIdSignatures(userId: String): PGPSignature? { - val info = inspectKeyRing(secretKeyRing, referenceTime) + val info = api.inspect(key, referenceTime) return info.getLatestUserIdCertification(userId) } @@ -632,7 +688,7 @@ class SecretKeyRingEditor( prevUserIdSig: PGPSignature ): PGPSignature { val builder = - SelfSignatureBuilder(secretKeyRing.secretKey, secretKeyRingProtector, prevUserIdSig) + SelfSignatureBuilder(key.primarySecretKey, secretKeyRingProtector, prevUserIdSig, api) builder.hashedSubpackets.setSignatureCreationTime(referenceTime) builder.applyCallback( object : SelfSignatureSubpackets.Callback { @@ -651,7 +707,8 @@ class SecretKeyRingEditor( @Nonnull primaryUserId: String, @Nonnull prevUserIdSig: PGPSignature ): PGPSignature { - return SelfSignatureBuilder(secretKeyRing.secretKey, secretKeyRingProtector, prevUserIdSig) + return SelfSignatureBuilder( + key.primarySecretKey, secretKeyRingProtector, prevUserIdSig, api) .apply { hashedSubpackets.setSignatureCreationTime(referenceTime) applyCallback( @@ -661,7 +718,7 @@ class SecretKeyRingEditor( ) { if (expiration != null) { hashedSubpackets.setKeyExpirationTime( - true, secretKeyRing.publicKey.creationTime, expiration) + true, key.primaryKey.creationTime, expiration) } else { hashedSubpackets.setKeyExpirationTime(KeyExpirationTime(true, 0)) } @@ -677,9 +734,10 @@ class SecretKeyRingEditor( expiration: Date?, secretKeyRingProtector: SecretKeyRingProtector, prevDirectKeySig: PGPSignature - ): PGPSignature { + ): OpenPGPSignature { + val secretKeyRing = key.pgpSecretKeyRing return DirectKeySelfSignatureBuilder( - secretKeyRing.secretKey, secretKeyRingProtector, prevDirectKeySig) + secretKeyRing, secretKeyRingProtector, prevDirectKeySig, api) .apply { hashedSubpackets.setSignatureCreationTime(referenceTime) applyCallback( @@ -700,21 +758,21 @@ class SecretKeyRingEditor( } private fun reissueSubkeyBindingSignature( - subkey: PGPPublicKey, + subkey: OpenPGPComponentKey, expiration: Date?, protector: SecretKeyRingProtector, prevSubkeyBindingSignature: PGPSignature - ): PGPSignature { - val primaryKey = secretKeyRing.publicKey - val secretPrimaryKey = secretKeyRing.secretKey - val secretSubkey: PGPSecretKey? = secretKeyRing.getSecretKey(subkey.keyID) + ): OpenPGPSignature { + val primaryKey = key.primaryKey + val secretSubkey: OpenPGPSecretKey? = key.getSecretKey(subkey) val builder = - SubkeyBindingSignatureBuilder(secretPrimaryKey, protector, prevSubkeyBindingSignature) + SubkeyBindingSignatureBuilder( + key.primarySecretKey, protector, prevSubkeyBindingSignature, api) builder.hashedSubpackets.apply { // set expiration setSignatureCreationTime(referenceTime) - setKeyExpirationTime(subkey, expiration) + setKeyExpirationTime(subkey.pgpPublicKey, expiration) setSignatureExpirationTime(null) // avoid copying sig exp time // signing-capable subkeys need embedded primary key binding sig @@ -723,14 +781,16 @@ class SecretKeyRingEditor( if (secretSubkey == null) { throw NoSuchElementException( "Secret key does not contain secret-key" + - " component for subkey ${subkey.keyID.openPgpKeyId()}") + " component for subkey ${subkey.keyIdentifier}") } // create new embedded back-sig clearEmbeddedSignatures() addEmbeddedSignature( - PrimaryKeyBindingSignatureBuilder(secretSubkey, protector) - .build(primaryKey)) + PrimaryKeyBindingSignatureBuilder( + key.getSecretKey(subkey.keyIdentifier), protector, api) + .build(primaryKey) + .signature) } } } @@ -739,11 +799,11 @@ class SecretKeyRingEditor( } private fun selectUserIds(predicate: Predicate): List = - inspectKeyRing(secretKeyRing).validUserIds.filter { predicate.test(it) } + key.validUserIds.map { it.userId }.filter { predicate.test(it) }.toList() private class WithKeyRingEncryptionSettingsImpl( private val editor: SecretKeyRingEditor, - private val keyId: Long?, + private val keyId: KeyIdentifier?, private val oldProtector: SecretKeyRingProtector ) : SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { @@ -760,7 +820,7 @@ class SecretKeyRingEditor( private class WithPassphraseImpl( private val editor: SecretKeyRingEditor, - private val keyId: Long?, + private val keyId: KeyIdentifier?, private val oldProtector: SecretKeyRingProtector, private val newProtectionSettings: KeyRingProtectionSettings ) : SecretKeyRingEditorInterface.WithPassphrase { @@ -769,15 +829,17 @@ class SecretKeyRingEditor( val protector = PasswordBasedSecretKeyRingProtector( newProtectionSettings, SolitaryPassphraseProvider(passphrase)) - val secretKeys = changePassphrase(keyId, editor.secretKeyRing, oldProtector, protector) - editor.secretKeyRing = secretKeys + val secretKeys = + changePassphrase(keyId, editor.key.pgpSecretKeyRing, oldProtector, protector) + editor.key = editor.api.toKey(secretKeys) return editor } override fun toNoPassphrase(): SecretKeyRingEditorInterface { val protector = UnprotectedKeysProtector() - val secretKeys = changePassphrase(keyId, editor.secretKeyRing, oldProtector, protector) - editor.secretKeyRing = secretKeys + val secretKeys = + changePassphrase(keyId, editor.key.pgpSecretKeyRing, oldProtector, protector) + editor.key = editor.api.toKey(secretKeys) return editor } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt index 140ff905..4a1f70ff 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt @@ -8,7 +8,11 @@ import java.io.IOException import java.security.InvalidAlgorithmParameterException import java.security.NoSuchAlgorithmException import java.util.* +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.api.OpenPGPSignature import org.pgpainless.algorithm.KeyFlag import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.generation.KeySpec @@ -261,26 +265,37 @@ interface SecretKeyRingEditorInterface { protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? = null ): SecretKeyRingEditorInterface = - revokeSubKey(fingerprint.keyId, protector, revocationAttributes) + revokeSubKey(fingerprint.keyIdentifier, protector, revocationAttributes) + + @Deprecated("Pass in a KeyIdentifier instead of keyId") + fun revokeSubKey(subkeyId: Long, protector: SecretKeyRingProtector) = + revokeSubKey(KeyIdentifier(subkeyId), protector) /** * Revoke the subkey binding signature of a subkey. The subkey with the provided key-id will be * revoked. If no suitable subkey is found, a [NoSuchElementException] will be thrown. * - * @param subkeyId id of the subkey + * @param subkeyIdentifier id of the subkey * @param protector protector to unlock the primary key * @return the builder * @throws PGPException in case we cannot generate a revocation signature for the subkey */ @Throws(PGPException::class) - fun revokeSubKey(subkeyId: Long, protector: SecretKeyRingProtector) = - revokeSubKey(subkeyId, protector, null as RevocationAttributes?) + fun revokeSubKey(subkeyIdentifier: KeyIdentifier, protector: SecretKeyRingProtector) = + revokeSubKey(subkeyIdentifier, protector, null as RevocationAttributes?) + + @Deprecated("Pass in a KeyIdentifier instead of keyId") + fun revokeSubKey( + subkeyId: Long, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null + ) = revokeSubKey(KeyIdentifier(subkeyId), protector, revocationAttributes) /** * Revoke the subkey binding signature of a subkey. The subkey with the provided key-id will be * revoked. If no suitable subkey is found, a [NoSuchElementException] will be thrown. * - * @param subkeyId id of the subkey + * @param subkeyIdentifier id of the subkey * @param protector protector to unlock the primary key * @param revocationAttributes reason for the revocation * @return the builder @@ -288,18 +303,25 @@ interface SecretKeyRingEditorInterface { */ @Throws(PGPException::class) fun revokeSubKey( - subkeyId: Long, + subkeyIdentifier: KeyIdentifier, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? = null ): SecretKeyRingEditorInterface + @Deprecated("Pass in a KeyIdentifier instead of keyId.") + fun revokeSubKey( + keyId: Long, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ) = revokeSubKey(KeyIdentifier(keyId), protector, callback) + /** * Revoke the subkey binding signature of a subkey. The subkey with the provided key-id will be * revoked. If no suitable subkey is found, a [NoSuchElementException] will be thrown. * * The provided subpackets callback is used to modify the revocation signatures subpackets. * - * @param subkeyId id of the subkey + * @param subkeyIdentifier id of the subkey * @param protector protector to unlock the secret key ring * @param callback callback which can be used to modify the subpackets of the revocation * signature @@ -308,7 +330,7 @@ interface SecretKeyRingEditorInterface { */ @Throws(PGPException::class) fun revokeSubKey( - subkeyId: Long, + subkeyIdentifier: KeyIdentifier, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback? ): SecretKeyRingEditorInterface @@ -469,6 +491,14 @@ interface SecretKeyRingEditorInterface { protector: SecretKeyRingProtector ): SecretKeyRingEditorInterface + @Deprecated("Pass in a KeyIdentifier instead of keyId") + @Throws(PGPException::class) + fun setExpirationDateOfSubkey( + expiration: Date?, + keyId: Long, + protector: SecretKeyRingProtector + ) = setExpirationDateOfSubkey(expiration, KeyIdentifier(keyId), protector) + /** * Set the expiration date for the subkey identified by the given keyId to the given expiration * date. If the key is supposed to never expire, then an expiration date of null is expected. @@ -483,7 +513,7 @@ interface SecretKeyRingEditorInterface { @Throws(PGPException::class) fun setExpirationDateOfSubkey( expiration: Date?, - keyId: Long, + keyId: KeyIdentifier, protector: SecretKeyRingProtector ): SecretKeyRingEditorInterface @@ -501,7 +531,7 @@ interface SecretKeyRingEditorInterface { fun createMinimalRevocationCertificate( protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? - ): PGPPublicKeyRing + ): OpenPGPCertificate /** * Create a detached revocation certificate, which can be used to revoke the whole key. The @@ -516,13 +546,21 @@ interface SecretKeyRingEditorInterface { fun createRevocation( protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? - ): PGPSignature + ): OpenPGPSignature + + @Throws(PGPException::class) + @Deprecated("Pass in a KeyIdentifier instead of a keyId") + fun createRevocation( + subkeyId: Long, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ) = createRevocation(KeyIdentifier(subkeyId), protector, revocationAttributes) /** * Create a detached revocation certificate, which can be used to revoke the specified subkey. * The original key will not be modified by this method. * - * @param subkeyId id of the subkey to be revoked + * @param subkeyIdentifier id of the subkey to be revoked * @param protector protector to unlock the primary key. * @param revocationAttributes reason for the revocation * @return revocation certificate @@ -530,16 +568,24 @@ interface SecretKeyRingEditorInterface { */ @Throws(PGPException::class) fun createRevocation( - subkeyId: Long, + subkeyIdentifier: KeyIdentifier, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? - ): PGPSignature + ): OpenPGPSignature + + @Deprecated("Pass in a KeyIdentifier instead of keyId") + @Throws(PGPException::class) + fun createRevocation( + subkeyId: Long, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ) = createRevocation(KeyIdentifier(subkeyId), protector, callback) /** * Create a detached revocation certificate, which can be used to revoke the specified subkey. * The original key will not be modified by this method. * - * @param subkeyId id of the subkey to be revoked + * @param subkeyIdentifier id of the subkey to be revoked * @param protector protector to unlock the primary key. * @param callback callback to modify the subpackets of the revocation certificate. * @return revocation certificate @@ -547,10 +593,10 @@ interface SecretKeyRingEditorInterface { */ @Throws(PGPException::class) fun createRevocation( - subkeyId: Long, + subkeyIdentifier: KeyIdentifier, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback? - ): PGPSignature + ): OpenPGPSignature /** * Create a detached revocation certificate, which can be used to revoke the specified subkey. @@ -567,7 +613,7 @@ interface SecretKeyRingEditorInterface { subkeyFingerprint: OpenPgpFingerprint, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? - ): PGPSignature + ): OpenPGPSignature /** * Change the passphrase of the whole key ring. @@ -592,19 +638,9 @@ interface SecretKeyRingEditorInterface { KeyRingProtectionSettings.secureDefaultSettings() ): WithKeyRingEncryptionSettings - /** - * Change the passphrase of a single subkey in the key ring. - * - * Note: While it is a valid use-case to have different passphrases per subKey, this is one of - * the reasons why OpenPGP sucks in practice. - * - * @param keyId id of the subkey - * @param oldPassphrase old passphrase (empty if the key was unprotected) - * @return next builder step - */ + @Deprecated("Pass KeyIdentifier instead.") fun changeSubKeyPassphraseFromOldPassphrase(keyId: Long, oldPassphrase: Passphrase) = - changeSubKeyPassphraseFromOldPassphrase( - keyId, oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()) + changeSubKeyPassphraseFromOldPassphrase(KeyIdentifier(keyId), oldPassphrase) /** * Change the passphrase of a single subkey in the key ring. @@ -612,13 +648,30 @@ interface SecretKeyRingEditorInterface { * Note: While it is a valid use-case to have different passphrases per subKey, this is one of * the reasons why OpenPGP sucks in practice. * - * @param keyId id of the subkey + * @param keyIdentifier id of the subkey + * @param oldPassphrase old passphrase (empty if the key was unprotected) + * @return next builder step + */ + fun changeSubKeyPassphraseFromOldPassphrase( + keyIdentifier: KeyIdentifier, + oldPassphrase: Passphrase + ) = + changeSubKeyPassphraseFromOldPassphrase( + keyIdentifier, oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()) + + /** + * Change the passphrase of a single subkey in the key ring. + * + * Note: While it is a valid use-case to have different passphrases per subKey, this is one of + * the reasons why OpenPGP sucks in practice. + * + * @param keyIdentifier id of the subkey * @param oldPassphrase old passphrase (empty if the key was unprotected) * @param oldProtectionSettings custom settings for the old passphrase * @return next builder step */ fun changeSubKeyPassphraseFromOldPassphrase( - keyId: Long, + keyIdentifier: KeyIdentifier, oldPassphrase: Passphrase, oldProtectionSettings: KeyRingProtectionSettings ): WithKeyRingEncryptionSettings @@ -668,7 +721,7 @@ interface SecretKeyRingEditorInterface { * * @return the key */ - fun done(): PGPSecretKeyRing + fun done(): OpenPGPKey fun addSubKey( keySpec: KeySpec, diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt index 6f6bde61..e8aeddeb 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt @@ -9,9 +9,8 @@ import java.io.InputStream import java.nio.charset.Charset import kotlin.jvm.Throws import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.api.OpenPGPImplementation import org.bouncycastle.util.io.Streams -import org.pgpainless.PGPainless -import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.collection.PGPKeyRingCollection import org.pgpainless.util.ArmorUtils @@ -130,8 +129,8 @@ class KeyRingReader { maxIterations: Int = MAX_ITERATIONS ): PGPKeyRing? { val objectFactory = - ImplementationFactory.getInstance() - .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) + OpenPGPImplementation.getInstance() + .pgpObjectFactory(ArmorUtils.getDecoderStream(inputStream)) try { for ((i, next) in objectFactory.withIndex()) { @@ -172,8 +171,8 @@ class KeyRingReader { maxIterations: Int = MAX_ITERATIONS ): PGPPublicKeyRing? { val objectFactory = - ImplementationFactory.getInstance() - .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) + OpenPGPImplementation.getInstance() + .pgpObjectFactory(ArmorUtils.getDecoderStream(inputStream)) try { for ((i, next) in objectFactory.withIndex()) { @@ -213,8 +212,8 @@ class KeyRingReader { maxIterations: Int = MAX_ITERATIONS ): PGPPublicKeyRingCollection { val objectFactory = - ImplementationFactory.getInstance() - .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) + OpenPGPImplementation.getInstance() + .pgpObjectFactory(ArmorUtils.getDecoderStream(inputStream)) val certificates = mutableListOf() try { for ((i, next) in objectFactory.withIndex()) { @@ -229,7 +228,7 @@ class KeyRingReader { continue } if (next is PGPSecretKeyRing) { - certificates.add(PGPainless.extractCertificate(next)) + certificates.add(next.toCertificate()) continue } if (next is PGPPublicKeyRingCollection) { @@ -260,8 +259,7 @@ class KeyRingReader { maxIterations: Int = MAX_ITERATIONS ): PGPSecretKeyRing? { val decoderStream = ArmorUtils.getDecoderStream(inputStream) - val objectFactory = - ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream) + val objectFactory = OpenPGPImplementation.getInstance().pgpObjectFactory(decoderStream) try { for ((i, next) in objectFactory.withIndex()) { @@ -300,8 +298,8 @@ class KeyRingReader { maxIterations: Int = MAX_ITERATIONS ): PGPSecretKeyRingCollection { val objectFactory = - ImplementationFactory.getInstance() - .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) + OpenPGPImplementation.getInstance() + .pgpObjectFactory(ArmorUtils.getDecoderStream(inputStream)) val secretKeys = mutableListOf() try { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt index c5db2086..0be20c02 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt @@ -4,9 +4,12 @@ package org.pgpainless.key.protection +import org.bouncycastle.bcpg.KeyIdentifier +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.api.OpenPGPImplementation +import org.bouncycastle.openpgp.api.OpenPGPKey import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor -import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider /** @@ -22,23 +25,38 @@ open class BaseSecretKeyRingProtector( passphraseProvider: SecretKeyPassphraseProvider ) : this(passphraseProvider, KeyRingProtectionSettings.secureDefaultSettings()) - override fun hasPassphraseFor(keyId: Long): Boolean = passphraseProvider.hasPassphrase(keyId) + override fun hasPassphraseFor(keyIdentifier: KeyIdentifier): Boolean { + return passphraseProvider.hasPassphrase(keyIdentifier) + } + @Deprecated("Pass in a KeyIdentifier instead.") override fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? = - passphraseProvider.getPassphraseFor(keyId)?.let { - if (it.isEmpty) null - else ImplementationFactory.getInstance().getPBESecretKeyDecryptor(it) - } + getDecryptor(KeyIdentifier(keyId)) - override fun getEncryptor(keyId: Long): PBESecretKeyEncryptor? = - passphraseProvider.getPassphraseFor(keyId)?.let { + override fun getDecryptor(keyIdentifier: KeyIdentifier): PBESecretKeyDecryptor? = + passphraseProvider.getPassphraseFor(keyIdentifier)?.let { if (it.isEmpty) null else - ImplementationFactory.getInstance() - .getPBESecretKeyEncryptor( - protectionSettings.encryptionAlgorithm, - protectionSettings.hashAlgorithm, - protectionSettings.s2kCount, - it) + OpenPGPImplementation.getInstance() + .pbeSecretKeyDecryptorBuilderProvider() + .provide() + .build(it.getChars()) } + + override fun getEncryptor(key: PGPPublicKey): PBESecretKeyEncryptor? { + return passphraseProvider.getPassphraseFor(key.keyIdentifier)?.let { + if (it.isEmpty) null + else + OpenPGPImplementation.getInstance() + .pbeSecretKeyEncryptorFactory( + protectionSettings.aead, + protectionSettings.encryptionAlgorithm.algorithmId, + protectionSettings.s2kCount) + .build(it.getChars(), key.publicKeyPacket) + } + } + + override fun getKeyPassword(key: OpenPGPKey.OpenPGPSecretKey): CharArray? { + return passphraseProvider.getPassphraseFor(key.keyIdentifier)?.getChars() + } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt index 20704685..51e3fe35 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt @@ -4,9 +4,14 @@ package org.pgpainless.key.protection -import openpgp.openPgpKeyId +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider import org.pgpainless.util.Passphrase @@ -21,7 +26,7 @@ import org.pgpainless.util.Passphrase */ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphraseProvider { - private val cache: MutableMap + private val cache: MutableMap private val protector: SecretKeyRingProtector private val provider: SecretKeyPassphraseProvider? @@ -30,12 +35,12 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras constructor( missingPassphraseCallback: SecretKeyPassphraseProvider? ) : this( - mapOf(), + mapOf(), KeyRingProtectionSettings.secureDefaultSettings(), missingPassphraseCallback) constructor( - passphrases: Map, + passphrases: Map, protectionSettings: KeyRingProtectionSettings, missingPassphraseCallback: SecretKeyPassphraseProvider? ) { @@ -44,6 +49,10 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras this.provider = missingPassphraseCallback } + @Deprecated("Pass KeyIdentifier instead.") + fun addPassphrase(keyId: Long, passphrase: Passphrase) = + addPassphrase(KeyIdentifier(keyId), passphrase) + /** * Add a passphrase to the cache. If the cache already contains a passphrase for the given * key-id, a [IllegalArgumentException] is thrown. The reason for this is to prevent accidental @@ -53,24 +62,30 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras * If you can ensure that there will be no key-id clash, and you want to replace the passphrase, * you can use [replacePassphrase] to replace the passphrase. * - * @param keyId id of the key + * @param keyIdentifier id of the key * @param passphrase passphrase */ - fun addPassphrase(keyId: Long, passphrase: Passphrase) = apply { - require(!cache.containsKey(keyId)) { - "The cache already holds a passphrase for ID ${keyId.openPgpKeyId()}.\n" + + fun addPassphrase(keyIdentifier: KeyIdentifier, passphrase: Passphrase) = apply { + require(!cache.containsKey(keyIdentifier)) { + "The cache already holds a passphrase for ID ${keyIdentifier}.\n" + "If you want to replace this passphrase, use replacePassphrase(Long, Passphrase) instead." } - cache[keyId] = passphrase + cache[keyIdentifier] = passphrase } + @Deprecated("Pass KeyIdentifier instead.") + fun replacePassphrase(keyId: Long, passphrase: Passphrase) = + replacePassphrase(KeyIdentifier(keyId), passphrase) + /** * Replace the passphrase for the given key-id in the cache. * * @param keyId keyId * @param passphrase passphrase */ - fun replacePassphrase(keyId: Long, passphrase: Passphrase) = apply { cache[keyId] = passphrase } + fun replacePassphrase(keyId: KeyIdentifier, passphrase: Passphrase) = apply { + cache[keyId] = passphrase + } /** * Remember the given passphrase for all keys in the given key ring. If for the key-id of any @@ -91,14 +106,14 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras fun addPassphrase(keyRing: PGPKeyRing, passphrase: Passphrase) = apply { // check for existing passphrases before doing anything keyRing.publicKeys.forEach { - require(!cache.containsKey(it.keyID)) { - "The cache already holds a passphrase for the key with ID ${it.keyID.openPgpKeyId()}.\n" + + require(!cache.containsKey(it.keyIdentifier)) { + "The cache already holds a passphrase for the key with ID ${it.keyIdentifier}.\n" + "If you want to replace the passphrase, use replacePassphrase(PGPKeyRing, Passphrase) instead." } } // only then instert - keyRing.publicKeys.forEach { cache[it.keyID] = passphrase } + keyRing.publicKeys.forEach { cache[it.keyIdentifier] = passphrase } } /** @@ -108,7 +123,7 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras * @param passphrase passphrase */ fun replacePassphrase(keyRing: PGPKeyRing, passphrase: Passphrase) = apply { - keyRing.publicKeys.forEach { cache[it.keyID] = passphrase } + keyRing.publicKeys.forEach { cache[it.keyIdentifier] = passphrase } } /** @@ -118,7 +133,13 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras * @param passphrase passphrase */ fun addPassphrase(key: PGPPublicKey, passphrase: Passphrase) = - addPassphrase(key.keyID, passphrase) + addPassphrase(key.keyIdentifier, passphrase) + + fun addPassphrase(cert: OpenPGPCertificate, passphrase: Passphrase) = + addPassphrase(cert.pgpKeyRing, passphrase) + + fun addPassphrase(key: OpenPGPComponentKey, passphrase: Passphrase) = + addPassphrase(key.keyIdentifier, passphrase) /** * Remember the given passphrase for the key with the given fingerprint. @@ -127,14 +148,17 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras * @param passphrase passphrase */ fun addPassphrase(fingerprint: OpenPgpFingerprint, passphrase: Passphrase) = - addPassphrase(fingerprint.keyId, passphrase) + addPassphrase(fingerprint.keyIdentifier, passphrase) + + @Deprecated("Pass KeyIdentifier instead.") + fun forgetPassphrase(keyId: Long) = forgetPassphrase(KeyIdentifier(keyId)) /** * Remove a passphrase from the cache. The passphrase will be cleared and then removed. * * @param keyId id of the key */ - fun forgetPassphrase(keyId: Long) = apply { cache.remove(keyId)?.clear() } + fun forgetPassphrase(keyId: KeyIdentifier) = apply { cache.remove(keyId)?.clear() } /** * Forget the passphrase to all keys in the provided key ring. @@ -145,23 +169,34 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras keyRing.publicKeys.forEach { forgetPassphrase(it) } } + fun forgetPassphrase(cert: OpenPGPCertificate) = forgetPassphrase(cert.pgpPublicKeyRing) + /** * Forget the passphrase of the given public key. * * @param key key */ - fun forgetPassphrase(key: PGPPublicKey) = apply { forgetPassphrase(key.keyID) } + fun forgetPassphrase(key: PGPPublicKey) = apply { forgetPassphrase(key.keyIdentifier) } - override fun getPassphraseFor(keyId: Long?): Passphrase? { - return if (hasPassphrase(keyId)) cache[keyId] - else provider?.getPassphraseFor(keyId)?.also { cache[keyId] = it } + override fun getPassphraseFor(keyIdentifier: KeyIdentifier): Passphrase? { + return if (hasPassphrase(keyIdentifier)) cache[keyIdentifier] + else provider?.getPassphraseFor(keyIdentifier)?.also { cache[keyIdentifier] = it } } - override fun hasPassphrase(keyId: Long?) = cache[keyId]?.isValid ?: false + override fun hasPassphraseFor(keyIdentifier: KeyIdentifier): Boolean { + return hasPassphrase(keyIdentifier) + } - override fun hasPassphraseFor(keyId: Long) = hasPassphrase(keyId) + override fun hasPassphrase(keyIdentifier: KeyIdentifier): Boolean { + return cache[keyIdentifier]?.isValid ?: false + } - override fun getDecryptor(keyId: Long) = protector.getDecryptor(keyId) + override fun getDecryptor(keyIdentifier: KeyIdentifier): PBESecretKeyDecryptor? = + protector.getDecryptor(keyIdentifier) - override fun getEncryptor(keyId: Long) = protector.getEncryptor(keyId) + override fun getEncryptor(key: PGPPublicKey): PBESecretKeyEncryptor? = + protector.getEncryptor(key) + + override fun getKeyPassword(p0: OpenPGPKey.OpenPGPSecretKey): CharArray? = + getPassphraseFor(p0.keyIdentifier)?.getChars() } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt index c7566f6d..b6c0e492 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt @@ -14,12 +14,13 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm * @param encryptionAlgorithm encryption algorithm * @param hashAlgorithm hash algorithm * @param s2kCount encoded (!) s2k iteration count - * @see Encoding Formula + * @see [Encoding Formula](https://www.rfc-editor.org/rfc/rfc4880#section-3.7.1.3) */ data class KeyRingProtectionSettings( val encryptionAlgorithm: SymmetricKeyAlgorithm, val hashAlgorithm: HashAlgorithm, - val s2kCount: Int + val s2kCount: Int, + val aead: Boolean ) { /** @@ -31,7 +32,7 @@ data class KeyRingProtectionSettings( */ constructor( encryptionAlgorithm: SymmetricKeyAlgorithm - ) : this(encryptionAlgorithm, HashAlgorithm.SHA1, 0x60) + ) : this(encryptionAlgorithm, HashAlgorithm.SHA1, 0x60, false) init { require(encryptionAlgorithm != SymmetricKeyAlgorithm.NULL) { @@ -50,6 +51,12 @@ data class KeyRingProtectionSettings( */ @JvmStatic fun secureDefaultSettings() = - KeyRingProtectionSettings(SymmetricKeyAlgorithm.AES_256, HashAlgorithm.SHA256, 0x60) + KeyRingProtectionSettings( + SymmetricKeyAlgorithm.AES_256, HashAlgorithm.SHA256, 0x60, false) + + @JvmStatic + fun aead() = + KeyRingProtectionSettings( + SymmetricKeyAlgorithm.AES_256, HashAlgorithm.SHA256, 0xff, true) } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt index 9eb47e88..1a106093 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt @@ -4,8 +4,10 @@ package org.pgpainless.key.protection +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.api.OpenPGPCertificate import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider import org.pgpainless.util.Passphrase @@ -31,6 +33,25 @@ class PasswordBasedSecretKeyRingProtector : BaseSecretKeyRingProtector { ) : super(passphraseProvider, settings) companion object { + + @JvmStatic + fun forKey( + cert: OpenPGPCertificate, + passphrase: Passphrase + ): PasswordBasedSecretKeyRingProtector { + return object : SecretKeyPassphraseProvider { + + override fun getPassphraseFor(keyIdentifier: KeyIdentifier): Passphrase? { + return if (hasPassphrase(keyIdentifier)) passphrase else null + } + + override fun hasPassphrase(keyIdentifier: KeyIdentifier): Boolean { + return cert.getKey(keyIdentifier) != null + } + } + .let { PasswordBasedSecretKeyRingProtector(it) } + } + @JvmStatic fun forKey( keyRing: PGPKeyRing, @@ -38,12 +59,12 @@ class PasswordBasedSecretKeyRingProtector : BaseSecretKeyRingProtector { ): PasswordBasedSecretKeyRingProtector { return object : SecretKeyPassphraseProvider { - override fun getPassphraseFor(keyId: Long?): Passphrase? { - return if (hasPassphrase(keyId)) passphrase else null + override fun getPassphraseFor(keyIdentifier: KeyIdentifier): Passphrase? { + return if (hasPassphrase(keyIdentifier)) passphrase else null } - override fun hasPassphrase(keyId: Long?): Boolean { - return keyId != null && keyRing.getPublicKey(keyId) != null + override fun hasPassphrase(keyIdentifier: KeyIdentifier): Boolean { + return keyRing.getPublicKey(keyIdentifier) != null } } .let { PasswordBasedSecretKeyRingProtector(it) } @@ -51,20 +72,20 @@ class PasswordBasedSecretKeyRingProtector : BaseSecretKeyRingProtector { @JvmStatic fun forKey(key: PGPSecretKey, passphrase: Passphrase): PasswordBasedSecretKeyRingProtector = - forKeyId(key.publicKey.keyID, passphrase) + forKeyId(key.publicKey.keyIdentifier, passphrase) @JvmStatic fun forKeyId( - singleKeyId: Long, + singleKeyIdentifier: KeyIdentifier, passphrase: Passphrase ): PasswordBasedSecretKeyRingProtector { return object : SecretKeyPassphraseProvider { - override fun getPassphraseFor(keyId: Long?): Passphrase? { - return if (hasPassphrase(keyId)) passphrase else null + override fun getPassphraseFor(keyIdentifier: KeyIdentifier): Passphrase? { + return if (hasPassphrase(keyIdentifier)) passphrase else null } - override fun hasPassphrase(keyId: Long?): Boolean { - return keyId == singleKeyId + override fun hasPassphrase(keyIdentifier: KeyIdentifier): Boolean { + return keyIdentifier.matchesExplicit(singleKeyIdentifier) } } .let { PasswordBasedSecretKeyRingProtector(it) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt index 5e86d950..d76f8e37 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt @@ -4,10 +4,16 @@ package org.pgpainless.key.protection -import kotlin.jvm.Throws +import kotlin.Throws +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.api.KeyPassphraseProvider +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPSecretKey import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider @@ -22,7 +28,7 @@ import org.pgpainless.util.Passphrase * While it is easy to create an implementation of this interface that fits your needs, there are a * bunch of implementations ready for use. */ -interface SecretKeyRingProtector { +interface SecretKeyRingProtector : KeyPassphraseProvider { /** * Returns true, if the protector has a passphrase for the key with the given key-id. @@ -30,7 +36,16 @@ interface SecretKeyRingProtector { * @param keyId key id * @return true if it has a passphrase, false otherwise */ - fun hasPassphraseFor(keyId: Long): Boolean + @Deprecated("Pass in a KeyIdentifier instead.") + fun hasPassphraseFor(keyId: Long): Boolean = hasPassphraseFor(KeyIdentifier(keyId)) + + /** + * Returns true, if the protector has a passphrase for the key with the given [keyIdentifier]. + * + * @param keyIdentifier key identifier + * @return true if it has a passphrase, false otherwise + */ + fun hasPassphraseFor(keyIdentifier: KeyIdentifier): Boolean /** * Return a decryptor for the key of id `keyId`. This method returns null if the key is @@ -39,16 +54,41 @@ interface SecretKeyRingProtector { * @param keyId id of the key * @return decryptor for the key */ - @Throws(PGPException::class) fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? + @Deprecated("Pass in a KeyIdentifier instead.") + @Throws(PGPException::class) + fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? = getDecryptor(KeyIdentifier(keyId)) + + @Throws(PGPException::class) + fun getDecryptor(key: OpenPGPSecretKey): PBESecretKeyDecryptor? = + getDecryptor(key.keyIdentifier) /** - * Return an encryptor for the key of id `keyId`. This method returns null if the key is - * unprotected. + * Return a decryptor for the key with the given [keyIdentifier]. This method returns null if + * the key is unprotected. * - * @param keyId id of the key - * @return encryptor for the key + * @param keyIdentifier identifier of the key + * @return decryptor for the key */ - @Throws(PGPException::class) fun getEncryptor(keyId: Long): PBESecretKeyEncryptor? + @Throws(PGPException::class) + fun getDecryptor(keyIdentifier: KeyIdentifier): PBESecretKeyDecryptor? + + /** + * Return an encryptor for the given key. + * + * @param key component key + * @return encryptor or null if the key shall not be encrypted + */ + @Throws(PGPException::class) + fun getEncryptor(key: OpenPGPComponentKey): PBESecretKeyEncryptor? = + getEncryptor(key.pgpPublicKey) + + /** + * Return an encryptor for the given key. + * + * @param key component key + * @return encryptor or null if the key shall not be encrypted + */ + @Throws(PGPException::class) fun getEncryptor(key: PGPPublicKey): PBESecretKeyEncryptor? companion object { @@ -71,6 +111,10 @@ interface SecretKeyRingProtector { KeyRingProtectionSettings.secureDefaultSettings(), missingPassphraseCallback) + @JvmStatic + fun unlockEachKeyWith(passphrase: Passphrase, keys: OpenPGPKey): SecretKeyRingProtector = + fromPassphraseMap(keys.secretKeys.keys.associateWith { passphrase }) + /** * Use the provided passphrase to lock/unlock all keys in the provided key ring. * @@ -87,7 +131,7 @@ interface SecretKeyRingProtector { passphrase: Passphrase, keys: PGPSecretKeyRing ): SecretKeyRingProtector = - fromPassphraseMap(keys.map { it.keyID }.associateWith { passphrase }) + fromPassphraseMap(keys.map { it.keyIdentifier }.associateWith { passphrase }) /** * Use the provided passphrase to unlock any key. @@ -114,6 +158,13 @@ interface SecretKeyRingProtector { fun unlockSingleKeyWith(passphrase: Passphrase, key: PGPSecretKey): SecretKeyRingProtector = PasswordBasedSecretKeyRingProtector.forKey(key, passphrase) + @JvmStatic + fun unlockSingleKeyWith( + passphrase: Passphrase, + key: OpenPGPSecretKey + ): SecretKeyRingProtector = + PasswordBasedSecretKeyRingProtector.forKey(key.pgpSecretKey, passphrase) + /** * Use the provided passphrase to lock/unlock only the provided (sub-)key. This protector * will only return a non-null encryptor/decryptor based on the provided passphrase if @@ -122,12 +173,15 @@ interface SecretKeyRingProtector { * Otherwise, this protector will always return null. * * @param passphrase passphrase - * @param keyId id of the key to lock/unlock + * @param keyIdentifier id of the key to lock/unlock * @return protector */ @JvmStatic - fun unlockSingleKeyWith(passphrase: Passphrase, keyId: Long): SecretKeyRingProtector = - PasswordBasedSecretKeyRingProtector.forKeyId(keyId, passphrase) + fun unlockSingleKeyWith( + passphrase: Passphrase, + keyIdentifier: KeyIdentifier + ): SecretKeyRingProtector = + PasswordBasedSecretKeyRingProtector.forKeyId(keyIdentifier, passphrase) /** * Protector for unprotected keys. This protector returns null for all @@ -149,7 +203,9 @@ interface SecretKeyRingProtector { * @return protector */ @JvmStatic - fun fromPassphraseMap(passphraseMap: Map): SecretKeyRingProtector = + fun fromPassphraseMap( + passphraseMap: Map + ): SecretKeyRingProtector = CachingSecretKeyRingProtector( passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), null) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt index b3b0308f..00ea9de9 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt @@ -10,12 +10,15 @@ import openpgp.openPgpKeyId import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPPrivateKey import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPPrivateKey +import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPSecretKey import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor import org.pgpainless.PGPainless import org.pgpainless.bouncycastle.extensions.isEncrypted import org.pgpainless.exception.KeyIntegrityException import org.pgpainless.exception.WrongPassphraseException import org.pgpainless.key.util.PublicKeyParameterValidationUtil +import org.pgpainless.policy.Policy import org.pgpainless.util.Passphrase class UnlockSecretKey { @@ -29,17 +32,51 @@ class UnlockSecretKey { protector: SecretKeyRingProtector ): PGPPrivateKey { return if (secretKey.isEncrypted()) { - unlockSecretKey(secretKey, protector.getDecryptor(secretKey.keyID)) + unlockSecretKey(secretKey, protector.getDecryptor(secretKey.keyIdentifier)) } else { unlockSecretKey(secretKey, null as PBESecretKeyDecryptor?) } } @JvmStatic + @JvmOverloads + @Throws(PGPException::class) + fun unlockSecretKey( + secretKey: OpenPGPSecretKey, + protector: SecretKeyRingProtector, + policy: Policy = PGPainless.getInstance().algorithmPolicy + ): OpenPGPPrivateKey { + val privateKey = + try { + secretKey.unlock(protector) + } catch (e: PGPException) { + throw WrongPassphraseException(secretKey.keyIdentifier, e) + } + + if (privateKey == null) { + if (secretKey.pgpSecretKey.s2K.type in 100..110) { + throw PGPException( + "Cannot decrypt secret key ${secretKey.keyIdentifier}: \n" + + "Unsupported private S2K type ${secretKey.pgpSecretKey.s2K.type}") + } + throw PGPException("Cannot decrypt secret key.") + } + + if (policy.isEnableKeyParameterValidation()) { + PublicKeyParameterValidationUtil.verifyPublicKeyParameterIntegrity( + privateKey.keyPair.privateKey, privateKey.keyPair.publicKey) + } + + return privateKey + } + + @JvmStatic + @JvmOverloads @Throws(PGPException::class) fun unlockSecretKey( secretKey: PGPSecretKey, - decryptor: PBESecretKeyDecryptor? + decryptor: PBESecretKeyDecryptor?, + policy: Policy = PGPainless.getInstance().algorithmPolicy ): PGPPrivateKey { val privateKey = try { @@ -57,7 +94,7 @@ class UnlockSecretKey { throw PGPException("Cannot decrypt secret key.") } - if (PGPainless.getPolicy().isEnableKeyParameterValidation()) { + if (policy.isEnableKeyParameterValidation()) { PublicKeyParameterValidationUtil.verifyPublicKeyParameterIntegrity( privateKey, secretKey.publicKey) } @@ -74,5 +111,12 @@ class UnlockSecretKey { secretKey, SecretKeyRingProtector.unlockSingleKeyWith(passphrase, secretKey)) } } + + @JvmStatic + fun unlockSecretKey( + secretKey: OpenPGPSecretKey, + passphrase: Passphrase + ): OpenPGPPrivateKey = + unlockSecretKey(secretKey, SecretKeyRingProtector.unlockAnyKeyWith(passphrase)) } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt index a25bb31a..993f97af 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt @@ -3,14 +3,22 @@ // SPDX-License-Identifier: Apache-2.0 package org.pgpainless.key.protection +import org.bouncycastle.bcpg.KeyIdentifier +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor + /** * Implementation of the [SecretKeyRingProtector] which assumes that all handled keys are not * password protected. */ class UnprotectedKeysProtector : SecretKeyRingProtector { - override fun hasPassphraseFor(keyId: Long) = true + override fun hasPassphraseFor(keyIdentifier: KeyIdentifier): Boolean = true - override fun getDecryptor(keyId: Long) = null + override fun getDecryptor(keyIdentifier: KeyIdentifier): PBESecretKeyDecryptor? = null - override fun getEncryptor(keyId: Long) = null + override fun getEncryptor(key: PGPPublicKey): PBESecretKeyEncryptor? = null + + override fun getKeyPassword(p0: OpenPGPKey.OpenPGPSecretKey?): CharArray? = null } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt index a1a9f6c2..fee9047f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt @@ -7,22 +7,21 @@ package org.pgpainless.key.protection.fixes import org.bouncycastle.bcpg.SecretKeyPacket import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.openpgp.PGPSecretKeyRing -import org.pgpainless.algorithm.HashAlgorithm +import org.bouncycastle.openpgp.api.OpenPGPImplementation +import org.pgpainless.bouncycastle.extensions.checksumCalculator import org.pgpainless.bouncycastle.extensions.unlock import org.pgpainless.exception.WrongPassphraseException -import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.protection.fixes.S2KUsageFix.Companion.replaceUsageChecksumWithUsageSha1 /** * Repair class to fix keys which use S2K usage of value [SecretKeyPacket.USAGE_CHECKSUM]. The * method [replaceUsageChecksumWithUsageSha1] ensures that such keys are encrypted using S2K usage * [SecretKeyPacket.USAGE_SHA1] instead. * - * @see Related PGPainless Bug - * Report - * @see Related PGPainless Feature - * Request - * @see Related upstream BC bug report + * @see [Related PGPainless Bug Report](https://github.com/pgpainless/pgpainless/issues/176) + * @see [Related PGPainless Feature Request](https://github.com/pgpainless/pgpainless/issues/178) + * @see [Related upstream BC bug report](https://github.com/bcgit/bc-java/issues/1020) */ class S2KUsageFix { @@ -38,7 +37,6 @@ class S2KUsageFix { * @param skipKeysWithMissingPassphrase if set to true, missing subkey passphrases will * cause the subkey to stay unaffected. * @return fixed key ring - * @throws PGPException in case of a PGP error. */ @JvmStatic @JvmOverloads @@ -47,8 +45,7 @@ class S2KUsageFix { protector: SecretKeyRingProtector, skipKeysWithMissingPassphrase: Boolean = false ): PGPSecretKeyRing { - val digestCalculator = - ImplementationFactory.getInstance().getPGPDigestCalculator(HashAlgorithm.SHA1) + val digestCalculator = OpenPGPImplementation.getInstance().checksumCalculator() val keyList = mutableListOf() for (key in keys) { // CHECKSUM is not recommended @@ -58,7 +55,7 @@ class S2KUsageFix { } val keyId = key.keyID - val encryptor = protector.getEncryptor(keyId) + val encryptor = protector.getEncryptor(key.publicKey) if (encryptor == null) { if (skipKeysWithMissingPassphrase) { keyList.add(key) @@ -76,7 +73,7 @@ class S2KUsageFix { key.publicKey, digestCalculator, key.isMasterKey, - protector.getEncryptor(keyId)) + protector.getEncryptor(key.publicKey)) keyList.add(fixedKey) } return PGPSecretKeyRing(keyList) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt index 3457cff7..2ba0e448 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt @@ -4,6 +4,7 @@ package org.pgpainless.key.protection.passphrase_provider +import org.bouncycastle.bcpg.KeyIdentifier import org.pgpainless.util.Passphrase /** @@ -14,9 +15,11 @@ import org.pgpainless.util.Passphrase * * TODO: Make this null-safe and throw an exception instead? */ -class MapBasedPassphraseProvider(val map: Map) : SecretKeyPassphraseProvider { +class MapBasedPassphraseProvider(val map: Map) : + SecretKeyPassphraseProvider { - override fun getPassphraseFor(keyId: Long?): Passphrase? = map[keyId] + override fun getPassphraseFor(keyIdentifier: KeyIdentifier): Passphrase? = map[keyIdentifier] - override fun hasPassphrase(keyId: Long?): Boolean = map.containsKey(keyId) + override fun hasPassphrase(keyIdentifier: KeyIdentifier): Boolean = + map.containsKey(keyIdentifier) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt index a80b8bb0..a557dde2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt @@ -4,12 +4,17 @@ package org.pgpainless.key.protection.passphrase_provider +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey import org.pgpainless.util.Passphrase /** Interface to allow the user to provide a [Passphrase] for an encrypted OpenPGP secret key. */ interface SecretKeyPassphraseProvider { + fun getPassphraseFor(key: OpenPGPComponentKey): Passphrase? = + getPassphraseFor(key.keyIdentifier) + /** * Return a passphrase for the given secret key. If no record is found, return null. Note: In * case of an unprotected secret key, this method must may not return null, but a [Passphrase] @@ -19,7 +24,7 @@ interface SecretKeyPassphraseProvider { * @return passphrase or null, if no passphrase record is found. */ fun getPassphraseFor(secretKey: PGPSecretKey): Passphrase? { - return getPassphraseFor(secretKey.keyID) + return getPassphraseFor(secretKey.keyIdentifier) } /** @@ -30,7 +35,13 @@ interface SecretKeyPassphraseProvider { * @param keyId if of the secret key * @return passphrase or null, if no passphrase record has been found. */ - fun getPassphraseFor(keyId: Long?): Passphrase? + @Deprecated("Pass in a KeyIdentifier instead.") + fun getPassphraseFor(keyId: Long): Passphrase? = getPassphraseFor(KeyIdentifier(keyId)) - fun hasPassphrase(keyId: Long?): Boolean + fun getPassphraseFor(keyIdentifier: KeyIdentifier): Passphrase? + + @Deprecated("Pass in a KeyIdentifier instead.") + fun hasPassphrase(keyId: Long): Boolean = hasPassphrase(KeyIdentifier(keyId)) + + fun hasPassphrase(keyIdentifier: KeyIdentifier): Boolean } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt index a9f6801d..b846df2d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt @@ -4,12 +4,13 @@ package org.pgpainless.key.protection.passphrase_provider +import org.bouncycastle.bcpg.KeyIdentifier import org.pgpainless.util.Passphrase /** Implementation of the [SecretKeyPassphraseProvider] that holds a single [Passphrase]. */ class SolitaryPassphraseProvider(val passphrase: Passphrase?) : SecretKeyPassphraseProvider { - override fun getPassphraseFor(keyId: Long?): Passphrase? = passphrase + override fun getPassphraseFor(keyIdentifier: KeyIdentifier): Passphrase? = passphrase - override fun hasPassphrase(keyId: Long?): Boolean = true + override fun hasPassphrase(keyIdentifier: KeyIdentifier): Boolean = true } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt index 3f1b98b1..78ca75ff 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt @@ -17,10 +17,7 @@ class KeyIdUtil { * @param longKeyId 16-digit hexadecimal string * @return key-id converted to [Long]. */ - @JvmStatic - @Deprecated( - "Superseded by Long extension method.", ReplaceWith("Long.fromHexKeyId(longKeyId)")) - fun fromLongKeyId(longKeyId: String) = Long.fromOpenPgpKeyId(longKeyId) + @JvmStatic fun fromLongKeyId(longKeyId: String) = Long.fromOpenPgpKeyId(longKeyId) /** * Format a long key-ID as upper-case hex string. diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt index f83b5486..f449487c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt @@ -7,14 +7,15 @@ package org.pgpainless.key.util import java.io.ByteArrayOutputStream import kotlin.jvm.Throws import openpgp.openPgpKeyId +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.bcpg.S2K import org.bouncycastle.bcpg.SecretKeyPacket import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.api.OpenPGPImplementation import org.bouncycastle.util.Strings import org.pgpainless.bouncycastle.extensions.certificate import org.pgpainless.bouncycastle.extensions.requireSecretKey import org.pgpainless.exception.MissingPassphraseException -import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.key.protection.fixes.S2KUsageFix import org.slf4j.Logger @@ -38,7 +39,7 @@ class KeyRingUtils { "Deprecated in favor of PGPSecretKeyRing extension function.", ReplaceWith("secretKeys.requireSecretKey(keyId)")) fun requirePrimarySecretKeyFrom(secretKeys: PGPSecretKeyRing): PGPSecretKey { - return secretKeys.requireSecretKey(secretKeys.publicKey.keyID) + return secretKeys.requireSecretKey(secretKeys.publicKey.keyIdentifier) } /** @@ -51,13 +52,7 @@ class KeyRingUtils { */ @JvmStatic fun getPrimarySecretKeyFrom(secretKeys: PGPSecretKeyRing): PGPSecretKey? { - return secretKeys.secretKey.let { - if (it.isMasterKey) { - it - } else { - null - } - } + return if (secretKeys.secretKey.isMasterKey) secretKeys.secretKey else null } /** @@ -81,13 +76,7 @@ class KeyRingUtils { */ @JvmStatic fun getPrimaryPublicKey(keyRing: PGPKeyRing): PGPPublicKey? { - return keyRing.publicKey.let { - if (it.isMasterKey) { - it - } else { - null - } - } + return if (keyRing.publicKey.isMasterKey) keyRing.publicKey else null } /** @@ -244,7 +233,7 @@ class KeyRingUtils { certificate.publicKeys .asSequence() .map { - if (it.keyID == certifiedKey.keyID) { + if (it.keyIdentifier == certifiedKey.keyIdentifier) { PGPPublicKey.addCertification(it, certification) } else { it @@ -414,18 +403,40 @@ class KeyRingUtils { * @throws PGPException in case of a broken key */ @JvmStatic - fun stripSecretKey(secretKeys: PGPSecretKeyRing, keyId: Long): PGPSecretKeyRing { - require(keyId != secretKeys.publicKey.keyID) { + @Deprecated("Pass in a KeyIdentifier instead.") + fun stripSecretKey(secretKeys: PGPSecretKeyRing, keyId: Long): PGPSecretKeyRing = + stripSecretKey(secretKeys, KeyIdentifier(keyId)) + + /** + * Remove the secret key of the subkey identified by the given [keyIdentifier] 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 keyIdentifier identifier 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 + */ + @JvmStatic + fun stripSecretKey( + secretKeys: PGPSecretKeyRing, + keyIdentifier: KeyIdentifier + ): PGPSecretKeyRing { + require(keyIdentifier != secretKeys.publicKey.keyIdentifier) { "Bouncy Castle currently cannot deal with stripped primary secret keys." } - if (secretKeys.getSecretKey(keyId) == null) { + if (secretKeys.getSecretKey(keyIdentifier) == null) { throw NoSuchElementException( - "PGPSecretKeyRing does not contain secret key ${keyId.openPgpKeyId()}.") + "PGPSecretKeyRing does not contain secret key ${keyIdentifier}.") } val out = ByteArrayOutputStream() secretKeys.forEach { - if (it.keyID == keyId) { + if (it.keyIdentifier == keyIdentifier) { // only encode the public key it.publicKey.encode(out) } else { @@ -435,7 +446,7 @@ class KeyRingUtils { } secretKeys.extraPublicKeys.forEach { it.encode(out) } return PGPSecretKeyRing( - out.toByteArray(), ImplementationFactory.getInstance().keyFingerprintCalculator) + out.toByteArray(), OpenPGPImplementation.getInstance().keyFingerPrintCalculator()) } /** @@ -449,7 +460,7 @@ class KeyRingUtils { fun getStrippedDownPublicKey(bloatedKey: PGPPublicKey): PGPPublicKey { return PGPPublicKey( bloatedKey.publicKeyPacket, - ImplementationFactory.getInstance().keyFingerprintCalculator) + OpenPGPImplementation.getInstance().keyFingerPrintCalculator()) } @JvmStatic @@ -468,7 +479,7 @@ class KeyRingUtils { @JvmStatic @Throws(MissingPassphraseException::class, PGPException::class) fun changePassphrase( - keyId: Long?, + keyId: KeyIdentifier?, secretKeys: PGPSecretKeyRing, oldProtector: SecretKeyRingProtector, newProtector: SecretKeyRingProtector @@ -484,7 +495,7 @@ class KeyRingUtils { secretKeys.secretKeys .asSequence() .map { - if (it.keyID == keyId) { + if (it.keyIdentifier.matchesExplicit(keyId)) { reencryptPrivateKey(it, oldProtector, newProtector) } else { it @@ -508,8 +519,8 @@ class KeyRingUtils { return PGPSecretKey.copyWithNewPassword( secretKey, - oldProtector.getDecryptor(secretKey.keyID), - newProtector.getEncryptor(secretKey.keyID)) + oldProtector.getDecryptor(secretKey.keyIdentifier), + newProtector.getEncryptor(secretKey.publicKey)) } @JvmStatic diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt index a1e79bf3..130e12ff 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt @@ -10,15 +10,12 @@ import java.math.BigInteger import java.security.SecureRandom import org.bouncycastle.bcpg.* import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.api.OpenPGPImplementation import org.bouncycastle.util.Arrays import org.bouncycastle.util.io.Streams -import org.pgpainless.algorithm.HashAlgorithm -import org.pgpainless.algorithm.PublicKeyAlgorithm.Companion.requireFromId import org.pgpainless.algorithm.SignatureType -import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.bouncycastle.extensions.publicKeyAlgorithm import org.pgpainless.exception.KeyIntegrityException -import org.pgpainless.implementation.ImplementationFactory.Companion.getInstance /** * Utility class to verify keys against Key Overwriting (KO) attacks. This class of attacks is only @@ -27,7 +24,7 @@ import org.pgpainless.implementation.ImplementationFactory.Companion.getInstance * 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 + * @see [Key Overwriting (KO) Attacks against OpenPGP](https://www.kopenpgp.com/) */ class PublicKeyParameterValidationUtil { @@ -156,13 +153,13 @@ class PublicKeyParameterValidationUtil { /** * Validate ElGamal public key parameters. * - * Original implementation by the openpgpjs authors: RFC5322 §3.4. Address - * Specification + * @see + * [RFC5322 §3.4. Address Specification](https://www.rfc-editor.org/rfc/rfc5322#page-16) */ @JvmStatic fun parse(string: String): UserId { @@ -170,10 +164,6 @@ class UserId internal constructor(name: String?, comment: String?, email: String fun compare(u1: UserId?, u2: UserId?, comparator: Comparator) = comparator.compare(u1, u2) - @JvmStatic - @Deprecated("Deprecated in favor of builde() method.", ReplaceWith("builder()")) - fun newBuilder() = builder() - @JvmStatic fun builder() = Builder() } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt index 7c6bb2d3..5d8ba4d0 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt @@ -5,21 +5,83 @@ package org.pgpainless.policy import java.util.* +import org.bouncycastle.openpgp.api.EncryptedDataPacketType +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism import org.pgpainless.algorithm.* +import org.pgpainless.key.protection.KeyRingProtectionSettings import org.pgpainless.util.DateUtil import org.pgpainless.util.NotationRegistry -class Policy( - var certificationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, - var revocationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, - var dataSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, - var symmetricKeyEncryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, - var symmetricKeyDecryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, - var compressionAlgorithmPolicy: CompressionAlgorithmPolicy, - var publicKeyAlgorithmPolicy: PublicKeyAlgorithmPolicy, - var notationRegistry: NotationRegistry -) { +class Policy { + val certificationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy + val revocationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy + val dataSignatureHashAlgorithmPolicy: HashAlgorithmPolicy + val messageEncryptionAlgorithmPolicy: MessageEncryptionMechanismPolicy + val messageDecryptionAlgorithmPolicy: MessageEncryptionMechanismPolicy + val compressionAlgorithmPolicy: CompressionAlgorithmPolicy + val publicKeyAlgorithmPolicy: PublicKeyAlgorithmPolicy + val featurePolicy: FeaturePolicy = FeaturePolicy.defaultFeaturePolicy() + val keyProtectionSettings: KeyRingProtectionSettings + val notationRegistry: NotationRegistry + val keyGenerationAlgorithmSuite: AlgorithmSuite + + constructor( + certificationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, + revocationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, + dataSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, + messageEncryptionMechanismPolicy: MessageEncryptionMechanismPolicy, + messageDecryptionMechanismPolicy: MessageEncryptionMechanismPolicy, + compressionAlgorithmPolicy: CompressionAlgorithmPolicy, + publicKeyAlgorithmPolicy: PublicKeyAlgorithmPolicy, + keyProtectionSettings: KeyRingProtectionSettings, + notationRegistry: NotationRegistry, + keyGenerationAlgorithmSuite: AlgorithmSuite + ) { + this.certificationSignatureHashAlgorithmPolicy = certificationSignatureHashAlgorithmPolicy + this.revocationSignatureHashAlgorithmPolicy = revocationSignatureHashAlgorithmPolicy + this.dataSignatureHashAlgorithmPolicy = dataSignatureHashAlgorithmPolicy + this.messageEncryptionAlgorithmPolicy = messageEncryptionMechanismPolicy + this.messageDecryptionAlgorithmPolicy = messageDecryptionMechanismPolicy + this.compressionAlgorithmPolicy = compressionAlgorithmPolicy + this.publicKeyAlgorithmPolicy = publicKeyAlgorithmPolicy + this.keyProtectionSettings = keyProtectionSettings + this.notationRegistry = notationRegistry + this.keyGenerationAlgorithmSuite = keyGenerationAlgorithmSuite + } + + @Deprecated( + "Constructors receiving SymmetricKeyAlgorithmPolicy objects are deprecated in favor of ones receiving MessageEncryptionMechanismPolicy objects.") + constructor( + certificationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, + revocationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, + dataSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, + symmetricKeyEncryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, + symmetricKeyDecryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, + compressionAlgorithmPolicy: CompressionAlgorithmPolicy, + publicKeyAlgorithmPolicy: PublicKeyAlgorithmPolicy, + keyProtectionSettings: KeyRingProtectionSettings, + notationRegistry: NotationRegistry, + keyGenerationAlgorithmSuite: AlgorithmSuite + ) { + this.certificationSignatureHashAlgorithmPolicy = certificationSignatureHashAlgorithmPolicy + this.revocationSignatureHashAlgorithmPolicy = revocationSignatureHashAlgorithmPolicy + this.dataSignatureHashAlgorithmPolicy = dataSignatureHashAlgorithmPolicy + this.messageEncryptionAlgorithmPolicy = + MessageEncryptionMechanismPolicy.rfc4880Plus9580PlusLibrePGP( + symmetricKeyEncryptionAlgorithmPolicy) + this.messageDecryptionAlgorithmPolicy = + MessageEncryptionMechanismPolicy.rfc4880Plus9580PlusLibrePGP( + symmetricKeyDecryptionAlgorithmPolicy) + this.compressionAlgorithmPolicy = compressionAlgorithmPolicy + this.publicKeyAlgorithmPolicy = publicKeyAlgorithmPolicy + this.keyProtectionSettings = keyProtectionSettings + this.notationRegistry = notationRegistry + this.keyGenerationAlgorithmSuite = keyGenerationAlgorithmSuite + } + + @Deprecated( + "Constructors receiving SymmetricKeyAlgorithmPolicy objects are deprecated in favor of ones receiving MessageEncryptionMechanismPolicy objects.") constructor() : this( HashAlgorithmPolicy.smartCertificationSignatureHashAlgorithmPolicy(), @@ -28,13 +90,33 @@ class Policy( SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022(), SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022(), CompressionAlgorithmPolicy.anyCompressionAlgorithmPolicy(), - PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy(), - NotationRegistry()) + PublicKeyAlgorithmPolicy.rfc9580PublicKeyAlgorithmPolicy(), + KeyRingProtectionSettings.secureDefaultSettings(), + NotationRegistry(), + AlgorithmSuite.defaultAlgorithmSuite) - var keyGenerationAlgorithmSuite = AlgorithmSuite.defaultAlgorithmSuite - var signerUserIdValidationLevel = SignerUserIdValidationLevel.DISABLED + @Deprecated("Deprecated in favor of messageEncryptionAlgorithmPolicy") + // TODO: Remove in 2.1 + val symmetricKeyEncryptionAlgorithmPolicy + get() = messageEncryptionAlgorithmPolicy.symmetricAlgorithmPolicy + + @Deprecated("Deprecated in favor of messageDecryptionAlgorithmPolicy") + // TODO: Remove in 2.1 + val symmetricKeyDecryptionAlgorithmPolicy + get() = messageDecryptionAlgorithmPolicy.symmetricAlgorithmPolicy + + /** + * Decide, whether to sanitize public key parameters when unlocking OpenPGP secret keys. OpenPGP + * v4 keys are susceptible to a class of attacks, where an attacker with access to the locked + * key material (e.g. a cloud email provider) might manipulate unprotected public key parameters + * of the key, leading to potential secret key leakage. + * + * @see [Key Overwriting (KO) Attacks against OpenPGP](https://www.kopenpgp.com/) + */ var enableKeyParameterValidation = false + fun copy() = Builder(this) + fun isEnableKeyParameterValidation() = enableKeyParameterValidation /** @@ -44,7 +126,8 @@ class Policy( * regardless of usage date. * * @param defaultHashAlgorithm default hash algorithm - * @param algorithmTerminationDates map of acceptable algorithms and their termination dates + * @param acceptableHashAlgorithmsAndTerminationDates map of acceptable algorithms and their + * termination dates */ class HashAlgorithmPolicy( val defaultHashAlgorithm: HashAlgorithm, @@ -175,6 +258,138 @@ class Policy( } } + abstract class MessageEncryptionMechanismPolicy( + val symmetricAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, + val asymmetricFallbackMechanism: MessageEncryptionMechanism, + val symmetricFallbackMechanism: MessageEncryptionMechanism = asymmetricFallbackMechanism + ) { + abstract fun isAcceptable(encryptionMechanism: MessageEncryptionMechanism): Boolean + + companion object { + + @JvmStatic + fun rfc4880( + symAlgPolicy: SymmetricKeyAlgorithmPolicy + ): MessageEncryptionMechanismPolicy { + return object : + MessageEncryptionMechanismPolicy( + symAlgPolicy, + MessageEncryptionMechanism.integrityProtected( + symAlgPolicy.defaultSymmetricKeyAlgorithm.algorithmId)) { + override fun isAcceptable( + encryptionMechanism: MessageEncryptionMechanism + ): Boolean { + return encryptionMechanism.mode == EncryptedDataPacketType.SEIPDv1 && + symAlgPolicy.isAcceptable(encryptionMechanism.symmetricKeyAlgorithm) + } + } + } + + @JvmStatic + fun rfc9580( + symAlgPolicy: SymmetricKeyAlgorithmPolicy + ): MessageEncryptionMechanismPolicy { + return object : + MessageEncryptionMechanismPolicy( + symAlgPolicy, + MessageEncryptionMechanism.aead( + symAlgPolicy.defaultSymmetricKeyAlgorithm.algorithmId, + AEADAlgorithm.OCB.algorithmId)) { + val acceptableAEADAlgorithms = + listOf(AEADAlgorithm.OCB, AEADAlgorithm.GCM, AEADAlgorithm.EAX).map { + it.algorithmId + } + + override fun isAcceptable( + encryptionMechanism: MessageEncryptionMechanism + ): Boolean { + return when (encryptionMechanism.mode) { + EncryptedDataPacketType.SEIPDv1 -> + symAlgPolicy.isAcceptable(encryptionMechanism.symmetricKeyAlgorithm) + EncryptedDataPacketType.SEIPDv2 -> + symAlgPolicy.isAcceptable( + encryptionMechanism.symmetricKeyAlgorithm) && + acceptableAEADAlgorithms.contains( + encryptionMechanism.aeadAlgorithm) + else -> false + } + } + } + } + + @JvmStatic + fun librePgp( + symAlgPolicy: SymmetricKeyAlgorithmPolicy + ): MessageEncryptionMechanismPolicy { + return object : + MessageEncryptionMechanismPolicy( + symAlgPolicy, + MessageEncryptionMechanism.integrityProtected( + symAlgPolicy.defaultSymmetricKeyAlgorithm.algorithmId)) { + val acceptableAEADAlgorithms = listOf(AEADAlgorithm.OCB).map { it.algorithmId } + + override fun isAcceptable( + encryptionMechanism: MessageEncryptionMechanism + ): Boolean { + return when (encryptionMechanism.mode) { + EncryptedDataPacketType.SEIPDv1 -> + symAlgPolicy.isAcceptable(encryptionMechanism.symmetricKeyAlgorithm) + EncryptedDataPacketType.LIBREPGP_OED -> + symAlgPolicy.isAcceptable( + encryptionMechanism.symmetricKeyAlgorithm) && + acceptableAEADAlgorithms.contains( + encryptionMechanism.aeadAlgorithm) + else -> false + } + } + } + } + + @JvmStatic + fun rfc4880Plus9580( + symAlgPolicy: SymmetricKeyAlgorithmPolicy + ): MessageEncryptionMechanismPolicy { + val rfc4880 = rfc4880(symAlgPolicy) + val rfc9580 = rfc9580(symAlgPolicy) + return object : + MessageEncryptionMechanismPolicy( + symAlgPolicy, + rfc4880.asymmetricFallbackMechanism, + rfc4880.symmetricFallbackMechanism) { + override fun isAcceptable( + encryptionMechanism: MessageEncryptionMechanism + ): Boolean { + return rfc9580.isAcceptable(encryptionMechanism) || + rfc4880.isAcceptable(encryptionMechanism) + } + } + } + + @JvmStatic + fun rfc4880Plus9580PlusLibrePGP( + symAlgPolicy: SymmetricKeyAlgorithmPolicy + ): MessageEncryptionMechanismPolicy { + return object : + MessageEncryptionMechanismPolicy( + symAlgPolicy, + MessageEncryptionMechanism.integrityProtected( + symAlgPolicy.defaultSymmetricKeyAlgorithm.algorithmId)) { + override fun isAcceptable( + encryptionMechanism: MessageEncryptionMechanism + ): Boolean { + val rfc4480 = rfc4880(symAlgPolicy) + val rfc9580 = rfc9580(symAlgPolicy) + val librePgp = librePgp(symAlgPolicy) + + return rfc4480.isAcceptable(encryptionMechanism) || + rfc9580.isAcceptable(encryptionMechanism) || + librePgp.isAcceptable(encryptionMechanism) + } + } + } + } + } + class SymmetricKeyAlgorithmPolicy( val defaultSymmetricKeyAlgorithm: SymmetricKeyAlgorithm, val acceptableSymmetricKeyAlgorithms: List @@ -305,7 +520,7 @@ class Policy( @JvmStatic fun anyCompressionAlgorithmPolicy() = CompressionAlgorithmPolicy( - CompressionAlgorithm.ZIP, + CompressionAlgorithm.UNCOMPRESSED, listOf( CompressionAlgorithm.UNCOMPRESSED, CompressionAlgorithm.ZIP, @@ -328,8 +543,7 @@ class Policy( companion object { /** - * Return PGPainless' default public key algorithm policy. This policy is based upon - * recommendations made by the German Federal Office for Information Security (BSI). + * Return PGPainless' default public key algorithm policy. * * @return default algorithm policy * @deprecated not expressive - might be removed in a future release @@ -337,8 +551,8 @@ class Policy( @JvmStatic @Deprecated( "not expressive - might be removed in a future release", - ReplaceWith("bsi2021PublicKeyAlgorithmPolicy()")) - fun defaultPublicKeyAlgorithmPolicy() = bsi2021PublicKeyAlgorithmPolicy() + ReplaceWith("rfc9580PublicKeyAlgorithmPolicy()")) + fun defaultPublicKeyAlgorithmPolicy() = rfc9580PublicKeyAlgorithmPolicy() /** * This policy is based upon recommendations made by the German Federal Office for @@ -349,12 +563,10 @@ class Policy( * problems to have a strength of at least 2000 bits. * * @return default algorithm policy - * @see BSI - - * Technical Guideline - Cryptographic Mechanisms: Recommendations and Key Lengths - * (2021-01) - * @see BlueKrypt | Cryptographic Key Length - * Recommendation + * @see + * [BSI - Technical Guideline - Cryptographic Mechanisms: Recommendations and Key Lengths (2021-01)](https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-1.pdf) + * @see + * [BlueKrypt | Cryptographic Key Length Recommendation](https://www.keylength.com/) */ @JvmStatic fun bsi2021PublicKeyAlgorithmPolicy() = @@ -379,32 +591,192 @@ class Policy( put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000) // §7.2.2 put(PublicKeyAlgorithm.ECDH, 250) + // Fixed lengths + put(PublicKeyAlgorithm.X25519, 256) + put(PublicKeyAlgorithm.ED25519, 256) + put(PublicKeyAlgorithm.X448, 448) + put(PublicKeyAlgorithm.ED448, 456) + }) + + /** Public Key Algorithm Policy based upon recommendations from RFC9580. */ + fun rfc9580PublicKeyAlgorithmPolicy(): PublicKeyAlgorithmPolicy = + PublicKeyAlgorithmPolicy( + buildMap { + // https://www.rfc-editor.org/rfc/rfc9580.html#section-12.4 + put(PublicKeyAlgorithm.RSA_GENERAL, 2000) + // https://www.rfc-editor.org/rfc/rfc9580.html#name-ecc-curves-for-openpgp + put(PublicKeyAlgorithm.EDDSA_LEGACY, 250) + // https://www.rfc-editor.org/rfc/rfc9580.html#name-ecc-curves-for-openpgp + put(PublicKeyAlgorithm.ECDH, 250) + put(PublicKeyAlgorithm.ECDSA, 250) + // https://www.rfc-editor.org/rfc/rfc9580.html#name-eddsa + put(PublicKeyAlgorithm.X25519, 256) + put(PublicKeyAlgorithm.ED25519, 256) + put(PublicKeyAlgorithm.X448, 448) + put(PublicKeyAlgorithm.ED448, 456) }) } } - enum class SignerUserIdValidationLevel { - /** - * PGPainless will verify [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, + class Builder(private val origin: Policy) { + private var certificationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy = + origin.certificationSignatureHashAlgorithmPolicy + private var revocationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy = + origin.revocationSignatureHashAlgorithmPolicy + private var dataSignatureHashAlgorithmPolicy: HashAlgorithmPolicy = + origin.dataSignatureHashAlgorithmPolicy + private var messageEncryptionMechanismPolicy: MessageEncryptionMechanismPolicy = + origin.messageEncryptionAlgorithmPolicy + private var messageDecryptionMechanismPolicy: MessageEncryptionMechanismPolicy = + origin.messageDecryptionAlgorithmPolicy + private var compressionAlgorithmPolicy: CompressionAlgorithmPolicy = + origin.compressionAlgorithmPolicy + private var publicKeyAlgorithmPolicy: PublicKeyAlgorithmPolicy = + origin.publicKeyAlgorithmPolicy + private var keyProtectionSettings: KeyRingProtectionSettings = origin.keyProtectionSettings + private var notationRegistry: NotationRegistry = origin.notationRegistry + private var keyGenerationAlgorithmSuite: AlgorithmSuite = origin.keyGenerationAlgorithmSuite - /** - * PGPainless will ignore [org.bouncycastle.bcpg.sig.SignerUserID] subpackets on signature. - */ - DISABLED + fun withCertificationSignatureHashAlgorithmPolicy( + certificationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy + ) = apply { + this.certificationSignatureHashAlgorithmPolicy = + certificationSignatureHashAlgorithmPolicy + } + + fun withRevocationSignatureHashAlgorithmPolicy( + revocationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy + ) = apply { + this.revocationSignatureHashAlgorithmPolicy = revocationSignatureHashAlgorithmPolicy + } + + fun withDataSignatureHashAlgorithmPolicy( + dataSignatureHashAlgorithmPolicy: HashAlgorithmPolicy + ) = apply { this.dataSignatureHashAlgorithmPolicy = dataSignatureHashAlgorithmPolicy } + + @Deprecated( + "Usage of SymmetricKeyAlgorithmPolicy is deprecated in favor of MessageEncryptionMechanismPolicy.") + // TODO: Remove in 2.1 + fun withSymmetricKeyEncryptionAlgorithmPolicy( + symmetricKeyEncryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy + ) = + withMessageEncryptionAlgorithmPolicy( + MessageEncryptionMechanismPolicy.rfc4880Plus9580PlusLibrePGP( + symmetricKeyEncryptionAlgorithmPolicy)) + + @Deprecated( + "Usage of SymmetricKeyAlgorithmPolicy is deprecated in favor of MessageEncryptionMechanismPolicy.") + // TODO: Remove in 2.1 + fun withSymmetricKeyDecryptionAlgorithmPolicy( + symmetricKeyDecryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy + ) = + withMessageDecryptionAlgorithmPolicy( + MessageEncryptionMechanismPolicy.rfc4880Plus9580PlusLibrePGP( + symmetricKeyDecryptionAlgorithmPolicy)) + + fun withMessageEncryptionAlgorithmPolicy( + encryptionMechanismPolicy: MessageEncryptionMechanismPolicy + ) = apply { messageEncryptionMechanismPolicy = encryptionMechanismPolicy } + + fun withMessageDecryptionAlgorithmPolicy( + decryptionMechanismPolicy: MessageEncryptionMechanismPolicy + ) = apply { messageDecryptionMechanismPolicy = decryptionMechanismPolicy } + + fun withCompressionAlgorithmPolicy(compressionAlgorithmPolicy: CompressionAlgorithmPolicy) = + apply { + this.compressionAlgorithmPolicy = compressionAlgorithmPolicy + } + + fun withPublicKeyAlgorithmPolicy(publicKeyAlgorithmPolicy: PublicKeyAlgorithmPolicy) = + apply { + this.publicKeyAlgorithmPolicy = publicKeyAlgorithmPolicy + } + + fun withKeyProtectionSettings(keyProtectionSettings: KeyRingProtectionSettings) = apply { + this.keyProtectionSettings = keyProtectionSettings + } + + fun withNotationRegistry(notationRegistry: NotationRegistry) = apply { + this.notationRegistry = notationRegistry + } + + fun withKeyGenerationAlgorithmSuite(keyGenerationAlgorithmSuite: AlgorithmSuite) = apply { + this.keyGenerationAlgorithmSuite = keyGenerationAlgorithmSuite + } + + fun build() = + Policy( + certificationSignatureHashAlgorithmPolicy, + revocationSignatureHashAlgorithmPolicy, + dataSignatureHashAlgorithmPolicy, + messageEncryptionMechanismPolicy, + messageDecryptionMechanismPolicy, + compressionAlgorithmPolicy, + publicKeyAlgorithmPolicy, + keyProtectionSettings, + notationRegistry, + keyGenerationAlgorithmSuite) + .apply { enableKeyParameterValidation = origin.enableKeyParameterValidation } + } + + abstract class FeaturePolicy { + fun isAcceptable(feature: Byte): Boolean = + Feature.fromId(feature)?.let { isAcceptable(it) } ?: false + + abstract fun isAcceptable(feature: Feature): Boolean + + companion object { + @JvmStatic + fun defaultFeaturePolicy(): FeaturePolicy { + return whiteList( + listOf(Feature.MODIFICATION_DETECTION, Feature.MODIFICATION_DETECTION_2)) + } + + @JvmStatic + fun whiteList(whitelistedFeatures: List): FeaturePolicy { + return object : FeaturePolicy() { + override fun isAcceptable(feature: Feature): Boolean { + return whitelistedFeatures.contains(feature) + } + } + } + } } companion object { - - @Volatile private var INSTANCE: Policy? = null - - @JvmStatic - fun getInstance() = - INSTANCE ?: synchronized(this) { INSTANCE ?: Policy().also { INSTANCE = it } } + fun wildcardPolicy(): Policy = + Policy( + HashAlgorithmPolicy(HashAlgorithm.SHA512, HashAlgorithm.entries), + HashAlgorithmPolicy(HashAlgorithm.SHA512, HashAlgorithm.entries), + HashAlgorithmPolicy(HashAlgorithm.SHA512, HashAlgorithm.entries), + object : + MessageEncryptionMechanismPolicy( + SymmetricKeyAlgorithmPolicy( + SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.entries), + MessageEncryptionMechanism.integrityProtected( + SymmetricKeyAlgorithm.AES_256.algorithmId)) { + override fun isAcceptable( + encryptionMechanism: MessageEncryptionMechanism + ): Boolean { + return true + } + }, + object : + MessageEncryptionMechanismPolicy( + SymmetricKeyAlgorithmPolicy( + SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.entries), + MessageEncryptionMechanism.integrityProtected( + SymmetricKeyAlgorithm.AES_256.algorithmId)) { + override fun isAcceptable( + encryptionMechanism: MessageEncryptionMechanism + ): Boolean { + return true + } + }, + CompressionAlgorithmPolicy.anyCompressionAlgorithmPolicy(), + PublicKeyAlgorithmPolicy(PublicKeyAlgorithm.entries.associateWith { 0 }.toMap()), + KeyRingProtectionSettings.secureDefaultSettings(), + NotationRegistry(setOf()), + AlgorithmSuite.defaultAlgorithmSuite) } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/provider/BouncyCastleProviderFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/provider/BouncyCastleProviderFactory.kt deleted file mode 100644 index 27192953..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/provider/BouncyCastleProviderFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.provider - -import java.security.Provider -import org.bouncycastle.jce.provider.BouncyCastleProvider - -class BouncyCastleProviderFactory : ProviderFactory() { - override val securityProvider: Provider = BouncyCastleProvider() -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/provider/ProviderFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/provider/ProviderFactory.kt deleted file mode 100644 index 531ae54b..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/provider/ProviderFactory.kt +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.provider - -import java.security.Provider - -/** - * Allow the use of different [Provider] implementations to provide cryptographic primitives by - * setting a [ProviderFactory] singleton. By default, the class is initialized with a - * [BouncyCastleProviderFactory]. To make use of your own custom [Provider], call [setFactory], - * passing your own custom [ProviderFactory] instance. - */ -abstract class ProviderFactory { - - protected abstract val securityProvider: Provider - protected open val securityProviderName: String - get() = securityProvider.name - - companion object { - // singleton instance - @JvmStatic var factory: ProviderFactory = BouncyCastleProviderFactory() - - @JvmStatic - val provider: Provider - @JvmName("getProvider") get() = factory.securityProvider - - @JvmStatic - val providerName: String - get() = factory.securityProviderName - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt index 770dfc56..0ef6dc5a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt @@ -10,10 +10,10 @@ import java.util.* import openpgp.plusSeconds import org.bouncycastle.bcpg.sig.KeyExpirationTime import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.api.OpenPGPImplementation import org.bouncycastle.util.encoders.Hex import org.bouncycastle.util.io.Streams import org.pgpainless.bouncycastle.extensions.* -import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.util.RevocationAttributes.Reason import org.pgpainless.util.ArmorUtils @@ -153,7 +153,7 @@ class SignatureUtils { fun readSignatures(inputStream: InputStream, maxIterations: Int): List { val signatures = mutableListOf() val pgpIn = ArmorUtils.getDecoderStream(inputStream) - val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn) + val objectFactory = OpenPGPImplementation.getInstance().pgpObjectFactory(pgpIn) var i = 0 var nextObject: Any? = null @@ -214,15 +214,6 @@ class SignatureUtils { return Hex.toHexString(signature.digestPrefix) } - @JvmStatic - @Deprecated( - "Deprecated in favor of PGPSignature extension method", - ReplaceWith( - "signature.wasIssuedBy(fingerprint)", "org.bouncycastle.extensions.wasIssuedBy")) - fun wasIssuedBy(fingerprint: ByteArray, signature: PGPSignature): Boolean { - return signature.wasIssuedBy(fingerprint) - } - @JvmStatic @Deprecated( "Deprecated in favor of PGPSignature extension method", diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/AbstractSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/AbstractSignatureBuilder.kt index eaf05df1..cc2fb19d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/AbstractSignatureBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/AbstractSignatureBuilder.kt @@ -6,78 +6,83 @@ package org.pgpainless.signature.builder import java.util.function.Predicate import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPPrivateKey -import org.bouncycastle.openpgp.PGPPublicKey -import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.openpgp.PGPSignature import org.bouncycastle.openpgp.PGPSignatureGenerator +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey +import org.bouncycastle.openpgp.api.OpenPGPKey import org.pgpainless.PGPainless import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.SignatureType -import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator -import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.bouncycastle.extensions.toHashAlgorithms import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.key.protection.UnlockSecretKey -import org.pgpainless.key.util.OpenPgpKeyAttributeUtil import org.pgpainless.signature.subpackets.SignatureSubpackets import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper abstract class AbstractSignatureBuilder>( - protected val privateSigningKey: PGPPrivateKey, - protected val publicSigningKey: PGPPublicKey, + protected val signingKey: OpenPGPKey.OpenPGPPrivateKey, protected var _hashAlgorithm: HashAlgorithm, protected var _signatureType: SignatureType, protected val _hashedSubpackets: SignatureSubpackets, - protected val _unhashedSubpackets: SignatureSubpackets + protected val _unhashedSubpackets: SignatureSubpackets, + protected val api: PGPainless ) { protected abstract val signatureTypePredicate: Predicate init { - require(signatureTypePredicate.test(_signatureType)) { "Invalid signature type." } + require(signatureTypePredicate.test(_signatureType)) { + "Invalid signature type: $_signatureType" + } } @Throws(PGPException::class) protected constructor( signatureType: SignatureType, - signingKey: PGPSecretKey, + signingKey: OpenPGPKey.OpenPGPSecretKey, protector: SecretKeyRingProtector, hashAlgorithm: HashAlgorithm, hashedSubpackets: SignatureSubpackets, - unhashedSubpackets: SignatureSubpackets + unhashedSubpackets: SignatureSubpackets, + api: PGPainless ) : this( UnlockSecretKey.unlockSecretKey(signingKey, protector), - signingKey.publicKey, hashAlgorithm, signatureType, hashedSubpackets, - unhashedSubpackets) + unhashedSubpackets, + api) @Throws(PGPException::class) constructor( signatureType: SignatureType, - signingKey: PGPSecretKey, - protector: SecretKeyRingProtector + signingKey: OpenPGPKey.OpenPGPSecretKey, + protector: SecretKeyRingProtector, + api: PGPainless ) : this( signatureType, signingKey, protector, - negotiateHashAlgorithm(signingKey.publicKey), - SignatureSubpackets.createHashedSubpackets(signingKey.publicKey), - SignatureSubpackets.createEmptySubpackets()) + negotiateHashAlgorithm(signingKey, api), + SignatureSubpackets.createHashedSubpackets(signingKey.pgpSecretKey.publicKey), + SignatureSubpackets.createEmptySubpackets(), + api) @Throws(PGPException::class) constructor( - signingKey: PGPSecretKey, + signingKey: OpenPGPKey.OpenPGPSecretKey, protector: SecretKeyRingProtector, - archetypeSignature: PGPSignature + archetypeSignature: PGPSignature, + api: PGPainless ) : this( SignatureType.requireFromCode(archetypeSignature.signatureType), signingKey, protector, - negotiateHashAlgorithm(signingKey.publicKey), - SignatureSubpackets.refreshHashedSubpackets(signingKey.publicKey, archetypeSignature), - SignatureSubpackets.refreshUnhashedSubpackets(archetypeSignature)) + negotiateHashAlgorithm(signingKey, api), + SignatureSubpackets.refreshHashedSubpackets( + signingKey.publicKey.pgpPublicKey, archetypeSignature), + SignatureSubpackets.refreshUnhashedSubpackets(archetypeSignature), + api) val hashAlgorithm = _hashAlgorithm @@ -109,27 +114,22 @@ abstract class AbstractSignatureBuilder>( @Throws(PGPException::class) protected fun buildAndInitSignatureGenerator(): PGPSignatureGenerator = PGPSignatureGenerator( - ImplementationFactory.getInstance() - .getPGPContentSignerBuilder( - publicSigningKey.algorithm, hashAlgorithm.algorithmId)) + api.implementation.pgpContentSignerBuilder( + signingKey.keyPair.publicKey.algorithm, hashAlgorithm.algorithmId), + signingKey.keyPair.publicKey) .apply { setUnhashedSubpackets(SignatureSubpacketsHelper.toVector(_unhashedSubpackets)) setHashedSubpackets(SignatureSubpacketsHelper.toVector(_hashedSubpackets)) - init(_signatureType.code, privateSigningKey) + init(_signatureType.code, signingKey.keyPair.privateKey) } companion object { - /** - * Negotiate a [HashAlgorithm] to be used when creating the signature. - * - * @param publicKey signing public key - * @return hash algorithm - */ @JvmStatic - fun negotiateHashAlgorithm(publicKey: PGPPublicKey): HashAlgorithm = - HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) - .negotiateHashAlgorithm( - OpenPgpKeyAttributeUtil.getOrGuessPreferredHashAlgorithms(publicKey)) + fun negotiateHashAlgorithm(key: OpenPGPComponentKey, api: PGPainless): HashAlgorithm = + key.hashAlgorithmPreferences?.toHashAlgorithms()?.first { + api.algorithmPolicy.dataSignatureHashAlgorithmPolicy.isAcceptable(it) + } + ?: api.algorithmPolicy.dataSignatureHashAlgorithmPolicy.defaultHashAlgorithm } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.kt index c4d11ea9..c8faa73e 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.kt @@ -6,8 +6,12 @@ package org.pgpainless.signature.builder import java.util.function.Predicate import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentSignature +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.bouncycastle.openpgp.api.OpenPGPSignature +import org.pgpainless.PGPainless import org.pgpainless.algorithm.SignatureType import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.signature.subpackets.SelfSignatureSubpackets @@ -23,16 +27,26 @@ class DirectKeySelfSignatureBuilder : AbstractSignatureBuilder { +class RevocationSignatureBuilder +@Throws(PGPException::class) +constructor( + signatureType: SignatureType, + signingKey: OpenPGPKey.OpenPGPSecretKey, + protector: SecretKeyRingProtector, + api: PGPainless +) : + AbstractSignatureBuilder( + signatureType, signingKey, protector, api) { override val signatureTypePredicate: Predicate get() = @@ -26,15 +40,6 @@ class RevocationSignatureBuilder : AbstractSignatureBuilder { @Throws(PGPException::class) constructor( - signingKey: PGPSecretKey, - protector: SecretKeyRingProtector - ) : super(SignatureType.GENERIC_CERTIFICATION, signingKey, protector) + signingKey: OpenPGPKey.OpenPGPSecretKey, + protector: SecretKeyRingProtector, + api: PGPainless + ) : super(SignatureType.GENERIC_CERTIFICATION, signingKey, protector, api) @Throws(PGPException::class) constructor( signatureType: SignatureType, - signingKey: PGPSecretKey, - protector: SecretKeyRingProtector - ) : super(signatureType, signingKey, protector) + signingKey: OpenPGPKey.OpenPGPSecretKey, + protector: SecretKeyRingProtector, + api: PGPainless + ) : super(signatureType, signingKey, protector, api) @Throws(PGPException::class) constructor( - primaryKey: PGPSecretKey, + primaryKey: OpenPGPKey.OpenPGPSecretKey, primaryKeyProtector: SecretKeyRingProtector, - oldCertification: PGPSignature - ) : super(primaryKey, primaryKeyProtector, oldCertification) + oldCertification: PGPSignature, + api: PGPainless + ) : super(primaryKey, primaryKeyProtector, oldCertification, api) val hashedSubpackets: SelfSignatureSubpackets = _hashedSubpackets val unhashedSubpackets: SelfSignatureSubpackets = _unhashedSubpackets @@ -61,9 +65,11 @@ class SelfSignatureBuilder : AbstractSignatureBuilder { @Throws(PGPException::class) fun build(userId: CharSequence): PGPSignature = - buildAndInitSignatureGenerator().generateCertification(userId.toString(), publicSigningKey) + buildAndInitSignatureGenerator() + .generateCertification(userId.toString(), signingKey.publicKey.pgpPublicKey) @Throws(PGPException::class) fun build(userAttributes: PGPUserAttributeSubpacketVector): PGPSignature = - buildAndInitSignatureGenerator().generateCertification(userAttributes, publicSigningKey) + buildAndInitSignatureGenerator() + .generateCertification(userAttributes, signingKey.publicKey.pgpPublicKey) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.kt index 6e2694e3..1dcfa966 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.kt @@ -7,8 +7,11 @@ package org.pgpainless.signature.builder import java.util.function.Predicate import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPPublicKey -import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey +import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentSignature +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.pgpainless.PGPainless import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.SignatureType import org.pgpainless.key.protection.SecretKeyRingProtector @@ -26,28 +29,32 @@ class SubkeyBindingSignatureBuilder : AbstractSignatureBuilder -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer - -import java.io.InputStream -import java.util.* -import openpgp.openPgpKeyId -import org.bouncycastle.openpgp.PGPPublicKey -import org.bouncycastle.openpgp.PGPPublicKeyRing -import org.bouncycastle.openpgp.PGPSignature -import org.pgpainless.PGPainless -import org.pgpainless.algorithm.KeyFlag -import org.pgpainless.algorithm.SignatureType -import org.pgpainless.bouncycastle.extensions.issuerKeyId -import org.pgpainless.exception.SignatureValidationException -import org.pgpainless.key.util.KeyRingUtils -import org.pgpainless.policy.Policy -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil -import org.slf4j.LoggerFactory - -/** - * A collection of static methods that validate signing certificates (public keys) and verify - * signature correctness. - */ -class CertificateValidator { - - companion object { - - @JvmStatic private val LOGGER = LoggerFactory.getLogger(CertificateValidator::class.java) - - /** - * Check if the signing key was eligible to create the provided signature. - * - * That entails: - * - Check, if the primary key is being revoked via key-revocation signatures. - * - Check, if the keys user-ids are revoked or not bound. - * - Check, if the signing subkey is revoked or expired. - * - Check, if the signing key is not capable of signing - * - * @param signature signature - * @param signingKeyRing signing key ring - * @param policy validation policy - * @return true if the signing key was eligible to create the signature - * @throws SignatureValidationException in case of a validation constraint violation - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun validateCertificate( - signature: PGPSignature, - signingKeyRing: PGPPublicKeyRing, - policy: Policy = PGPainless.getPolicy() - ): Boolean { - val signingSubkey: PGPPublicKey = - signingKeyRing.getPublicKey(signature.issuerKeyId) - ?: throw SignatureValidationException( - "Provided key ring does not contain a subkey with id ${signature.issuerKeyId.openPgpKeyId()}.") - val primaryKey = signingKeyRing.publicKey!! - val directKeyAndRevSigs = mutableListOf() - val rejections = mutableMapOf() - // revocations - primaryKey - .getSignaturesOfType(SignatureType.KEY_REVOCATION.code) - .asSequence() - .filter { - it.issuerKeyId == primaryKey.keyID - } // We do not support external rev keys - .forEach { - try { - if (SignatureVerifier.verifyKeyRevocationSignature( - it, primaryKey, policy, signature.creationTime)) { - directKeyAndRevSigs.add(it) - } - } catch (e: SignatureValidationException) { - rejections[it] = e - LOGGER.debug("Rejecting key revocation signature: ${e.message}", e) - } - } - - // direct-key sigs - primaryKey - .getSignaturesOfType(SignatureType.DIRECT_KEY.code) - .asSequence() - .filter { it.issuerKeyId == primaryKey.keyID } - .forEach { - try { - if (SignatureVerifier.verifyDirectKeySignature( - it, primaryKey, policy, signature.creationTime)) { - directKeyAndRevSigs.add(it) - } - } catch (e: SignatureValidationException) { - rejections[it] = e - LOGGER.debug("Rejecting key signature: ${e.message}, e") - } - } - - directKeyAndRevSigs.sortWith( - SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) - if (directKeyAndRevSigs.isNotEmpty()) { - if (directKeyAndRevSigs[0].signatureType == SignatureType.KEY_REVOCATION.code) { - throw SignatureValidationException("Primary key has been revoked.") - } - } - - // UserID signatures - val userIdSignatures = mutableMapOf>() - KeyRingUtils.getUserIdsIgnoringInvalidUTF8(primaryKey).forEach { userId -> - buildList { - primaryKey - .getSignaturesForID(userId) - .asSequence() - .filter { it.issuerKeyId == primaryKey.keyID } - .forEach { uidSig -> - try { - if (SignatureVerifier.verifySignatureOverUserId( - userId, - uidSig, - primaryKey, - policy, - signature.creationTime)) { - add(uidSig) - } - } catch (e: SignatureValidationException) { - rejections[uidSig] = e - LOGGER.debug("Rejecting user-id signature: ${e.message}", e) - } - } - } - .sortedWith( - SignatureValidityComparator( - SignatureCreationDateComparator.Order.NEW_TO_OLD)) - .let { userIdSignatures[userId] = it } - } - - val hasAnyUserIds = userIdSignatures.isNotEmpty() - val isAnyUserIdValid = - userIdSignatures.any { entry -> - entry.value.isNotEmpty() && - entry.value[0].signatureType != SignatureType.CERTIFICATION_REVOCATION.code - } - - if (hasAnyUserIds && !isAnyUserIdValid) { - throw SignatureValidationException("No valid user-id found.", rejections) - } - - // Specific signer user-id - if (policy.signerUserIdValidationLevel == Policy.SignerUserIdValidationLevel.STRICT) { - SignatureSubpacketsUtil.getSignerUserID(signature)?.let { - if (userIdSignatures[it.id] == null || userIdSignatures[it.id]!!.isEmpty()) { - throw SignatureValidationException( - "Signature was allegedly made by user-id '${it.id}'," + - " but we have no valid signatures for that on the certificate.") - } - - if (userIdSignatures[it.id]!![0].signatureType == - SignatureType.CERTIFICATION_REVOCATION.code) { - throw SignatureValidationException( - "Signature was made with user-id '${it.id}' which is revoked.") - } - } - } - - if (signingSubkey.keyID == primaryKey.keyID) { // signing key is primary key - if (directKeyAndRevSigs.isNotEmpty()) { - val directKeySig = directKeyAndRevSigs[0]!! - val flags = SignatureSubpacketsUtil.getKeyFlags(directKeySig) - if (flags != null && KeyFlag.hasKeyFlag(flags.flags, KeyFlag.SIGN_DATA)) { - return true - } - } - // Reject sigs by non-signing keys - if (userIdSignatures.none { (_, sigs) -> - sigs.any { - SignatureSubpacketsUtil.getKeyFlags(it)?.let { f -> - KeyFlag.hasKeyFlag(f.flags, KeyFlag.SIGN_DATA) - } == true - } - }) { - throw SignatureValidationException( - "Signature was generated by non-signing key.") - } - } else { // signing key is subkey - val subkeySigs = mutableListOf() - signingSubkey - .getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.code) - .asSequence() - .filter { it.issuerKeyId == primaryKey.keyID } - .forEach { - try { - if (SignatureVerifier.verifySubkeyBindingRevocation( - it, primaryKey, signingSubkey, policy, signature.creationTime)) { - subkeySigs.add(it) - } - } catch (e: SignatureValidationException) { - rejections[it] = e - LOGGER.debug("Rejecting subkey revocation signature: ${e.message}", e) - } - } - - signingSubkey - .getSignaturesOfType(SignatureType.SUBKEY_BINDING.code) - .asSequence() - .forEach { - try { - if (SignatureVerifier.verifySubkeyBindingSignature( - it, primaryKey, signingSubkey, policy, signature.creationTime)) { - subkeySigs.add(it) - } - } catch (e: SignatureValidationException) { - rejections[it] = e - LOGGER.debug("Rejecting subkey binding signature: ${e.message}", e) - } - } - - subkeySigs.sortWith( - SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) - if (subkeySigs.isEmpty()) { - throw SignatureValidationException("Subkey is not bound.", rejections) - } - - if (subkeySigs[0].signatureType == SignatureType.SUBKEY_REVOCATION.code) { - throw SignatureValidationException("Subkey is revoked.") - } - - val keyFlags = SignatureSubpacketsUtil.getKeyFlags(subkeySigs[0]) - if (keyFlags == null || !KeyFlag.hasKeyFlag(keyFlags.flags, KeyFlag.SIGN_DATA)) { - throw SignatureValidationException( - "Signature was made by key which is not capable of signing (no keyflag).") - } - } - return true - } - - /** - * Validate the given signing key and then verify the given signature while parsing out the - * signed data. Uninitialized means that no signed data has been read and the hash - * generators state has not yet been updated. - * - * @param signature uninitialized signature - * @param signedData input stream containing signed data - * @param signingKeyRing key ring containing signing key - * @param policy validation policy - * @param validationDate date of validation - * @return true if the signature is valid, false otherwise - * @throws SignatureValidationException for validation constraint violations - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun validateCertificateAndVerifyUninitializedSignature( - signature: PGPSignature, - signedData: InputStream, - signingKeyRing: PGPPublicKeyRing, - policy: Policy, - referenceTime: Date = signature.creationTime - ): Boolean { - return validateCertificate(signature, signingKeyRing, policy) && - SignatureVerifier.verifyUninitializedSignature( - signature, - signedData, - signingKeyRing.getPublicKey(signature.issuerKeyId)!!, - policy, - referenceTime) - } - - /** - * Validate the signing key and the given initialized signature. Initialized means that the - * signatures hash generator has already been updated by reading the signed data completely. - * - * @param signature initialized signature - * @param verificationKeys key ring containing the verification key - * @param policy validation policy - * @return true if the signature is valid, false otherwise - * @throws SignatureValidationException in case of a validation constraint violation - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun validateCertificateAndVerifyInitializedSignature( - signature: PGPSignature, - verificationKeys: PGPPublicKeyRing, - policy: Policy - ): Boolean { - return validateCertificate(signature, verificationKeys, policy) && - SignatureVerifier.verifyInitializedSignature( - signature, - verificationKeys.getPublicKey(signature.issuerKeyId), - policy, - signature.creationTime) - } - - /** - * Validate the signing key certificate and the given [OnePassSignatureCheck]. - * - * @param onePassSignature corresponding one-pass-signature - * @param policy policy - * @return true if the certificate is valid and the signature is correct, false otherwise. - * @throws SignatureValidationException in case of a validation error - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun validateCertificateAndVerifyOnePassSignature( - onePassSignature: OnePassSignatureCheck, - policy: Policy - ): Boolean { - return validateCertificate( - onePassSignature.signature!!, onePassSignature.verificationKeys, policy) && - SignatureVerifier.verifyOnePassSignature( - onePassSignature.signature!!, - onePassSignature.verificationKeys.getPublicKey( - onePassSignature.signature!!.issuerKeyId), - onePassSignature, - policy) - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt index 4a89e0b2..80773d7d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt @@ -5,30 +5,20 @@ package org.pgpainless.signature.consumer import org.bouncycastle.openpgp.PGPOnePassSignature -import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPSignature -import org.pgpainless.key.SubkeyIdentifier +import org.bouncycastle.openpgp.api.OpenPGPCertificate /** - * Tuple-class that bundles together a [PGPOnePassSignature] object, a [PGPPublicKeyRing] destined - * to verify the signature, the [PGPSignature] itself and a record of whether the signature was - * verified. + * Tuple-class that bundles together a [PGPOnePassSignature] object, an [OpenPGPCertificate] + * destined to verify the signature. * * @param onePassSignature the one-pass-signature packet * @param verificationKeys certificate containing the signing subkey - * @param signature the signature packet */ data class OnePassSignatureCheck( val onePassSignature: PGPOnePassSignature, - val verificationKeys: PGPPublicKeyRing, - var signature: PGPSignature? = null + val verificationKeys: OpenPGPCertificate ) { - /** - * Return an identifier for the signing key. - * - * @return signing key fingerprint - */ - val signingKey: SubkeyIdentifier - get() = SubkeyIdentifier(verificationKeys, onePassSignature.keyID) + var signature: PGPSignature? = null } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt deleted file mode 100644 index 15564773..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer - -import org.bouncycastle.openpgp.PGPKeyRing -import org.bouncycastle.openpgp.PGPSignature -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. - * - * @param signature OpenPGP signature - * @param signingKeyIdentifier identifier pointing to the exact signing key which was used to create - * the signature - * @param signingKeyRing certificate or key ring that contains the signing key that created the - * signature - */ -data class SignatureCheck( - val signature: PGPSignature, - val signingKeyRing: PGPKeyRing, - val signingKeyIdentifier: SubkeyIdentifier -) {} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt deleted file mode 100644 index a913bf32..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer - -import org.bouncycastle.openpgp.PGPSignature - -/** - * Create a new comparator which sorts signatures according to the passed ordering. - * - * @param order ordering - */ -class SignatureCreationDateComparator(private val order: Order = Order.OLD_TO_NEW) : - Comparator { - - enum class Order { - /** Oldest signatures first. */ - OLD_TO_NEW, - /** Newest signatures first. */ - NEW_TO_OLD - } - - override fun compare(one: PGPSignature, two: PGPSignature): Int { - return when (order) { - Order.OLD_TO_NEW -> one.creationTime.compareTo(two.creationTime) - Order.NEW_TO_OLD -> two.creationTime.compareTo(one.creationTime) - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt deleted file mode 100644 index 5952003e..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt +++ /dev/null @@ -1,383 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer - -import java.util.Date -import org.bouncycastle.openpgp.PGPKeyRing -import org.bouncycastle.openpgp.PGPPublicKey -import org.bouncycastle.openpgp.PGPSignature -import org.pgpainless.algorithm.SignatureType -import org.pgpainless.bouncycastle.extensions.getPublicKeyFor -import org.pgpainless.bouncycastle.extensions.isExpired -import org.pgpainless.bouncycastle.extensions.wasIssuedBy -import org.pgpainless.exception.SignatureValidationException -import org.pgpainless.policy.Policy - -/** - * Pick signatures from keys. - * - * The format of a V4 OpenPGP key is: - * - * Primary-Key [Revocation Self Signature] [Direct Key Signature...] User ID [Signature ...] [User - * ID [Signature ...] ...] [User Attribute [Signature ...] ...] [[Subkey - * [Binding-Signature-Revocation] Primary-Key-Binding-Signature] ...] - */ -class SignaturePicker { - - companion object { - - /** - * Pick the at validation date most recent valid key revocation signature. If there are hard - * revocation signatures, the latest hard revocation sig is picked, even if it was created - * after validationDate or if it is already expired. - * - * @param keyRing key ring - * @param policy policy - * @param referenceTime date of signature validation - * @return most recent, valid key revocation signature - */ - @JvmStatic - fun pickCurrentRevocationSelfSignature( - keyRing: PGPKeyRing, - policy: Policy, - referenceTime: Date - ): PGPSignature? { - val primaryKey = keyRing.publicKey - return getSortedSignaturesOfType(primaryKey, SignatureType.KEY_REVOCATION).lastOrNull { - return@lastOrNull try { - SignatureVerifier.verifyKeyRevocationSignature( - it, primaryKey, policy, referenceTime) - true // valid - } catch (e: SignatureValidationException) { - false // not valid - } - } - } - - /** - * Pick the at validationDate most recent, valid direct key signature. This method might - * return null, if there is no direct key self-signature which is valid at validationDate. - * - * @param keyRing key ring - * @param policy policy - * @param referenceTime validation date - * @return direct-key self-signature - */ - @JvmStatic - fun pickCurrentDirectKeySelfSignature( - keyRing: PGPKeyRing, - policy: Policy, - referenceTime: Date - ): PGPSignature? { - val primaryKey = keyRing.publicKey - return pickCurrentDirectKeySignature(primaryKey, primaryKey, policy, referenceTime) - } - - @JvmStatic - fun pickCurrentDirectKeySignature( - signingKey: PGPPublicKey, - signedKey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): PGPSignature? { - return getSortedSignaturesOfType(signedKey, SignatureType.DIRECT_KEY).lastOrNull { - return@lastOrNull try { - SignatureVerifier.verifyDirectKeySignature( - it, signingKey, signedKey, policy, referenceTime) - true - } catch (e: SignatureValidationException) { - false - } - } - } - - /** - * Pick the at validationDate latest direct key signature. This method might return an - * expired signature. If there are more than one direct-key signature, and some of those are - * not expired, the latest non-expired yet already effective direct-key signature will be - * returned. - * - * @param keyRing key ring - * @param policy policy - * @param referenceTime validation date - * @return latest direct key signature - */ - @JvmStatic - fun pickLatestDirectKeySignature( - keyRing: PGPKeyRing, - policy: Policy, - referenceTime: Date - ): PGPSignature? { - return pickLatestDirectKeySignature( - keyRing.publicKey, keyRing.publicKey, policy, referenceTime) - } - - /** - * Pick the at validationDate latest direct key signature made by signingKey on signedKey. - * This method might return an expired signature. If a non-expired direct-key signature - * exists, the latest non-expired yet already effective direct-key signature will be - * returned. - * - * @param signingKey signing key (key that made the sig) - * @param signedKey signed key (key that carries the sig) - * @param policy policy - * @param referenceTime date of validation - * @return latest direct key sig - */ - @JvmStatic - fun pickLatestDirectKeySignature( - signingKey: PGPPublicKey, - signedKey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): PGPSignature? { - var latest: PGPSignature? = null - return getSortedSignaturesOfType(signedKey, SignatureType.DIRECT_KEY).lastOrNull { - try { - SignatureValidator.signatureIsOfType(SignatureType.DIRECT_KEY).verify(it) - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(it) - SignatureValidator.signatureIsAlreadyEffective(referenceTime).verify(it) - if (latest != null && !latest!!.isExpired(referenceTime)) { - SignatureValidator.signatureIsNotYetExpired(referenceTime).verify(it) - } - SignatureValidator.correctSignatureOverKey(signingKey, signedKey).verify(it) - latest = it - true - } catch (e: SignatureValidationException) { - false - } - } - } - - /** - * Pick the at validationDate most recent, valid user-id revocation signature. If there are - * hard revocation signatures, the latest hard revocation sig is picked, even if it was - * created after validationDate or if it is already expired. - * - * @param keyRing key ring - * @param userId user-Id that gets revoked - * @param policy policy - * @param referenceTime validation date - * @return revocation signature - */ - @JvmStatic - fun pickCurrentUserIdRevocationSignature( - keyRing: PGPKeyRing, - userId: CharSequence, - policy: Policy, - referenceTime: Date - ): PGPSignature? { - val primaryKey = keyRing.publicKey - return getSortedSignaturesOfType(primaryKey, SignatureType.CERTIFICATION_REVOCATION) - .lastOrNull { - keyRing.getPublicKeyFor(it) - ?: return@lastOrNull false // signature made by external key. skip. - return@lastOrNull try { - SignatureVerifier.verifyUserIdRevocation( - userId.toString(), it, primaryKey, policy, referenceTime) - true - } catch (e: SignatureValidationException) { - false // signature not valid - } - } - } - - /** - * Pick the at validationDate latest, valid certification self-signature for the given - * user-id. This method might return null, if there is no certification self signature for - * that user-id which is valid at validationDate. - * - * @param keyRing keyring - * @param userId userid - * @param policy policy - * @param referenceTime validation date - * @return user-id certification - */ - @JvmStatic - fun pickCurrentUserIdCertificationSignature( - keyRing: PGPKeyRing, - userId: CharSequence, - policy: Policy, - referenceTime: Date - ): PGPSignature? { - val primaryKey = keyRing.publicKey - return primaryKey - .getSignaturesForID(userId.toString()) - .asSequence() - .sortedWith(SignatureCreationDateComparator()) - .lastOrNull { - return@lastOrNull it.wasIssuedBy(primaryKey) && - try { - SignatureVerifier.verifyUserIdCertification( - userId.toString(), it, primaryKey, policy, referenceTime) - true - } catch (e: SignatureValidationException) { - false - } - } - } - - /** - * Pick the at validationDate latest certification self-signature for the given user-id. - * This method might return an expired signature. If a non-expired user-id certification - * signature exists, the latest non-expired yet already effective user-id certification - * signature for the given user-id will be returned. - * - * @param keyRing keyring - * @param userId userid - * @param policy policy - * @param referenceTime validation date - * @return user-id certification - */ - @JvmStatic - fun pickLatestUserIdCertificationSignature( - keyRing: PGPKeyRing, - userId: CharSequence, - policy: Policy, - referenceTime: Date - ): PGPSignature? { - val primaryKey = keyRing.publicKey - return primaryKey - .getSignaturesForID(userId.toString()) - .asSequence() - .sortedWith(SignatureCreationDateComparator()) - .lastOrNull { - return@lastOrNull try { - SignatureValidator.wasPossiblyMadeByKey(primaryKey).verify(it) - SignatureValidator.signatureIsCertification().verify(it) - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy) - .verify(it) - SignatureValidator.signatureIsAlreadyEffective(referenceTime).verify(it) - SignatureValidator.correctSignatureOverUserId( - userId.toString(), primaryKey, primaryKey) - .verify(it) - true - } catch (e: SignatureValidationException) { - false - } - } - } - - /** - * Pick the at validationDate most recent, valid subkey revocation signature. If there are - * hard revocation signatures, the latest hard revocation sig is picked, even if it was - * created after validationDate or if it is already expired. - * - * @param keyRing keyring - * @param subkey subkey - * @param policy policy - * @param referenceTime validation date - * @return subkey revocation signature - */ - @JvmStatic - fun pickCurrentSubkeyBindingRevocationSignature( - keyRing: PGPKeyRing, - subkey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): PGPSignature? { - val primaryKey = keyRing.publicKey - require(primaryKey.keyID != subkey.keyID) { - "Primary key cannot have subkey binding revocations." - } - return getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_REVOCATION).lastOrNull { - return@lastOrNull try { - SignatureVerifier.verifySubkeyBindingRevocation( - it, primaryKey, subkey, policy, referenceTime) - true - } catch (e: SignatureValidationException) { - false - } - } - } - - /** - * Pick the at validationDate latest, valid subkey binding signature for the given subkey. - * This method might return null, if there is no subkey binding signature which is valid at - * validationDate. - * - * @param keyRing key ring - * @param subkey subkey - * @param policy policy - * @param referenceTime date of validation - * @return most recent valid subkey binding signature - */ - @JvmStatic - fun pickCurrentSubkeyBindingSignature( - keyRing: PGPKeyRing, - subkey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): PGPSignature? { - val primaryKey = keyRing.publicKey - require(primaryKey.keyID != subkey.keyID) { - "Primary key cannot have subkey binding signatures." - } - return getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_BINDING).lastOrNull { - return@lastOrNull try { - SignatureVerifier.verifySubkeyBindingSignature( - it, primaryKey, subkey, policy, referenceTime) - true - } catch (e: SignatureValidationException) { - false - } - } - } - - /** - * Pick the at validationDate latest subkey binding signature for the given subkey. This - * method might return an expired signature. If a non-expired subkey binding signature - * exists, the latest non-expired yet already effective subkey binding signature for the - * given subkey will be returned. - * - * @param keyRing key ring - * @param subkey subkey - * @param policy policy - * @param referenceTime validationDate - * @return subkey binding signature - */ - @JvmStatic - fun pickLatestSubkeyBindingSignature( - keyRing: PGPKeyRing, - subkey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): PGPSignature? { - val primaryKey = keyRing.publicKey - require(primaryKey.keyID != subkey.keyID) { - "Primary key cannot have subkey binding signatures." - } - var latest: PGPSignature? = null - return getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_BINDING).lastOrNull { - return@lastOrNull try { - SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_BINDING).verify(it) - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(it) - SignatureValidator.signatureDoesNotPredateSignee(subkey).verify(it) - SignatureValidator.signatureIsAlreadyEffective(referenceTime).verify(it) - // if the currently latest signature is not yet expired, check if the next - // candidate is not yet expired - if (latest != null && !latest!!.isExpired(referenceTime)) { - SignatureValidator.signatureIsNotYetExpired(referenceTime).verify(it) - } - SignatureValidator.correctSubkeyBindingSignature(primaryKey, subkey).verify(it) - latest = it - true - } catch (e: SignatureValidationException) { - false - } - } - } - - @JvmStatic - private fun getSortedSignaturesOfType( - key: PGPPublicKey, - type: SignatureType - ): List = - key.getSignaturesOfType(type.code) - .asSequence() - .sortedWith(SignatureCreationDateComparator()) - .toList() - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt deleted file mode 100644 index 7cc384e1..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt +++ /dev/null @@ -1,701 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer - -import java.lang.Exception -import java.util.Date -import openpgp.formatUTC -import openpgp.openPgpKeyId -import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPPublicKey -import org.bouncycastle.openpgp.PGPSignature -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector -import org.pgpainless.algorithm.KeyFlag -import org.pgpainless.algorithm.SignatureSubpacket -import org.pgpainless.algorithm.SignatureType -import org.pgpainless.bouncycastle.extensions.fingerprint -import org.pgpainless.bouncycastle.extensions.isHardRevocation -import org.pgpainless.bouncycastle.extensions.isOfType -import org.pgpainless.bouncycastle.extensions.publicKeyAlgorithm -import org.pgpainless.bouncycastle.extensions.signatureExpirationDate -import org.pgpainless.bouncycastle.extensions.signatureHashAlgorithm -import org.pgpainless.exception.SignatureValidationException -import org.pgpainless.implementation.ImplementationFactory -import org.pgpainless.key.OpenPgpFingerprint -import org.pgpainless.policy.Policy -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil -import org.pgpainless.util.NotationRegistry - -abstract class SignatureValidator { - - @Throws(SignatureValidationException::class) abstract fun verify(signature: PGPSignature) - - companion object { - - /** - * Check, whether there is the possibility that the given signature was created by the given - * key. [verify] throws a [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), [verify] will simply return since it is plausible that the given key - * created the sig. - * - * @param signingKey signing key - * @return validator that throws a [SignatureValidationException] if the signature was not - * possibly made by the given key. - */ - @JvmStatic - fun wasPossiblyMadeByKey(signingKey: PGPPublicKey): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - val signingKeyFingerprint = OpenPgpFingerprint.of(signingKey) - val issuer = SignatureSubpacketsUtil.getIssuerKeyIdAsLong(signature) - - if (issuer != null) { - if (issuer != signingKey.keyID) { - throw SignatureValidationException( - "Signature was not created by" + - " $signingKeyFingerprint (signature issuer: ${issuer.openPgpKeyId()})") - } - } - - if (signature.fingerprint != null && - signature.fingerprint != signingKeyFingerprint) { - throw SignatureValidationException( - "Signature was not created by" + - " $signingKeyFingerprint (signature fingerprint: ${signature.fingerprint})") - } - } - - // No issuer information found, so we cannot rule out that we did not create the sig - } - } - - /** - * Verify that a subkey binding signature - if the subkey is signing-capable - contains a - * valid primary key binding signature. - * - * @param primaryKey primary key - * @param subkey subkey - * @param policy policy - * @param referenceDate reference date for signature verification - * @return validator - */ - @JvmStatic - fun hasValidPrimaryKeyBindingSignatureIfRequired( - primaryKey: PGPPublicKey, - subkey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - if (!signature.publicKeyAlgorithm.isSigningCapable()) { - // subkey is not signing capable -> No need to process embedded signatures - return - } - - // Make sure we have key flags - SignatureSubpacketsUtil.getKeyFlags(signature)?.let { - if (!KeyFlag.hasKeyFlag(it.flags, KeyFlag.SIGN_DATA) && - !KeyFlag.hasKeyFlag(it.flags, KeyFlag.CERTIFY_OTHER)) { - return - } - } - ?: return - - try { - val embeddedSignatures = - SignatureSubpacketsUtil.getEmbeddedSignature(signature) - if (embeddedSignatures.isEmpty) { - throw SignatureValidationException( - "Missing primary key binding" + - " signature on signing capable subkey ${subkey.keyID.openPgpKeyId()}", - mapOf()) - } - - val rejectedEmbeddedSignatures = mutableMapOf() - if (!embeddedSignatures.any { embedded -> - if (embedded.isOfType(SignatureType.PRIMARYKEY_BINDING)) { - try { - signatureStructureIsAcceptable(subkey, policy).verify(embedded) - signatureIsEffective(referenceTime).verify(embedded) - correctPrimaryKeyBindingSignature(primaryKey, subkey) - .verify(embedded) - return@any true - } catch (e: SignatureValidationException) { - rejectedEmbeddedSignatures[embedded] = e - } - } - false - }) { - throw SignatureValidationException( - "Missing primary key binding signature on signing capable subkey ${subkey.keyID.openPgpKeyId()}", - rejectedEmbeddedSignatures) - } - } catch (e: PGPException) { - throw SignatureValidationException( - "Cannot process list of embedded signatures.", e) - } - } - } - } - - /** - * Verify that a signature has an acceptable structure. - * - * @param signingKey signing key - * @param policy policy - * @return validator - */ - @JvmStatic - fun signatureStructureIsAcceptable( - signingKey: PGPPublicKey, - policy: Policy - ): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - signatureIsNotMalformed(signingKey).verify(signature) - if (signature.version >= 4) { - signatureDoesNotHaveCriticalUnknownNotations(policy.notationRegistry) - .verify(signature) - signatureDoesNotHaveCriticalUnknownSubpackets().verify(signature) - } - signatureUsesAcceptableHashAlgorithm(policy).verify(signature) - signatureUsesAcceptablePublicKeyAlgorithm(policy, signingKey).verify(signature) - } - } - } - - /** - * Verify that a signature was made using an acceptable [PublicKeyAlgorithm]. - * - * @param policy policy - * @param signingKey signing key - * @return validator - */ - @JvmStatic - fun signatureUsesAcceptablePublicKeyAlgorithm( - policy: Policy, - signingKey: PGPPublicKey - ): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - if (signingKey.bitStrength == -1) { - throw SignatureValidationException( - "Cannot determine bit strength of signing key.") - } - if (!policy.publicKeyAlgorithmPolicy.isAcceptable( - signingKey.publicKeyAlgorithm, signingKey.bitStrength)) { - throw SignatureValidationException( - "Signature was made using unacceptable key. " + - "${signingKey.publicKeyAlgorithm} (${signingKey.bitStrength} bits) is " + - "not acceptable according to the public key algorithm policy.") - } - } - } - } - - @JvmStatic - fun signatureUsesAcceptableHashAlgorithm(policy: Policy): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - try { - val algorithmPolicy = getHashAlgorithmPolicyForSignature(signature, policy) - if (!algorithmPolicy.isAcceptable( - signature.signatureHashAlgorithm, signature.creationTime)) { - throw SignatureValidationException( - "Signature uses unacceptable" + - " hash algorithm ${signature.signatureHashAlgorithm}" + - " (Signature creation time: ${signature.creationTime.formatUTC()})") - } - } catch (e: NoSuchElementException) { - throw SignatureValidationException( - "Signature uses unknown hash" + " algorithm ${signature.hashAlgorithm}") - } - } - } - } - - /** - * Return the applicable [Policy.HashAlgorithmPolicy] for the given [PGPSignature]. - * Revocation signatures are being policed using a different policy than non-revocation - * signatures. - * - * @param signature signature - * @param policy revocation policy for revocation sigs, normal policy for non-rev sigs - * @return policy - */ - @JvmStatic - private fun getHashAlgorithmPolicyForSignature( - signature: PGPSignature, - policy: Policy - ): Policy.HashAlgorithmPolicy { - return when (SignatureType.fromCode(signature.signatureType)) { - null -> policy.certificationSignatureHashAlgorithmPolicy - SignatureType.CERTIFICATION_REVOCATION, - SignatureType.KEY_REVOCATION, - SignatureType.SUBKEY_REVOCATION -> policy.revocationSignatureHashAlgorithmPolicy - SignatureType.GENERIC_CERTIFICATION, - SignatureType.NO_CERTIFICATION, - SignatureType.CASUAL_CERTIFICATION, - SignatureType.POSITIVE_CERTIFICATION, - SignatureType.DIRECT_KEY, - SignatureType.SUBKEY_BINDING, - SignatureType.PRIMARYKEY_BINDING -> policy.certificationSignatureHashAlgorithmPolicy - else -> policy.dataSignatureHashAlgorithmPolicy - } - } - - /** - * Verify that a signature does not carry critical unknown notations. - * - * @param registry notation registry of known notations - * @return validator - */ - @JvmStatic - fun signatureDoesNotHaveCriticalUnknownNotations( - registry: NotationRegistry - ): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - SignatureSubpacketsUtil.getHashedNotationData(signature) - .filter { it.isCritical && !registry.isKnownNotation(it.notationName) } - .forEach { - throw SignatureValidationException( - "Signature contains unknown critical notation '${it.notationName}' in its hashed area.") - } - } - } - } - - /** - * Verify that a signature does not contain critical unknown subpackets. - * - * @return validator - */ - @JvmStatic - fun signatureDoesNotHaveCriticalUnknownSubpackets(): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - signature.hashedSubPackets.criticalTags.forEach { - try { - SignatureSubpacket.requireFromCode(it) - } catch (e: NoSuchElementException) { - throw SignatureValidationException( - "Signature contains unknown critical subpacket of type 0x${Integer.toHexString(it)}") - } - } - } - } - } - - /** - * Verify that a signature is effective at the given reference date. - * - * @param referenceTime reference date for signature verification - * @return validator - */ - @JvmStatic - @JvmOverloads - fun signatureIsEffective(referenceTime: Date = Date()): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - signatureIsAlreadyEffective(referenceTime).verify(signature) - signatureIsNotYetExpired(referenceTime).verify(signature) - } - } - } - - /** - * Verify that a signature was created prior to the given reference date. - * - * @param referenceTime reference date for signature verification - * @return validator - */ - @JvmStatic - fun signatureIsAlreadyEffective(referenceTime: Date): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - if (signature.isHardRevocation) { - return - } - if (signature.creationTime > referenceTime) { - throw SignatureValidationException( - "Signature was created at ${signature.creationTime.formatUTC()} and" + - " is therefore not yet valid at ${referenceTime.formatUTC()}") - } - } - } - } - - /** - * Verify that a signature is not yet expired. - * - * @param referenceTime reference date for signature verification - * @return validator - */ - @JvmStatic - fun signatureIsNotYetExpired(referenceTime: Date): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - if (signature.isHardRevocation) { - return - } - val expirationDate = signature.signatureExpirationDate - if (expirationDate != null && expirationDate < referenceTime) { - throw SignatureValidationException( - "Signature is already expired " + - "(expiration: ${expirationDate.formatUTC()}," + - " validation: ${referenceTime.formatUTC()})") - } - } - } - } - - /** - * Verify that a signature is not malformed. A signature is malformed if it has no hashed - * creation time subpacket, it predates the creation time of the signing key, or it predates - * the creation date of the signing key binding signature. - * - * @param signingKey signing key - * @return validator - */ - @JvmStatic - fun signatureIsNotMalformed(signingKey: PGPPublicKey): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - if (signature.version >= 4) { - signatureHasHashedCreationTime().verify(signature) - } - signatureDoesNotPredateSigningKey(signingKey).verify(signature) - if (!signature.isOfType(SignatureType.PRIMARYKEY_BINDING)) { - signatureDoesNotPredateSigningKeyBindingDate(signingKey).verify(signature) - } - } - } - } - - @JvmStatic - fun signatureDoesNotPredateSignee(signee: PGPPublicKey): SignatureValidator { - return signatureDoesNotPredateKeyCreation(signee) - } - - /** - * Verify that a signature does not predate the creation time of the signing key. - * - * @param key signing key - * @return validator - */ - @JvmStatic - fun signatureDoesNotPredateSigningKey(signingKey: PGPPublicKey): SignatureValidator { - return signatureDoesNotPredateKeyCreation(signingKey) - } - - /** - * Verify that a signature does not predate the creation time of the given key. - * - * @param key key - * @return validator - */ - @JvmStatic - fun signatureDoesNotPredateKeyCreation(key: PGPPublicKey): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - if (key.creationTime > signature.creationTime) { - throw SignatureValidationException( - "Signature predates key" + - " (key creation: ${key.creationTime.formatUTC()}," + - " signature creation: ${signature.creationTime.formatUTC()})") - } - } - } - } - - /** - * Verify that a signature has a hashed creation time subpacket. - * - * @return validator - */ - @JvmStatic - fun signatureHasHashedCreationTime(): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - if (SignatureSubpacketsUtil.getSignatureCreationTime(signature) == null) { - throw SignatureValidationException( - "Malformed signature." + - "Signature has no signature creation time subpacket in its hashed area.") - } - } - } - } - - /** - * Verify that a signature does not predate the binding date of the signing key. - * - * @param signingKey signing key - * @return validator - */ - @JvmStatic - fun signatureDoesNotPredateSigningKeyBindingDate( - signingKey: PGPPublicKey - ): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - if (signingKey.isMasterKey) { - return - } - if (signingKey - .getSignaturesOfType(SignatureType.SUBKEY_BINDING.code) - .asSequence() - .map { - if (signature.creationTime < it.creationTime) { - throw SignatureValidationException( - "Signature was created " + - "before the signing key was bound to the certificate.") - } - } - .none()) { - throw SignatureValidationException( - "Signing subkey does not have a subkey binding signature.") - } - } - } - } - - /** - * Verify that a subkey binding signature is correct. - * - * @param primaryKey primary key - * @param subkey subkey - * @return validator - */ - @JvmStatic - fun correctSubkeyBindingSignature( - primaryKey: PGPPublicKey, - subkey: PGPPublicKey - ): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - if (primaryKey.keyID == subkey.keyID) { - throw SignatureValidationException("Primary key cannot be its own subkey.") - } - try { - signature.init( - ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, - primaryKey) - if (!signature.verifyCertification(primaryKey, subkey)) { - throw SignatureValidationException("Signature is not correct.") - } - } catch (e: PGPException) { - throw SignatureValidationException( - "Cannot verify subkey binding signature correctness", e) - } catch (e: ClassCastException) { - throw SignatureValidationException( - "Cannot verify subkey binding signature correctness", e) - } - } - } - } - - /** - * Verify that a primary key binding signature is correct. - * - * @param primaryKey primary key - * @param subkey subkey - * @return validator - */ - @JvmStatic - fun correctPrimaryKeyBindingSignature( - primaryKey: PGPPublicKey, - subkey: PGPPublicKey - ): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - if (primaryKey.keyID == subkey.keyID) { - throw SignatureValidationException("Primary key cannot be its own subkey.") - } - try { - signature.init( - ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, - subkey) - if (!signature.verifyCertification(primaryKey, subkey)) { - throw SignatureValidationException( - "Primary Key Binding Signature is not correct.") - } - } catch (e: PGPException) { - throw SignatureValidationException( - "Cannot verify primary key binding signature correctness", e) - } catch (e: ClassCastException) { - throw SignatureValidationException( - "Cannot verify primary key binding signature correctness", e) - } - } - } - } - - /** - * Verify that a direct-key signature is correct. - * - * @param signingKey signing key - * @param signedKey signed key - * @return validator - */ - @JvmStatic - fun correctSignatureOverKey( - signingKey: PGPPublicKey, - signedKey: PGPPublicKey - ): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - try { - signature.init( - ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, - signingKey) - val valid = - if (signingKey.keyID == signedKey.keyID || - signature.isOfType(SignatureType.DIRECT_KEY)) { - signature.verifyCertification(signedKey) - } else { - signature.verifyCertification(signingKey, signedKey) - } - if (!valid) { - throw SignatureValidationException("Signature is not correct.") - } - } catch (e: PGPException) { - throw SignatureValidationException( - "Cannot verify direct-key signature correctness", e) - } catch (e: ClassCastException) { - throw SignatureValidationException( - "Cannot verify direct-key signature correctness", e) - } - } - } - } - - @JvmStatic - fun signatureIsCertification(): SignatureValidator { - return signatureIsOfType( - SignatureType.POSITIVE_CERTIFICATION, - SignatureType.CASUAL_CERTIFICATION, - SignatureType.GENERIC_CERTIFICATION, - SignatureType.NO_CERTIFICATION) - } - - /** - * Verify that a signature type equals one of the given [SignatureType]. - * - * @param signatureType one or more signature types - * @return validator - */ - @JvmStatic - fun signatureIsOfType(vararg signatureType: SignatureType): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - if (signatureType.none { signature.isOfType(it) }) { - throw SignatureValidationException( - "Signature is of type" + - " ${SignatureType.fromCode(signature.signatureType) ?: - ("0x" + signature.signatureType.toString(16))}, " + - "while only ${signatureType.contentToString()} are allowed here.") - } - } - } - } - - /** - * Verify that a signature over a user-id is correct. - * - * @param userId user-id - * @param certifiedKey key carrying the user-id - * @param certifyingKey key that created the signature. - * @return validator - */ - @JvmStatic - fun correctSignatureOverUserId( - userId: CharSequence, - certifiedKey: PGPPublicKey, - certifyingKey: PGPPublicKey - ): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - try { - signature.init( - ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, - certifyingKey) - if (!signature.verifyCertification(userId.toString(), certifiedKey)) { - throw SignatureValidationException( - "Signature over user-id '$userId' is not valid.") - } - } catch (e: PGPException) { - throw SignatureValidationException( - "Cannot verify signature over user-id '$userId'.", e) - } catch (e: ClassCastException) { - throw SignatureValidationException( - "Cannot verify signature over user-id '$userId'.", e) - } - } - } - } - - /** - * Verify that a signature over a user-attribute packet is correct. - * - * @param userAttributes user attributes - * @param certifiedKey key carrying the user-attributes - * @param certifyingKey key that created the certification signature - * @return validator - */ - @JvmStatic - fun correctSignatureOverUserAttributes( - userAttributes: PGPUserAttributeSubpacketVector, - certifiedKey: PGPPublicKey, - certifyingKey: PGPPublicKey - ): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - try { - signature.init( - ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, - certifyingKey) - if (!signature.verifyCertification(userAttributes, certifiedKey)) { - throw SignatureValidationException( - "Signature over user-attributes is not correct.") - } - } catch (e: PGPException) { - throw SignatureValidationException( - "Cannot verify signature over user-attribute vector.", e) - } catch (e: ClassCastException) { - throw SignatureValidationException( - "Cannot verify signature over user-attribute vector.", e) - } - } - } - } - - @JvmStatic - fun signatureWasCreatedInBounds(notBefore: Date?, notAfter: Date?): SignatureValidator { - return object : SignatureValidator() { - override fun verify(signature: PGPSignature) { - val timestamp = signature.creationTime - if (notBefore != null && timestamp < notBefore) { - throw SignatureValidationException( - "Signature was made before the earliest allowed signature creation time." + - " Created: ${timestamp.formatUTC()}," + - " earliest allowed: ${notBefore.formatUTC()}") - } - if (notAfter != null && timestamp > notAfter) { - throw SignatureValidationException( - "Signature was made after the latest allowed signature creation time." + - " Created: ${timestamp.formatUTC()}," + - " latest allowed: ${notAfter.formatUTC()}") - } - } - } - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt deleted file mode 100644 index 1153b875..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer - -import org.bouncycastle.openpgp.PGPSignature -import org.pgpainless.bouncycastle.extensions.isHardRevocation - -/** - * Comparator which sorts signatures based on an ordering and on revocation hardness. - * - * If a list of signatures gets ordered using this comparator, hard revocations will always come - * first. Further, signatures are ordered by date according to the - * [SignatureCreationDateComparator.Order]. - */ -class SignatureValidityComparator( - order: SignatureCreationDateComparator.Order = SignatureCreationDateComparator.Order.OLD_TO_NEW -) : Comparator { - - private val creationDateComparator: SignatureCreationDateComparator = - SignatureCreationDateComparator(order) - - override fun compare(one: PGPSignature, two: PGPSignature): Int { - return if (one.isHardRevocation == two.isHardRevocation) { - // Both have the same hardness, so compare creation time - creationDateComparator.compare(one, two) - } - // else favor the "harder" signature - else if (one.isHardRevocation) -1 else 1 - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt deleted file mode 100644 index d51b2379..00000000 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt +++ /dev/null @@ -1,597 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer - -import java.io.IOException -import java.io.InputStream -import java.util.* -import openpgp.openPgpKeyId -import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPPublicKey -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.Companion.getInstance -import org.pgpainless.policy.Policy -import org.pgpainless.signature.consumer.SignatureValidator.Companion.correctSignatureOverKey -import org.pgpainless.signature.consumer.SignatureValidator.Companion.correctSignatureOverUserAttributes -import org.pgpainless.signature.consumer.SignatureValidator.Companion.correctSignatureOverUserId -import org.pgpainless.signature.consumer.SignatureValidator.Companion.correctSubkeyBindingSignature -import org.pgpainless.signature.consumer.SignatureValidator.Companion.hasValidPrimaryKeyBindingSignatureIfRequired -import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureDoesNotPredateSignee -import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureIsCertification -import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureIsEffective -import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureIsOfType -import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureStructureIsAcceptable -import org.pgpainless.signature.consumer.SignatureValidator.Companion.wasPossiblyMadeByKey - -/** - * Collection of static methods for signature verification. Signature verification entails - * validation of certain criteria (see [SignatureValidator]), as well as cryptographic verification - * of signature correctness. - */ -class SignatureVerifier { - - companion object { - - /** - * Verify a signature (certification or revocation) over a user-id. - * - * @param userId user-id - * @param signature certification signature - * @param signingKey key that created the certification - * @param keyWithUserId key carrying the user-id - * @param policy policy - * @param referenceTime reference date for signature verification - * @return true if signature verification is successful - * @throws SignatureValidationException if signature verification fails for some reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifySignatureOverUserId( - userId: CharSequence, - signature: PGPSignature, - signingKey: PGPPublicKey, - keyWithUserId: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - val type = SignatureType.fromCode(signature.signatureType) - return when (type) { - SignatureType.GENERIC_CERTIFICATION, - SignatureType.NO_CERTIFICATION, - SignatureType.CASUAL_CERTIFICATION, - SignatureType.POSITIVE_CERTIFICATION, - null -> - verifyUserIdCertification( - userId, signature, signingKey, keyWithUserId, policy, referenceTime) - SignatureType.CERTIFICATION_REVOCATION -> - verifyUserIdRevocation( - userId, signature, signingKey, keyWithUserId, policy, referenceTime) - else -> - throw SignatureValidationException( - "Signature is not a valid user-id certification/revocation signature: $type") - } - } - - /** - * Verify a certification self-signature over a user-id. - * - * @param userId user-id - * @param signature certification signature - * @param primaryKey primary key - * @param policy policy - * @param referenceTime reference date for signature verification - * @return true if the self-signature is verified successfully - * @throws SignatureValidationException if signature verification fails for some reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifyUserIdCertification( - userId: CharSequence, - signature: PGPSignature, - primaryKey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - return verifyUserIdCertification( - userId, signature, primaryKey, primaryKey, policy, referenceTime) - } - - /** - * Verify a user-id certification. - * - * @param userId user-id - * @param signature certification signature - * @param signingKey key that created the certification - * @param keyWithUserId primary key that carries the user-id - * @param policy policy - * @param referenceTime reference date for signature verification - * @return true if signature verification is successful - * @throws SignatureValidationException if signature verification fails for some reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifyUserIdCertification( - userId: CharSequence, - signature: PGPSignature, - signingKey: PGPPublicKey, - keyWithUserId: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - wasPossiblyMadeByKey(signingKey).verify(signature) - signatureIsCertification().verify(signature) - signatureStructureIsAcceptable(signingKey, policy).verify(signature) - signatureIsEffective(referenceTime).verify(signature) - correctSignatureOverUserId(userId, keyWithUserId, signingKey).verify(signature) - - return true - } - - /** - * Verify a user-id revocation self-signature. - * - * @param userId user-id - * @param signature user-id revocation signature - * @param primaryKey primary key - * @param policy policy - * @param referenceTime 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 - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifyUserIdRevocation( - userId: CharSequence, - signature: PGPSignature, - primaryKey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - return verifyUserIdRevocation( - userId, signature, primaryKey, primaryKey, policy, referenceTime) - } - - /** - * Verify a user-id revocation signature. - * - * @param userId user-id - * @param signature revocation signature - * @param signingKey key that created the revocation signature - * @param keyWithUserId primary key carrying the user-id - * @param policy policy - * @param referenceTime 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 - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifyUserIdRevocation( - userId: CharSequence, - signature: PGPSignature, - signingKey: PGPPublicKey, - keyWithUserId: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - wasPossiblyMadeByKey(signingKey).verify(signature) - signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature) - signatureStructureIsAcceptable(signingKey, policy).verify(signature) - signatureIsEffective(referenceTime).verify(signature) - correctSignatureOverUserId(userId, keyWithUserId, signingKey).verify(signature) - - return true - } - - /** - * Verify a certification self-signature over a user-attributes packet. - * - * @param userAttributes user attributes - * @param signature certification self-signature - * @param primaryKey primary key that carries the user-attributes - * @param policy policy - * @param referenceTime reference date for signature verification - * @return true if the signature can be verified successfully - * @throws SignatureValidationException if signature verification fails for some reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifyUserAttributesCertification( - userAttributes: PGPUserAttributeSubpacketVector, - signature: PGPSignature, - primaryKey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - return verifyUserAttributesCertification( - userAttributes, signature, primaryKey, primaryKey, policy, referenceTime) - } - - /** - * Verify a certification signature over a user-attributes packet. - * - * @param userAttributes user attributes - * @param signature certification signature - * @param signingKey key that created the user-attributes certification - * @param keyWithAttributes key that carries the user-attributes certification - * @param policy policy - * @param referenceTime reference date for signature verification - * @return true if the signature can be verified successfully - * @throws SignatureValidationException if signature verification fails for some reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifyUserAttributesCertification( - userAttributes: PGPUserAttributeSubpacketVector, - signature: PGPSignature, - signingKey: PGPPublicKey, - keyWithAttributes: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - wasPossiblyMadeByKey(signingKey).verify(signature) - signatureIsCertification().verify(signature) - signatureStructureIsAcceptable(signingKey, policy).verify(signature) - signatureIsEffective(referenceTime).verify(signature) - correctSignatureOverUserAttributes(userAttributes, keyWithAttributes, signingKey) - .verify(signature) - - return true - } - - /** - * Verify a user-attributes revocation self-signature. - * - * @param userAttributes user-attributes - * @param signature user-attributes revocation signature - * @param primaryKey primary key that carries the user-attributes - * @param policy policy - * @param referenceTime reference date for signature verification - * @return true if the revocation signature can be verified successfully - * @throws SignatureValidationException if signature verification fails for some reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifyUserAttributesRevocation( - userAttributes: PGPUserAttributeSubpacketVector, - signature: PGPSignature, - primaryKey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - return verifyUserAttributesRevocation( - userAttributes, signature, primaryKey, primaryKey, policy, referenceTime) - } - - /** - * Verify a user-attributes revocation signature. - * - * @param userAttributes user-attributes - * @param signature revocation signature - * @param signingKey revocation key - * @param keyWithAttributes key that carries the user-attributes - * @param policy policy - * @param referenceTime reference date for signature verification - * @return true if the revocation signature can be verified successfully - * @throws SignatureValidationException if signature verification fails for some reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifyUserAttributesRevocation( - userAttributes: PGPUserAttributeSubpacketVector, - signature: PGPSignature, - signingKey: PGPPublicKey, - keyWithAttributes: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - wasPossiblyMadeByKey(signingKey).verify(signature) - signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature) - signatureStructureIsAcceptable(signingKey, policy).verify(signature) - signatureIsEffective(referenceTime).verify(signature) - correctSignatureOverUserAttributes(userAttributes, keyWithAttributes, signingKey) - .verify(signature) - - return true - } - - /** - * Verify a subkey binding signature. - * - * @param signature binding signature - * @param primaryKey primary key - * @param subkey subkey - * @param policy policy - * @param referenceTime reference date for signature verification - * @return true if the binding signature can be verified successfully - * @throws SignatureValidationException if signature verification fails for some reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifySubkeyBindingSignature( - signature: PGPSignature, - primaryKey: PGPPublicKey, - subkey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - signatureIsOfType(SignatureType.SUBKEY_BINDING).verify(signature) - signatureStructureIsAcceptable(primaryKey, policy).verify(signature) - signatureDoesNotPredateSignee(subkey).verify(signature) - signatureIsEffective(referenceTime).verify(signature) - hasValidPrimaryKeyBindingSignatureIfRequired(primaryKey, subkey, policy, referenceTime) - .verify(signature) - correctSubkeyBindingSignature(primaryKey, subkey).verify(signature) - - return true - } - - /** - * Verify a subkey revocation signature. - * - * @param signature subkey revocation signature - * @param primaryKey primary key - * @param subkey subkey - * @param policy policy - * @param referenceTime 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 - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifySubkeyBindingRevocation( - signature: PGPSignature, - primaryKey: PGPPublicKey, - subkey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - signatureIsOfType(SignatureType.SUBKEY_REVOCATION).verify(signature) - signatureStructureIsAcceptable(primaryKey, policy).verify(signature) - signatureDoesNotPredateSignee(subkey).verify(signature) - signatureIsEffective(referenceTime).verify(signature) - correctSignatureOverKey(primaryKey, subkey).verify(signature) - - return true - } - - /** - * Verify a direct-key self-signature. - * - * @param signature signature - * @param primaryKey primary key - * @param policy policy - * @param referenceTime reference date for signature verification - * @return true if the signature can be verified successfully - * @throws SignatureValidationException if signature verification fails for some reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifyDirectKeySignature( - signature: PGPSignature, - primaryKey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - return verifyDirectKeySignature( - signature, primaryKey, primaryKey, policy, referenceTime) - } - - /** - * Verify a direct-key signature. - * - * @param signature signature - * @param signingKey signing key - * @param signedKey signed key - * @param policy policy - * @param referenceTime reference date for signature verification - * @return true if signature verification is successful - * @throws SignatureValidationException if signature verification fails for some reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifyDirectKeySignature( - signature: PGPSignature, - signingKey: PGPPublicKey, - signedKey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - signatureIsOfType(SignatureType.DIRECT_KEY).verify(signature) - signatureStructureIsAcceptable(signingKey, policy).verify(signature) - signatureDoesNotPredateSignee(signedKey).verify(signature) - signatureIsEffective(referenceTime).verify(signature) - correctSignatureOverKey(signingKey, signedKey).verify(signature) - - return true - } - - /** - * Verify a key revocation signature. - * - * @param signature signature - * @param primaryKey primary key - * @param policy policy - * @param referenceTime reference date for signature verification - * @return true if signature verification is successful - * @throws SignatureValidationException if signature verification fails for some reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifyKeyRevocationSignature( - signature: PGPSignature, - primaryKey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - signatureIsOfType(SignatureType.KEY_REVOCATION).verify(signature) - signatureStructureIsAcceptable(primaryKey, policy).verify(signature) - signatureIsEffective(referenceTime).verify(signature) - correctSignatureOverKey(primaryKey, primaryKey).verify(signature) - - return true - } - - /** - * Initialize a signature and verify it afterwards by updating it with the signed data. - * - * @param signature OpenPGP signature - * @param signedData input stream containing the signed data - * @param signingKey the key that created the signature - * @param policy policy - * @param referenceTime reference date of signature verification - * @return true if the signature is successfully verified - * @throws SignatureValidationException if the signature verification fails for some reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifyUninitializedSignature( - signature: PGPSignature, - signedData: InputStream, - signingKey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - initializeSignatureAndUpdateWithSignedData(signature, signedData, signingKey) - return verifyInitializedSignature(signature, signingKey, policy, referenceTime) - } - - /** - * Initialize a signature and then update it with the signed data from the given - * [InputStream]. - * - * @param signature OpenPGP signature - * @param signedData input stream containing signed data - * @param signingKey key that created the signature - * @throws SignatureValidationException in case the signature cannot be verified for some - * reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun initializeSignatureAndUpdateWithSignedData( - signature: PGPSignature, - signedData: InputStream, - signingKey: PGPPublicKey - ) { - try { - signature.init( - getInstance().pgpContentVerifierBuilderProvider, - signingKey, - ) - var read: Int - val buf = ByteArray(8192) - var lastByte: Byte = -1 - while (signedData.read(buf).also { read = it } != -1) { - // If we previously omitted a newline, but the stream is not yet empty, add it - // now - if (lastByte == '\n'.code.toByte()) { - signature.update(lastByte) - } - lastByte = buf[read - 1] - if (lastByte == '\n'.code.toByte()) { - // if last byte in buffer is newline, omit it for now - signature.update(buf, 0, read - 1) - } else { - // otherwise, write buffer as usual - signature.update(buf, 0, read) - } - } - } catch (e: PGPException) { - throw SignatureValidationException("Cannot init signature.", e) - } catch (e: IOException) { - throw SignatureValidationException("Cannot update signature.", e) - } - } - - /** - * Verify an initialized signature. An initialized signature was already updated with the - * signed data. - * - * @param signature OpenPGP signature - * @param signingKey key that created the signature - * @param policy policy - * @param referenceTime reference date for signature verification - * @return true if signature is verified successfully - * @throws SignatureValidationException if signature verification fails for some reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifyInitializedSignature( - signature: PGPSignature, - signingKey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - wasPossiblyMadeByKey(signingKey).verify(signature) - signatureStructureIsAcceptable(signingKey, policy).verify(signature) - signatureIsEffective(referenceTime).verify(signature) - - return try { - if (!signature.verify()) { - throw SignatureValidationException("Signature is not correct.") - } - true - } catch (e: PGPException) { - throw SignatureValidationException("Could not verify signature correctness.", e) - } - } - - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifyOnePassSignature( - signature: PGPSignature, - signingKey: PGPPublicKey, - onePassSignature: OnePassSignatureCheck, - policy: Policy - ): Boolean { - try { - wasPossiblyMadeByKey(signingKey).verify(signature) - signatureStructureIsAcceptable(signingKey, policy).verify(signature) - signatureIsEffective().verify(signature) - } catch (e: SignatureValidationException) { - throw SignatureValidationException("Signature is not valid: ${e.message}", e) - } - - try { - checkNotNull(onePassSignature.signature) { "No comparison signature provided." } - if (!onePassSignature.onePassSignature.verify(signature)) { - throw SignatureValidationException( - "Bad signature of key ${signingKey.keyID.openPgpKeyId()}") - } - } catch (e: PGPException) { - throw SignatureValidationException( - "Could not verify correctness of One-Pass-Signature: ${e.message}", e) - } - - return true - } - - /** - * Verify a signature (certification or revocation) over a user-id. - * - * @param userId user-id - * @param signature self-signature - * @param primaryKey primary key that created the signature - * @param policy policy - * @param referenceTime reference date for signature verification - * @return true if the signature is successfully verified - * @throws SignatureValidationException if signature verification fails for some reason - */ - @JvmStatic - @Throws(SignatureValidationException::class) - fun verifySignatureOverUserId( - userId: CharSequence, - signature: PGPSignature, - primaryKey: PGPPublicKey, - policy: Policy, - referenceTime: Date - ): Boolean { - return verifySignatureOverUserId( - userId, signature, primaryKey, primaryKey, policy, referenceTime) - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt index b9d7fb3f..87e8d57e 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt @@ -11,12 +11,28 @@ import org.bouncycastle.bcpg.sig.* import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.OpenPGPKeyVersion import org.pgpainless.algorithm.PublicKeyAlgorithm interface BaseSignatureSubpackets { interface Callback : SignatureSubpacketCallback + fun setAppropriateIssuerInfo(key: PGPPublicKey): BaseSignatureSubpackets + + /** + * Depending on the given [version], use the appropriate means of setting issuer information. V6 + * signatures for example MUST NOT contain an [IssuerKeyID] packet. + * + * @param key issuer key + * @param version signature version + * @return this + */ + fun setAppropriateIssuerInfo( + key: PGPPublicKey, + version: OpenPGPKeyVersion + ): BaseSignatureSubpackets + /** * Add both an [IssuerKeyID] and [IssuerFingerprint] subpacket pointing to the given key. * @@ -62,10 +78,13 @@ interface BaseSignatureSubpackets { expirationTime: SignatureExpirationTime? ): BaseSignatureSubpackets + @Deprecated("Usage of subpacket is discouraged") fun setSignerUserId(userId: CharSequence): BaseSignatureSubpackets + @Deprecated("Usage of subpacket is discouraged") fun setSignerUserId(isCritical: Boolean, userId: CharSequence): BaseSignatureSubpackets + @Deprecated("Usage of subpacket is discouraged") fun setSignerUserId(signerUserID: SignerUserID?): BaseSignatureSubpackets fun addNotationData( @@ -156,52 +175,4 @@ interface BaseSignatureSubpackets { fun addEmbeddedSignature(embeddedSignature: EmbeddedSignature): BaseSignatureSubpackets fun clearEmbeddedSignatures(): BaseSignatureSubpackets - - companion object { - - /** Factory method for a [Callback] that does nothing. */ - @JvmStatic fun nop() = object : Callback {} - - /** - * Factory function with receiver, which returns a [Callback] that modifies the hashed - * subpacket area of a [BaseSignatureSubpackets] object. - * - * Can be called like this: - * ``` - * val callback = BaseSignatureSubpackets.applyHashed { - * setCreationTime(date) - * ... - * } - * ``` - */ - @JvmStatic - fun applyHashed(function: BaseSignatureSubpackets.() -> Unit): Callback { - return object : Callback { - override fun modifyHashedSubpackets(hashedSubpackets: BaseSignatureSubpackets) { - function(hashedSubpackets) - } - } - } - - /** - * Factory function with receiver, which returns a [Callback] that modifies the unhashed - * subpacket area of a [BaseSignatureSubpackets] object. - * - * Can be called like this: - * ``` - * val callback = BaseSignatureSubpackets.applyUnhashed { - * setCreationTime(date) - * ... - * } - * ``` - */ - @JvmStatic - fun applyUnhashed(function: BaseSignatureSubpackets.() -> Unit): Callback { - return object : Callback { - override fun modifyUnhashedSubpackets(unhashedSubpackets: BaseSignatureSubpackets) { - function(unhashedSubpackets) - } - } - } - } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt index bb1d6550..a37c6984 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt @@ -6,7 +6,22 @@ package org.pgpainless.signature.subpackets interface CertificationSubpackets : BaseSignatureSubpackets { - interface Callback : SignatureSubpacketCallback + interface Callback : SignatureSubpacketCallback { + fun then(nextCallback: SignatureSubpacketCallback): Callback { + val currCallback = this + return object : Callback { + override fun modifyHashedSubpackets(hashedSubpackets: CertificationSubpackets) { + currCallback.modifyHashedSubpackets(hashedSubpackets) + nextCallback.modifyHashedSubpackets(hashedSubpackets) + } + + override fun modifyUnhashedSubpackets(unhashedSubpackets: CertificationSubpackets) { + currCallback.modifyUnhashedSubpackets(unhashedSubpackets) + nextCallback.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + } + } companion object { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt index 79807322..5eaf5313 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt @@ -6,10 +6,32 @@ package org.pgpainless.signature.subpackets import org.bouncycastle.bcpg.sig.RevocationReason import org.pgpainless.key.util.RevocationAttributes +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets.Callback interface RevocationSignatureSubpackets : BaseSignatureSubpackets { - interface Callback : SignatureSubpacketCallback + interface Callback : SignatureSubpacketCallback { + fun then( + nextCallback: SignatureSubpacketCallback + ): Callback { + val currCallback = this + return object : Callback { + override fun modifyHashedSubpackets( + hashedSubpackets: RevocationSignatureSubpackets + ) { + currCallback.modifyHashedSubpackets(hashedSubpackets) + nextCallback.modifyHashedSubpackets(hashedSubpackets) + } + + override fun modifyUnhashedSubpackets( + unhashedSubpackets: RevocationSignatureSubpackets + ) { + currCallback.modifyUnhashedSubpackets(unhashedSubpackets) + nextCallback.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + } + } fun setRevocationReason( revocationAttributes: RevocationAttributes diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt index 318b7adf..8619155c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt @@ -8,6 +8,7 @@ import java.util.* import org.bouncycastle.bcpg.sig.Features import org.bouncycastle.bcpg.sig.KeyExpirationTime import org.bouncycastle.bcpg.sig.KeyFlags +import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites import org.bouncycastle.bcpg.sig.PreferredAlgorithms import org.bouncycastle.bcpg.sig.PrimaryUserID import org.bouncycastle.bcpg.sig.RevocationKey @@ -112,18 +113,35 @@ interface SelfSignatureSubpackets : BaseSignatureSubpackets { fun setPreferredHashAlgorithms(algorithms: PreferredAlgorithms?): SelfSignatureSubpackets + fun setPreferredAEADCiphersuites( + aeadAlgorithms: Collection + ): SelfSignatureSubpackets + + fun setPreferredAEADCiphersuites( + algorithms: PreferredAEADCiphersuites.Builder? + ): SelfSignatureSubpackets + + fun setPreferredAEADCiphersuites( + preferredAEADCiphersuites: PreferredAEADCiphersuites? + ): SelfSignatureSubpackets + + @Deprecated("Use of this subpacket is discouraged.") fun addRevocationKey(revocationKey: PGPPublicKey): SelfSignatureSubpackets + @Deprecated("Use of this subpacket is discouraged.") fun addRevocationKey(isCritical: Boolean, revocationKey: PGPPublicKey): SelfSignatureSubpackets + @Deprecated("Use of this subpacket is discouraged.") fun addRevocationKey( isCritical: Boolean, isSensitive: Boolean, revocationKey: PGPPublicKey ): SelfSignatureSubpackets + @Deprecated("Use of this subpacket is discouraged.") fun addRevocationKey(revocationKey: RevocationKey): SelfSignatureSubpackets + @Deprecated("Use of this subpacket is discouraged.") fun clearRevocationKeys(): SelfSignatureSubpackets fun setFeatures(vararg features: Feature): SelfSignatureSubpackets diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt index 886cedb6..28f9ee82 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt @@ -10,16 +10,18 @@ import java.util.* import kotlin.experimental.or import openpgp.secondsTill import openpgp.toSecondsPrecision -import org.bouncycastle.bcpg.SignatureSubpacket import org.bouncycastle.bcpg.SignatureSubpacketTags import org.bouncycastle.bcpg.sig.* import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator import org.bouncycastle.openpgp.PGPSignatureSubpacketVector import org.pgpainless.algorithm.* import org.pgpainless.key.util.RevocationAttributes -class SignatureSubpackets : +class SignatureSubpackets( + val subpacketsGenerator: PGPSignatureSubpacketGenerator = PGPSignatureSubpacketGenerator() +) : BaseSignatureSubpackets, SelfSignatureSubpackets, CertificationSubpackets, @@ -27,31 +29,6 @@ class SignatureSubpackets : interface Callback : SignatureSubpacketCallback - var signatureCreationTimeSubpacket: SignatureCreationTime? = null - var signatureExpirationTimeSubpacket: SignatureExpirationTime? = null - var issuerKeyIdSubpacket: IssuerKeyID? = null - var issuerFingerprintSubpacket: IssuerFingerprint? = null - val notationDataSubpackets: List = mutableListOf() - val intendedRecipientFingerprintSubpackets: List = mutableListOf() - val revocationKeySubpackets: List = mutableListOf() - var exportableSubpacket: Exportable? = null - var signatureTargetSubpacket: SignatureTarget? = null - var featuresSubpacket: Features? = null - var keyFlagsSubpacket: KeyFlags? = null - var trustSubpacket: TrustSignature? = null - var preferredCompressionAlgorithmsSubpacket: PreferredAlgorithms? = null - var preferredSymmetricKeyAlgorithmsSubpacket: PreferredAlgorithms? = null - var preferredHashAlgorithmsSubpacket: PreferredAlgorithms? = null - val embeddedSignatureSubpackets: List = mutableListOf() - var signerUserIdSubpacket: SignerUserID? = null - var keyExpirationTimeSubpacket: KeyExpirationTime? = null - var policyURISubpacket: PolicyURI? = null - var primaryUserIdSubpacket: PrimaryUserID? = null - var regularExpressionSubpacket: RegularExpression? = null - var revocableSubpacket: Revocable? = null - var revocationReasonSubpacket: RevocationReason? = null - val residualSubpackets: List = mutableListOf() - companion object { @JvmStatic @@ -72,67 +49,26 @@ class SignatureSubpackets : issuer: PGPPublicKey, base: PGPSignatureSubpacketVector ): SignatureSubpackets { - return createSubpacketsFrom(base).apply { setIssuerFingerprintAndKeyId(issuer) } + return createSubpacketsFrom(base).apply { setAppropriateIssuerInfo(issuer) } } @JvmStatic fun createSubpacketsFrom(base: PGPSignatureSubpacketVector): SignatureSubpackets { - return SignatureSubpacketsHelper.applyFrom(base, SignatureSubpackets()) + return SignatureSubpackets(PGPSignatureSubpacketGenerator(base)).apply { + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.ISSUER_KEY_ID) + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.ISSUER_FINGERPRINT) + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.CREATION_TIME) + } } @JvmStatic fun createHashedSubpackets(issuer: PGPPublicKey): SignatureSubpackets { - return createEmptySubpackets().setIssuerFingerprintAndKeyId(issuer) + return createEmptySubpackets().setAppropriateIssuerInfo(issuer) } @JvmStatic fun createEmptySubpackets(): SignatureSubpackets { - return SignatureSubpackets() - } - - /** Factory method for a [Callback] that does nothing. */ - @JvmStatic fun nop() = object : Callback {} - - /** - * Factory function with receiver, which returns a [Callback] that modifies the hashed - * subpacket area of a [SignatureSubpackets] object. - * - * Can be called like this: - * ``` - * val callback = SignatureSubpackets.applyHashed { - * setCreationTime(date) - * ... - * } - * ``` - */ - @JvmStatic - fun applyHashed(function: SignatureSubpackets.() -> Unit): Callback { - return object : Callback { - override fun modifyHashedSubpackets(hashedSubpackets: SignatureSubpackets) { - function(hashedSubpackets) - } - } - } - - /** - * Factory function with receiver, which returns a [Callback] that modifies the unhashed - * subpacket area of a [SignatureSubpackets] object. - * - * Can be called like this: - * ``` - * val callback = SignatureSubpackets.applyUnhashed { - * setCreationTime(date) - * ... - * } - * ``` - */ - @JvmStatic - fun applyUnhashed(function: SignatureSubpackets.() -> Unit): Callback { - return object : Callback { - override fun modifyUnhashedSubpackets(unhashedSubpackets: SignatureSubpackets) { - function(unhashedSubpackets) - } - } + return SignatureSubpackets(PGPSignatureSubpacketGenerator()) } } @@ -157,7 +93,11 @@ class SignatureSubpackets : } override fun setRevocationReason(reason: RevocationReason?): SignatureSubpackets = apply { - this.revocationReasonSubpacket = reason + reason?.let { + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.REVOCATION_REASON) + subpacketsGenerator.setRevocationReason( + it.isCritical, it.revocationReason, it.revocationDescription) + } } override fun setKeyFlags(vararg keyflags: KeyFlag): SignatureSubpackets = apply { @@ -174,7 +114,8 @@ class SignatureSubpackets : } override fun setKeyFlags(keyFlags: KeyFlags?): SignatureSubpackets = apply { - this.keyFlagsSubpacket = keyFlags + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.KEY_FLAGS) + keyFlags?.let { subpacketsGenerator.setKeyFlags(it.isCritical, it.flags) } } override fun setPrimaryUserId(): SignatureSubpackets = apply { setPrimaryUserId(true) } @@ -184,7 +125,10 @@ class SignatureSubpackets : } override fun setPrimaryUserId(primaryUserID: PrimaryUserID?): SignatureSubpackets = apply { - this.primaryUserIdSubpacket = primaryUserID + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.PRIMARY_USER_ID) + primaryUserID?.let { + subpacketsGenerator.setPrimaryUserID(it.isCritical, it.isPrimaryUserID) + } } override fun setKeyExpirationTime( @@ -221,7 +165,10 @@ class SignatureSubpackets : override fun setKeyExpirationTime(keyExpirationTime: KeyExpirationTime?): SignatureSubpackets = apply { - this.keyExpirationTimeSubpacket = keyExpirationTime + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.KEY_EXPIRE_TIME) + keyExpirationTime?.let { + subpacketsGenerator.setKeyExpirationTime(it.isCritical, it.time) + } } override fun setPreferredCompressionAlgorithms( @@ -250,7 +197,10 @@ class SignatureSubpackets : algorithms == null || algorithms.type == SignatureSubpacketTags.PREFERRED_COMP_ALGS) { "Invalid preferred compression algorithms type." } - this.preferredCompressionAlgorithmsSubpacket = algorithms + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.PREFERRED_COMP_ALGS) + algorithms?.let { + subpacketsGenerator.setPreferredCompressionAlgorithms(it.isCritical, it.preferences) + } } override fun setPreferredSymmetricKeyAlgorithms( @@ -279,7 +229,10 @@ class SignatureSubpackets : algorithms == null || algorithms.type == SignatureSubpacketTags.PREFERRED_SYM_ALGS) { "Invalid preferred symmetric algorithms type." } - this.preferredSymmetricKeyAlgorithmsSubpacket = algorithms + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.PREFERRED_SYM_ALGS) + algorithms?.let { + subpacketsGenerator.setPreferredSymmetricAlgorithms(it.isCritical, it.preferences) + } } override fun setPreferredHashAlgorithms(vararg algorithms: HashAlgorithm): SignatureSubpackets = @@ -309,34 +262,66 @@ class SignatureSubpackets : algorithms.type == SignatureSubpacketTags.PREFERRED_HASH_ALGS) { "Invalid preferred hash algorithms type." } - this.preferredHashAlgorithmsSubpacket = algorithms + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.PREFERRED_HASH_ALGS) + algorithms?.let { + subpacketsGenerator.setPreferredHashAlgorithms(it.isCritical, it.preferences) + } } + override fun setPreferredAEADCiphersuites( + aeadAlgorithms: Collection + ): SignatureSubpackets = + setPreferredAEADCiphersuites( + PreferredAEADCiphersuites.builder(false).apply { + for (algorithm in aeadAlgorithms) { + addCombination( + algorithm.ciphermode.algorithmId, algorithm.aeadAlgorithm.algorithmId) + } + }) + + override fun setPreferredAEADCiphersuites( + algorithms: PreferredAEADCiphersuites.Builder? + ): SignatureSubpackets = setPreferredAEADCiphersuites(algorithms?.build()) + + override fun setPreferredAEADCiphersuites( + preferredAEADCiphersuites: PreferredAEADCiphersuites? + ) = apply { + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.PREFERRED_AEAD_ALGORITHMS) + preferredAEADCiphersuites?.let { + subpacketsGenerator.setPreferredAEADCiphersuites(it.isCritical, it.rawAlgorithms) + } + } + + @Deprecated("Use of this subpacket is discouraged.") override fun addRevocationKey(revocationKey: PGPPublicKey): SignatureSubpackets = apply { addRevocationKey(true, revocationKey) } + @Deprecated("Use of this subpacket is discouraged.") override fun addRevocationKey( isCritical: Boolean, revocationKey: PGPPublicKey ): SignatureSubpackets = apply { addRevocationKey(isCritical, false, revocationKey) } + @Deprecated("Use of this subpacket is discouraged.") override fun addRevocationKey( isCritical: Boolean, isSensitive: Boolean, revocationKey: PGPPublicKey ): SignatureSubpackets = apply { - val clazz = 0x80.toByte() or if (isSensitive) 0x40.toByte() else 0x00.toByte() + val clazz = if (isSensitive) 0x80.toByte() or 0x40.toByte() else 0x80.toByte() addRevocationKey( RevocationKey(isCritical, clazz, revocationKey.algorithm, revocationKey.fingerprint)) } + @Deprecated("Use of this subpacket is discouraged.") override fun addRevocationKey(revocationKey: RevocationKey): SignatureSubpackets = apply { - (this.revocationKeySubpackets as MutableList).add(revocationKey) + subpacketsGenerator.addCustomSubpacket(revocationKey) } + @Deprecated("Use of this subpacket is discouraged.") override fun clearRevocationKeys(): SignatureSubpackets = apply { - (this.revocationKeySubpackets as MutableList).clear() + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.REVOCATION_KEY) } override fun setFeatures(vararg features: Feature): SignatureSubpackets = apply { @@ -349,7 +334,21 @@ class SignatureSubpackets : } override fun setFeatures(features: Features?): SignatureSubpackets = apply { - this.featuresSubpacket = features + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.FEATURES) + features?.let { subpacketsGenerator.setFeature(it.isCritical, it.features) } + } + + override fun setAppropriateIssuerInfo(key: PGPPublicKey) = apply { + setAppropriateIssuerInfo(key, OpenPGPKeyVersion.from(key.version)) + } + + override fun setAppropriateIssuerInfo(key: PGPPublicKey, version: OpenPGPKeyVersion) = apply { + when (version) { + OpenPGPKeyVersion.v3 -> setIssuerKeyId(key.keyID) + OpenPGPKeyVersion.v4 -> setIssuerFingerprintAndKeyId(key) + OpenPGPKeyVersion.librePgp, + OpenPGPKeyVersion.v6 -> setIssuerFingerprint(key) + } } override fun setIssuerFingerprintAndKeyId(key: PGPPublicKey): SignatureSubpackets = apply { @@ -366,23 +365,22 @@ class SignatureSubpackets : } override fun setIssuerKeyId(issuerKeyID: IssuerKeyID?): SignatureSubpackets = apply { - this.issuerKeyIdSubpacket = issuerKeyID + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.ISSUER_KEY_ID) + issuerKeyID?.let { subpacketsGenerator.setIssuerKeyID(it.isCritical, it.keyID) } } override fun setIssuerFingerprint( isCritical: Boolean, issuer: PGPPublicKey - ): SignatureSubpackets = apply { - setIssuerFingerprint(IssuerFingerprint(isCritical, issuer.version, issuer.fingerprint)) - } + ): SignatureSubpackets = apply { subpacketsGenerator.setIssuerFingerprint(isCritical, issuer) } - override fun setIssuerFingerprint(issuer: PGPPublicKey): SignatureSubpackets = apply { - setIssuerFingerprint(false, issuer) - } + override fun setIssuerFingerprint(issuer: PGPPublicKey): SignatureSubpackets = + setIssuerFingerprint(true, issuer) override fun setIssuerFingerprint(fingerprint: IssuerFingerprint?): SignatureSubpackets = apply { - this.issuerFingerprintSubpacket = fingerprint + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.ISSUER_FINGERPRINT) + fingerprint?.let { subpacketsGenerator.addCustomSubpacket(it) } } override fun setSignatureCreationTime(creationTime: Date): SignatureSubpackets = apply { @@ -398,7 +396,10 @@ class SignatureSubpackets : override fun setSignatureCreationTime( creationTime: SignatureCreationTime? - ): SignatureSubpackets = apply { this.signatureCreationTimeSubpacket = creationTime } + ): SignatureSubpackets = apply { + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.CREATION_TIME) + creationTime?.let { subpacketsGenerator.setSignatureCreationTime(it.isCritical, it.time) } + } override fun setSignatureExpirationTime( creationTime: Date, @@ -447,19 +448,28 @@ class SignatureSubpackets : override fun setSignatureExpirationTime( expirationTime: SignatureExpirationTime? - ): SignatureSubpackets = apply { this.signatureExpirationTimeSubpacket = expirationTime } + ): SignatureSubpackets = apply { + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.EXPIRE_TIME) + expirationTime?.let { + subpacketsGenerator.setSignatureExpirationTime(it.isCritical, it.time) + } + } + @Deprecated("Usage of subpacket is discouraged") override fun setSignerUserId(userId: CharSequence): SignatureSubpackets = apply { setSignerUserId(false, userId) } + @Deprecated("Usage of subpacket is discouraged") override fun setSignerUserId(isCritical: Boolean, userId: CharSequence): SignatureSubpackets = apply { setSignerUserId(SignerUserID(isCritical, userId.toString())) } + @Deprecated("Usage of subpacket is discouraged") override fun setSignerUserId(signerUserID: SignerUserID?): SignatureSubpackets = apply { - this.signerUserIdSubpacket = signerUserID + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.SIGNER_USER_ID) + signerUserID?.let { subpacketsGenerator.setSignerUserID(it.isCritical, it.rawID) } } override fun addNotationData( @@ -480,11 +490,15 @@ class SignatureSubpackets : } override fun addNotationData(notationData: NotationData): SignatureSubpackets = apply { - (this.notationDataSubpackets as MutableList).add(notationData) + subpacketsGenerator.addNotationData( + notationData.isCritical, + notationData.isHumanReadable, + notationData.notationName, + notationData.notationValue) } override fun clearNotationData(): SignatureSubpackets = apply { - (this.notationDataSubpackets as MutableList).clear() + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.NOTATION_DATA) } override fun addIntendedRecipientFingerprint(recipientKey: PGPPublicKey): SignatureSubpackets = @@ -496,19 +510,16 @@ class SignatureSubpackets : isCritical: Boolean, recipientKey: PGPPublicKey ): SignatureSubpackets = apply { - addIntendedRecipientFingerprint( - IntendedRecipientFingerprint( - isCritical, recipientKey.version, recipientKey.fingerprint)) + subpacketsGenerator.addIntendedRecipientFingerprint(isCritical, recipientKey) } override fun addIntendedRecipientFingerprint( intendedRecipient: IntendedRecipientFingerprint - ): SignatureSubpackets = apply { - (this.intendedRecipientFingerprintSubpackets as MutableList).add(intendedRecipient) - } + ): SignatureSubpackets = apply { subpacketsGenerator.addCustomSubpacket(intendedRecipient) } override fun clearIntendedRecipientFingerprints(): SignatureSubpackets = apply { - (this.intendedRecipientFingerprintSubpackets as MutableList).clear() + subpacketsGenerator.removePacketsOfType( + SignatureSubpacketTags.INTENDED_RECIPIENT_FINGERPRINT) } override fun setExportable(): SignatureSubpackets = apply { setExportable(true) } @@ -523,7 +534,8 @@ class SignatureSubpackets : } override fun setExportable(exportable: Exportable?): SignatureSubpackets = apply { - this.exportableSubpacket = exportable + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.EXPORTABLE) + exportable?.let { subpacketsGenerator.setExportable(it.isCritical, it.isExportable) } } override fun setPolicyUrl(policyUrl: URL): SignatureSubpackets = apply { @@ -535,7 +547,7 @@ class SignatureSubpackets : } override fun setPolicyUrl(policyUrl: PolicyURI?): SignatureSubpackets = apply { - this.policyURISubpacket = policyURISubpacket + policyUrl?.let { subpacketsGenerator.addPolicyURI(it.isCritical, it.uri) } } override fun setRegularExpression(regex: CharSequence): SignatureSubpackets = apply { @@ -550,7 +562,7 @@ class SignatureSubpackets : } override fun setRegularExpression(regex: RegularExpression?): SignatureSubpackets = apply { - this.regularExpressionSubpacket = regex + regex?.let { subpacketsGenerator.addRegularExpression(it.isCritical, it.regex) } } override fun setRevocable(): SignatureSubpackets = apply { setRevocable(true) } @@ -565,7 +577,8 @@ class SignatureSubpackets : } override fun setRevocable(revocable: Revocable?): SignatureSubpackets = apply { - this.revocableSubpacket = revocable + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.REVOCABLE) + revocable?.let { subpacketsGenerator.setRevocable(it.isCritical, it.isRevocable) } } override fun setSignatureTarget( @@ -589,7 +602,11 @@ class SignatureSubpackets : override fun setSignatureTarget(signatureTarget: SignatureTarget?): SignatureSubpackets = apply { - this.signatureTargetSubpacket = signatureTarget + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.SIGNATURE_TARGET) + signatureTarget?.let { + subpacketsGenerator.setSignatureTarget( + it.isCritical, it.publicKeyAlgorithm, it.hashAlgorithm, it.hashData) + } } override fun setTrust(depth: Int, amount: Int): SignatureSubpackets = apply { @@ -602,7 +619,8 @@ class SignatureSubpackets : } override fun setTrust(trust: TrustSignature?): SignatureSubpackets = apply { - this.trustSubpacket = trust + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.TRUST_SIG) + trust?.let { subpacketsGenerator.setTrust(it.isCritical, it.depth, it.trustAmount) } } override fun addEmbeddedSignature(signature: PGPSignature): SignatureSubpackets = apply { @@ -613,27 +631,19 @@ class SignatureSubpackets : isCritical: Boolean, signature: PGPSignature ): SignatureSubpackets = apply { - val sig = signature.encoded - val data = - if (sig.size - 1 > 256) { - ByteArray(sig.size - 3) - } else { - ByteArray(sig.size - 2) - } - System.arraycopy(sig, sig.size - data.size, data, 0, data.size) - addEmbeddedSignature(EmbeddedSignature(isCritical, false, data)) + subpacketsGenerator.addEmbeddedSignature(isCritical, signature) } override fun addEmbeddedSignature(embeddedSignature: EmbeddedSignature): SignatureSubpackets = apply { - (this.embeddedSignatureSubpackets as MutableList).add(embeddedSignature) + subpacketsGenerator.addCustomSubpacket(embeddedSignature) } override fun clearEmbeddedSignatures(): SignatureSubpackets = apply { - (this.embeddedSignatureSubpackets as MutableList).clear() + subpacketsGenerator.removePacketsOfType(SignatureSubpacketTags.EMBEDDED_SIGNATURE) } fun addResidualSubpacket( subpacket: org.bouncycastle.bcpg.SignatureSubpacket - ): SignatureSubpackets = apply { (residualSubpackets as MutableList).add(subpacket) } + ): SignatureSubpackets = apply { subpacketsGenerator.addCustomSubpacket(subpacket) } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt index 8a6c16bf..203b8e4f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt @@ -5,7 +5,6 @@ package org.pgpainless.signature.subpackets import org.bouncycastle.bcpg.sig.* -import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator import org.bouncycastle.openpgp.PGPSignatureSubpacketVector import org.pgpainless.algorithm.* import org.pgpainless.key.util.RevocationAttributes @@ -62,6 +61,11 @@ class SignatureSubpacketsHelper { PreferredAlgorithms( it.type, it.isCritical, it.isLongLength, it.data)) } + SignatureSubpacket.preferredAEADAlgorithms -> + (subpacket as PreferredAEADCiphersuites).let { + subpackets.setPreferredAEADCiphersuites( + PreferredAEADCiphersuites(it.isCritical, it.rawAlgorithms)) + } SignatureSubpacket.revocationKey -> (subpacket as RevocationKey).let { subpackets.addRevocationKey( @@ -116,11 +120,11 @@ class SignatureSubpacketsHelper { } SignatureSubpacket.embeddedSignature -> (subpacket as EmbeddedSignature).let { - subpackets.addEmbeddedSignature(it) + subpackets.addResidualSubpacket(it) } SignatureSubpacket.intendedRecipientFingerprint -> (subpacket as IntendedRecipientFingerprint).let { - subpackets.addIntendedRecipientFingerprint(it) + subpackets.addResidualSubpacket(it) } SignatureSubpacket.policyUrl -> (subpacket as PolicyURI).let { subpackets.setPolicyUrl(it) } @@ -131,7 +135,6 @@ class SignatureSubpacketsHelper { SignatureSubpacket.keyServerPreferences, SignatureSubpacket.preferredKeyServers, SignatureSubpacket.placeholder, - SignatureSubpacket.preferredAEADAlgorithms, SignatureSubpacket.attestedCertification -> subpackets.addResidualSubpacket(subpacket) else -> subpackets.addResidualSubpacket(subpacket) @@ -139,72 +142,14 @@ class SignatureSubpacketsHelper { } } - @JvmStatic - fun applyTo( - subpackets: SignatureSubpackets, - generator: PGPSignatureSubpacketGenerator - ): PGPSignatureSubpacketGenerator { - return generator.apply { - addSubpacket(subpackets.issuerKeyIdSubpacket) - addSubpacket(subpackets.issuerFingerprintSubpacket) - addSubpacket(subpackets.signatureCreationTimeSubpacket) - addSubpacket(subpackets.signatureExpirationTimeSubpacket) - addSubpacket(subpackets.exportableSubpacket) - addSubpacket(subpackets.policyURISubpacket) - addSubpacket(subpackets.regularExpressionSubpacket) - for (notation in subpackets.notationDataSubpackets) { - addSubpacket(notation) - } - for (recipient in subpackets.intendedRecipientFingerprintSubpackets) { - addSubpacket(recipient) - } - for (revocationKey in subpackets.revocationKeySubpackets) { - addSubpacket(revocationKey) - } - addSubpacket(subpackets.signatureTargetSubpacket) - addSubpacket(subpackets.featuresSubpacket) - addSubpacket(subpackets.keyFlagsSubpacket) - addSubpacket(subpackets.trustSubpacket) - addSubpacket(subpackets.preferredCompressionAlgorithmsSubpacket) - addSubpacket(subpackets.preferredSymmetricKeyAlgorithmsSubpacket) - addSubpacket(subpackets.preferredHashAlgorithmsSubpacket) - for (embedded in subpackets.embeddedSignatureSubpackets) { - addSubpacket(embedded) - } - addSubpacket(subpackets.signerUserIdSubpacket) - addSubpacket(subpackets.keyExpirationTimeSubpacket) - addSubpacket(subpackets.primaryUserIdSubpacket) - addSubpacket(subpackets.revocableSubpacket) - addSubpacket(subpackets.revocationReasonSubpacket) - for (residual in subpackets.residualSubpackets) { - addSubpacket(residual) - } - } - } - - @JvmStatic - private fun PGPSignatureSubpacketGenerator.addSubpacket( - subpacket: org.bouncycastle.bcpg.SignatureSubpacket? - ) { - if (subpacket != null) { - this.addCustomSubpacket(subpacket) - } - } - @JvmStatic fun toVector(subpackets: SignatureSubpackets): PGPSignatureSubpacketVector { - return PGPSignatureSubpacketGenerator().let { - applyTo(subpackets, it) - it.generate() - } + return subpackets.subpacketsGenerator.generate() } @JvmStatic fun toVector(subpackets: RevocationSignatureSubpackets): PGPSignatureSubpacketVector { - return PGPSignatureSubpacketGenerator().let { - applyTo(subpackets as SignatureSubpackets, it) - it.generate() - } + return (subpackets as SignatureSubpackets).subpacketsGenerator.generate() } } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt index dcc85630..15ee2897 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt @@ -269,6 +269,15 @@ class SignatureSubpacketsUtil { ?.toSet() ?: setOf() + @JvmStatic + fun parsePreferredAEADCipherSuites(signature: PGPSignature): Set = + getPreferredAeadAlgorithms(signature) + ?.algorithms + ?.asSequence() + ?.map { AEADCipherMode(it) } + ?.toSet() + ?: setOf() + @JvmStatic fun getPreferredAeadAlgorithms(signature: PGPSignature): PreferredAEADCiphersuites? = hashed(signature, SignatureSubpacket.preferredAEADAlgorithms) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt index b5d5b839..92da93c5 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt @@ -21,7 +21,7 @@ import org.bouncycastle.openpgp.PGPSignature import org.bouncycastle.openpgp.PGPUtil import org.bouncycastle.util.io.Streams import org.pgpainless.algorithm.HashAlgorithm -import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.decryption_verification.OpenPGPAnimalSnifferInputStream import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.util.KeyRingUtils @@ -229,7 +229,7 @@ class ArmorUtils { * @return header map */ @JvmStatic - private fun keyToHeader(publicKey: PGPPublicKey): Map> { + fun keyToHeader(publicKey: PGPPublicKey): Map> { val headerMap = mutableMapOf>() val userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey) val first: String? = userIds.firstOrNull() @@ -247,7 +247,8 @@ class ArmorUtils { .add(OpenPgpFingerprint.of(publicKey).prettyPrint()) // Primary / First User ID (primary ?: first)?.let { - headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add(it) + headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() } + .add(it.replace("\n", "\\n").replace("\r", "\\r")) } // X-1 further identities when (userIds.size) { @@ -282,8 +283,8 @@ class ArmorUtils { * * @param armor armored output stream * @param hashAlgorithm hash algorithm - * @see RFC 4880 - - * OpenPGP Message Format §6.2. Forming ASCII Armor + * @see + * [RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor](https://datatracker.ietf.org/doc/html/rfc4880#section-6.2) */ @JvmStatic @Deprecated( @@ -297,8 +298,8 @@ class ArmorUtils { * * @param armor armored output stream * @param comment free-text comment - * @see RFC 4880 - - * OpenPGP Message Format §6.2. Forming ASCII Armor + * @see + * [RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor](https://datatracker.ietf.org/doc/html/rfc4880#section-6.2) */ @JvmStatic @Deprecated( @@ -312,8 +313,8 @@ class ArmorUtils { * * @param armor armored output stream * @param messageId message id - * @see RFC 4880 - - * OpenPGP Message Format §6.2. Forming ASCII Armor + * @see + * [RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor](https://datatracker.ietf.org/doc/html/rfc4880#section-6.2) */ @JvmStatic @Deprecated( @@ -421,7 +422,7 @@ class ArmorUtils { @JvmStatic @Throws(IOException::class) fun getDecoderStream(inputStream: InputStream): InputStream = - OpenPgpInputStream(inputStream).let { + OpenPGPAnimalSnifferInputStream(inputStream).let { if (it.isAsciiArmored) { PGPUtil.getDecoderStream(ArmoredInputStreamFactory.get(it)) } else { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/OpenPGPCertificateUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/OpenPGPCertificateUtil.kt new file mode 100644 index 00000000..48475547 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/OpenPGPCertificateUtil.kt @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +import java.io.OutputStream +import org.bouncycastle.bcpg.ArmoredOutputStream +import org.bouncycastle.bcpg.PacketFormat +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.pgpainless.bouncycastle.extensions.asciiArmor +import org.pgpainless.bouncycastle.extensions.encode + +class OpenPGPCertificateUtil private constructor() { + + companion object { + @JvmStatic + @JvmOverloads + fun encode( + certs: Collection, + outputStream: OutputStream, + packetFormat: PacketFormat = PacketFormat.ROUNDTRIP + ) { + for (cert in certs) { + cert.encode(outputStream, packetFormat) + } + } + + @JvmStatic + @JvmOverloads + fun armor( + certs: Collection, + outputStream: OutputStream, + packetFormat: PacketFormat = PacketFormat.ROUNDTRIP + ) { + if (certs.size == 1) { + // Add pretty armor header to single cert/key + certs.iterator().next().asciiArmor(outputStream, packetFormat) + } else { + // Do not add a pretty header + val aOut = ArmoredOutputStream(outputStream) + for (cert in certs) { + cert.encode(aOut, packetFormat) + } + aOut.close() + } + } + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt index 4d1e49d2..bd25f2b9 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt @@ -11,14 +11,9 @@ import org.bouncycastle.util.Arrays * * @param chars may be null for empty passwords. */ -class Passphrase(chars: CharArray?) { +class Passphrase(private val chars: CharArray?) { private val lock = Any() private var valid = true - private val chars: CharArray? - - init { - this.chars = trimWhitespace(chars) - } /** * Return a copy of the underlying char array. A return value of null represents an empty @@ -67,6 +62,13 @@ class Passphrase(chars: CharArray?) { override fun hashCode(): Int = getChars()?.let { String(it) }.hashCode() + /** + * Return a copy of this [Passphrase], but with whitespace characters trimmed off. + * + * @return copy with trimmed whitespace + */ + fun withTrimmedWhitespace(): Passphrase = Passphrase(trimWhitespace(chars)) + companion object { /** diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt index f2794925..41f9e8e0 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt @@ -6,6 +6,7 @@ package org.pgpainless.util.selection.userid import java.util.function.Predicate import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.api.OpenPGPCertificate import org.pgpainless.PGPainless abstract class SelectUserId : Predicate, (String) -> Boolean { @@ -73,13 +74,17 @@ abstract class SelectUserId : Predicate, (String) -> Boolean { fun byEmail(email: CharSequence) = or(exactMatch(email), containsEmailAddress(email)) @JvmStatic - fun validUserId(keyRing: PGPKeyRing) = + fun validUserId(key: OpenPGPCertificate) = object : SelectUserId() { - private val info = PGPainless.inspectKeyRing(keyRing) + private val info = PGPainless.getInstance().inspect(key) override fun invoke(userId: String): Boolean = info.isUserIdValid(userId) } + @JvmStatic + fun validUserId(keyRing: PGPKeyRing) = + validUserId(PGPainless.getInstance().toKeyOrCertificate(keyRing)) + @JvmStatic fun and(vararg filters: SelectUserId) = object : SelectUserId() { diff --git a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java index a0ea747a..fd623ed9 100644 --- a/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java +++ b/pgpainless-core/src/test/java/investigations/InvestigateMultiSEIPMessageHandlingTest.java @@ -14,6 +14,7 @@ import java.nio.charset.StandardCharsets; import java.util.Date; import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralDataGenerator; @@ -21,6 +22,8 @@ import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.api.OpenPGPImplementation; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; @@ -28,11 +31,9 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.HashAlgorithm; 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.exception.MalformedOpenPgpMessageException; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; @@ -120,16 +121,16 @@ public class InvestigateMultiSEIPMessageHandlingTest { public void generateTestMessage() throws PGPException, IOException { PGPSecretKeyRing ring1 = PGPainless.readKeyRing().secretKeyRing(KEY1); KeyRingInfo info1 = PGPainless.inspectKeyRing(ring1); - PGPPublicKey cryptKey1 = info1.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); - PGPSecretKey signKey1 = ring1.getSecretKey(info1.getSigningSubkeys().get(0).getKeyID()); + PGPPublicKey cryptKey1 = info1.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getPGPPublicKey(); + PGPSecretKey signKey1 = ring1.getSecretKey(info1.getSigningSubkeys().get(0).getKeyIdentifier()); PGPSecretKeyRing ring2 = PGPainless.readKeyRing().secretKeyRing(KEY2); KeyRingInfo info2 = PGPainless.inspectKeyRing(ring2); - PGPSecretKey signKey2 = ring2.getSecretKey(info2.getSigningSubkeys().get(0).getKeyID()); + PGPSecretKey signKey2 = ring2.getSecretKey(info2.getSigningSubkeys().get(0).getKeyIdentifier()); ByteArrayOutputStream out = new ByteArrayOutputStream(); ArmoredOutputStream armorOut = new ArmoredOutputStream(out); - PGPDataEncryptorBuilder cryptBuilder = ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(SymmetricKeyAlgorithm.AES_256); + PGPDataEncryptorBuilder cryptBuilder = OpenPGPImplementation.getInstance().pgpDataEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_256); cryptBuilder.setWithIntegrityPacket(true); encryptAndSign(cryptKey1, signKey1, armorOut, data1.getBytes(StandardCharsets.UTF_8)); @@ -144,15 +145,16 @@ public class InvestigateMultiSEIPMessageHandlingTest { private void encryptAndSign(PGPPublicKey cryptKey, PGPSecretKey signKey, ArmoredOutputStream armorOut, byte[] data) throws IOException, PGPException { - PGPDataEncryptorBuilder cryptBuilder = ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(SymmetricKeyAlgorithm.AES_256); + PGPDataEncryptorBuilder cryptBuilder = OpenPGPImplementation.getInstance().pgpDataEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_256); cryptBuilder.setWithIntegrityPacket(true); PGPEncryptedDataGenerator cryptGen = new PGPEncryptedDataGenerator(cryptBuilder); - cryptGen.addMethod(ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(cryptKey)); + cryptGen.addMethod(OpenPGPImplementation.getInstance().publicKeyKeyEncryptionMethodGenerator(cryptKey)); OutputStream cryptStream = cryptGen.open(armorOut, new byte[512]); - PGPSignatureGenerator sigGen = new PGPSignatureGenerator(ImplementationFactory.getInstance() - .getPGPContentSignerBuilder(signKey.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId())); + PGPSignatureGenerator sigGen = new PGPSignatureGenerator(OpenPGPImplementation.getInstance() + .pgpContentSignerBuilder(signKey.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId()), + signKey.getPublicKey()); sigGen.init(SignatureType.BINARY_DOCUMENT.getCode(), UnlockSecretKey .unlockSecretKey(signKey, (Passphrase) null)); @@ -176,15 +178,16 @@ public class InvestigateMultiSEIPMessageHandlingTest { @Test public void testDecryptAndVerifyDetectsAppendedSEIPData() throws IOException, PGPException { - PGPSecretKeyRing ring1 = PGPainless.readKeyRing().secretKeyRing(KEY1); - PGPSecretKeyRing ring2 = PGPainless.readKeyRing().secretKeyRing(KEY2); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey ring1 = api.readKey().parseKey(KEY1); + OpenPGPKey ring2 = api.readKey().parseKey(KEY2); - ConsumerOptions options = new ConsumerOptions() - .addVerificationCert(PGPainless.extractCertificate(ring1)) - .addVerificationCert(PGPainless.extractCertificate(ring2)) + ConsumerOptions options = ConsumerOptions.get() + .addVerificationCert(ring2) + .addVerificationCert(ring2) .addDecryptionKey(ring1); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(new ByteArrayInputStream(MESSAGE.getBytes(StandardCharsets.UTF_8))) .withOptions(options); diff --git a/pgpainless-core/src/test/java/investigations/ModifiedPublicKeysInvestigation.java b/pgpainless-core/src/test/java/investigations/ModifiedPublicKeysInvestigation.java index 6930f78f..1d79658b 100644 --- a/pgpainless-core/src/test/java/investigations/ModifiedPublicKeysInvestigation.java +++ b/pgpainless-core/src/test/java/investigations/ModifiedPublicKeysInvestigation.java @@ -8,19 +8,17 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.exception.KeyIntegrityException; import org.pgpainless.key.generation.type.rsa.RsaLength; 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.util.Passphrase; public class ModifiedPublicKeysInvestigation { @@ -77,6 +75,8 @@ public class ModifiedPublicKeysInvestigation { "RuvLdJPtAP9VND4sdnrXUXoUn6OgUmKoV0KKcTUPEnMqQ8QgfVDEJA==\n" + "=p9kX\n" + "-----END PGP PRIVATE KEY BLOCK-----"; + private static final KeyIdentifier dsaKID1 = new KeyIdentifier("0FE102C159C818EF2D7D9F7EB1BD1F049EC87F3D"); + private static final KeyIdentifier dsaKID2 = new KeyIdentifier("AB30CFBA5A2ED4848B096123F5FFDF6D71DD5789"); private static final String ELGAMAL = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: OpenPGP.js VERSION\n" + @@ -129,6 +129,8 @@ public class ModifiedPublicKeysInvestigation { "TsMBqN9H5d+2XQ==\n" + "=lI+G\n" + "-----END PGP PRIVATE KEY BLOCK-----\n"; + private static final KeyIdentifier elgamalKID1 = new KeyIdentifier("9B0F5D6800DEA53499F455C75F04ACF44FD822B1"); + private static final KeyIdentifier elgamalKID2 = new KeyIdentifier("AB30CFBA5A2ED4848B096123F5FFDF6D71DD5789"); // created with exploit code by cure53.de private static final String INJECTED_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + @@ -209,79 +211,90 @@ public class ModifiedPublicKeysInvestigation { @Test public void assertModifiedDSAKeyThrowsKeyIntegrityException() throws IOException { - SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("12345678")); - PGPSecretKeyRing dsa = PGPainless.readKeyRing().secretKeyRing(DSA); + PGPainless api = PGPainless.getInstance(); + Policy policy = api.getAlgorithmPolicy(); + policy.setEnableKeyParameterValidation(true); + + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("12345678")); + OpenPGPKey dsa = api.readKey().parseKey(DSA); - PGPainless.getPolicy().setEnableKeyParameterValidation(true); assertThrows(KeyIntegrityException.class, () -> - UnlockSecretKey.unlockSecretKey(dsa.getSecretKey(KeyIdUtil.fromLongKeyId("b1bd1f049ec87f3d")), protector)); + UnlockSecretKey.unlockSecretKey(dsa.getSecretKey(dsaKID1), protector, policy)); assertThrows(KeyIntegrityException.class, () -> - UnlockSecretKey.unlockSecretKey(dsa.getSecretKey(KeyIdUtil.fromLongKeyId("f5ffdf6d71dd5789")), protector)); - PGPainless.getPolicy().setEnableKeyParameterValidation(false); + UnlockSecretKey.unlockSecretKey(dsa.getSecretKey(dsaKID2), protector, policy)); } @Test public void assertModifiedElGamalKeyThrowsKeyIntegrityException() throws IOException { - SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("12345678")); - PGPSecretKeyRing elgamal = PGPainless.readKeyRing().secretKeyRing(ELGAMAL); + PGPainless api = PGPainless.getInstance(); + Policy policy = api.getAlgorithmPolicy(); + policy.setEnableKeyParameterValidation(true); + + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("12345678")); + OpenPGPKey elgamal = api.readKey().parseKey(ELGAMAL); - PGPainless.getPolicy().setEnableKeyParameterValidation(true); assertThrows(KeyIntegrityException.class, () -> - UnlockSecretKey.unlockSecretKey(elgamal.getSecretKey(KeyIdUtil.fromLongKeyId("f5ffdf6d71dd5789")), protector)); - PGPainless.getPolicy().setEnableKeyParameterValidation(false); + UnlockSecretKey.unlockSecretKey(elgamal.getSecretKey(elgamalKID2), protector, policy)); } @Test public void assertInjectedKeyRingFailsToUnlockPrimaryKey() throws IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(INJECTED_KEY); + PGPainless api = PGPainless.getInstance(); + Policy policy = api.getAlgorithmPolicy(); + policy.setEnableKeyParameterValidation(true); + + OpenPGPKey secretKeys = api.readKey().parseKey(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); + UnlockSecretKey.unlockSecretKey(secretKeys.getPrimarySecretKey(), protector, policy)); } @Test public void assertCannotUnlockElGamalPrimaryKeyDueToDummyS2K() throws IOException { + PGPainless api = PGPainless.getInstance(); + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("12345678")); - PGPSecretKeyRing elgamal = PGPainless.readKeyRing().secretKeyRing(ELGAMAL); + OpenPGPKey elgamal = api.readKey().parseKey(ELGAMAL); assertThrows(PGPException.class, () -> - UnlockSecretKey.unlockSecretKey(elgamal.getSecretKey(KeyIdUtil.fromLongKeyId("5f04acf44fd822b1")), protector)); + UnlockSecretKey.unlockSecretKey(elgamal.getSecretKey(elgamalKID1), protector)); } @Test - public void assertUnmodifiedRSAKeyDoesNotThrow() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void assertUnmodifiedRSAKeyDoesNotThrow() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .simpleRsaKeyRing("Unmodified", RsaLength._4096, "987654321"); SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("987654321")); - for (PGPSecretKey secretKey : secretKeys) { + for (OpenPGPKey.OpenPGPSecretKey secretKey : secretKeys.getSecretKeys().values()) { assertDoesNotThrow(() -> UnlockSecretKey.unlockSecretKey(secretKey, protector)); } } @Test - public void assertUnmodifiedECKeyDoesNotThrow() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void assertUnmodifiedECKeyDoesNotThrow() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .simpleEcKeyRing("Unmodified", "987654321"); SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("987654321")); - for (PGPSecretKey secretKey : secretKeys) { + for (OpenPGPKey.OpenPGPSecretKey secretKey : secretKeys.getSecretKeys().values()) { assertDoesNotThrow(() -> UnlockSecretKey.unlockSecretKey(secretKey, protector)); } } @Test - public void assertUnmodifiedModernKeyDoesNotThrow() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void assertUnmodifiedModernKeyDoesNotThrow() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Unmodified", "987654321"); SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("987654321")); - for (PGPSecretKey secretKey : secretKeys) { + for (OpenPGPKey.OpenPGPSecretKey secretKey : secretKeys.getSecretKeys().values()) { assertDoesNotThrow(() -> UnlockSecretKey.unlockSecretKey(secretKey, protector)); } diff --git a/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java b/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java index 7ec53edb..35fb8043 100644 --- a/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java +++ b/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java @@ -122,9 +122,9 @@ public class OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionT ByteArrayInputStream in = new ByteArrayInputStream(dearmored.toByteArray()); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addVerificationCert(cert) .addDecryptionKey(secretKeys)); diff --git a/pgpainless-core/src/test/java/openpgp/DateExtensionTest.java b/pgpainless-core/src/test/java/openpgp/DateExtensionTest.java new file mode 100644 index 00000000..c36315bb --- /dev/null +++ b/pgpainless-core/src/test/java/openpgp/DateExtensionTest.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package openpgp; + +import org.junit.jupiter.api.Test; + +import java.util.Date; + +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 DateExtensionTest { + + @Test + public void testDatePlusSecondsBaseCase() { + Date t0 = DateExtensionsKt.parseUTC("2025-05-12 10:36:53 UTC"); + Date t1 = DateExtensionsKt.plusSeconds(t0, 7); + assertEquals("2025-05-12 10:37:00 UTC", DateExtensionsKt.formatUTC(t1)); + } + + @Test + public void testDatePlusZeroReturnsNull() { + Date t0 = DateExtensionsKt.parseUTC("2025-05-12 10:36:53 UTC"); + Date t1 = DateExtensionsKt.plusSeconds(t0, 0); + assertNull(t1); + } + + @Test + public void testDatePlusSecondsOverflowing() { + Date now = new Date(); + // expect IAE because of time field overflowing + assertThrows(IllegalArgumentException.class, () -> + DateExtensionsKt.plusSeconds(now, Long.MAX_VALUE - 10000)); + } + + @Test + public void testParsingMalformedUTCTimestampThrows() { + assertThrows(IllegalArgumentException.class, () -> + DateExtensionsKt.parseUTC("2025-05-12 10:36:XX UTC")); + } +} diff --git a/pgpainless-core/src/test/java/openpgp/LongExtensionTest.java b/pgpainless-core/src/test/java/openpgp/LongExtensionTest.java new file mode 100644 index 00000000..8f046968 --- /dev/null +++ b/pgpainless-core/src/test/java/openpgp/LongExtensionTest.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package openpgp; + +import org.junit.jupiter.api.Test; +import org.pgpainless.key.util.KeyIdUtil; + +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class LongExtensionTest { + + private final Random random = new Random(); + + @Test + public void testFromOpenPGPKeyId() { + long id = random.nextLong(); + String hexId = LongExtensionsKt.openPgpKeyId(id); + assertEquals(16, hexId.length()); + // Calling companion object extension methods from java is tricky. + // KeyIdUtil delegates to Long.Companion extension method though. + long parsed = KeyIdUtil.fromLongKeyId(hexId); + assertEquals(id, parsed, "Long MUST still match after converting to hex and back."); + } + + @Test + public void testParsingMalformedHexIdFails() { + assertThrows(IllegalArgumentException.class, () -> + KeyIdUtil.fromLongKeyId("00"), + "Hex encoding is too short, expect 16 chars.");assertThrows(IllegalArgumentException.class, () -> + KeyIdUtil.fromLongKeyId("00010203040506XX"), + "Hex encoding contains non-hex chars."); + } +} diff --git a/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java b/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java index f9bd7a61..3fff4f00 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java @@ -15,9 +15,8 @@ import java.nio.charset.StandardCharsets; import java.util.List; import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -182,7 +181,7 @@ public class AsciiArmorCRCTests { "=AAAA\n" + "-----END PGP PRIVATE KEY BLOCK-----"; - assertThrows(IOException.class, () -> PGPainless.readKeyRing().secretKeyRing(KEY)); + assertThrows(IOException.class, () -> PGPainless.getInstance().readKey().parseKeysOrCertificates(KEY)); } @Test @@ -230,7 +229,7 @@ public class AsciiArmorCRCTests { "=AAAA\n" + "-----END PGP PUBLIC KEY BLOCK-----"; - assertThrows(IOException.class, () -> PGPainless.readKeyRing().publicKeyRing(CERT)); + assertThrows(IOException.class, () -> PGPainless.getInstance().readKey().parseCertificate(CERT)); } @Test @@ -270,7 +269,7 @@ public class AsciiArmorCRCTests { "-----END PGP SIGNATURE-----"; @Test - public void assertMissingCRCSumInSignatureArmorIsOkay() throws PGPException, IOException { + public void assertMissingCRCSumInSignatureArmorIsOkay() { List signatureList = SignatureUtils.readSignatures(ARMORED_SIGNATURE_WITH_MISSING_CRC_SUM); assertEquals(1, signatureList.size()); } @@ -365,7 +364,7 @@ public class AsciiArmorCRCTests { "xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=\n" + "-----END PGP PRIVATE KEY BLOCK-----"; - PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY); + OpenPGPKey key = PGPainless.getInstance().readKey().parseKey(KEY); assertNotNull(key); assertEquals(new OpenPgpV4Fingerprint("D1A66E1A23B182C9980F788CFBFCC82A015E7330"), new OpenPgpV4Fingerprint(key)); } @@ -541,11 +540,11 @@ public class AsciiArmorCRCTests { "=FdCC\n" + "-----END PGP MESSAGE-----\n"; - PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(ASCII_KEY); + OpenPGPKey key = PGPainless.getInstance().readKey().parseKey(ASCII_KEY); assertThrows(IOException.class, () -> { - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions().addDecryptionKey( + .withOptions(ConsumerOptions.get().addDecryptionKey( key, SecretKeyRingProtector.unlockAnyKeyWith(passphrase) )); diff --git a/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java index 10cf4b1f..2d857254 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -68,15 +68,15 @@ public class CachingBcPublicKeyDataDecryptorFactoryTest { SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); SubkeyIdentifier decryptionKey = new SubkeyIdentifier(secretKeys, - info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID()); + info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyIdentifier()); - PGPSecretKey secretKey = secretKeys.getSecretKey(decryptionKey.getSubkeyId()); + PGPSecretKey secretKey = secretKeys.getSecretKey(decryptionKey.getKeyIdentifier()); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); CachingBcPublicKeyDataDecryptorFactory cachingFactory = new CachingBcPublicKeyDataDecryptorFactory( privateKey, decryptionKey); ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(MSG.getBytes()); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(ciphertextIn) .withOptions(ConsumerOptions.get() .addCustomDecryptorFactory(cachingFactory)); @@ -87,7 +87,7 @@ public class CachingBcPublicKeyDataDecryptorFactoryTest { assertEquals("Hello, World!\n", out.toString()); ciphertextIn = new ByteArrayInputStream(MSG.getBytes()); - decryptionStream = PGPainless.decryptAndOrVerify() + decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(ciphertextIn) .withOptions(ConsumerOptions.get() .addCustomDecryptorFactory(cachingFactory)); diff --git a/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java b/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java index ebb344b0..1fb624f6 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java @@ -4,26 +4,23 @@ package org.bouncycastle; -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.Iterator; 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.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; -import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.util.CollectionUtils; public class PGPPublicKeyRingTest { + private final PGPainless api = PGPainless.getInstance(); + /** * Learning test to see if BC also makes userids available on subkeys. * It does not. @@ -31,29 +28,25 @@ public class PGPPublicKeyRingTest { * @see userIds = subkey.getUserIDs(); - if (primaryKey == subkey) { - assertEquals("primary@user.id", userIds.next()); - } + public void subkeysDoNotHaveUserIDsTest() { + OpenPGPKey key = api.generateKey().simpleEcKeyRing("primary@user.id"); + OpenPGPCertificate certificate = key.toCertificate(); + for (OpenPGPCertificate.OpenPGPComponentKey subkey : certificate.getSubkeys().values()) { + Iterator userIds = subkey.getPGPPublicKey().getUserIDs(); assertFalse(userIds.hasNext()); } } @Test - public void removeUserIdTest() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void removeUserIdTest() { String userId = "alice@wonderland.lit"; - PGPSecretKeyRing secretKeyRing = PGPainless.generateKeyRing().simpleEcKeyRing(userId); - PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeyRing); + OpenPGPKey key = api.generateKey().simpleEcKeyRing(userId); + OpenPGPCertificate certificate = key.toCertificate(); + PGPPublicKey publicKey = certificate.getPrimaryKey().getPGPPublicKey(); - List userIds = CollectionUtils.iteratorToList(publicKeys.getPublicKey().getUserIDs()); + List userIds = CollectionUtils.iteratorToList(publicKey.getUserIDs()); assertTrue(userIds.contains(userId)); - PGPPublicKey publicKey = publicKeys.getPublicKey(); publicKey = PGPPublicKey.removeCertification(publicKey, userId); userIds = CollectionUtils.iteratorToList(publicKey.getUserIDs()); diff --git a/pgpainless-core/src/test/java/org/bouncycastle/PolicyAdapterTest.java b/pgpainless-core/src/test/java/org/bouncycastle/PolicyAdapterTest.java new file mode 100644 index 00000000..9407d7d0 --- /dev/null +++ b/pgpainless-core/src/test/java/org/bouncycastle/PolicyAdapterTest.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle; + +import org.bouncycastle.openpgp.api.OpenPGPPolicy; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.bouncycastle.PolicyAdapter; +import org.pgpainless.policy.Policy; +import org.pgpainless.util.NotationRegistry; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PolicyAdapterTest { + + @Test + public void testNotationRegistryAdaption() { + NotationRegistry pgpainlessNotationReg = new NotationRegistry(); + pgpainlessNotationReg.addKnownNotation("foo"); + + Policy policy = PGPainless.getInstance().getAlgorithmPolicy() + .copy() + .withNotationRegistry(pgpainlessNotationReg) + .build(); + + PolicyAdapter adapter = new PolicyAdapter(policy); + OpenPGPPolicy.OpenPGPNotationRegistry bcNotationReg = adapter.getNotationRegistry(); + assertTrue(bcNotationReg.isNotationKnown("foo")); + assertFalse(bcNotationReg.isNotationKnown("bar")); + bcNotationReg.addKnownNotation("bar"); + + assertTrue(pgpainlessNotationReg.isKnownNotation("bar")); + } +} diff --git a/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java b/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java index 87c5b02e..1d9ea6d3 100644 --- a/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java +++ b/pgpainless-core/src/test/java/org/gnupg/GnuPGDummyKeyUtilTest.java @@ -9,14 +9,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.bcpg.S2K; import org.bouncycastle.bcpg.SecretKeyPacket; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPKeyReader; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.SubkeyIdentifier; @@ -178,8 +182,9 @@ public class GnuPGDummyKeyUtilTest { @Test public void testMoveAllKeysToCard() throws IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); - PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_ON_CARD); + OpenPGPKeyReader reader = PGPainless.getInstance().readKey(); + OpenPGPKey secretKeys = reader.parseKey(FULL_KEY); + OpenPGPKey expected = reader.parseKey(ALL_KEYS_ON_CARD); PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any(), cardSerial); @@ -190,46 +195,64 @@ public class GnuPGDummyKeyUtilTest { assertEquals(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD, s2K.getProtectionMode()); } - assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + assertArrayEquals(expected.getPGPSecretKeyRing().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); + OpenPGPKeyReader reader = PGPainless.getInstance().readKey(); + OpenPGPKey secretKeys = reader.parseKey(FULL_KEY); + OpenPGPKey expected = reader.parseKey(PRIMARY_KEY_ON_CARD); PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(primaryKeyId), cardSerial); - assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + assertArrayEquals(expected.getPGPSecretKeyRing().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); + OpenPGPKeyReader reader = PGPainless.getInstance().readKey(); + OpenPGPKey secretKeys = reader.parseKey(FULL_KEY); + OpenPGPKey expected = reader.parseKey(ENCRYPTION_KEY_ON_CARD); PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId), cardSerial); - assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + assertArrayEquals(expected.getPGPSecretKeyRing().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); + OpenPGPKeyReader reader = PGPainless.getInstance().readKey(); + OpenPGPKey secretKeys = reader.parseKey(FULL_KEY); + OpenPGPKey expected = reader.parseKey(SIGNATURE_KEY_ON_CARD); PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(signatureKeyId), cardSerial); - assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + assertArrayEquals(expected.getPGPSecretKeyRing().getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMoveSelectedKeyToCard() throws IOException { + OpenPGPKeyReader reader = PGPainless.getInstance().readKey(); + OpenPGPKey secretKeys = reader.parseKey(FULL_KEY); + OpenPGPKey expected = reader.parseKey(SIGNATURE_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.selected( + Arrays.asList(new KeyIdentifier(signatureKeyId))) + , cardSerial); + + assertArrayEquals(expected.getPGPSecretKeyRing().getEncoded(), onCard.getEncoded()); } @Test public void testRemoveAllKeys() throws IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); - PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_REMOVED); + OpenPGPKeyReader reader = PGPainless.getInstance().readKey(); + OpenPGPKey secretKeys = reader.parseKey(FULL_KEY); + OpenPGPKey expected = reader.parseKey(ALL_KEYS_REMOVED); PGPSecretKeyRing removedSecretKeys = GnuPGDummyKeyUtil.modify(secretKeys) .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any()); @@ -240,33 +263,35 @@ public class GnuPGDummyKeyUtilTest { assertEquals(GnuPGDummyExtension.NO_PRIVATE_KEY.getId(), s2k.getProtectionMode()); } - assertArrayEquals(expected.getEncoded(), removedSecretKeys.getEncoded()); + assertArrayEquals(expected.getPGPSecretKeyRing().getEncoded(), removedSecretKeys.getEncoded()); } @Test public void testGetSingleIdOfHardwareBackedKey() throws IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); - assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty()); + OpenPGPKeyReader reader = PGPainless.getInstance().readKey(); + OpenPGPKey secretKeys = reader.parseKey(FULL_KEY); + assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys.getPGPSecretKeyRing()).isEmpty()); PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId)); Set hardwareBackedKeys = GnuPGDummyKeyUtil .getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey); - assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys); + assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys.getPGPSecretKeyRing(), encryptionKeyId)), hardwareBackedKeys); } @Test public void testGetIdsOfFullyHardwareBackedKey() throws IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); - assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty()); + OpenPGPKeyReader reader = PGPainless.getInstance().readKey(); + OpenPGPKey secretKeys = reader.parseKey(FULL_KEY); + assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys.getPGPSecretKeyRing()).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())); + for (PGPSecretKey key : secretKeys.getPGPSecretKeyRing()) { + expected.add(new SubkeyIdentifier(secretKeys.getPGPSecretKeyRing(), key.getKeyIdentifier())); } Set hardwareBackedKeys = GnuPGDummyKeyUtil diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/negotiation/CompressionAlgorithmNegotiatorTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/negotiation/CompressionAlgorithmNegotiatorTest.java new file mode 100644 index 00000000..ced8b4a1 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/negotiation/CompressionAlgorithmNegotiatorTest.java @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm.negotiation; + +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.policy.Policy; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CompressionAlgorithmNegotiatorTest { + + @Test + public void staticNegotiateWithoutOverride() { + Policy policy = PGPainless.getInstance().getAlgorithmPolicy() + .copy() + .withCompressionAlgorithmPolicy(new Policy.CompressionAlgorithmPolicy( + CompressionAlgorithm.BZIP2, + Arrays.asList(CompressionAlgorithm.BZIP2, CompressionAlgorithm.UNCOMPRESSED) + )) + .build(); + CompressionAlgorithmNegotiator negotiator = CompressionAlgorithmNegotiator.staticNegotiation(); + + // If the user did not pass an override, return the policy default + assertEquals( + CompressionAlgorithm.BZIP2, + negotiator.negotiate(policy, null, Collections.emptySet())); + } + + @Test + public void staticNegotiateWithOverride() { + Policy policy = PGPainless.getInstance().getAlgorithmPolicy() + .copy() + .withCompressionAlgorithmPolicy(new Policy.CompressionAlgorithmPolicy( + CompressionAlgorithm.BZIP2, + Arrays.asList(CompressionAlgorithm.BZIP2, CompressionAlgorithm.UNCOMPRESSED) + )) + .build(); + CompressionAlgorithmNegotiator negotiator = CompressionAlgorithmNegotiator.staticNegotiation(); + + // If the user passed an override, return that + assertEquals( + CompressionAlgorithm.ZLIB, + negotiator.negotiate(policy, CompressionAlgorithm.ZLIB, Collections.emptySet())); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/SymmetricKeyAlgorithmNegotiatorTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiatorTest.java similarity index 97% rename from pgpainless-core/src/test/java/org/pgpainless/algorithm/SymmetricKeyAlgorithmNegotiatorTest.java rename to pgpainless-core/src/test/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiatorTest.java index 3b9cd1b1..2ff83f6c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/algorithm/SymmetricKeyAlgorithmNegotiatorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiatorTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.algorithm; +package org.pgpainless.algorithm.negotiation; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -15,7 +15,7 @@ import java.util.List; import java.util.Set; import org.junit.jupiter.api.Test; -import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.policy.Policy; public class SymmetricKeyAlgorithmNegotiatorTest { 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 f9936db0..7b586277 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 @@ -22,11 +22,10 @@ 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.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.BeforeAll; @@ -109,15 +108,16 @@ public class CanonicalizedDataEncryptionTest { String message = "Hello, World!\n"; - private static PGPSecretKeyRing secretKeys; - private static PGPPublicKeyRing publicKeys; + private static OpenPGPKey secretKeys; + private static OpenPGPCertificate publicKeys; @BeforeAll public static void readKeys() throws IOException { - secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - publicKeys = PGPainless.extractCertificate(secretKeys); + PGPainless api = PGPainless.getInstance(); + secretKeys = api.readKey().parseKey(KEY); + publicKeys = secretKeys.toCertificate(); // CHECKSTYLE:OFF - System.out.println(PGPainless.asciiArmor(secretKeys)); + System.out.println(api.toAsciiArmor(secretKeys)); // CHECKSTYLE:ON } @@ -298,9 +298,9 @@ public class CanonicalizedDataEncryptionTest { String encrypted = encryptAndSign(before, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, true); ByteArrayInputStream in = new ByteArrayInputStream(encrypted.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKey(secretKeys, SecretKeyRingProtector.unprotectedKeys()) .addVerificationCert(publicKeys)); @@ -328,9 +328,9 @@ public class CanonicalizedDataEncryptionTest { String encrypted = encryptAndSign(beforeAndAfter, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, false); ByteArrayInputStream in = new ByteArrayInputStream(encrypted.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKey(secretKeys, SecretKeyRingProtector.unprotectedKeys()) .addVerificationCert(publicKeys)); @@ -360,7 +360,7 @@ public class CanonicalizedDataEncryptionTest { options.applyCRLFEncoding(); } - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = PGPainless.getInstance().generateMessage() .onOutputStream(out) .withOptions(options); @@ -374,9 +374,9 @@ public class CanonicalizedDataEncryptionTest { private MessageMetadata decryptAndVerify(String msg) throws PGPException, IOException { ByteArrayInputStream in = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKey(secretKeys, SecretKeyRingProtector.unprotectedKeys()) .addVerificationCert(publicKeys)); @@ -397,7 +397,7 @@ public class CanonicalizedDataEncryptionTest { public void manualSignAndVerify(DocumentSignatureType sigType, StreamEncoding streamEncoding) throws IOException, PGPException { - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); + OpenPGPKey.OpenPGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKeys.getPrimarySecretKey(), SecretKeyRingProtector.unprotectedKeys()); ByteArrayOutputStream out = new ByteArrayOutputStream(); ArmoredOutputStream armorOut = new ArmoredOutputStream(out); @@ -406,9 +406,10 @@ public class CanonicalizedDataEncryptionTest { PGPSignatureGenerator signer = new PGPSignatureGenerator( new BcPGPContentSignerBuilder( - secretKeys.getPublicKey().getAlgorithm(), - HashAlgorithm.SHA256.getAlgorithmId())); - signer.init(sigType.getSignatureType().getCode(), privateKey); + secretKeys.getPrimaryKey().getPGPPublicKey().getAlgorithm(), + HashAlgorithm.SHA256.getAlgorithmId()), + secretKeys.getPrimaryKey().getPGPPublicKey()); + signer.init(sigType.getSignatureType().getCode(), privateKey.getKeyPair().getPrivateKey()); PGPOnePassSignature ops = signer.generateOnePassVersion(false); ops.encode(compressedOut); @@ -444,9 +445,9 @@ public class CanonicalizedDataEncryptionTest { ByteArrayInputStream cipherIn = new ByteArrayInputStream(ciphertext.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(cipherIn) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addVerificationCert(publicKeys)); Streams.pipeAll(decryptionStream, decrypted); 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 e5f9e370..4483c2d5 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 @@ -14,12 +14,12 @@ 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.bcpg.KeyIdentifier; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -63,44 +63,46 @@ public class CertificateWithMissingSecretKeyTest { "=eTh7\n" + "-----END PGP PRIVATE KEY BLOCK-----"; - private static final long signingSubkeyId = -7647663290973502178L; - private static PGPSecretKeyRing missingSigningSecKey; + private static final KeyIdentifier signingSubkeyId = new KeyIdentifier(-7647663290973502178L); + private static OpenPGPKey missingSigningSecKey; - private static long encryptionSubkeyId; - private static PGPSecretKeyRing missingDecryptionSecKey; + private static KeyIdentifier encryptionSubkeyId; + private static OpenPGPKey missingDecryptionSecKey; private static final SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); @BeforeAll - public static void prepare() throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public static void prepare() throws IOException { + PGPainless api = PGPainless.getInstance(); // missing signing sec key we read from bytes - missingSigningSecKey = PGPainless.readKeyRing().secretKeyRing(MISSING_SIGNING_SECKEY); + missingSigningSecKey = api.readKey().parseKey(MISSING_SIGNING_SECKEY); // missing encryption sec key we generate on the fly - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Missing Decryption Key "); - encryptionSubkeyId = PGPainless.inspectKeyRing(secretKeys) - .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID(); + encryptionSubkeyId = api.inspect(secretKeys) + .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyIdentifier(); // remove the encryption/decryption secret key - missingDecryptionSecKey = KeyRingUtils.stripSecretKey(secretKeys, encryptionSubkeyId); + PGPSecretKeyRing withSecretKeyStripped = KeyRingUtils.stripSecretKey(secretKeys.getPGPSecretKeyRing(), encryptionSubkeyId); + missingDecryptionSecKey = api.toKey(withSecretKeyStripped); } @Test public void assureMissingSigningSecKeyOnlyContainSigningPubKey() { - assertNotNull(missingSigningSecKey.getPublicKey(signingSubkeyId)); + assertNotNull(missingSigningSecKey.getKey(signingSubkeyId)); assertNull(missingSigningSecKey.getSecretKey(signingSubkeyId)); - KeyRingInfo info = PGPainless.inspectKeyRing(missingSigningSecKey); + KeyRingInfo info = PGPainless.getInstance().inspect(missingSigningSecKey); assertFalse(info.getSigningSubkeys().isEmpty()); // This method only tests for pub keys. } @Test public void assureMissingDecryptionSecKeyOnlyContainsEncryptionPubKey() { - assertNotNull(missingDecryptionSecKey.getPublicKey(encryptionSubkeyId)); + assertNotNull(missingDecryptionSecKey.getKey(encryptionSubkeyId)); assertNull(missingDecryptionSecKey.getSecretKey(encryptionSubkeyId)); - KeyRingInfo info = PGPainless.inspectKeyRing(missingDecryptionSecKey); + KeyRingInfo info = PGPainless.getInstance().inspect(missingDecryptionSecKey); assertFalse(info.getEncryptionSubkeys(EncryptionPurpose.ANY).isEmpty()); // pub key is still there } @@ -119,12 +121,14 @@ public class CertificateWithMissingSecretKeyTest { ByteArrayInputStream in = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - PGPPublicKeyRing certificate = PGPainless.extractCertificate(missingDecryptionSecKey); + PGPainless api = PGPainless.getInstance(); + + OpenPGPCertificate certificate = missingDecryptionSecKey.toCertificate(); ProducerOptions producerOptions = ProducerOptions.encrypt( - EncryptionOptions.encryptCommunications() + EncryptionOptions.encryptCommunications(api) .addRecipient(certificate)); // we can still encrypt, since the pub key is still there - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(out) .withOptions(producerOptions); @@ -136,11 +140,11 @@ public class CertificateWithMissingSecretKeyTest { // Test decryption ByteArrayInputStream cipherIn = new ByteArrayInputStream(out.toByteArray()); - ConsumerOptions consumerOptions = new ConsumerOptions() + ConsumerOptions consumerOptions = ConsumerOptions.get(api) .addDecryptionKey(missingDecryptionSecKey); assertThrows(MissingDecryptionMethodException.class, () -> - PGPainless.decryptAndOrVerify() + api.processMessage() .onInputStream(cipherIn) .withOptions(consumerOptions)); // <- cannot find decryption 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 720a0d53..6b393e90 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 @@ -16,15 +16,12 @@ 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; import java.util.Random; 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.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -38,10 +35,7 @@ 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; import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.consumer.SignatureVerifier; -import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.TestUtils; public class CleartextSignatureVerificationTest { @@ -78,17 +72,18 @@ public class CleartextSignatureVerificationTest { public static final String alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; public static final Random random = new Random(); + private static final PGPainless api = PGPainless.getInstance(); @Test public void cleartextSignVerification_InMemoryMultiPassStrategy() throws IOException, PGPException { - PGPPublicKeyRing signingKeys = TestKeys.getEmilPublicKeyRing(); - ConsumerOptions options = new ConsumerOptions() - .addVerificationCert(signingKeys); + OpenPGPCertificate signingCert = TestKeys.getEmilCertificate(); + ConsumerOptions options = ConsumerOptions.get(api) + .addVerificationCert(signingCert); InMemoryMultiPassStrategy multiPassStrategy = MultiPassStrategy.keepMessageInMemory(); options.setMultiPassStrategy(multiPassStrategy); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(new ByteArrayInputStream(MESSAGE_SIGNED)) .withOptions(options); @@ -102,22 +97,22 @@ public class CleartextSignatureVerificationTest { PGPSignature signature = result.getVerifiedSignatures().iterator().next().getSignature(); - assertEquals(signature.getKeyID(), signingKeys.getPublicKey().getKeyID()); + assertTrue(signature.hasKeyIdentifier(signingCert.getKeyIdentifier())); assertArrayEquals(MESSAGE_BODY, out.toByteArray()); } @Test public void cleartextSignVerification_FileBasedMultiPassStrategy() throws IOException, PGPException { - PGPPublicKeyRing signingKeys = TestKeys.getEmilPublicKeyRing(); - ConsumerOptions options = new ConsumerOptions() - .addVerificationCert(signingKeys); + OpenPGPCertificate signingCert = TestKeys.getEmilCertificate(); + ConsumerOptions options = ConsumerOptions.get(api) + .addVerificationCert(signingCert); File tempDir = TestUtils.createTempDirectory(); File file = new File(tempDir, "file"); MultiPassStrategy multiPassStrategy = MultiPassStrategy.writeMessageToFile(file); options.setMultiPassStrategy(multiPassStrategy); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(new ByteArrayInputStream(MESSAGE_SIGNED)) .withOptions(options); @@ -130,7 +125,7 @@ public class CleartextSignatureVerificationTest { PGPSignature signature = result.getVerifiedSignatures().iterator().next().getSignature(); - assertEquals(signature.getKeyID(), signingKeys.getPublicKey().getKeyID()); + assertTrue(signature.hasKeyIdentifier(signingCert.getKeyIdentifier())); FileInputStream fileIn = new FileInputStream(file); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); Streams.pipeAll(fileIn, bytes); @@ -138,23 +133,10 @@ public class CleartextSignatureVerificationTest { assertArrayEquals(MESSAGE_BODY, bytes.toByteArray()); } - @Test - public void verifySignatureDetached() - throws IOException, PGPException { - PGPPublicKeyRing signingKeys = TestKeys.getEmilPublicKeyRing(); - - PGPSignature signature = SignatureUtils.readSignatures(SIGNATURE).get(0); - PGPPublicKey signingKey = signingKeys.getPublicKey(signature.getKeyID()); - - SignatureVerifier.initializeSignatureAndUpdateWithSignedData(signature, new ByteArrayInputStream(MESSAGE_BODY), signingKey); - - CertificateValidator.validateCertificateAndVerifyInitializedSignature(signature, signingKeys, PGPainless.getPolicy()); - } - public static void main(String[] args) throws IOException { // CHECKSTYLE:OFF - PGPPublicKeyRing keys = TestKeys.getEmilPublicKeyRing(); - System.out.println(ArmorUtils.toAsciiArmoredString(keys)); + OpenPGPCertificate cert = TestKeys.getEmilCertificate(); + System.out.println(cert.toAsciiArmoredString()); System.out.println(new String(MESSAGE_SIGNED)); System.out.println(new String(MESSAGE_BODY)); System.out.println(new String(SIGNATURE)); @@ -166,11 +148,11 @@ public class CleartextSignatureVerificationTest { throws IOException, PGPException { PGPSignature signature = SignatureUtils.readSignatures(SIGNATURE).get(0); - ConsumerOptions options = new ConsumerOptions() - .addVerificationCert(TestKeys.getEmilPublicKeyRing()) + ConsumerOptions options = ConsumerOptions.get(api) + .addVerificationCert(TestKeys.getEmilCertificate()) .addVerificationOfDetachedSignature(signature); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(new ByteArrayInputStream(MESSAGE_BODY)) .withOptions(options); @@ -188,10 +170,10 @@ public class CleartextSignatureVerificationTest { String message = "Foo\nBar"; // PGPUtil.getDecoderStream() would have mistaken this for base64 data ByteArrayInputStream msgIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); - PGPSecretKeyRing secretKey = TestKeys.getEmilSecretKeyRing(); + OpenPGPKey secretKey = TestKeys.getEmilKey(); ByteArrayOutputStream signedOut = new ByteArrayOutputStream(); - EncryptionStream signingStream = PGPainless.encryptAndOrSign().onOutputStream(signedOut) - .withOptions(ProducerOptions.sign(SigningOptions.get() + EncryptionStream signingStream = api.generateMessage().onOutputStream(signedOut) + .withOptions(ProducerOptions.sign(SigningOptions.get(api) .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) .setCleartextSigned()); @@ -201,10 +183,10 @@ public class CleartextSignatureVerificationTest { String signed = signedOut.toString(); ByteArrayInputStream signedIn = new ByteArrayInputStream(signed.getBytes(StandardCharsets.UTF_8)); - DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() + DecryptionStream verificationStream = api.processMessage() .onInputStream(signedIn) - .withOptions(new ConsumerOptions() - .addVerificationCert(TestKeys.getEmilPublicKeyRing())); + .withOptions(ConsumerOptions.get(api) + .addVerificationCert(TestKeys.getEmilCertificate())); ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); Streams.pipeAll(verificationStream, msgOut); @@ -216,15 +198,15 @@ public class CleartextSignatureVerificationTest { @Test public void testDecryptionOfVeryLongClearsignedMessage() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + throws PGPException, IOException { String message = randomString(28, 4000); - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("Alice"); ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(out) .withOptions(ProducerOptions.sign( - SigningOptions.get() + SigningOptions.get(api) .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) ).setCleartextSigned()); @@ -235,10 +217,10 @@ public class CleartextSignatureVerificationTest { String cleartextSigned = out.toString(); ByteArrayInputStream in = new ByteArrayInputStream(cleartextSigned.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() - .addVerificationCert(PGPainless.extractCertificate(secretKeys))); + .withOptions(ConsumerOptions.get() + .addVerificationCert(secretKeys.toCertificate())); out = new ByteArrayOutputStream(); Streams.pipeAll(decryptionStream, out); 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 71fbf9be..b428ae5c 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 @@ -4,12 +4,10 @@ package org.pgpainless.decryption_verification; +import org.bouncycastle.bcpg.KeyIdentifier; 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.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.bouncycastle.util.io.Streams; @@ -28,8 +26,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 static org.junit.jupiter.api.Assertions.assertEquals; @@ -37,31 +33,33 @@ 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); + throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKey = api.generateKey().modernKeyRing("Alice"); + OpenPGPCertificate cert = secretKey.toCertificate(); + KeyRingInfo info = api.inspect(secretKey); + OpenPGPCertificate.OpenPGPComponentKey encryptionKey = + info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); // Encrypt a test message String plaintext = "Hello, World!\n"; ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(ciphertextOut) - .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get(api) .addRecipient(cert))); encryptionStream.write(plaintext.getBytes(StandardCharsets.UTF_8)); encryptionStream.close(); HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = new HardwareSecurity.DecryptionCallback() { @Override - public byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData, int pkeskVersion) + public byte[] decryptSessionKey(KeyIdentifier keyIdentifier, int keyAlgorithm, byte[] sessionKeyData, int pkeskVersion) 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); + OpenPGPKey.OpenPGPSecretKey decryptionKey = secretKey.getSecretKey(keyIdentifier); + OpenPGPKey.OpenPGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, Passphrase.emptyPassphrase()); + PublicKeyDataDecryptorFactory internal = new BcPublicKeyDataDecryptorFactory(privateKey.getKeyPair().getPrivateKey()); return internal.recoverSessionData(keyAlgorithm, new byte[][] {sessionKeyData}, pkeskVersion); } catch (PGPException e) { throw new HardwareSecurity.HardwareSecurityException(); @@ -70,12 +68,12 @@ public class CustomPublicKeyDataDecryptorFactoryTest { }; // Decrypt - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(new ByteArrayInputStream(ciphertextOut.toByteArray())) .withOptions(ConsumerOptions.get() .addCustomDecryptorFactory( new HardwareSecurity.HardwareDataDecryptorFactory( - new SubkeyIdentifier(cert, encryptionKey.getKeyID()), + new SubkeyIdentifier(encryptionKey), hardwareDecryptionCallback))); ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); 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 82796cb9..895cc6f2 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 @@ -52,11 +52,11 @@ public class DecryptAndVerifyMessageTest { public void decryptMessageAndVerifySignatureTest() throws Exception { String encryptedMessage = TestKeys.MSG_SIGN_CRYPT_JULIET_JULIET; - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get() .addDecryptionKey(juliet) .addVerificationCert(KeyRingUtils.publicKeyRingFrom(juliet)); - DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + DecryptionStream decryptor = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(encryptedMessage.getBytes())) .withOptions(options); @@ -87,11 +87,11 @@ public class DecryptAndVerifyMessageTest { public void decryptMessageAndReadBeyondEndTest() throws Exception { final String encryptedMessage = TestKeys.MSG_SIGN_CRYPT_JULIET_JULIET; - final ConsumerOptions options = new ConsumerOptions() + final ConsumerOptions options = ConsumerOptions.get() .addDecryptionKey(juliet) .addVerificationCert(KeyRingUtils.publicKeyRingFrom(juliet)); - try (DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + try (DecryptionStream decryptor = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(encryptedMessage.getBytes())) .withOptions(options); ByteArrayOutputStream toPlain = new ByteArrayOutputStream()) { @@ -105,11 +105,11 @@ public class DecryptAndVerifyMessageTest { public void decryptMessageAndVerifySignatureByteByByteTest() throws Exception { String encryptedMessage = TestKeys.MSG_SIGN_CRYPT_JULIET_JULIET; - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get() .addDecryptionKey(juliet) .addVerificationCert(KeyRingUtils.publicKeyRingFrom(juliet)); - DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + DecryptionStream decryptor = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(encryptedMessage.getBytes())) .withOptions(options); @@ -150,7 +150,7 @@ public class DecryptAndVerifyMessageTest { "-----END PGP MESSAGE-----"; ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertext.getBytes()); assertThrows(MissingDecryptionMethodException.class, - () -> PGPainless.decryptAndOrVerify() + () -> PGPainless.getInstance().processMessage() .onInputStream(ciphertextIn) .withOptions(ConsumerOptions.get() .addMessagePassphrase(Passphrase.fromPassword("sw0rdf1sh")))); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java index 4eb7b203..341c4b91 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java @@ -13,8 +13,8 @@ import java.nio.charset.StandardCharsets; import java.util.List; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @@ -128,10 +128,10 @@ public class DecryptHiddenRecipientMessageTest { "=1knQ\n" + "-----END PGP MESSAGE-----\n"; ByteArrayInputStream messageIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get() .addDecryptionKey(secretKeys); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(messageIn) .withOptions(options); @@ -144,10 +144,11 @@ public class DecryptHiddenRecipientMessageTest { assertEquals(0L, metadata.getRecipientKeyIds().get(0)); KeyRingInfo info = new KeyRingInfo(secretKeys); - List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); + List encryptionKeys = + info.getEncryptionSubkeys(EncryptionPurpose.ANY); assertEquals(1, encryptionKeys.size()); - assertEquals(new SubkeyIdentifier(secretKeys, encryptionKeys.get(0).getKeyID()), metadata.getDecryptionKey()); + assertEquals(new SubkeyIdentifier(secretKeys, encryptionKeys.get(0).getKeyIdentifier()), metadata.getDecryptionKey()); assertEquals("Hello Recipient :)", out.toString()); } } 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 2b222c83..155030da 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 @@ -176,8 +176,8 @@ public class IgnoreUnknownSignatureVersionsTest { } private MessageMetadata verifySignature(PGPPublicKeyRing cert, String BASE_CASE) throws PGPException, IOException { - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify().onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage().onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) + .withOptions(ConsumerOptions.get() .addVerificationCert(cert) .addVerificationOfDetachedSignatures(new ByteArrayInputStream(BASE_CASE.getBytes(StandardCharsets.UTF_8)))); 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 b7382360..fce4186d 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 @@ -10,9 +10,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.openpgp.PGPException; import org.junit.jupiter.api.Test; -import org.pgpainless.key.util.KeyIdUtil; public class MessageInspectorTest { @@ -26,14 +26,15 @@ public class MessageInspectorTest { "Z1/i3TYsmy8B0mMKkNYtpMk=\n" + "=IICf\n" + "-----END PGP MESSAGE-----\n"; + MessageInspector inspector = new MessageInspector(); - MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(message); + MessageInspector.EncryptionInfo info = inspector.determineEncryptionInfoForMessage(message); assertFalse(info.isPassphraseEncrypted()); assertFalse(info.isSignedOnly()); assertTrue(info.isEncrypted()); assertEquals(1, info.getKeyIds().size()); - assertEquals(KeyIdUtil.fromLongKeyId("4766F6B9D5F21EB6"), (long) info.getKeyIds().get(0)); + assertEquals(new KeyIdentifier("4766F6B9D5F21EB6"), info.getKeyIdentifiers().get(0)); } @Test @@ -51,15 +52,16 @@ public class MessageInspectorTest { "nxVuXey3iyihCFAfD8ZK1Rnh\n" + "=z6e0\n" + "-----END PGP MESSAGE-----"; + MessageInspector inspector = new MessageInspector(); - MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(message); + MessageInspector.EncryptionInfo info = inspector.determineEncryptionInfoForMessage(message); assertTrue(info.isEncrypted()); assertTrue(info.isPassphraseEncrypted()); assertEquals(2, info.getKeyIds().size()); assertFalse(info.isSignedOnly()); - assertTrue(info.getKeyIds().contains(KeyIdUtil.fromLongKeyId("4C6E8F99F6E47184"))); - assertTrue(info.getKeyIds().contains(KeyIdUtil.fromLongKeyId("1839079A640B2FAC"))); + assertTrue(info.getKeyIdentifiers().contains(new KeyIdentifier("4C6E8F99F6E47184"))); + assertTrue(info.getKeyIdentifiers().contains(new KeyIdentifier("1839079A640B2FAC"))); } @Test @@ -73,8 +75,8 @@ public class MessageInspectorTest { "Dvxwv8UPAA==\n" + "=nt5n\n" + "-----END PGP MESSAGE-----"; - - MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(message); + MessageInspector inspector = new MessageInspector(); + MessageInspector.EncryptionInfo info = inspector.determineEncryptionInfoForMessage(message); assertTrue(info.isSignedOnly()); @@ -97,7 +99,8 @@ public class MessageInspectorTest { "KK0Ymg5GrsBTEGFm4jb1p+V85PPhsIioX3np/N3fkIfxFguTGZza33/GHy61+DTy\n" + "=SZU6\n" + "-----END PGP MESSAGE-----"; - MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(message); + MessageInspector inspector = new MessageInspector(); + MessageInspector.EncryptionInfo info = inspector.determineEncryptionInfoForMessage(message); // Message is encrypted, so we cannot determine if it is signed or not. // It is not signed only @@ -117,8 +120,8 @@ public class MessageInspectorTest { "yyl0CF9DT05TT0xFYXlXgUp1c3Qgc29tZSB1bmVuY3J5cHRlZCBkYXRhLg==\n" + "=jVNT\n" + "-----END PGP MESSAGE-----"; - - MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(message); + MessageInspector inspector = new MessageInspector(); + MessageInspector.EncryptionInfo info = inspector.determineEncryptionInfoForMessage(message); assertFalse(info.isEncrypted()); assertFalse(info.isSignedOnly()); assertFalse(info.isPassphraseEncrypted()); @@ -136,7 +139,8 @@ public class MessageInspectorTest { "=jw3E\n" + "-----END PGP MESSAGE-----"; - MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage(message); + MessageInspector inspector = new MessageInspector(); + MessageInspector.EncryptionInfo info = inspector.determineEncryptionInfoForMessage(message); assertFalse(info.isEncrypted()); assertFalse(info.isSignedOnly()); assertFalse(info.isPassphraseEncrypted()); 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 7c443829..dfbbe7b6 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 @@ -4,6 +4,7 @@ package org.pgpainless.decryption_verification; +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism; import org.junit.JUtils; import org.junit.jupiter.api.Test; import org.pgpainless.algorithm.CompressionAlgorithm; @@ -29,8 +30,8 @@ public class MessageMetadataTest { MessageMetadata.Message message = new MessageMetadata.Message(); MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP, message.getDepth() + 1); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128, compressedData.getDepth() + 1); - MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256, encryptedData.getDepth() + 1); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(MessageEncryptionMechanism.integrityProtected(SymmetricKeyAlgorithm.AES_128.getAlgorithmId()), compressedData.getDepth() + 1); + MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(MessageEncryptionMechanism.integrityProtected(SymmetricKeyAlgorithm.AES_256.getAlgorithmId()), encryptedData.getDepth() + 1); MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData(); message.setChild(compressedData); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java index 42562713..cde31de5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java @@ -14,15 +14,14 @@ 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.List; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -40,17 +39,18 @@ import org.pgpainless.util.Passphrase; public class MissingPassphraseForDecryptionTest { private final String passphrase = "dragon123"; - private PGPSecretKeyRing secretKeys; + private OpenPGPKey secretKeys; private byte[] message; + private final PGPainless api = PGPainless.getInstance(); @BeforeEach - public void setup() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - secretKeys = PGPainless.generateKeyRing().modernKeyRing("Test", passphrase); - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + public void setup() throws PGPException, IOException { + secretKeys = api.generateKey().modernKeyRing("Test", passphrase); + OpenPGPCertificate certificate = secretKeys.toCertificate(); ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(out) - .withOptions(ProducerOptions.encrypt(EncryptionOptions.encryptCommunications() + .withOptions(ProducerOptions.encrypt(EncryptionOptions.encryptCommunications(api) .addRecipient(certificate))); Streams.pipeAll(new ByteArrayInputStream("Hey, what's up?".getBytes(StandardCharsets.UTF_8)), encryptionStream); @@ -63,21 +63,21 @@ public class MissingPassphraseForDecryptionTest { // interactive callback SecretKeyPassphraseProvider callback = new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(@NotNull KeyIdentifier keyIdentifier) { // is called in interactive mode return Passphrase.fromPassword(passphrase); } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(@NotNull KeyIdentifier keyIdentifier) { return true; } }; - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get(api) .setMissingKeyPassphraseStrategy(MissingKeyPassphraseStrategy.INTERACTIVE) .addDecryptionKey(secretKeys, SecretKeyRingProtector.defaultSecretKeyRingProtector(callback)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(new ByteArrayInputStream(message)) .withOptions(options); @@ -90,36 +90,37 @@ public class MissingPassphraseForDecryptionTest { @Test public void throwExceptionStrategy() throws PGPException, IOException { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); + KeyRingInfo info = api.inspect(secretKeys); + List encryptionKeys = + info.getEncryptionSubkeys(EncryptionPurpose.ANY); SecretKeyPassphraseProvider callback = new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(@NotNull KeyIdentifier keyIdentifier) { fail("MUST NOT get called in non-interactive mode."); return null; } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(@NotNull KeyIdentifier keyIdentifier) { return true; } }; - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get(api) .setMissingKeyPassphraseStrategy(MissingKeyPassphraseStrategy.THROW_EXCEPTION) .addDecryptionKey(secretKeys, SecretKeyRingProtector.defaultSecretKeyRingProtector(callback)); try { - PGPainless.decryptAndOrVerify() + api.processMessage() .onInputStream(new ByteArrayInputStream(message)) .withOptions(options); fail("Expected exception!"); } catch (MissingPassphraseException e) { assertFalse(e.getKeyIds().isEmpty()); assertEquals(encryptionKeys.size(), e.getKeyIds().size()); - for (PGPPublicKey encryptionKey : encryptionKeys) { - assertTrue(e.getKeyIds().contains(new SubkeyIdentifier(secretKeys, encryptionKey.getKeyID()))); + for (OpenPGPCertificate.OpenPGPComponentKey encryptionKey : encryptionKeys) { + assertTrue(e.getKeyIds().contains(new SubkeyIdentifier(encryptionKey))); } } } 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 59021f95..07a06ba9 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 @@ -215,9 +215,9 @@ public class ModificationDetectionTests { PGPSecretKeyRingCollection secretKeyRings = getDecryptionKey(); InputStream in = new ByteArrayInputStream(MESSAGE_MISSING_MDC.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKeys(secretKeyRings, SecretKeyRingProtector.unprotectedKeys()) ); @@ -232,9 +232,9 @@ public class ModificationDetectionTests { @ExtendWith(TestAllImplementations.class) public void testTamperedCiphertextThrows() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_TAMPERED_CIPHERTEXT.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKeys(getDecryptionKey(), SecretKeyRingProtector.unprotectedKeys()) ); @@ -249,9 +249,9 @@ public class ModificationDetectionTests { @ExtendWith(TestAllImplementations.class) public void testIgnoreTamperedCiphertext() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_TAMPERED_CIPHERTEXT.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKeys(getDecryptionKey(), SecretKeyRingProtector.unprotectedKeys()) .setIgnoreMDCErrors(true) ); @@ -265,9 +265,9 @@ public class ModificationDetectionTests { @ExtendWith(TestAllImplementations.class) public void testTamperedMDCThrowsByDefault() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_TAMPERED_MDC.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKeys(getDecryptionKey(), SecretKeyRingProtector.unprotectedKeys()) ); @@ -282,9 +282,9 @@ public class ModificationDetectionTests { @ExtendWith(TestAllImplementations.class) public void testIgnoreTamperedMDC() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_TAMPERED_MDC.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKeys(getDecryptionKey(), SecretKeyRingProtector.unprotectedKeys()) .setIgnoreMDCErrors(true) ); @@ -297,9 +297,9 @@ public class ModificationDetectionTests { @ExtendWith(TestAllImplementations.class) public void testTruncatedMDCThrows() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_TRUNCATED_MDC.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKeys(getDecryptionKey(), SecretKeyRingProtector.unprotectedKeys()) ); @@ -311,9 +311,9 @@ public class ModificationDetectionTests { @ExtendWith(TestAllImplementations.class) public void testMDCWithBadCTBThrows() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_MDC_WITH_BAD_CTB.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKeys(getDecryptionKey(), SecretKeyRingProtector.unprotectedKeys()) ); @@ -328,9 +328,9 @@ public class ModificationDetectionTests { @ExtendWith(TestAllImplementations.class) public void testIgnoreMDCWithBadCTB() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_MDC_WITH_BAD_CTB.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKeys(getDecryptionKey(), SecretKeyRingProtector.unprotectedKeys()) .setIgnoreMDCErrors(true) ); @@ -344,9 +344,9 @@ public class ModificationDetectionTests { @ExtendWith(TestAllImplementations.class) public void testMDCWithBadLengthThrows() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_MDC_WITH_BAD_LENGTH.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKeys(getDecryptionKey(), SecretKeyRingProtector.unprotectedKeys()) ); @@ -361,9 +361,9 @@ public class ModificationDetectionTests { @ExtendWith(TestAllImplementations.class) public void testIgnoreMDCWithBadLength() throws IOException, PGPException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE_MDC_WITH_BAD_LENGTH.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKeys(getDecryptionKey(), SecretKeyRingProtector.unprotectedKeys()) .setIgnoreMDCErrors(true) ); @@ -532,13 +532,13 @@ public class ModificationDetectionTests { "-----END PGP MESSAGE-----\n" + "\n"; - assertThrows(MessageNotIntegrityProtectedException.class, () -> PGPainless.decryptAndOrVerify() + assertThrows(MessageNotIntegrityProtectedException.class, () -> PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(ciphertext.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions().addDecryptionKey(secretKeyRing, + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeyRing, SecretKeyRingProtector.unlockAnyKeyWith(passphrase))) ); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(ciphertext.getBytes(StandardCharsets.UTF_8))) .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeyRing, SecretKeyRingProtector.unlockAnyKeyWith(passphrase)) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPGPAnimalSnifferInputStreamTest.java similarity index 97% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPGPAnimalSnifferInputStreamTest.java index d731fb87..2d00c8ae 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPGPAnimalSnifferInputStreamTest.java @@ -13,15 +13,14 @@ 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.openpgp.api.OpenPGPKey; +import org.bouncycastle.util.encoders.Hex; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -31,7 +30,7 @@ import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.key.protection.SecretKeyRingProtector; -public class OpenPgpInputStreamTest { +public class OpenPGPAnimalSnifferInputStreamTest { private static final Random RANDOM = new Random(); @@ -41,9 +40,10 @@ public class OpenPgpInputStreamTest { RANDOM.nextBytes(randomBytes); ByteArrayInputStream randomIn = new ByteArrayInputStream(randomBytes); - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(randomIn); + OpenPGPAnimalSnifferInputStream openPgpInputStream = new OpenPGPAnimalSnifferInputStream(randomIn); assertFalse(openPgpInputStream.isAsciiArmored()); - assertFalse(openPgpInputStream.isLikelyOpenPgpMessage()); + assertFalse(openPgpInputStream.isLikelyOpenPgpMessage(), + Hex.toHexString(randomBytes, 0, 150)); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(openPgpInputStream, out); @@ -56,7 +56,7 @@ public class OpenPgpInputStreamTest { 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]; + byte[] randomBytes = new byte[OpenPGPAnimalSnifferInputStream.MAX_BUFFER_SIZE * 10]; RANDOM.nextBytes(randomBytes); ByteArrayOutputStream compressedDataPacket = new ByteArrayOutputStream(); @@ -65,7 +65,7 @@ public class OpenPgpInputStreamTest { compressor.write(randomBytes); compressor.close(); - OpenPgpInputStream inputStream = new OpenPgpInputStream(new ByteArrayInputStream(compressedDataPacket.toByteArray())); + OpenPGPAnimalSnifferInputStream inputStream = new OpenPGPAnimalSnifferInputStream(new ByteArrayInputStream(compressedDataPacket.toByteArray())); assertFalse(inputStream.isAsciiArmored()); assertFalse(inputStream.isNonOpenPgp()); assertTrue(inputStream.isBinaryOpenPgp()); @@ -90,7 +90,7 @@ public class OpenPgpInputStreamTest { "-----END PGP MESSAGE-----"; ByteArrayInputStream asciiIn = new ByteArrayInputStream(asciiArmoredMessage.getBytes(StandardCharsets.UTF_8)); - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(asciiIn); + OpenPGPAnimalSnifferInputStream openPgpInputStream = new OpenPGPAnimalSnifferInputStream(asciiIn); assertTrue(openPgpInputStream.isAsciiArmored()); assertFalse(openPgpInputStream.isNonOpenPgp()); @@ -663,9 +663,9 @@ public class OpenPgpInputStreamTest { @Test public void longAsciiArmoredMessageIsAsciiArmored() throws IOException { byte[] asciiArmoredBytes = longAsciiArmoredMessage.getBytes(StandardCharsets.UTF_8); - assertTrue(asciiArmoredBytes.length > OpenPgpInputStream.MAX_BUFFER_SIZE); + assertTrue(asciiArmoredBytes.length > OpenPGPAnimalSnifferInputStream.MAX_BUFFER_SIZE); ByteArrayInputStream asciiIn = new ByteArrayInputStream(asciiArmoredBytes); - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(asciiIn); + OpenPGPAnimalSnifferInputStream openPgpInputStream = new OpenPGPAnimalSnifferInputStream(asciiIn); assertTrue(openPgpInputStream.isAsciiArmored()); assertFalse(openPgpInputStream.isNonOpenPgp()); @@ -694,7 +694,7 @@ public class OpenPgpInputStreamTest { byte[] binaryBytes = binaryOut.toByteArray(); ByteArrayInputStream binaryIn = new ByteArrayInputStream(binaryBytes); - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(binaryIn); + OpenPGPAnimalSnifferInputStream openPgpInputStream = new OpenPGPAnimalSnifferInputStream(binaryIn); assertTrue(openPgpInputStream.isBinaryOpenPgp()); assertFalse(openPgpInputStream.isAsciiArmored()); @@ -714,7 +714,7 @@ public class OpenPgpInputStreamTest { byte[] binaryBytes = binaryOut.toByteArray(); ByteArrayInputStream binaryIn = new ByteArrayInputStream(binaryBytes); - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(binaryIn); + OpenPGPAnimalSnifferInputStream openPgpInputStream = new OpenPGPAnimalSnifferInputStream(binaryIn); assertTrue(openPgpInputStream.isBinaryOpenPgp()); assertFalse(openPgpInputStream.isAsciiArmored()); @@ -728,7 +728,7 @@ public class OpenPgpInputStreamTest { @Test public void emptyStreamTest() throws IOException { ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]); - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(in); + OpenPGPAnimalSnifferInputStream openPgpInputStream = new OpenPGPAnimalSnifferInputStream(in); assertFalse(openPgpInputStream.isBinaryOpenPgp()); assertFalse(openPgpInputStream.isAsciiArmored()); @@ -736,15 +736,16 @@ public class OpenPgpInputStreamTest { } @Test - public void testSignedMessageConsumption() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void testSignedMessageConsumption() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); ByteArrayInputStream plaintext = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Sigmund "); ByteArrayOutputStream signedOut = new ByteArrayOutputStream(); - EncryptionStream signer = PGPainless.encryptAndOrSign() + EncryptionStream signer = api.generateMessage() .onOutputStream(signedOut) - .withOptions(ProducerOptions.sign(new SigningOptions() + .withOptions(ProducerOptions.sign(SigningOptions.get(api) .addSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys)) .setAsciiArmor(false) .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); @@ -754,7 +755,7 @@ public class OpenPgpInputStreamTest { byte[] binary = signedOut.toByteArray(); - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(new ByteArrayInputStream(binary)); + OpenPGPAnimalSnifferInputStream openPgpIn = new OpenPGPAnimalSnifferInputStream(new ByteArrayInputStream(binary)); assertFalse(openPgpIn.isAsciiArmored()); assertTrue(openPgpIn.isLikelyOpenPgpMessage()); } 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 6102372a..39e4b7e6 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 @@ -15,8 +15,6 @@ 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; @@ -28,9 +26,9 @@ 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.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.JUtils; import org.junit.jupiter.api.Named; @@ -51,7 +49,6 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmoredInputStreamFactory; import org.pgpainless.util.Passphrase; -import org.pgpainless.util.Tuple; public class OpenPgpMessageInputStreamTest { @@ -165,6 +162,8 @@ public class OpenPgpMessageInputStreamTest { "=tkTV\n" + "-----END PGP MESSAGE-----"; + private static final PGPainless api = PGPainless.getInstance(); + public static void main(String[] args) throws Exception { // genLIT(); // genLIT_LIT(); @@ -240,20 +239,22 @@ public class OpenPgpMessageInputStreamTest { armorOut.close(); } - public static void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPainless.asciiArmor( - PGPainless.generateKeyRing().modernKeyRing("Alice "), - System.out); + public static void genKey() throws IOException { + OpenPGPKey key = api.generateKey() + .modernKeyRing("Alice "); + // CHECKSTYLE:OFF + System.out.println(key.toAsciiArmoredString()); + // CHECKSTYLE:ON } public static void genSIG_COMP_LIT() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + OpenPGPKey secretKeys = api.readKey().parseKey(KEY); ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); - EncryptionStream signer = PGPainless.encryptAndOrSign() + EncryptionStream signer = api.generateMessage() .onOutputStream(msgOut) .withOptions( ProducerOptions.sign( - SigningOptions.get() + SigningOptions.get(api) .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys) ).setAsciiArmor(false) ); @@ -279,9 +280,9 @@ public class OpenPgpMessageInputStreamTest { } public static void genSENC_LIT() throws PGPException, IOException { - EncryptionStream enc = PGPainless.encryptAndOrSign() + EncryptionStream enc = api.generateMessage() .onOutputStream(System.out) - .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get(api) .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))) .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); @@ -289,11 +290,11 @@ public class OpenPgpMessageInputStreamTest { } public static void genPENC_COMP_LIT() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); - EncryptionStream enc = PGPainless.encryptAndOrSign() + OpenPGPKey secretKeys = api.readKey().parseKey(KEY); + OpenPGPCertificate cert = secretKeys.toCertificate(); + EncryptionStream enc = api.generateMessage() .onOutputStream(System.out) - .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get(api) .addRecipient(cert)) .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); @@ -302,11 +303,11 @@ public class OpenPgpMessageInputStreamTest { } public static void genOPS_LIT_SIG() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + OpenPGPKey secretKeys = api.readKey().parseKey(KEY); - EncryptionStream enc = PGPainless.encryptAndOrSign() + EncryptionStream enc = api.generateMessage() .onOutputStream(System.out) - .withOptions(ProducerOptions.sign(SigningOptions.get() + .withOptions(ProducerOptions.sign(SigningOptions.get(api) .addSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys)) .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); @@ -314,7 +315,17 @@ public class OpenPgpMessageInputStreamTest { } interface Processor { - Tuple process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException; + Result process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException; + } + + static class Result { + private final String plaintext; + private final MessageMetadata metadata; + + Result(String plaintext, MessageMetadata metadata) { + this.plaintext = plaintext; + this.metadata = metadata; + } } private static Stream provideMessageProcessors() { @@ -326,12 +337,12 @@ public class OpenPgpMessageInputStreamTest { @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(); + void testProcessLIT(Processor processor) throws IOException, PGPException { + Result result = processor.process(LIT, ConsumerOptions.get()); + String plain = result.plaintext; assertEquals(PLAINTEXT, plain); - MessageMetadata metadata = result.getB(); + MessageMetadata metadata = result.metadata; assertNull(metadata.getCompressionAlgorithm()); assertNull(metadata.getEncryptionAlgorithm()); assertEquals("", metadata.getFilename()); @@ -343,19 +354,19 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process LIT LIT using {0}") @MethodSource("provideMessageProcessors") - public void testProcessLIT_LIT_fails(Processor processor) { + void testProcessLIT_LIT_fails(Processor processor) { assertThrows(MalformedOpenPgpMessageException.class, () -> processor.process(LIT_LIT, ConsumerOptions.get())); } @ParameterizedTest(name = "Process COMP(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessCOMP_LIT(Processor processor) + void testProcessCOMP_LIT(Processor processor) throws PGPException, IOException { - Tuple result = processor.process(COMP_LIT, ConsumerOptions.get()); - String plain = result.getA(); + Result result = processor.process(COMP_LIT, ConsumerOptions.get()); + String plain = result.plaintext; assertEquals(PLAINTEXT, plain); - MessageMetadata metadata = result.getB(); + MessageMetadata metadata = result.metadata; assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); @@ -363,19 +374,19 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process COMP using {0}") @MethodSource("provideMessageProcessors") - public void testProcessCOMP_fails(Processor processor) { + void testProcessCOMP_fails(Processor processor) { assertThrows(MalformedOpenPgpMessageException.class, () -> processor.process(COMP, ConsumerOptions.get())); } @ParameterizedTest(name = "Process COMP(COMP(LIT)) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessCOMP_COMP_LIT(Processor processor) + void testProcessCOMP_COMP_LIT(Processor processor) throws PGPException, IOException { - Tuple result = processor.process(COMP_COMP_LIT, ConsumerOptions.get()); - String plain = result.getA(); + Result result = processor.process(COMP_COMP_LIT, ConsumerOptions.get()); + String plain = result.plaintext; assertEquals(PLAINTEXT, plain); - MessageMetadata metadata = result.getB(); + MessageMetadata metadata = result.metadata; assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); Iterator compressionAlgorithms = metadata.getCompressionAlgorithms(); assertEquals(CompressionAlgorithm.ZIP, compressionAlgorithms.next()); @@ -388,16 +399,16 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessSIG_COMP_LIT(Processor processor) + void testProcessSIG_COMP_LIT(Processor processor) throws PGPException, IOException { - PGPPublicKeyRing cert = PGPainless.extractCertificate( - PGPainless.readKeyRing().secretKeyRing(KEY)); + OpenPGPKey key = api.readKey().parseKey(KEY); + OpenPGPCertificate cert = key.toCertificate(); - Tuple result = processor.process(SIG_COMP_LIT, ConsumerOptions.get() + Result result = processor.process(SIG_COMP_LIT, ConsumerOptions.get() .addVerificationCert(cert)); - String plain = result.getA(); + String plain = result.plaintext; assertEquals(PLAINTEXT, plain); - MessageMetadata metadata = result.getB(); + MessageMetadata metadata = result.metadata; assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); assertNull(metadata.getEncryptionAlgorithm()); assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); @@ -406,13 +417,13 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process SENC(LIT) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessSENC_LIT(Processor processor) + void testProcessSENC_LIT(Processor processor) throws PGPException, IOException { - Tuple result = processor.process(SENC_LIT, ConsumerOptions.get() + Result result = processor.process(SENC_LIT, ConsumerOptions.get() .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))); - String plain = result.getA(); + String plain = result.plaintext; assertEquals(PLAINTEXT, plain); - MessageMetadata metadata = result.getB(); + MessageMetadata metadata = result.metadata; assertNull(metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); @@ -421,14 +432,14 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") @MethodSource("provideMessageProcessors") - public void testProcessPENC_COMP_LIT(Processor processor) + void testProcessPENC_COMP_LIT(Processor processor) throws IOException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - Tuple result = processor.process(PENC_COMP_LIT, ConsumerOptions.get() + OpenPGPKey secretKeys = api.readKey().parseKey(KEY); + Result result = processor.process(PENC_COMP_LIT, ConsumerOptions.get(api) .addDecryptionKey(secretKeys)); - String plain = result.getA(); + String plain = result.plaintext; assertEquals(PLAINTEXT, plain); - MessageMetadata metadata = result.getB(); + MessageMetadata metadata = result.metadata; assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); @@ -437,14 +448,15 @@ public class OpenPgpMessageInputStreamTest { @ParameterizedTest(name = "Process OPS LIT SIG using {0}") @MethodSource("provideMessageProcessors") - public void testProcessOPS_LIT_SIG(Processor processor) + 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() + OpenPGPKey key = api.readKey().parseKey(KEY); + OpenPGPCertificate cert = key.toCertificate(); + Result result = processor.process(OPS_LIT_SIG, ConsumerOptions.get() .addVerificationCert(cert)); - String plain = result.getA(); + String plain = result.plaintext; assertEquals(PLAINTEXT, plain); - MessageMetadata metadata = result.getB(); + MessageMetadata metadata = result.metadata; assertNull(metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); @@ -536,7 +548,7 @@ public class OpenPgpMessageInputStreamTest { @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 { + void testProcessPENC_OPS_OPS_LIT_SIG_SIG(Processor processor) throws IOException, PGPException { String MSG = "-----BEGIN PGP MESSAGE-----\n" + "\n" + "wcDMA3wvqk35PDeyAQv/RhY9sgxMXj1UxumNMOeN+1+c5bB5e3jSrvA93L8yLFqB\n" + @@ -573,15 +585,15 @@ public class OpenPgpMessageInputStreamTest { "s7O7MH2b1YkDPsTDuLoDjBzDRoA+2vi034km9Qdcs3w8+vrydw4=\n" + "=mdYs\n" + "-----END PGP MESSAGE-----\n"; - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + OpenPGPKey secretKeys = api.readKey().parseKey(BOB_KEY); + OpenPGPCertificate certificate = secretKeys.toCertificate(); - Tuple result = processor.process(MSG, ConsumerOptions.get() + Result result = processor.process(MSG, ConsumerOptions.get(api) .addVerificationCert(certificate) .addDecryptionKey(secretKeys)); - String plain = result.getA(); + String plain = result.plaintext; assertEquals("encrypt ∘ sign ∘ sign", plain); - MessageMetadata metadata = result.getB(); + MessageMetadata metadata = result.metadata; assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); @@ -590,7 +602,7 @@ public class OpenPgpMessageInputStreamTest { @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 { + void testProcessOPS_OPS_OPS_LIT_SIG_SIG_SIG(Processor processor) throws IOException, PGPException { String MSG = "-----BEGIN PGP MESSAGE-----\n" + "\n" + "wcDMA3wvqk35PDeyAQwA0yaEgydkAMEfl7rDTYVGanLKiFiWIs34mkF+LB8qR5eY\n" + @@ -638,15 +650,15 @@ public class OpenPgpMessageInputStreamTest { "x12WVuyITVU3fCfHp6/0A6wPtJezCvoodqPlw/3fd5eSVYzb5C3v564uhz4=\n" + "=JP9T\n" + "-----END PGP MESSAGE-----"; - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + OpenPGPKey secretKeys = api.readKey().parseKey(BOB_KEY); + OpenPGPCertificate certificate = secretKeys.toCertificate(); - Tuple result = processor.process(MSG, ConsumerOptions.get() + Result result = processor.process(MSG, ConsumerOptions.get(api) .addVerificationCert(certificate) .addDecryptionKey(secretKeys)); - String plain = result.getA(); + String plain = result.plaintext; assertEquals("encrypt ∘ sign ∘ sign ∘ sign", plain); - MessageMetadata metadata = result.getB(); + MessageMetadata metadata = result.metadata; assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertNull(metadata.getCompressionAlgorithm()); assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); @@ -654,7 +666,7 @@ public class OpenPgpMessageInputStreamTest { } @Test - public void readAfterCloseTest() throws PGPException, IOException { + void readAfterCloseTest() throws IOException { OpenPgpMessageInputStream pgpIn = get(SENC_LIT, ConsumerOptions.get() .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))); Streams.drain(pgpIn); // read all @@ -669,18 +681,18 @@ public class OpenPgpMessageInputStreamTest { pgpIn.getMetadata(); } - private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) - throws PGPException, IOException { + private static Result processReadBuffered(String armoredMessage, ConsumerOptions options) + throws IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(in, out); in.close(); MessageMetadata metadata = in.getMetadata(); - return new Tuple<>(out.toString(), metadata); + return new Result(out.toString(), metadata); } - private static Tuple processReadSequential(String armoredMessage, ConsumerOptions options) - throws PGPException, IOException { + private static Result processReadSequential(String armoredMessage, ConsumerOptions options) + throws IOException { OpenPgpMessageInputStream in = get(armoredMessage, options); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -691,14 +703,14 @@ public class OpenPgpMessageInputStreamTest { in.close(); MessageMetadata metadata = in.getMetadata(); - return new Tuple<>(out.toString(), metadata); + return new Result(out.toString(), metadata); } private static OpenPgpMessageInputStream get(String armored, ConsumerOptions options) - throws IOException, PGPException { + throws IOException { ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); - OpenPgpMessageInputStream pgpIn = OpenPgpMessageInputStream.create(armorIn, options); + OpenPgpMessageInputStream pgpIn = OpenPgpMessageInputStream.create(armorIn, options, PGPainless.getInstance()); return pgpIn; } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java index 8489da9a..63f2ae76 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java @@ -12,6 +12,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; @@ -23,6 +24,8 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; import org.pgpainless.util.Passphrase; +import javax.annotation.Nonnull; + public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { private static PGPSecretKeyRing k1; @@ -120,21 +123,21 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { public void missingPassphraseFirst() throws PGPException, IOException { SecretKeyRingProtector protector1 = new CachingSecretKeyRingProtector(new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(@Nonnull KeyIdentifier keyIdentifier) { fail("Although the first PKESK is for k1, we should have skipped it and tried k2 first, which has passphrase available."); return null; } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(@Nonnull KeyIdentifier keyIdentifier) { return false; } }); SecretKeyRingProtector protector2 = SecretKeyRingProtector.unlockEachKeyWith(p2, k2); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(ENCRYPTED_FOR_K1_K2.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKey(k1, protector1) .addDecryptionKey(k2, protector2)); @@ -150,20 +153,20 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { SecretKeyRingProtector protector1 = SecretKeyRingProtector.unlockEachKeyWith(p1, k1); SecretKeyRingProtector protector2 = new CachingSecretKeyRingProtector(new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(@Nonnull KeyIdentifier keyIdentifier) { fail("This callback should not get called, since the first PKESK is for k1, which has a passphrase available."); return null; } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(@Nonnull KeyIdentifier keyIdentifier) { return false; } }); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(ENCRYPTED_FOR_K1_K2.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKey(k1, protector1) .addDecryptionKey(k2, protector2)); @@ -178,21 +181,21 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { public void messagePassphraseFirst() throws PGPException, IOException { SecretKeyPassphraseProvider provider = new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(@Nonnull KeyIdentifier keyIdentifier) { fail("Since we provide a decryption passphrase, we should not try to decrypt any key."); return null; } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(@Nonnull KeyIdentifier keyIdentifier) { return false; } }; SecretKeyRingProtector protector = new CachingSecretKeyRingProtector(provider); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(ENCRYPTED_FOR_K2_PASS_K1.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addMessagePassphrase(PASSPHRASE) .addDecryptionKey(k1, protector) .addDecryptionKey(k2, protector)); 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 f06f0233..7889cb2d 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,7 +14,6 @@ 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; @@ -178,9 +177,9 @@ public class PreventDecryptionUsingNonEncryptionKeyTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_CAPABLE_KEY); ByteArrayInputStream msgIn = new ByteArrayInputStream(MSG.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(msgIn) - .withOptions(new ConsumerOptions().addDecryptionKey(secretKeys)); + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeys)); Streams.drain(decryptionStream); decryptionStream.close(); @@ -194,9 +193,11 @@ public class PreventDecryptionUsingNonEncryptionKeyTest { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_INCAPABLE_KEY); ByteArrayInputStream msgIn = new ByteArrayInputStream(MSG.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(msgIn) - .withOptions(new ConsumerOptions().addDecryptionKey(secretKeys)); + .withOptions(ConsumerOptions.get() + .setAllowDecryptionWithMissingKeyFlags() + .addDecryptionKey(secretKeys)); Streams.drain(decryptionStream); decryptionStream.close(); @@ -206,15 +207,30 @@ public class PreventDecryptionUsingNonEncryptionKeyTest { } @Test - @Disabled public void nonEncryptionKeyCannotDecrypt() throws IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_INCAPABLE_KEY); ByteArrayInputStream msgIn = new ByteArrayInputStream(MSG.getBytes(StandardCharsets.UTF_8)); assertThrows(MissingDecryptionMethodException.class, () -> - PGPainless.decryptAndOrVerify() + PGPainless.getInstance().processMessage() + .onInputStream(msgIn) + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeys))); + } + + @Test + public void nonEncryptionKeyCanDecryptIfAllowed() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_INCAPABLE_KEY); + + ByteArrayInputStream msgIn = new ByteArrayInputStream(MSG.getBytes(StandardCharsets.UTF_8)); + + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(msgIn) - .withOptions(new ConsumerOptions().addDecryptionKey(secretKeys))); + .withOptions(ConsumerOptions.get() + .setAllowDecryptionWithMissingKeyFlags() + .addDecryptionKey(secretKeys)); + + byte[] decrypted = Streams.readAll(decryptionStream); + decryptionStream.close(); } } 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 52c5a906..d34258bf 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 @@ -144,9 +144,9 @@ public class RecursionDepthTest { assertThrows(MalformedOpenPgpMessageException.class, () -> { - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions().addDecryptionKey(secretKey)); + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKey)); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); Streams.pipeAll(decryptionStream, outputStream); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryptionTest.java index 1201ef69..05af3233 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RejectWeakSymmetricAlgorithmDuringDecryptionTest.java @@ -138,9 +138,9 @@ public class RejectWeakSymmetricAlgorithmDuringDecryptionTest { InputStream messageIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); assertThrows(UnacceptableAlgorithmException.class, () -> - PGPainless.decryptAndOrVerify() + PGPainless.getInstance().processMessage() .onInputStream(messageIn) - .withOptions(new ConsumerOptions().addDecryptionKey(secretKeys)) + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeys)) ); } @@ -166,9 +166,9 @@ public class RejectWeakSymmetricAlgorithmDuringDecryptionTest { InputStream messageIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); assertThrows(UnacceptableAlgorithmException.class, () -> - PGPainless.decryptAndOrVerify() + PGPainless.getInstance().processMessage() .onInputStream(messageIn) - .withOptions(new ConsumerOptions().addDecryptionKey(secretKeys)) + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeys)) ); } @@ -193,8 +193,8 @@ public class RejectWeakSymmetricAlgorithmDuringDecryptionTest { InputStream messageIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); assertThrows(UnacceptableAlgorithmException.class, () -> - PGPainless.decryptAndOrVerify().onInputStream(messageIn) - .withOptions(new ConsumerOptions().addDecryptionKey(secretKeys)) + PGPainless.getInstance().processMessage().onInputStream(messageIn) + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeys)) ); } @@ -218,8 +218,8 @@ public class RejectWeakSymmetricAlgorithmDuringDecryptionTest { "-----END PGP ARMORED FILE-----\n"; InputStream messageIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); - PGPainless.decryptAndOrVerify().onInputStream(messageIn) - .withOptions(new ConsumerOptions().addDecryptionKey(secretKeys)); + PGPainless.getInstance().processMessage().onInputStream(messageIn) + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeys)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java index 9f85b241..f7dda4a7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java @@ -30,8 +30,8 @@ public class SignedMessageVerificationWithoutCertIsStillSignedTest { @Test public void verifyMissingVerificationCertOptionStillResultsInMessageIsSigned() throws IOException, PGPException { - ConsumerOptions withoutVerificationCert = new ConsumerOptions(); - DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() + ConsumerOptions withoutVerificationCert = ConsumerOptions.get(); + DecryptionStream verificationStream = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) .withOptions(withoutVerificationCert); 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 b28af822..74e73e03 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 @@ -44,7 +44,7 @@ public class TestDecryptionOfMessageWithoutESKUsingSessionKey { @Test public void decryptMessageWithSKESK() throws PGPException, IOException { ByteArrayInputStream in = new ByteArrayInputStream(encryptedMessageWithSKESK.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) .withOptions(ConsumerOptions.get() .setSessionKey(sessionKey)); @@ -57,7 +57,7 @@ public class TestDecryptionOfMessageWithoutESKUsingSessionKey { @Test public void decryptMessageWithoutSKESK() throws PGPException, IOException { ByteArrayInputStream in = new ByteArrayInputStream(encryptedMessageWithoutESK.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) .withOptions(ConsumerOptions.get() .setSessionKey(sessionKey)); 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 640025b1..125219b5 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 @@ -5,8 +5,8 @@ package org.pgpainless.decryption_verification; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -19,8 +19,6 @@ import org.pgpainless.exception.MissingDecryptionMethodException; 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; @@ -28,26 +26,28 @@ public class TryDecryptWithUnavailableGnuDummyKeyTest { @Test public void testAttemptToDecryptWithRemovedPrivateKeysThrows() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Hardy Hardware "); - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + OpenPGPCertificate certificate = secretKeys.toCertificate(); ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(ciphertextOut) .withOptions( - ProducerOptions.encrypt(EncryptionOptions.get().addRecipient(certificate))); + ProducerOptions.encrypt(EncryptionOptions.get(api).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()); + OpenPGPKey removedKeys = api.toKey( + GnuPGDummyKeyUtil.modify(secretKeys) + .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any())); ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray()); - assertThrows(MissingDecryptionMethodException.class, () -> PGPainless.decryptAndOrVerify() + assertThrows(MissingDecryptionMethodException.class, () -> api.processMessage() .onInputStream(ciphertextIn) - .withOptions(ConsumerOptions.get().addDecryptionKey(removedKeys))); + .withOptions(ConsumerOptions.get(api).addDecryptionKey(removedKeys))); } } 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 f8ce9e29..7981a4ae 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 @@ -392,7 +392,7 @@ public class UnsupportedPacketVersionsTest { 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() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(inputStream) .withOptions(ConsumerOptions.get() .addDecryptionKey(key) 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 e1406f87..77b9ac68 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 @@ -54,10 +54,10 @@ public class VerifyDetachedSignatureTest { "-----END PGP PUBLIC KEY BLOCK-----\n"; - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(signedContent.getBytes(StandardCharsets.UTF_8))) .withOptions( - new ConsumerOptions() + ConsumerOptions.get() .addVerificationOfDetachedSignatures(new ByteArrayInputStream(signature.getBytes(StandardCharsets.UTF_8))) .addVerificationCerts(PGPainless.readKeyRing().keyRingCollection(pubkey, true).getPgpPublicKeyRingCollection()) .setMultiPassStrategy(new InMemoryMultiPassStrategy()) @@ -129,10 +129,10 @@ public class VerifyDetachedSignatureTest { "=pXF6\n" + "-----END PGP PUBLIC KEY BLOCK-----\n"; - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(signedContent.getBytes(StandardCharsets.UTF_8))) .withOptions( - new ConsumerOptions() + ConsumerOptions.get() .addVerificationOfDetachedSignatures(new ByteArrayInputStream(signature.getBytes(StandardCharsets.UTF_8))) .addVerificationCerts(PGPainless.readKeyRing().keyRingCollection(pubkey, true).getPgpPublicKeyRingCollection()) .setMultiPassStrategy(new InMemoryMultiPassStrategy()) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java index 069a5f2d..a88bf3e6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java @@ -62,9 +62,9 @@ public class VerifyNotBeforeNotAfterTest { @Test public void noConstraintsVerifyInlineSig() throws PGPException, IOException { - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get() .addVerificationCert(certificate); - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); @@ -74,10 +74,10 @@ public class VerifyNotBeforeNotAfterTest { @Test public void noConstraintsVerifyDetachedSig() throws PGPException, IOException { - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get() .addVerificationCert(certificate) .addVerificationOfDetachedSignatures(new ByteArrayInputStream(detachedSignature)); - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); @@ -87,10 +87,10 @@ public class VerifyNotBeforeNotAfterTest { @Test public void notBeforeT1DoesNotRejectInlineSigMadeAtT1() throws PGPException, IOException { - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get() .verifyNotBefore(T1) .addVerificationCert(certificate); - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); MessageMetadata metadata = processSignedData(verifier); @@ -99,11 +99,11 @@ public class VerifyNotBeforeNotAfterTest { @Test public void notBeforeT1DoesNotRejectDetachedSigMadeAtT1() throws PGPException, IOException { - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get() .verifyNotBefore(T1) .addVerificationCert(certificate) .addVerificationOfDetachedSignatures(new ByteArrayInputStream(detachedSignature)); - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); MessageMetadata metadata = processSignedData(verifier); @@ -112,10 +112,10 @@ public class VerifyNotBeforeNotAfterTest { @Test public void verifyNotBeforeT2DoesRejectInlineSignatureMadeAtT1() throws PGPException, IOException { - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get() .verifyNotBefore(T2) .addVerificationCert(certificate); - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); MessageMetadata metadata = processSignedData(verifier); @@ -124,11 +124,11 @@ public class VerifyNotBeforeNotAfterTest { @Test public void verifyNotBeforeT2DoesRejectDetachedSigMadeAtT1() throws PGPException, IOException { - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get() .verifyNotBefore(T2) .addVerificationCert(certificate) .addVerificationOfDetachedSignatures(new ByteArrayInputStream(detachedSignature)); - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); MessageMetadata metadata = processSignedData(verifier); @@ -137,10 +137,10 @@ public class VerifyNotBeforeNotAfterTest { @Test public void verifyNotAfterT1DoesNotRejectInlineSigMadeAtT1() throws PGPException, IOException { - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get() .verifyNotAfter(T1) .addVerificationCert(certificate); - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); MessageMetadata metadata = processSignedData(verifier); @@ -149,11 +149,11 @@ public class VerifyNotBeforeNotAfterTest { @Test public void verifyNotAfterT1DoesRejectDetachedSigMadeAtT1() throws PGPException, IOException { - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get() .verifyNotAfter(T1) .addVerificationCert(certificate) .addVerificationOfDetachedSignatures(new ByteArrayInputStream(detachedSignature)); - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); MessageMetadata metadata = processSignedData(verifier); @@ -162,10 +162,10 @@ public class VerifyNotBeforeNotAfterTest { @Test public void verifyNotAfterT0DoesRejectInlineSigMadeAtT1() throws PGPException, IOException { - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get() .verifyNotAfter(T0) .addVerificationCert(certificate); - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); MessageMetadata metadata = processSignedData(verifier); @@ -174,11 +174,11 @@ public class VerifyNotBeforeNotAfterTest { @Test public void verifyNotAfterT0DoesRejectDetachedSigMadeAtT1() throws PGPException, IOException { - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get() .verifyNotAfter(T0) .addVerificationCert(certificate) .addVerificationOfDetachedSignatures(new ByteArrayInputStream(detachedSignature)); - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); MessageMetadata metadata = processSignedData(verifier); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifySignatureByCertificationKeyFailsTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifySignatureByCertificationKeyFailsTest.java index 3d2ea092..f2250455 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifySignatureByCertificationKeyFailsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifySignatureByCertificationKeyFailsTest.java @@ -213,7 +213,7 @@ public class VerifySignatureByCertificationKeyFailsTest { public void testSignatureByNonSigningPrimaryKeyIsRejected() throws Exception { PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY); - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(DATA)) .withOptions(ConsumerOptions.get() .addVerificationCert(PGPainless.extractCertificate(key)) 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 6b9d9cab..a6240072 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 @@ -9,6 +9,7 @@ import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPV3SignatureGenerator; +import org.bouncycastle.openpgp.api.OpenPGPImplementation; import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; @@ -16,7 +17,6 @@ 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; @@ -35,11 +35,11 @@ class VerifyVersion3SignaturePacketTest { void verifyDetachedVersion3Signature() throws PGPException, IOException { PGPSignature version3Signature = generateV3Signature(); - ConsumerOptions options = new ConsumerOptions() + ConsumerOptions options = ConsumerOptions.get() .addVerificationCert(TestKeys.getEmilPublicKeyRing()) .addVerificationOfDetachedSignatures(new ByteArrayInputStream(version3Signature.getEncoded())); - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(DATA)) .withOptions(options); @@ -48,7 +48,7 @@ class VerifyVersion3SignaturePacketTest { } private static PGPSignature generateV3Signature() throws IOException, PGPException { - PGPContentSignerBuilder builder = ImplementationFactory.getInstance().getPGPContentSignerBuilder(PublicKeyAlgorithm.ECDSA, HashAlgorithm.SHA512); + PGPContentSignerBuilder builder = OpenPGPImplementation.getInstance().pgpContentSignerBuilder(PublicKeyAlgorithm.ECDSA.getAlgorithmId(), HashAlgorithm.SHA512.getAlgorithmId()); PGPV3SignatureGenerator signatureGenerator = new PGPV3SignatureGenerator(builder); PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithCertificateAuthorityTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithCertificateAuthorityTest.java new file mode 100644 index 00000000..6dfdf7db --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithCertificateAuthorityTest.java @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.bcpg.KeyIdentifier; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.util.io.Streams; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.pgpainless.PGPainless; +import org.pgpainless.authentication.CertificateAuthenticity; +import org.pgpainless.authentication.CertificateAuthority; +import org.pgpainless.authentication.CertificationChain; +import org.pgpainless.authentication.ChainLink; +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.TestAllImplementations; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class VerifyWithCertificateAuthorityTest { + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testVerifySignatureFromAuthenticatedCert() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + + OpenPGPKey aliceKey = api.generateKey().modernKeyRing("Alice "); + OpenPGPCertificate aliceCert = aliceKey.toCertificate(); + + SimpleCertificateAuthority authority = new SimpleCertificateAuthority(); + authority.addDirectlyAuthenticatedCert(aliceCert, 120); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + EncryptionStream eOut = api.generateMessage() + .onOutputStream(bOut) + .withOptions(ProducerOptions.signAndEncrypt( + EncryptionOptions.encryptCommunications() + .addAuthenticatableRecipients("Alice ", false, authority), + SigningOptions.get().addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), aliceKey) + )); + + eOut.write("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + eOut.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + DecryptionStream dIn = api.processMessage() + .onInputStream(bIn) + .withOptions(ConsumerOptions.get() + .addVerificationCert(aliceCert) + .addDecryptionKey(aliceKey)); + Streams.drain(dIn); + dIn.close(); + + MessageMetadata metadata = dIn.getMetadata(); + assertTrue(metadata.isEncryptedFor(aliceCert)); + + assertTrue(metadata.isAuthenticatablySignedBy("Alice ", false, authority)); + assertTrue(metadata.isAuthenticatablySignedBy("alice@pgpainless.org", true, authority)); + assertFalse(metadata.isAuthenticatablySignedBy("mallory@pgpainless.org", true, authority)); + } + + public static class SimpleCertificateAuthority implements CertificateAuthority { + + Map directlyAuthenticatedCerts = new HashMap<>(); + + public void addDirectlyAuthenticatedCert(OpenPGPCertificate cert, int trustAmount) { + directlyAuthenticatedCerts.put(cert, trustAmount); + } + + @Override + public CertificateAuthenticity authenticateBinding( + @NotNull KeyIdentifier certIdentifier, + @NotNull CharSequence userId, + boolean email, + @NotNull Date referenceTime, + int targetAmount) { + Optional opt = directlyAuthenticatedCerts.keySet().stream() + .filter(it -> it.getKey(certIdentifier) != null) + .findFirst(); + if (opt.isEmpty()) { + return null; + } + + OpenPGPCertificate cert = opt.get(); + Optional uid; + if (email) { + uid = cert.getAllUserIds().stream().filter(it -> it.getUserId().contains("<" + userId + ">")) + .findFirst(); + } else { + uid = cert.getAllUserIds().stream().filter(it -> it.getUserId().contentEquals(userId)) + .findFirst(); + } + return uid.map(openPGPUserId -> authenticatedUserId(openPGPUserId, targetAmount)).orElse(null); + } + + @NotNull + @Override + public List lookupByUserId( + @NotNull CharSequence userId, + boolean email, + @NotNull Date referenceTime, + int targetAmount) { + List matches = new ArrayList<>(); + + for (OpenPGPCertificate cert : directlyAuthenticatedCerts.keySet()) { + cert.getAllUserIds() + .stream().filter(it -> { + if (email) return it.getUserId().contains("<" + userId + ">"); + else return it.getUserId().contentEquals(userId); + }).forEach(it -> { + matches.add(authenticatedUserId(it, targetAmount)); + }); + } + return matches; + } + + @NotNull + @Override + public List identifyByFingerprint( + @NotNull KeyIdentifier certIdentifier, + @NotNull Date referenceTime, + int targetAmount) { + List matches = new ArrayList<>(); + + directlyAuthenticatedCerts.keySet() + .stream().filter(it -> it.getKey(certIdentifier) != null) + .forEach(it -> { + for (OpenPGPCertificate.OpenPGPUserId userId : it.getAllUserIds()) { + matches.add(authenticatedUserId(userId, targetAmount)); + } + }); + + return matches; + } + + private CertificateAuthenticity authenticatedUserId(OpenPGPCertificate.OpenPGPUserId userId, int targetAmount) { + OpenPGPCertificate cert = userId.getCertificate(); + int certTrust = directlyAuthenticatedCerts.get(cert); + Map chains = new HashMap<>(); + chains.put(new CertificationChain(certTrust, Collections.singletonList(new ChainLink(cert))), certTrust); + return new CertificateAuthenticity(userId.getUserId(), cert, chains, targetAmount); + } + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java index 0d58e7dd..65e8ae08 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java @@ -12,58 +12,62 @@ 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.bcpg.KeyIdentifier; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPKeyMaterialProvider; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.algorithm.OpenPGPKeyVersion; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.key.TestKeys; -import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.util.KeyRingUtils; + +import javax.annotation.Nonnull; /** - * Test functionality of the {@link MissingPublicKeyCallback} which is called when during signature verification, - * a signature is encountered which was made by a key that was not provided in - * {@link ConsumerOptions#addVerificationCert(PGPPublicKeyRing)}. + * Test functionality of the {@link org.bouncycastle.openpgp.api.OpenPGPKeyMaterialProvider.OpenPGPCertificateProvider} + * which is called when during signature verification, a signature is encountered which was made by a key that was + * not provided in {@link ConsumerOptions#addVerificationCert(PGPPublicKeyRing)}. */ public class VerifyWithMissingPublicKeyCallbackTest { @Test - public void testMissingPublicKeyCallback() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing signingSecKeys = PGPainless.generateKeyRing().modernKeyRing("alice"); - PGPPublicKey signingKey = new KeyRingInfo(signingSecKeys).getSigningSubkeys().get(0); - PGPPublicKeyRing signingPubKeys = KeyRingUtils.publicKeyRingFrom(signingSecKeys); + public void testMissingPublicKeyCallback() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + + OpenPGPKey signingSecKeys = api.generateKey(OpenPGPKeyVersion.v4).modernKeyRing("alice"); + OpenPGPCertificate.OpenPGPComponentKey signingKey = + signingSecKeys.getSigningKeys().get(0); + OpenPGPCertificate signingPubKeys = signingSecKeys.toCertificate(); PGPPublicKeyRing unrelatedKeys = TestKeys.getJulietPublicKeyRing(); String msg = "Arguing that you don't care about the right to privacy because you have nothing to hide" + "is no different than saying you don't care about free speech because you have nothing to say."; ByteArrayOutputStream signOut = new ByteArrayOutputStream(); - EncryptionStream signingStream = PGPainless.encryptAndOrSign().onOutputStream(signOut) - .withOptions(ProducerOptions.sign(new SigningOptions().addInlineSignature( + EncryptionStream signingStream = api.generateMessage().onOutputStream(signOut) + .withOptions(ProducerOptions.sign(SigningOptions.get().addInlineSignature( SecretKeyRingProtector.unprotectedKeys(), - signingSecKeys, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT + signingSecKeys.getPGPSecretKeyRing(), DocumentSignatureType.CANONICAL_TEXT_DOCUMENT ))); Streams.pipeAll(new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)), signingStream); signingStream.close(); - DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() + DecryptionStream verificationStream = api.processMessage() .onInputStream(new ByteArrayInputStream(signOut.toByteArray())) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addVerificationCert(unrelatedKeys) - .setMissingCertificateCallback(new MissingPublicKeyCallback() { + .setMissingCertificateCallback(new OpenPGPKeyMaterialProvider.OpenPGPCertificateProvider() { @Override - public PGPPublicKeyRing onMissingPublicKeyEncountered(long keyId) { - assertEquals(signingKey.getKeyID(), keyId, "Signing key-ID mismatch."); + public OpenPGPCertificate provide(@Nonnull KeyIdentifier keyIdentifier) { + assertEquals(signingKey.getKeyIdentifier(), keyIdentifier, "Signing key-ID mismatch."); return signingPubKeys; } })); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java deleted file mode 100644 index f3336373..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java +++ /dev/null @@ -1,174 +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.assertFalse; -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.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.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.policy.Policy; -import org.pgpainless.util.Passphrase; - -public class WrongSignerUserIdTest { - - 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" + - " 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 "; - - @Test - public void verificationSucceedsWithDisabledCheck() throws PGPException, IOException { - executeTest(false, true); - } - - @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(); - MessageMetadata metadata = decryptionStream.getMetadata(); - - if (expectSuccessfulVerification) { - assertTrue(metadata.isVerifiedSigned()); - } else { - assertFalse(metadata.isVerifiedSigned()); - } - - } - - private String generateTestMessage(PGPSecretKeyRing secretKeys) throws PGPException, IOException { - 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(); - - return cipherText.toString(); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java index 1346e6b7..017125ec 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/BcHashContextSignerTest.java @@ -11,15 +11,14 @@ 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.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPSignature; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -61,23 +60,25 @@ public class BcHashContextSignerTest { @Test public void signContextWithEdDSAKeys() throws PGPException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + OpenPGPKey secretKeys = PGPainless.getInstance().readKey().parseKey(KEY); signWithKeys(secretKeys); } @Test - public void signContextWithRSAKeys() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleRsaKeyRing("Sigfried", RsaLength._3072); + public void signContextWithRSAKeys() throws PGPException, NoSuchAlgorithmException, IOException { + OpenPGPKey secretKeys = PGPainless.getInstance().generateKey() + .simpleRsaKeyRing("Sigfried", RsaLength._3072); signWithKeys(secretKeys); } @Test - public void signContextWithEcKeys() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("Sigfried"); + public void signContextWithEcKeys() throws PGPException, NoSuchAlgorithmException, IOException { + OpenPGPKey secretKeys = PGPainless.getInstance().generateKey() + .simpleEcKeyRing("Sigfried"); signWithKeys(secretKeys); } - private void signWithKeys(PGPSecretKeyRing secretKeys) throws PGPException, NoSuchAlgorithmException, IOException { + private void signWithKeys(OpenPGPKey secretKeys) throws PGPException, NoSuchAlgorithmException, IOException { for (HashAlgorithm hashAlgorithm : new HashAlgorithm[] { HashAlgorithm.SHA256, HashAlgorithm.SHA384, HashAlgorithm.SHA512 }) { @@ -85,19 +86,19 @@ public class BcHashContextSignerTest { } } - private void signFromContext(PGPSecretKeyRing secretKeys, HashAlgorithm hashAlgorithm) + private void signFromContext(OpenPGPKey secretKeys, HashAlgorithm hashAlgorithm) throws PGPException, NoSuchAlgorithmException, IOException { - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + OpenPGPCertificate certificate = secretKeys.toCertificate(); byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); ByteArrayInputStream messageIn = new ByteArrayInputStream(messageBytes); - PGPSignature signature = signMessage(messageBytes, hashAlgorithm, secretKeys); - assertEquals(hashAlgorithm.getAlgorithmId(), signature.getHashAlgorithm()); + OpenPGPSignature.OpenPGPDocumentSignature signature = signMessage(messageBytes, hashAlgorithm, secretKeys); + assertEquals(hashAlgorithm.getAlgorithmId(), signature.getSignature().getHashAlgorithm()); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(messageIn) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addVerificationCert(certificate) .addVerificationOfDetachedSignature(signature)); @@ -109,8 +110,8 @@ public class BcHashContextSignerTest { assertTrue(metadata.isVerifiedSigned()); } - private PGPSignature signMessage(byte[] message, HashAlgorithm hashAlgorithm, PGPSecretKeyRing secretKeys) - throws NoSuchAlgorithmException, PGPException { + private OpenPGPSignature.OpenPGPDocumentSignature signMessage(byte[] message, HashAlgorithm hashAlgorithm, OpenPGPKey secretKeys) + throws NoSuchAlgorithmException { // Prepare the hash context // This would be done by the caller application MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm.getAlgorithmName(), new BouncyCastleProvider()); 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 3e620386..8de0d9ad 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 @@ -7,6 +7,7 @@ package org.pgpainless.encryption_signing; 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; @@ -14,22 +15,24 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.Charset; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; +import java.nio.charset.StandardCharsets; import java.util.Set; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.AEADAlgorithm; +import org.pgpainless.algorithm.AlgorithmSuite; import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.algorithm.Feature; import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.OpenPGPKeyVersion; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; @@ -39,14 +42,13 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.elgamal.ElGamal; -import org.pgpainless.key.generation.type.elgamal.ElGamalLength; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.rsa.RsaLength; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.policy.Policy; import org.pgpainless.util.ArmoredOutputStreamFactory; +import org.pgpainless.util.Passphrase; import org.pgpainless.util.TestAllImplementations; public class EncryptDecryptTest { @@ -63,85 +65,65 @@ public class EncryptDecryptTest { "Unfold the imagined happiness that both\n" + "Receive in either by this dear encounter."; - @BeforeEach - public void setDefaultPolicy() { - PGPainless.getPolicy().setSymmetricKeyEncryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022()); - PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022()); - } - - @TestTemplate - @ExtendWith(TestAllImplementations.class) - public void freshKeysRsaToElGamalTest() - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, IOException { - PGPSecretKeyRing sender = PGPainless.generateKeyRing().simpleRsaKeyRing("romeo@montague.lit", RsaLength._3072); - PGPSecretKeyRing recipient = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder( - KeyType.RSA(RsaLength._4096), - KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder( - ElGamal.withLength(ElGamalLength._3072), - KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) - .addUserId("juliet@capulet.lit").build(); - - encryptDecryptForSecretKeyRings(sender, recipient); - } - @TestTemplate @ExtendWith(TestAllImplementations.class) public void freshKeysRsaToRsaTest() - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, IOException { - PGPSecretKeyRing sender = PGPainless.generateKeyRing().simpleRsaKeyRing("romeo@montague.lit", RsaLength._3072); - PGPSecretKeyRing recipient = PGPainless.generateKeyRing().simpleRsaKeyRing("juliet@capulet.lit", RsaLength._3072); + throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey sender = api.generateKey().simpleRsaKeyRing("romeo@montague.lit", RsaLength._3072); + OpenPGPKey recipient = api.generateKey().simpleRsaKeyRing("juliet@capulet.lit", RsaLength._3072); - encryptDecryptForSecretKeyRings(sender, recipient); + encryptDecryptForSecretKeyRings(api, sender, recipient); } @TestTemplate @ExtendWith(TestAllImplementations.class) public void freshKeysEcToEcTest() - throws IOException, PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { - PGPSecretKeyRing sender = PGPainless.generateKeyRing().simpleEcKeyRing("romeo@montague.lit"); - PGPSecretKeyRing recipient = PGPainless.generateKeyRing().simpleEcKeyRing("juliet@capulet.lit"); + throws IOException, PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey sender = api.generateKey().simpleEcKeyRing("romeo@montague.lit"); + OpenPGPKey recipient = api.generateKey().simpleEcKeyRing("juliet@capulet.lit"); - encryptDecryptForSecretKeyRings(sender, recipient); + encryptDecryptForSecretKeyRings(api, sender, recipient); } @TestTemplate @ExtendWith(TestAllImplementations.class) public void freshKeysEcToRsaTest() - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, IOException { - PGPSecretKeyRing sender = PGPainless.generateKeyRing().simpleEcKeyRing("romeo@montague.lit"); - PGPSecretKeyRing recipient = PGPainless.generateKeyRing().simpleRsaKeyRing("juliet@capulet.lit", RsaLength._3072); + throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey sender = api.generateKey().simpleEcKeyRing("romeo@montague.lit"); + OpenPGPKey recipient = api.generateKey().simpleRsaKeyRing("juliet@capulet.lit", RsaLength._3072); - encryptDecryptForSecretKeyRings(sender, recipient); + encryptDecryptForSecretKeyRings(api, sender, recipient); } @TestTemplate @ExtendWith(TestAllImplementations.class) public void freshKeysRsaToEcTest() - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, IOException { - PGPSecretKeyRing sender = PGPainless.generateKeyRing().simpleRsaKeyRing("romeo@montague.lit", RsaLength._3072); - PGPSecretKeyRing recipient = PGPainless.generateKeyRing().simpleEcKeyRing("juliet@capulet.lit"); + throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey sender = api.generateKey().simpleRsaKeyRing("romeo@montague.lit", RsaLength._3072); + OpenPGPKey recipient = api.generateKey().simpleEcKeyRing("juliet@capulet.lit"); - encryptDecryptForSecretKeyRings(sender, recipient); + encryptDecryptForSecretKeyRings(api, sender, recipient); } @TestTemplate @ExtendWith(TestAllImplementations.class) public void existingRsaKeysTest() throws IOException, PGPException { - PGPSecretKeyRing sender = TestKeys.getJulietSecretKeyRing(); - PGPSecretKeyRing recipient = TestKeys.getRomeoSecretKeyRing(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey sender = TestKeys.getJulietKey(); + OpenPGPKey recipient = TestKeys.getRomeoKey(); - encryptDecryptForSecretKeyRings(sender, recipient); + encryptDecryptForSecretKeyRings(api, sender, recipient); } - private void encryptDecryptForSecretKeyRings(PGPSecretKeyRing senderSec, PGPSecretKeyRing recipientSec) + private void encryptDecryptForSecretKeyRings(PGPainless api, OpenPGPKey senderSec, OpenPGPKey recipientSec) throws PGPException, IOException { - PGPPublicKeyRing recipientPub = KeyRingUtils.publicKeyRingFrom(recipientSec); - PGPPublicKeyRing senderPub = KeyRingUtils.publicKeyRingFrom(senderSec); + OpenPGPCertificate recipientPub = recipientSec.toCertificate(); + OpenPGPCertificate senderPub = senderSec.toCertificate(); SecretKeyRingProtector keyDecryptor = new UnprotectedKeysProtector(); @@ -149,11 +131,13 @@ public class EncryptDecryptTest { ByteArrayOutputStream envelope = new ByteArrayOutputStream(); - EncryptionStream encryptor = PGPainless.encryptAndOrSign() + EncryptionStream encryptor = api.generateMessage() .onOutputStream(envelope) .withOptions(ProducerOptions.signAndEncrypt( - EncryptionOptions.encryptCommunications().addRecipient(recipientPub), - new SigningOptions().addInlineSignature(keyDecryptor, senderSec, DocumentSignatureType.BINARY_DOCUMENT) + EncryptionOptions.encryptCommunications(api) + .addRecipient(recipientPub), + SigningOptions.get(api) + .addInlineSignature(keyDecryptor, senderSec, DocumentSignatureType.BINARY_DOCUMENT) )); Streams.pipeAll(new ByteArrayInputStream(secretMessage), encryptor); @@ -164,7 +148,7 @@ public class EncryptDecryptTest { assertFalse(encryptionResult.getRecipients().isEmpty()); for (SubkeyIdentifier encryptionKey : encryptionResult.getRecipients()) { - assertTrue(KeyRingUtils.keyRingContainsKeyWithId(recipientPub, encryptionKey.getKeyId())); + assertNotNull(recipientPub.getKey(encryptionKey.getKeyIdentifier())); } assertEquals(SymmetricKeyAlgorithm.AES_256, encryptionResult.getEncryptionAlgorithm()); @@ -172,9 +156,9 @@ public class EncryptDecryptTest { // Juliet trieth to comprehend Romeos words ByteArrayInputStream envelopeIn = new ByteArrayInputStream(encryptedSecretMessage); - DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + DecryptionStream decryptor = api.processMessage() .onInputStream(envelopeIn) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get(api) .addDecryptionKey(recipientSec, keyDecryptor) .addVerificationCert(senderPub) ); @@ -194,22 +178,24 @@ public class EncryptDecryptTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void testDetachedSignatureCreationAndVerification() throws IOException, PGPException { - - PGPSecretKeyRing signingKeys = TestKeys.getJulietSecretKeyRing(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey signingKeys = TestKeys.getJulietKey(); SecretKeyRingProtector keyRingProtector = new UnprotectedKeysProtector(); byte[] data = testMessage.getBytes(); ByteArrayInputStream inputStream = new ByteArrayInputStream(data); ByteArrayOutputStream dummyOut = new ByteArrayOutputStream(); - EncryptionStream signer = PGPainless.encryptAndOrSign().onOutputStream(dummyOut) + EncryptionStream signer = api.generateMessage().onOutputStream(dummyOut) .withOptions(ProducerOptions.sign( - new SigningOptions().addDetachedSignature(keyRingProtector, signingKeys, DocumentSignatureType.BINARY_DOCUMENT) + SigningOptions.get(api) + .addDetachedSignature(keyRingProtector, signingKeys, DocumentSignatureType.BINARY_DOCUMENT) )); Streams.pipeAll(inputStream, signer); signer.close(); EncryptionResult metadata = signer.getResult(); - Set signatureSet = metadata.getDetachedSignatures().get(metadata.getDetachedSignatures().keySet().iterator().next()); + Set signatureSet = metadata.getDetachedSignatures() + .get(metadata.getDetachedSignatures().keySet().iterator().next()); ByteArrayOutputStream sigOut = new ByteArrayOutputStream(); ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(sigOut); signatureSet.iterator().next().encode(armorOut); @@ -221,11 +207,11 @@ public class EncryptDecryptTest { // CHECKSTYLE:ON inputStream = new ByteArrayInputStream(testMessage.getBytes()); - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = api.processMessage() .onInputStream(inputStream) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get(api) .addVerificationOfDetachedSignatures(new ByteArrayInputStream(armorSig.getBytes())) - .addVerificationCert(KeyRingUtils.publicKeyRingFrom(signingKeys)) + .addVerificationCert(signingKeys.toCertificate()) ); dummyOut = new ByteArrayOutputStream(); @@ -239,24 +225,25 @@ public class EncryptDecryptTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void testOnePassSignatureCreationAndVerification() throws IOException, PGPException { - PGPSecretKeyRing signingKeys = TestKeys.getJulietSecretKeyRing(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey signingKeys = TestKeys.getJulietKey(); SecretKeyRingProtector keyRingProtector = new UnprotectedKeysProtector(); byte[] data = testMessage.getBytes(); ByteArrayInputStream inputStream = new ByteArrayInputStream(data); ByteArrayOutputStream signOut = new ByteArrayOutputStream(); - EncryptionStream signer = PGPainless.encryptAndOrSign().onOutputStream(signOut) + EncryptionStream signer = api.generateMessage().onOutputStream(signOut) .withOptions(ProducerOptions.sign( - SigningOptions.get() + SigningOptions.get(api) .addInlineSignature(keyRingProtector, signingKeys, DocumentSignatureType.BINARY_DOCUMENT) ).setAsciiArmor(true)); Streams.pipeAll(inputStream, signer); signer.close(); inputStream = new ByteArrayInputStream(signOut.toByteArray()); - DecryptionStream verifier = PGPainless.decryptAndOrVerify() + DecryptionStream verifier = api.processMessage() .onInputStream(inputStream) - .withOptions(new ConsumerOptions() - .addVerificationCert(KeyRingUtils.publicKeyRingFrom(signingKeys)) + .withOptions(ConsumerOptions.get(api) + .addVerificationCert(signingKeys.toCertificate()) ); signOut = new ByteArrayOutputStream(); Streams.pipeAll(verifier, signOut); @@ -323,11 +310,86 @@ public class EncryptDecryptTest { "Ks2WqI282/DM+Lq/GCSd2nXtS3/KwErTFiF1uHi/N3TwdWA=\n" + "=j1TE\n" + "-----END PGP PUBLIC KEY BLOCK-----\n"; - - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); + PGPainless api = PGPainless.getInstance(); + OpenPGPCertificate publicKeys = api.readKey().parseCertificate(key); assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> - EncryptionOptions.encryptCommunications() + EncryptionOptions.encryptCommunications(api) .addRecipient(publicKeys)); } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testAsymmetricEncryptionWithMechanismOverride() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + + OpenPGPKey keyWithoutSEIPD2Feature = api.buildKey(OpenPGPKeyVersion.v4) + .withPreferences(AlgorithmSuite.emptyBuilder() + .overrideFeatures(Feature.MODIFICATION_DETECTION) + .overrideSymmetricKeyAlgorithms(SymmetricKeyAlgorithm.AES_128) + .build()) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) + .build(); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + EncryptionStream eOut = api.generateMessage() + .onOutputStream(bOut) + .withOptions(ProducerOptions.encrypt( + EncryptionOptions.encryptCommunications() + .overrideEncryptionMechanism(AEADAlgorithm.OCB.toMechanism(SymmetricKeyAlgorithm.AES_192)) + .addRecipient(keyWithoutSEIPD2Feature.toCertificate()))); + + eOut.write(testMessage.getBytes(StandardCharsets.UTF_8)); + eOut.close(); + EncryptionResult result = eOut.getResult(); + assertEquals(AEADAlgorithm.OCB.toMechanism(SymmetricKeyAlgorithm.AES_192), + result.getEncryptionMechanism()); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + DecryptionStream decIn = api.processMessage() + .onInputStream(bIn) + .withOptions(ConsumerOptions.get() + .addDecryptionKey(keyWithoutSEIPD2Feature)); + Streams.drain(decIn); + decIn.close(); + + MessageMetadata metadata = decIn.getMetadata(); + assertEquals(AEADAlgorithm.OCB.toMechanism(SymmetricKeyAlgorithm.AES_192), + metadata.getEncryptionMechanism()); + } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testSymmetricEncryptionWithMechanismOverride() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + EncryptionStream eOut = api.generateMessage().onOutputStream(bOut) + .withOptions( + ProducerOptions.encrypt(EncryptionOptions.encryptCommunications() + .overrideEncryptionMechanism( + AEADAlgorithm.OCB.toMechanism(SymmetricKeyAlgorithm.AES_192)) + .addMessagePassphrase(Passphrase.fromPassword("sw0rdf1sh")) + )); + + eOut.write(testMessage.getBytes(StandardCharsets.UTF_8)); + eOut.close(); + EncryptionResult result = eOut.getResult(); + + assertEquals(AEADAlgorithm.OCB.toMechanism(SymmetricKeyAlgorithm.AES_192), + result.getEncryptionMechanism()); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + DecryptionStream decIn = api.processMessage() + .onInputStream(bIn) + .withOptions(ConsumerOptions.get() + .addMessagePassphrase(Passphrase.fromPassword("sw0rdf1sh"))); + Streams.drain(decIn); + decIn.close(); + MessageMetadata metadata = decIn.getMetadata(); + assertEquals(AEADAlgorithm.OCB.toMechanism(SymmetricKeyAlgorithm.AES_192), + metadata.getEncryptionMechanism()); + } + } 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 5f6c6c15..7adfefd5 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,8 +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.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -18,11 +16,9 @@ import java.util.Iterator; import java.util.List; import java.util.Set; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -30,27 +26,26 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.exception.KeyException; -import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; -import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.util.Passphrase; import javax.annotation.Nonnull; public class EncryptionOptionsTest { - private static PGPSecretKeyRing secretKeys; - private static PGPPublicKeyRing publicKeys; - private static SubkeyIdentifier primaryKey; - private static SubkeyIdentifier encryptComms; - private static SubkeyIdentifier encryptStorage; + private static OpenPGPKey secretKeys; + private static OpenPGPCertificate publicKeys; + private static OpenPGPCertificate.OpenPGPComponentKey primaryKey; + private static OpenPGPCertificate.OpenPGPComponentKey encryptComms; + private static OpenPGPCertificate.OpenPGPComponentKey encryptStorage; + private static final PGPainless api = PGPainless.getInstance(); @BeforeAll - public static void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - secretKeys = PGPainless.buildKeyRing() + public static void generateKey() { + secretKeys = api.buildKey() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER) .build()) .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS) @@ -60,17 +55,17 @@ public class EncryptionOptionsTest { .addUserId("test@pgpainless.org") .build(); - publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys); + publicKeys = secretKeys.toCertificate(); - Iterator iterator = publicKeys.iterator(); - primaryKey = new SubkeyIdentifier(publicKeys, iterator.next().getKeyID()); - encryptComms = new SubkeyIdentifier(publicKeys, iterator.next().getKeyID()); - encryptStorage = new SubkeyIdentifier(publicKeys, iterator.next().getKeyID()); + Iterator iterator = publicKeys.getKeys().iterator(); + primaryKey = iterator.next(); + encryptComms = iterator.next(); + encryptStorage = iterator.next(); } @Test public void testOverrideEncryptionAlgorithmFailsForNULL() { - EncryptionOptions options = new EncryptionOptions(); + EncryptionOptions options = EncryptionOptions.get(api); assertNull(options.getEncryptionAlgorithmOverride()); assertThrows(IllegalArgumentException.class, () -> options.overrideEncryptionAlgorithm(SymmetricKeyAlgorithm.NULL)); @@ -80,7 +75,7 @@ public class EncryptionOptionsTest { @Test public void testOverrideEncryptionOptions() { - EncryptionOptions options = new EncryptionOptions(); + EncryptionOptions options = EncryptionOptions.get(api); assertNull(options.getEncryptionAlgorithmOverride()); options.overrideEncryptionAlgorithm(SymmetricKeyAlgorithm.AES_128); @@ -89,39 +84,40 @@ public class EncryptionOptionsTest { @Test public void testAddRecipients_EncryptCommunications() { - EncryptionOptions options = EncryptionOptions.encryptCommunications(); + EncryptionOptions options = EncryptionOptions.encryptCommunications(api); options.addRecipient(publicKeys); - Set encryptionKeys = options.getEncryptionKeyIdentifiers(); + Set encryptionKeys = options.getEncryptionKeys(); assertEquals(1, encryptionKeys.size()); assertEquals(encryptComms, encryptionKeys.iterator().next()); } @Test public void testAddRecipients_EncryptDataAtRest() { - EncryptionOptions options = EncryptionOptions.encryptDataAtRest(); + EncryptionOptions options = EncryptionOptions.encryptDataAtRest(api); options.addRecipient(publicKeys); - Set encryptionKeys = options.getEncryptionKeyIdentifiers(); + Set encryptionKeys = options.getEncryptionKeys(); assertEquals(1, encryptionKeys.size()); assertEquals(encryptStorage, encryptionKeys.iterator().next()); } @Test public void testAddRecipients_AllKeys() { - EncryptionOptions options = new EncryptionOptions(); + EncryptionOptions options = EncryptionOptions.get(api); options.addRecipient(publicKeys, EncryptionOptions.encryptToAllCapableSubkeys()); - Set encryptionKeys = options.getEncryptionKeyIdentifiers(); + Set encryptionKeys = options.getEncryptionKeys(); assertEquals(2, encryptionKeys.size()); assertTrue(encryptionKeys.contains(encryptComms)); assertTrue(encryptionKeys.contains(encryptStorage)); } + @SuppressWarnings("deprecation") @Test public void testAddEmptyRecipientsFails() { - EncryptionOptions options = new EncryptionOptions(); + EncryptionOptions options = EncryptionOptions.get(api); assertThrows(IllegalArgumentException.class, () -> options.addRecipients(Collections.emptyList())); assertThrows(IllegalArgumentException.class, () -> options.addRecipients(Collections.emptyList(), ArrayList::new)); @@ -129,61 +125,63 @@ public class EncryptionOptionsTest { @Test public void testAddEmptyPassphraseFails() { - EncryptionOptions options = new EncryptionOptions(); + EncryptionOptions options = EncryptionOptions.get(api); assertThrows(IllegalArgumentException.class, () -> options.addMessagePassphrase(Passphrase.emptyPassphrase())); } @Test - public void testAddRecipient_KeyWithoutEncryptionKeyFails() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - EncryptionOptions options = new EncryptionOptions(); - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + public void testAddRecipient_KeyWithoutEncryptionKeyFails() { + EncryptionOptions options = EncryptionOptions.get(); + OpenPGPKey secretKeys = api.buildKey() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addUserId("test@pgpainless.org") .build(); - PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys); + OpenPGPCertificate publicKeys = secretKeys.toCertificate(); assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys)); } @Test public void testEncryptionKeySelectionStrategyEmpty_ThrowsAssertionError() { - EncryptionOptions options = new EncryptionOptions(); + EncryptionOptions options = EncryptionOptions.get(api); assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, new EncryptionOptions.EncryptionKeySelector() { @NotNull @Override - public List selectEncryptionSubkeys(@NotNull List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@NotNull List encryptionCapableKeys) { return Collections.emptyList(); } })); assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, "test@pgpainless.org", new EncryptionOptions.EncryptionKeySelector() { + @NotNull @Override - public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { return Collections.emptyList(); } })); } @Test - public void testAddRecipients_PGPPublicKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPPublicKeyRing secondKeyRing = KeyRingUtils.publicKeyRingFrom( - PGPainless.generateKeyRing().modernKeyRing("other@pgpainless.org")); + public void testAddRecipients_PGPPublicKeyRingCollection() { + OpenPGPKey secondKey = api.generateKey().modernKeyRing("Other "); + OpenPGPCertificate secondCert = secondKey.toCertificate(); PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection( - Arrays.asList(publicKeys, secondKeyRing)); + Arrays.asList(publicKeys.getPGPPublicKeyRing(), secondCert.getPGPPublicKeyRing())); - EncryptionOptions options = new EncryptionOptions(); + EncryptionOptions options = EncryptionOptions.get(); + // noinspection deprecation options.addRecipients(collection, EncryptionOptions.encryptToFirstSubkey()); assertEquals(2, options.getEncryptionKeyIdentifiers().size()); } @Test public void testAddRecipient_withValidUserId() { - EncryptionOptions options = new EncryptionOptions(); + EncryptionOptions options = EncryptionOptions.get(api); options.addRecipient(publicKeys, "test@pgpainless.org", EncryptionOptions.encryptToFirstSubkey()); assertEquals(1, options.getEncryptionMethods().size()); @@ -191,7 +189,7 @@ public class EncryptionOptionsTest { @Test public void testAddRecipient_withInvalidUserId() { - EncryptionOptions options = new EncryptionOptions(); + EncryptionOptions options = EncryptionOptions.get(api); assertThrows(KeyException.UnboundUserIdException.class, () -> options.addRecipient(publicKeys, "invalid@user.id")); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java index 5fd4a674..05352c96 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java @@ -164,7 +164,7 @@ public class EncryptionWithMissingKeyFlagsTest { // Prepare encryption ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream encOut = PGPainless.encryptAndOrSign() + EncryptionStream encOut = PGPainless.getInstance().generateMessage() .onOutputStream(out) .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() .setEvaluationDate(evaluationDate) @@ -177,9 +177,11 @@ public class EncryptionWithMissingKeyFlagsTest { // Prepare decryption ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeys)); + .withOptions(ConsumerOptions.get() + .setAllowDecryptionWithMissingKeyFlags() + .addDecryptionKey(secretKeys)); ByteArrayOutputStream plain = new ByteArrayOutputStream(); // Decrypt 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 50e9722b..8c22e96c 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 @@ -13,14 +13,12 @@ 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.Date; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.JUtils; import org.junit.jupiter.api.BeforeAll; @@ -34,13 +32,14 @@ import org.pgpainless.decryption_verification.MessageMetadata; public class FileInformationTest { private static final String data = "Hello, World!\n"; - private static PGPSecretKeyRing secretKey; - private static PGPPublicKeyRing certificate; + private static OpenPGPKey secretKey; + private static OpenPGPCertificate certificate; + private static final PGPainless api = PGPainless.getInstance(); @BeforeAll - public static void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - secretKey = PGPainless.generateKeyRing().modernKeyRing("alice@wonderland.lit"); - certificate = PGPainless.extractCertificate(secretKey); + public static void generateKey() { + secretKey = api.generateKey().modernKeyRing("alice@wonderland.lit"); + certificate = secretKey.toCertificate(); } @Test @@ -51,11 +50,12 @@ public class FileInformationTest { ByteArrayInputStream dataIn = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream dataOut = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + // noinspection deprecation + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(dataOut) .withOptions(ProducerOptions.encrypt( EncryptionOptions - .encryptCommunications() + .encryptCommunications(api) .addRecipient(certificate)) .setFileName(fileName) .setModificationDate(modificationDate) @@ -73,9 +73,9 @@ public class FileInformationTest { ByteArrayInputStream cryptIn = new ByteArrayInputStream(dataOut.toByteArray()); ByteArrayOutputStream plainOut = new ByteArrayOutputStream(); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(cryptIn) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKey(secretKey)); Streams.pipeAll(decryptionStream, plainOut); @@ -92,11 +92,11 @@ public class FileInformationTest { public void testDefaults() throws PGPException, IOException { ByteArrayInputStream dataIn = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream dataOut = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(dataOut) .withOptions(ProducerOptions.encrypt( EncryptionOptions - .encryptCommunications() + .encryptCommunications(api) .addRecipient(certificate)) ); @@ -112,9 +112,9 @@ public class FileInformationTest { ByteArrayInputStream cryptIn = new ByteArrayInputStream(dataOut.toByteArray()); ByteArrayOutputStream plainOut = new ByteArrayOutputStream(); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(cryptIn) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKey(secretKey)); Streams.pipeAll(decryptionStream, plainOut); @@ -126,6 +126,7 @@ public class FileInformationTest { JUtils.assertDateEquals(PGPLiteralData.NOW, decResult.getModificationDate()); assertNotNull(decResult.getLiteralDataEncoding()); assertEquals(PGPLiteralData.BINARY, decResult.getLiteralDataEncoding().getCode()); + // noinspection deprecation assertFalse(decResult.isForYourEyesOnly()); } @@ -133,11 +134,12 @@ public class FileInformationTest { public void testForYourEyesOnly() throws PGPException, IOException { ByteArrayInputStream dataIn = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream dataOut = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + // noinspection deprecation + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(dataOut) .withOptions(ProducerOptions.encrypt( EncryptionOptions - .encryptCommunications() + .encryptCommunications(api) .addRecipient(certificate)) .setForYourEyesOnly() ); @@ -154,9 +156,9 @@ public class FileInformationTest { ByteArrayInputStream cryptIn = new ByteArrayInputStream(dataOut.toByteArray()); ByteArrayOutputStream plainOut = new ByteArrayOutputStream(); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(cryptIn) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKey(secretKey)); Streams.pipeAll(decryptionStream, plainOut); @@ -168,6 +170,7 @@ public class FileInformationTest { JUtils.assertDateEquals(PGPLiteralData.NOW, decResult.getModificationDate()); assertNotNull(decResult.getLiteralDataEncoding()); assertEquals(PGPLiteralData.BINARY, decResult.getLiteralDataEncoding().getCode()); + // noinspection deprecation assertTrue(decResult.isForYourEyesOnly()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HiddenRecipientEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HiddenRecipientEncryptionTest.java index 1cb0d3cb..28bd91d3 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HiddenRecipientEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HiddenRecipientEncryptionTest.java @@ -11,12 +11,10 @@ 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.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -31,18 +29,19 @@ import org.pgpainless.key.SubkeyIdentifier; public class HiddenRecipientEncryptionTest { @Test - public void testAnonymousRecipientRoundtrip() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void testAnonymousRecipientRoundtrip() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Alice "); - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + OpenPGPCertificate certificate = secretKeys.toCertificate(); String msg = "Hello, World!\n"; ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(ciphertextOut) .withOptions(ProducerOptions.encrypt( - EncryptionOptions.get() + EncryptionOptions.get(api) .addHiddenRecipient(certificate) )); encryptionStream.write(msg.getBytes(StandardCharsets.UTF_8)); @@ -53,7 +52,7 @@ public class HiddenRecipientEncryptionTest { byte[] ciphertext = ciphertextOut.toByteArray(); ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertext); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(ciphertextIn) .withOptions(ConsumerOptions.get() .addDecryptionKey(secretKeys)); @@ -66,6 +65,8 @@ public class HiddenRecipientEncryptionTest { assertEquals(msg, plaintextOut.toString()); assertTrue(metadata.getRecipientKeyIds().contains(0L)); + assertEquals(1, metadata.getRecipientKeyIdentifiers().size()); + assertTrue(metadata.getRecipientKeyIdentifiers().get(0).isWildcard()); assertEquals(actualEncryptionKey, metadata.getDecryptionKey()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MechanismNegotiationTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MechanismNegotiationTest.java new file mode 100644 index 00000000..5f32e678 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MechanismNegotiationTest.java @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism; +import org.bouncycastle.openpgp.api.OpenPGPKey; +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.algorithm.AEADAlgorithm; +import org.pgpainless.algorithm.AEADCipherMode; +import org.pgpainless.algorithm.AlgorithmSuite; +import org.pgpainless.algorithm.Feature; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.OpenPGPKeyVersion; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.MessageMetadata; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; +import org.pgpainless.util.TestAllImplementations; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MechanismNegotiationTest { + + private static final String testMessage = + "Ah, Juliet, if the measure of thy joy\n" + + "Be heaped like mine, and that thy skill be more\n" + + "To blazon it, then sweeten with thy breath\n" + + "This neighbor air, and let rich music’s tongue\n" + + "Unfold the imagined happiness that both\n" + + "Receive in either by this dear encounter."; + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncryptToOnlyV4CertWithOnlySEIPD1Feature() throws PGPException, IOException { + testEncryptDecryptAndCheckExpectations( + MessageEncryptionMechanism.integrityProtected(SymmetricKeyAlgorithm.AES_192.getAlgorithmId()), + new KeySpecification(OpenPGPKeyVersion.v4, AlgorithmSuite.emptyBuilder() + .overrideSymmetricKeyAlgorithms(SymmetricKeyAlgorithm.AES_192) + .overrideFeatures(Feature.MODIFICATION_DETECTION) + .build())); + } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncryptToOnlyV4CertWithOnlySEIPD2Feature() throws PGPException, IOException { + testEncryptDecryptAndCheckExpectations( + MessageEncryptionMechanism.aead(SymmetricKeyAlgorithm.AES_256.getAlgorithmId(), AEADAlgorithm.OCB.getAlgorithmId()), + new KeySpecification(OpenPGPKeyVersion.v4, AlgorithmSuite.emptyBuilder() + .overrideAeadAlgorithms(new AEADCipherMode(AEADAlgorithm.OCB, SymmetricKeyAlgorithm.AES_256)) + .overrideFeatures(Feature.MODIFICATION_DETECTION_2) + .build())); + } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncryptToOnlyV6CertWithOnlySEIPD2Features() throws IOException, PGPException { + testEncryptDecryptAndCheckExpectations( + MessageEncryptionMechanism.aead(SymmetricKeyAlgorithm.AES_256.getAlgorithmId(), AEADAlgorithm.OCB.getAlgorithmId()), + new KeySpecification(OpenPGPKeyVersion.v6, AlgorithmSuite.emptyBuilder() + .overrideAeadAlgorithms(new AEADCipherMode(AEADAlgorithm.OCB, SymmetricKeyAlgorithm.AES_256)) + .overrideFeatures(Feature.MODIFICATION_DETECTION_2) + .build())); + } + + + /** + * Here, we fall back to SEIPD1(AES128), as that is the policy fallback mechanism. + */ + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncryptToV6SEIPD1CertAndV6SEIPD2Cert() throws IOException, PGPException { + testEncryptDecryptAndCheckExpectations( + MessageEncryptionMechanism.integrityProtected(SymmetricKeyAlgorithm.AES_128.getAlgorithmId()), + + new KeySpecification(OpenPGPKeyVersion.v6, AlgorithmSuite.emptyBuilder() + .overrideAeadAlgorithms(new AEADCipherMode(AEADAlgorithm.OCB, SymmetricKeyAlgorithm.AES_256)) + .overrideFeatures(Feature.MODIFICATION_DETECTION_2) + .build()), + new KeySpecification(OpenPGPKeyVersion.v6, AlgorithmSuite.emptyBuilder() + .overrideSymmetricKeyAlgorithms(SymmetricKeyAlgorithm.AES_192) + .overrideFeatures(Feature.MODIFICATION_DETECTION) + .build())); + } + + + private void testEncryptDecryptAndCheckExpectations(MessageEncryptionMechanism expectation, KeySpecification... keys) + throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + List keyList = new ArrayList<>(); + for (KeySpecification spec : keys) { + if (spec.version == OpenPGPKeyVersion.v4) { + keyList.add(api.buildKey(spec.version) + .withPreferences(spec.preferences) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) + .build()); + } else { + keyList.add(api.buildKey(spec.version) + .withPreferences(spec.preferences) + .setPrimaryKey(KeySpec.getBuilder(KeyType.Ed25519(), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.X25519(), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .build()); + } + } + + EncryptionOptions encOpts = EncryptionOptions.encryptCommunications(); + for (OpenPGPKey k : keyList) { + encOpts.addRecipient(k.toCertificate()); + } + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + EncryptionStream eOut = api.generateMessage().onOutputStream(bOut) + .withOptions(ProducerOptions.encrypt(encOpts)); + eOut.write(testMessage.getBytes(StandardCharsets.UTF_8)); + eOut.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + DecryptionStream dIn = api.processMessage() + .onInputStream(bIn) + .withOptions(ConsumerOptions.get().addDecryptionKey(keyList.get(0))); + + bOut = new ByteArrayOutputStream(); + Streams.pipeAll(dIn, bOut); + dIn.close(); + + assertEquals(testMessage, bOut.toString()); + MessageMetadata metadata = dIn.getMetadata(); + MessageEncryptionMechanism encryptionMechanism = metadata.getEncryptionMechanism(); + assertEquals(expectation, encryptionMechanism); + } + + private static class KeySpecification { + private final OpenPGPKeyVersion version; + private final AlgorithmSuite preferences; + + KeySpecification(OpenPGPKeyVersion version, + AlgorithmSuite preferences) { + this.version = version; + this.preferences = preferences; + } + } + +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java index 28097db6..21e8283f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java @@ -5,10 +5,10 @@ package org.pgpainless.encryption_signing; 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.openpgp.api.OpenPGPCertificate; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -31,8 +31,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 java.util.Iterator; import java.util.List; import java.util.stream.Collectors; @@ -50,19 +48,21 @@ public class MultiSigningSubkeyTest { private static SecretKeyRingProtector protector; @BeforeAll - public static void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - signingKey = PGPainless.buildKeyRing() + public static void generateKey() { + PGPainless api = PGPainless.getInstance(); + signingKey = api.buildKey() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._3072), KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .addUserId("Alice ") - .build(); + .build() + .getPGPSecretKeyRing(); signingCert = PGPainless.extractCertificate(signingKey); - Iterator signingSubkeys = PGPainless.inspectKeyRing(signingKey).getSigningSubkeys().listIterator(); - primaryKey = new SubkeyIdentifier(signingKey, signingSubkeys.next().getKeyID()); - signingKey1 = new SubkeyIdentifier(signingKey, signingSubkeys.next().getKeyID()); - signingKey2 = new SubkeyIdentifier(signingKey, signingSubkeys.next().getKeyID()); + Iterator signingSubkeys = PGPainless.inspectKeyRing(signingKey).getSigningSubkeys().listIterator(); + primaryKey = new SubkeyIdentifier(signingKey, signingSubkeys.next().getKeyIdentifier()); + signingKey1 = new SubkeyIdentifier(signingKey, signingSubkeys.next().getKeyIdentifier()); + signingKey2 = new SubkeyIdentifier(signingKey, signingSubkeys.next().getKeyIdentifier()); protector = SecretKeyRingProtector.unprotectedKeys(); } @@ -70,7 +70,7 @@ public class MultiSigningSubkeyTest { public void detachedSignWithAllSubkeys() throws PGPException, IOException { ByteArrayInputStream dataIn = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream signingStream = PGPainless.encryptAndOrSign() + EncryptionStream signingStream = PGPainless.getInstance().generateMessage() .onOutputStream(out) .withOptions(ProducerOptions.sign(SigningOptions.get().addDetachedSignature(protector, signingKey, DocumentSignatureType.BINARY_DOCUMENT))); Streams.pipeAll(dataIn, signingStream); @@ -86,7 +86,7 @@ public class MultiSigningSubkeyTest { public void detachedSignWithSingleSubkey() throws PGPException, IOException { ByteArrayInputStream dataIn = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream signingStream = PGPainless.encryptAndOrSign() + EncryptionStream signingStream = PGPainless.getInstance().generateMessage() .onOutputStream(out) .withOptions(ProducerOptions.sign(SigningOptions.get().addDetachedSignature(protector, signingKey, signingKey1.getKeyId()))); Streams.pipeAll(dataIn, signingStream); @@ -99,16 +99,17 @@ public class MultiSigningSubkeyTest { @Test public void inlineSignWithAllSubkeys() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); ByteArrayInputStream dataIn = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream signingStream = PGPainless.encryptAndOrSign() + EncryptionStream signingStream = api.generateMessage() .onOutputStream(out) .withOptions(ProducerOptions.sign(SigningOptions.get().addInlineSignature(protector, signingKey, DocumentSignatureType.BINARY_DOCUMENT))); Streams.pipeAll(dataIn, signingStream); signingStream.close(); ByteArrayInputStream signedIn = new ByteArrayInputStream(out.toByteArray()); - DecryptionStream verificationStream = PGPainless.decryptAndOrVerify().onInputStream(signedIn) + DecryptionStream verificationStream = api.processMessage().onInputStream(signedIn) .withOptions(ConsumerOptions.get().addVerificationCert(signingCert)); Streams.drain(verificationStream); verificationStream.close(); @@ -123,16 +124,17 @@ public class MultiSigningSubkeyTest { @Test public void inlineSignWithSingleSubkey() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); ByteArrayInputStream dataIn = new ByteArrayInputStream("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream signingStream = PGPainless.encryptAndOrSign() + EncryptionStream signingStream = api.generateMessage() .onOutputStream(out) .withOptions(ProducerOptions.sign(SigningOptions.get().addInlineSignature(protector, signingKey, signingKey1.getKeyId()))); Streams.pipeAll(dataIn, signingStream); signingStream.close(); ByteArrayInputStream signedIn = new ByteArrayInputStream(out.toByteArray()); - DecryptionStream verificationStream = PGPainless.decryptAndOrVerify().onInputStream(signedIn) + DecryptionStream verificationStream = api.processMessage().onInputStream(signedIn) .withOptions(ConsumerOptions.get().addVerificationCert(signingCert)); Streams.drain(verificationStream); verificationStream.close(); 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 55564d84..d0350628 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 @@ -55,7 +55,7 @@ public class RespectPreferredSymmetricAlgorithmDuringEncryptionTest { ByteArrayOutputStream out = new ByteArrayOutputStream(); EncryptionStream encryptionStream = PGPainless.encryptAndOrSign().onOutputStream(out) .withOptions( - ProducerOptions.encrypt(new EncryptionOptions() + ProducerOptions.encrypt(EncryptionOptions.get() .addRecipient(publicKeys) // no user-id passed )); @@ -67,7 +67,7 @@ public class RespectPreferredSymmetricAlgorithmDuringEncryptionTest { out = new ByteArrayOutputStream(); encryptionStream = PGPainless.encryptAndOrSign().onOutputStream(out) .withOptions( - ProducerOptions.encrypt(new EncryptionOptions() + ProducerOptions.encrypt(EncryptionOptions.get() .addRecipient(publicKeys, "Bob Babbage ") )); @@ -79,7 +79,7 @@ public class RespectPreferredSymmetricAlgorithmDuringEncryptionTest { out = new ByteArrayOutputStream(); encryptionStream = PGPainless.encryptAndOrSign().onOutputStream(out) .withOptions( - ProducerOptions.encrypt(new EncryptionOptions() + ProducerOptions.encrypt(EncryptionOptions.get() .addRecipient(publicKeys, "Bobby128 ") )); 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 c62116b3..d67a119a 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 @@ -14,17 +14,11 @@ 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.Arrays; import org.bouncycastle.openpgp.PGPException; -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.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @@ -43,7 +37,6 @@ import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.util.MultiMap; import org.pgpainless.util.Passphrase; import org.pgpainless.util.TestAllImplementations; @@ -54,26 +47,26 @@ public class SigningTest { @ExtendWith(TestAllImplementations.class) public void testEncryptionAndSignatureVerification() throws IOException, PGPException { + PGPainless api = PGPainless.getInstance(); - PGPPublicKeyRing julietKeys = TestKeys.getJulietPublicKeyRing(); - PGPPublicKeyRing romeoKeys = TestKeys.getRomeoPublicKeyRing(); + OpenPGPCertificate julietCert = TestKeys.getJulietCertificate(); + OpenPGPCertificate romeoCert = TestKeys.getRomeoCertificate(); - PGPSecretKeyRing cryptieKeys = TestKeys.getCryptieSecretKeyRing(); - KeyRingInfo cryptieInfo = new KeyRingInfo(cryptieKeys); - PGPSecretKey cryptieSigningKey = cryptieKeys.getSecretKey(cryptieInfo.getSigningSubkeys().get(0).getKeyID()); - - PGPPublicKeyRingCollection keys = new PGPPublicKeyRingCollection(Arrays.asList(julietKeys, romeoKeys)); + OpenPGPKey cryptieKey = TestKeys.getCryptieKey(); + KeyRingInfo cryptieInfo = api.inspect(cryptieKey); + OpenPGPKey.OpenPGPSecretKey cryptieSigningKey = cryptieKey.getSecretKey(cryptieInfo.getSigningSubkeys().get(0).getKeyIdentifier()); ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(out) .withOptions(ProducerOptions.signAndEncrypt( - EncryptionOptions.encryptDataAtRest() - .addRecipients(keys) - .addRecipient(KeyRingUtils.publicKeyRingFrom(cryptieKeys)), - new SigningOptions().addInlineSignature( + EncryptionOptions.encryptDataAtRest(api) + .addRecipient(romeoCert) + .addRecipient(julietCert) + .addRecipient(cryptieKey.toCertificate()), + SigningOptions.get(api).addInlineSignature( SecretKeyRingProtector.unlockSingleKeyWith(TestKeys.CRYPTIE_PASSPHRASE, cryptieSigningKey), - cryptieKeys, TestKeys.CRYPTIE_UID, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) + cryptieKey, TestKeys.CRYPTIE_UID, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) ).setAsciiArmor(true)); byte[] messageBytes = "This message is signed and encrypted to Romeo and Juliet." @@ -86,19 +79,16 @@ public class SigningTest { byte[] encrypted = out.toByteArray(); ByteArrayInputStream cryptIn = new ByteArrayInputStream(encrypted); - PGPSecretKeyRing romeoSecret = TestKeys.getRomeoSecretKeyRing(); - PGPSecretKeyRing julietSecret = TestKeys.getJulietSecretKeyRing(); + OpenPGPKey romeoKey = TestKeys.getRomeoKey(); + OpenPGPKey julietKey = TestKeys.getJulietKey(); - PGPSecretKeyRingCollection secretKeys = new PGPSecretKeyRingCollection( - Arrays.asList(romeoSecret, julietSecret)); - PGPPublicKeyRingCollection verificationKeys = new PGPPublicKeyRingCollection( - Arrays.asList(KeyRingUtils.publicKeyRingFrom(cryptieKeys), romeoKeys)); - - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(cryptIn) - .withOptions(new ConsumerOptions() - .addDecryptionKeys(secretKeys, SecretKeyRingProtector.unprotectedKeys()) - .addVerificationCerts(verificationKeys) + .withOptions(ConsumerOptions.get(api) + .addDecryptionKey(romeoKey, SecretKeyRingProtector.unprotectedKeys()) + .addDecryptionKey(julietKey, SecretKeyRingProtector.unprotectedKeys()) + .addVerificationCert(cryptieKey.toCertificate()) + .addVerificationCert(romeoCert) ); ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); @@ -109,19 +99,19 @@ public class SigningTest { MessageMetadata metadata = decryptionStream.getMetadata(); assertTrue(metadata.isEncrypted()); assertTrue(metadata.isVerifiedSigned()); - assertTrue(metadata.isVerifiedSignedBy(KeyRingUtils.publicKeyRingFrom(cryptieKeys))); - assertFalse(metadata.isVerifiedSignedBy(julietKeys)); + assertTrue(metadata.isVerifiedSignedBy(cryptieKey)); + assertFalse(metadata.isVerifiedSignedBy(julietCert)); } @TestTemplate @ExtendWith(TestAllImplementations.class) - public void testSignWithInvalidUserIdFails() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void testSignWithInvalidUserIdFails() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("alice", "password123"); SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("password123")); - SigningOptions opts = new SigningOptions(); + SigningOptions opts = SigningOptions.get(api); // "bob" is not a valid user-id assertThrows(KeyException.UnboundUserIdException.class, () -> opts.addInlineSignature(protector, secretKeys, "bob", @@ -131,18 +121,19 @@ public class SigningTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void testSignWithRevokedUserIdFails() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + throws PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("alice", "password123"); SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith( Passphrase.fromPassword("password123")); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .revokeUserId("alice", protector) .done(); - final PGPSecretKeyRing fSecretKeys = secretKeys; + final OpenPGPKey fSecretKeys = secretKeys; - SigningOptions opts = new SigningOptions(); + SigningOptions opts = SigningOptions.get(); // "alice" has been revoked assertThrows(KeyException.UnboundUserIdException.class, () -> opts.addInlineSignature(protector, fSecretKeys, "alice", @@ -152,10 +143,11 @@ public class SigningTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void signWithHashAlgorithmOverride() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - SigningOptions options = new SigningOptions(); + SigningOptions options = SigningOptions.get(api); assertNull(options.getHashAlgorithmOverride()); options.overrideHashAlgorithm(HashAlgorithm.SHA224); @@ -164,7 +156,7 @@ public class SigningTest { options.addDetachedSignature(protector, secretKeys, DocumentSignatureType.BINARY_DOCUMENT); String data = "Hello, World!\n"; - EncryptionStream signer = PGPainless.encryptAndOrSign() + EncryptionStream signer = api.generateMessage() .onOutputStream(new ByteArrayOutputStream()) .withOptions(ProducerOptions.sign(options)); @@ -184,19 +176,20 @@ public class SigningTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void negotiateHashAlgorithmChoseFallbackIfEmptyPreferences() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.buildKey() .setPrimaryKey(KeySpec.getBuilder( KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) .overridePreferredHashAlgorithms()) .addUserId("Alice") .build(); - SigningOptions options = new SigningOptions() + SigningOptions options = SigningOptions.get(api) .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT); String data = "Hello, World!\n"; - EncryptionStream signer = PGPainless.encryptAndOrSign() + EncryptionStream signer = api.generateMessage() .onOutputStream(new ByteArrayOutputStream()) .withOptions(ProducerOptions.sign(options)); @@ -207,26 +200,27 @@ public class SigningTest { SubkeyIdentifier signingKey = sigs.keySet().iterator().next(); PGPSignature signature = sigs.get(signingKey).iterator().next(); - assertEquals(PGPainless.getPolicy().getDataSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), + assertEquals(api.getAlgorithmPolicy().getDataSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), signature.getHashAlgorithm()); } @TestTemplate @ExtendWith(TestAllImplementations.class) public void negotiateHashAlgorithmChoseFallbackIfUnacceptablePreferences() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.buildKey() .setPrimaryKey( KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) .overridePreferredHashAlgorithms(HashAlgorithm.MD5)) .addUserId("Alice") .build(); - SigningOptions options = new SigningOptions() + SigningOptions options = SigningOptions.get(api) .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT); String data = "Hello, World!\n"; - EncryptionStream signer = PGPainless.encryptAndOrSign() + EncryptionStream signer = api.generateMessage() .onOutputStream(new ByteArrayOutputStream()) .withOptions(ProducerOptions.sign(options)); @@ -237,20 +231,20 @@ public class SigningTest { SubkeyIdentifier signingKey = sigs.keySet().iterator().next(); PGPSignature signature = sigs.get(signingKey).iterator().next(); - assertEquals(PGPainless.getPolicy().getDataSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), + assertEquals(api.getAlgorithmPolicy().getDataSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), signature.getHashAlgorithm()); } @TestTemplate @ExtendWith(TestAllImplementations.class) - public void signingWithNonCapableKeyThrowsKeyCannotSignException() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + public void signingWithNonCapableKeyThrowsKeyCannotSignException() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.buildKey() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addUserId("Alice") .build(); - SigningOptions options = new SigningOptions(); + SigningOptions options = SigningOptions.get(api); assertThrows(KeyException.UnacceptableSigningKeyException.class, () -> options.addDetachedSignature( SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT)); assertThrows(KeyException.UnacceptableSigningKeyException.class, () -> options.addInlineSignature( @@ -259,15 +253,15 @@ public class SigningTest { @TestTemplate @ExtendWith(TestAllImplementations.class) - public void signWithInvalidUserIdThrowsKeyValidationError() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + public void signWithInvalidUserIdThrowsKeyValidationError() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.buildKey() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addUserId("Alice") .build(); - SigningOptions options = new SigningOptions(); + SigningOptions options = SigningOptions.get(api); assertThrows(KeyException.UnboundUserIdException.class, () -> options.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, "Bob", DocumentSignatureType.BINARY_DOCUMENT)); 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 fc99fa44..8e6d20b3 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ConvertKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ConvertKeys.java @@ -7,12 +7,8 @@ package org.pgpainless.example; 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 org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.info.KeyRingInfo; @@ -23,17 +19,18 @@ public class ConvertKeys { * This example demonstrates how to extract a public key certificate from a secret key. */ @Test - public void secretKeyToCertificate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void secretKeyToCertificate() { + PGPainless api = PGPainless.getInstance(); String userId = "alice@wonderland.lit"; - PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() + OpenPGPKey secretKey = api.generateKey() .modernKeyRing(userId); + // Extract certificate (public key) from secret key - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); + OpenPGPCertificate certificate = secretKey.toCertificate(); - - KeyRingInfo secretKeyInfo = PGPainless.inspectKeyRing(secretKey); + KeyRingInfo secretKeyInfo = api.inspect(secretKey); assertTrue(secretKeyInfo.isSecretKey()); - KeyRingInfo certificateInfo = PGPainless.inspectKeyRing(certificate); + KeyRingInfo certificateInfo = api.inspect(certificate); assertFalse(certificateInfo.isSecretKey()); } } 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 c35b3572..f427c681 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/DecryptOrVerify.java @@ -15,8 +15,9 @@ 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.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPKeyReader; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -65,7 +66,7 @@ public class DecryptOrVerify { /** * 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(); @@ -134,33 +135,31 @@ public class DecryptOrVerify { "=9PiO\n" + "-----END PGP MESSAGE-----"; - private static PGPSecretKeyRing secretKey; - private static PGPPublicKeyRing certificate; + private static OpenPGPKey secretKey; + private static OpenPGPCertificate certificate; @BeforeAll public static void prepare() throws IOException { + OpenPGPKeyReader reader = PGPainless.getInstance().readKey(); // read the secret key - secretKey = PGPainless.readKeyRing().secretKeyRing(KEY); + secretKey = reader.parseKey(KEY); // certificate is the public part of the key - certificate = PGPainless.extractCertificate(secretKey); + certificate = secretKey.toCertificate(); } /** * 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() + ConsumerOptions consumerOptions = ConsumerOptions.get() .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() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(ciphertextIn) .withOptions(consumerOptions); @@ -180,20 +179,17 @@ public class DecryptOrVerify { /** * 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() + ConsumerOptions consumerOptions = ConsumerOptions.get() .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)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(ciphertextIn) .withOptions(consumerOptions); @@ -213,19 +209,17 @@ public class DecryptOrVerify { /** * 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() + ConsumerOptions options = ConsumerOptions.get() .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 = PGPainless.decryptAndOrVerify() + DecryptionStream verificationStream = PGPainless.getInstance().processMessage() .onInputStream(in) .withOptions(options); @@ -243,11 +237,10 @@ public class DecryptOrVerify { /** * This example shows how to create - and verify - cleartext signed messages. - * @throws PGPException - * @throws IOException */ @Test public void createAndVerifyCleartextSignedMessage() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); // 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 @@ -255,14 +248,14 @@ public class DecryptOrVerify { // and write the signed message to an output stream ByteArrayOutputStream out = new ByteArrayOutputStream(); - SigningOptions signingOptions = SigningOptions.get(); + SigningOptions signingOptions = SigningOptions.get(api); // 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() + EncryptionStream signingStream = api.generateMessage() .onOutputStream(out) // on the output stream .withOptions(producerOptions); // with the options @@ -277,9 +270,9 @@ public class DecryptOrVerify { ByteArrayInputStream signedIn = new ByteArrayInputStream(signedMessage); // and pass it to the decryption stream - DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() + DecryptionStream verificationStream = api.processMessage() .onInputStream(signedIn) - .withOptions(new ConsumerOptions().addVerificationCert(certificate)); + .withOptions(ConsumerOptions.get(api).addVerificationCert(certificate)); // plain will receive the plaintext message ByteArrayOutputStream plain = new ByteArrayOutputStream(); 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 d97891d8..360476a6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java @@ -13,8 +13,9 @@ 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.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPKeyReader; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -126,33 +127,35 @@ public class Encrypt { /** * 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. - * + *

* Bob subsequently decrypts the message using his key and verifies that the message was signed by Alice using * her certificate. */ @Test public void encryptAndSignMessage() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); // Prepare keys - PGPSecretKeyRing keyAlice = PGPainless.readKeyRing().secretKeyRing(ALICE_KEY); - PGPPublicKeyRing certificateAlice = PGPainless.readKeyRing().publicKeyRing(ALICE_CERT); + OpenPGPKeyReader reader = api.readKey(); + OpenPGPKey keyAlice = reader.parseKey(ALICE_KEY); + OpenPGPCertificate certificateAlice = reader.parseCertificate(ALICE_CERT); SecretKeyRingProtector protectorAlice = SecretKeyRingProtector.unprotectedKeys(); - PGPSecretKeyRing keyBob = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); - PGPPublicKeyRing certificateBob = PGPainless.readKeyRing().publicKeyRing(BOB_CERT); + OpenPGPKey keyBob = reader.parseKey(BOB_KEY); + OpenPGPCertificate certificateBob = reader.parseCertificate(BOB_CERT); SecretKeyRingProtector protectorBob = SecretKeyRingProtector.unprotectedKeys(); // plaintext message to encrypt String message = "Hello, World!\n"; ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); // Encrypt and sign - EncryptionStream encryptor = PGPainless.encryptAndOrSign() + EncryptionStream encryptor = api.generateMessage() .onOutputStream(ciphertext) .withOptions(ProducerOptions.signAndEncrypt( // we want to encrypt communication (affects key selection based on key flags) - EncryptionOptions.encryptCommunications() + EncryptionOptions.encryptCommunications(api) .addRecipient(certificateBob) .addRecipient(certificateAlice), - new SigningOptions() + SigningOptions.get(api) .addInlineSignature(protectorAlice, keyAlice, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) ).setAsciiArmor(true) ); @@ -163,9 +166,9 @@ public class Encrypt { String encryptedMessage = ciphertext.toString(); // Decrypt and verify signatures - DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + DecryptionStream decryptor = api.processMessage() .onInputStream(new ByteArrayInputStream(encryptedMessage.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get(api) .addDecryptionKey(keyBob, protectorBob) .addVerificationCert(certificateAlice) ); @@ -188,13 +191,14 @@ public class Encrypt { */ @Test public void encryptUsingPassphrase() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); String message = "Hello, World!"; ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); // Encrypt - EncryptionStream encryptor = PGPainless.encryptAndOrSign() + EncryptionStream encryptor = api.generateMessage() .onOutputStream(ciphertext) .withOptions(ProducerOptions - .encrypt(EncryptionOptions.encryptCommunications() + .encrypt(EncryptionOptions.encryptCommunications(api) .addMessagePassphrase(Passphrase.fromPassword("p4ssphr4s3")) ).setAsciiArmor(true) ); @@ -205,9 +209,9 @@ public class Encrypt { String asciiCiphertext = ciphertext.toString(); // Decrypt - DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + DecryptionStream decryptor = api.processMessage() .onInputStream(new ByteArrayInputStream(asciiCiphertext.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions().addMessagePassphrase(Passphrase.fromPassword("p4ssphr4s3"))); + .withOptions(ConsumerOptions.get(api).addMessagePassphrase(Passphrase.fromPassword("p4ssphr4s3"))); ByteArrayOutputStream plaintext = new ByteArrayOutputStream(); Streams.pipeAll(decryptor, plaintext); @@ -221,16 +225,18 @@ 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 multiline comment header is added using the fluent ProducerOption syntax. - * + *

* Bob subsequently decrypts the message using his key. */ @Test public void encryptWithCommentHeader() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); // Prepare keys - PGPPublicKeyRing certificateAlice = PGPainless.readKeyRing().publicKeyRing(ALICE_CERT); + OpenPGPKeyReader reader = api.readKey(); + OpenPGPCertificate certificateAlice = reader.parseCertificate(ALICE_CERT); - PGPSecretKeyRing keyBob = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); - PGPPublicKeyRing certificateBob = PGPainless.readKeyRing().publicKeyRing(BOB_CERT); + OpenPGPKey keyBob = reader.parseKey(BOB_KEY); + OpenPGPCertificate certificateBob = reader.parseCertificate(BOB_CERT); SecretKeyRingProtector protectorBob = SecretKeyRingProtector.unprotectedKeys(); // plaintext message to encrypt @@ -244,11 +250,11 @@ public class Encrypt { String comment = comments[0] + "\n" + comments[1] + "\n" + comments[2] + "\n" + comments[3]; ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); // Encrypt and sign - EncryptionStream encryptor = PGPainless.encryptAndOrSign() + EncryptionStream encryptor = api.generateMessage() .onOutputStream(ciphertext) .withOptions(ProducerOptions.encrypt( // we want to encrypt communication (affects key selection based on key flags) - EncryptionOptions.encryptCommunications() + EncryptionOptions.encryptCommunications(api) .addRecipient(certificateBob) .addRecipient(certificateAlice) ).setAsciiArmor(true) @@ -268,9 +274,9 @@ public class Encrypt { // also test, that decryption still works... // Decrypt and verify signatures - DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + DecryptionStream decryptor = api.processMessage() .onInputStream(new ByteArrayInputStream(encryptedMessage.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get(api) .addDecryptionKey(keyBob, protectorBob) .addVerificationCert(certificateAlice) ); diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java index d6bcb0b1..125400c5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java @@ -8,13 +8,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; 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 org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; @@ -35,15 +32,15 @@ import org.pgpainless.util.Passphrase; /** * This class demonstrates how to use PGPainless to generate secret keys. - * In general the starting point for generating secret keys using PGPainless is {@link PGPainless#generateKeyRing()}. + * In general the starting point for generating secret keys using PGPainless is {@link PGPainless#generateKey()}. * The result ({@link org.pgpainless.key.generation.KeyRingBuilder}) provides some factory methods for key archetypes * such as {@link org.pgpainless.key.generation.KeyRingTemplates#modernKeyRing(CharSequence, String)} or * {@link org.pgpainless.key.generation.KeyRingTemplates#simpleRsaKeyRing(CharSequence, RsaLength)}. - * + *

* Those methods always take a user-id which is used as primary user-id, as well as a passphrase which is used to encrypt * the secret key. * To generate unencrypted secret keys, just pass {@code null} as passphrase. - * + *

* Besides the archetype methods, it is possible to generate fully customized keys (see {@link #generateCustomOpenPGPKey()}). */ public class GenerateKeys { @@ -52,79 +49,80 @@ public class GenerateKeys { * This example demonstrates how to generate a modern OpenPGP key which consists of an ed25519 EdDSA primary key * used solely for certification of subkeys, as well as an ed25519 EdDSA signing subkey, and an X25519 ECDH * encryption subkey. - * + *

* This is the recommended way to generate OpenPGP keys with PGPainless. */ @Test - public void generateModernEcKey() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void generateModernEcKey() throws IOException { + PGPainless api = PGPainless.getInstance(); // Define a primary user-id String userId = "gbaker@pgpainless.org"; // Set a password to protect the secret key String password = "ra1nb0w"; // Generate the OpenPGP key - PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() + OpenPGPKey key = api.generateKey() .modernKeyRing(userId, password); + // Extract public key - PGPPublicKeyRing publicKey = PGPainless.extractCertificate(secretKey); + OpenPGPCertificate certificate = key.toCertificate(); // Encode the public key to an ASCII armored string ready for sharing - String asciiArmoredPublicKey = PGPainless.asciiArmor(publicKey); + String asciiArmoredPublicKey = certificate.toAsciiArmoredString(); assertTrue(asciiArmoredPublicKey.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")); - KeyRingInfo keyInfo = new KeyRingInfo(secretKey); + KeyRingInfo keyInfo = api.inspect(key); assertEquals(3, keyInfo.getSecretKeys().size()); assertEquals(userId, keyInfo.getPrimaryUserId()); assertEquals(PublicKeyAlgorithm.EDDSA_LEGACY.getAlgorithmId(), - keyInfo.getPublicKey().getAlgorithm()); + keyInfo.getAlgorithm().getAlgorithmId()); assertEquals(PublicKeyAlgorithm.EDDSA_LEGACY.getAlgorithmId(), - keyInfo.getSigningSubkeys().get(0).getAlgorithm()); + keyInfo.getSigningSubkeys().get(0).getPGPPublicKey().getAlgorithm()); assertEquals(PublicKeyAlgorithm.ECDH.getAlgorithmId(), - keyInfo.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getAlgorithm()); + keyInfo.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getPGPPublicKey().getAlgorithm()); } /** * This example demonstrates how to generate a simple OpenPGP key consisting of a 4096-bit RSA key. * The RSA key is used for both signing and certifying, as well as encryption. - * + *

* This method is recommended if the application has to deal with legacy clients with poor algorithm support. */ @Test - public void generateSimpleRSAKey() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void generateSimpleRSAKey() { + PGPainless api = PGPainless.getInstance(); // Define a primary user-id String userId = "mpage@pgpainless.org"; // Set a password to protect the secret key String password = "b1angl3s"; // Generate the OpenPGP key - PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() + OpenPGPKey secretKey = api.generateKey() .simpleRsaKeyRing(userId, RsaLength._4096, password); - KeyRingInfo keyInfo = new KeyRingInfo(secretKey); + KeyRingInfo keyInfo = api.inspect(secretKey); assertEquals(1, keyInfo.getSecretKeys().size()); assertEquals(userId, keyInfo.getPrimaryUserId()); - assertEquals(PublicKeyAlgorithm.RSA_GENERAL.getAlgorithmId(), keyInfo.getPublicKey().getAlgorithm()); + assertEquals(PublicKeyAlgorithm.RSA_GENERAL.getAlgorithmId(), keyInfo.getAlgorithm().getAlgorithmId()); } /** * This example demonstrates how to generate a simple OpenPGP key based on elliptic curves. * The key consists of an ECDSA primary key that is used both for certification of subkeys, and signing of data, * and a single ECDH encryption subkey. - * + *

* This method is recommended if small keys and high performance are desired. */ @Test - public void generateSimpleECKey() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void generateSimpleECKey() { + PGPainless api = PGPainless.getInstance(); // Define a primary user-id String userId = "mhelms@pgpainless.org"; // Set a password to protect the secret key String password = "tr4ns"; // Generate the OpenPGP key - PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() + OpenPGPKey secretKey = api.generateKey() .simpleEcKeyRing(userId, password); - KeyRingInfo keyInfo = new KeyRingInfo(secretKey); + KeyRingInfo keyInfo = api.inspect(secretKey); assertEquals(2, keyInfo.getSecretKeys().size()); assertEquals(userId, keyInfo.getPrimaryUserId()); } @@ -133,43 +131,43 @@ public class GenerateKeys { * This example demonstrates how to generate a custom OpenPGP secret key. * Among user-id and password, the user can add an arbitrary number of subkeys and specify their algorithms and * algorithm preferences. - * + *

* If the target key amalgamation (key ring) should consist of more than just a single (sub-)key, start by providing * the primary key specification using {@link org.pgpainless.key.generation.KeyRingBuilder#setPrimaryKey(KeySpec)}. * Any additional subkeys can be then added using {@link org.pgpainless.key.generation.KeyRingBuilder#addSubkey(KeySpec)}. - * - * {@link KeySpec} objects can best be obtained by using the {@link KeySpec#getBuilder(KeyType, KeyFlag, KeyFlag...)} + *

+ * {@link KeySpec} objects can best be obtained by using the {@link KeySpec#getBuilder(KeyType, KeyFlag...)} * method and providing a {@link KeyType}. * There are a bunch of factory methods for different {@link KeyType} implementations present in {@link KeyType} itself * (such as {@link KeyType#ECDH(EllipticCurve)}). {@link KeyFlag KeyFlags} determine * the use of the key, like encryption, signing data or certifying subkeys. - * + *

* If you so desire, you can now specify your own algorithm preferences. * For that, see {@link org.pgpainless.key.generation.KeySpecBuilder#overridePreferredCompressionAlgorithms(CompressionAlgorithm...)}, * {@link org.pgpainless.key.generation.KeySpecBuilder#overridePreferredHashAlgorithms(HashAlgorithm...)} or * {@link org.pgpainless.key.generation.KeySpecBuilder#overridePreferredSymmetricKeyAlgorithms(SymmetricKeyAlgorithm...)}. - * + *

* Note, that if you set preferred algorithms, the preference lists are sorted from high priority to low priority. - * + *

* When setting the primary key spec ({@link org.pgpainless.key.generation.KeyRingBuilder#setPrimaryKey(KeySpecBuilder)}), * make sure that the primary key spec has the {@link KeyFlag} {@link KeyFlag#CERTIFY_OTHER} set, as this is a requirement * for primary keys. - * + *

* Furthermore, you have to set at least the primary user-id via - * {@link org.pgpainless.key.generation.KeyRingBuilder#addUserId(String)}, + * {@link org.pgpainless.key.generation.KeyRingBuilder#addUserId(CharSequence)}, * but you can also add additional user-ids. - * + *

* If you want the key to expire at a certain point in time, call * {@link org.pgpainless.key.generation.KeyRingBuilder#setExpirationDate(Date)}. * Lastly you can decide whether to set a passphrase to protect the secret key using * {@link org.pgpainless.key.generation.KeyRingBuilder#setPassphrase(Passphrase)}. */ @Test - public void generateCustomOpenPGPKey() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void generateCustomOpenPGPKey() { + PGPainless api = PGPainless.getInstance(); // Instead of providing a string, we can assemble a user-id by using the user-id builder. // The example below corresponds to "Morgan Carpenter (Pride!) " - UserId userId = UserId.newBuilder() + UserId userId = UserId.builder() .withName("Morgan Carpenter") .withEmail("mcarpenter@pgpainless.org") .withComment("Pride!") @@ -179,7 +177,7 @@ public class GenerateKeys { // It is recommended to use the Passphrase class, as it can be used to safely invalidate passwords from memory Passphrase passphrase = Passphrase.fromPassword("1nters3x"); - PGPSecretKeyRing secretKey = PGPainless.buildKeyRing() + OpenPGPKey secretKey = api.buildKey() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), // The primary key MUST carry the CERTIFY_OTHER flag, but CAN carry additional flags KeyFlag.CERTIFY_OTHER)) @@ -212,7 +210,7 @@ public class GenerateKeys { .build(); - KeyRingInfo keyInfo = new KeyRingInfo(secretKey); + KeyRingInfo keyInfo = api.inspect(secretKey); assertEquals(3, keyInfo.getSecretKeys().size()); assertEquals("Morgan Carpenter (Pride!) ", keyInfo.getPrimaryUserId()); assertTrue(keyInfo.isUserIdValid(additionalUserId)); 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 3b29e35d..7fc55f7b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java @@ -10,8 +10,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Arrays; import java.util.HashMap; -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.algorithm.HashAlgorithm; @@ -25,68 +23,38 @@ import org.pgpainless.util.NotationRegistry; * can be rejected. * Note, that PGPainless distinguishes between hash algorithms used in revocation and non-revocation signatures, * and has different policies for those. - * + *

* Furthermore, PGPainless has policies for symmetric encryption algorithms (both for encrypting and decrypting), * for public key algorithms and key lengths, as well as compression algorithms. - * + *

* The following examples show how these policies can be modified. - * - * PGPainless' policy is being accessed by calling {@link PGPainless#getPolicy()}. + *

+ * PGPainless' policy is being accessed by calling {@link PGPainless#getAlgorithmPolicy()}. * Custom sub-policies can be set by calling the setter methods of {@link Policy}. */ public class ManagePolicy { - /** - * Reset PGPainless' policy class to default values. - */ - @BeforeEach - @AfterEach - public void resetPolicy() { - // Policy for hash algorithms in non-revocation signatures - PGPainless.getPolicy().setCertificationSignatureHashAlgorithmPolicy( - Policy.HashAlgorithmPolicy.static2022SignatureHashAlgorithmPolicy()); - // Policy for hash algorithms in data signatures - PGPainless.getPolicy().setDataSignatureHashAlgorithmPolicy( - Policy.HashAlgorithmPolicy.static2022SignatureHashAlgorithmPolicy()); - // Policy for hash algorithms in revocation signatures - PGPainless.getPolicy().setRevocationSignatureHashAlgorithmPolicy( - Policy.HashAlgorithmPolicy.static2022RevocationSignatureHashAlgorithmPolicy()); - // Policy for public key algorithms and bit lengths - PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( - Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); - // Policy for acceptable symmetric encryption algorithms when decrypting messages - PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022()); - // Policy for acceptable symmetric encryption algorithms when encrypting messages - PGPainless.getPolicy().setSymmetricKeyEncryptionAlgorithmPolicy( - Policy.SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022()); - // Policy for acceptable compression algorithms - PGPainless.getPolicy().setCompressionAlgorithmPolicy( - Policy.CompressionAlgorithmPolicy.anyCompressionAlgorithmPolicy()); - // Known notations - PGPainless.getPolicy().getNotationRegistry().clear(); - } - /** * {@link HashAlgorithm Hash Algorithms} may get outdated with time. {@link HashAlgorithm#SHA1} is a prominent * example for an algorithm that is nowadays considered unsafe to use and which shall be avoided. - * + *

* PGPainless comes with a {@link Policy} class that defines which algorithms are trustworthy and acceptable. * It also allows the user to specify a custom policy tailored to their needs. - * + *

* 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#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. */ @Test public void setCustomSignatureHashPolicy() { - // Get PGPainless' policy singleton - Policy policy = PGPainless.getPolicy(); + PGPainless api = PGPainless.getInstance(); + // Get PGPainless' policy + Policy oldPolicy = api.getAlgorithmPolicy(); - Policy.HashAlgorithmPolicy sigHashAlgoPolicy = policy.getDataSignatureHashAlgorithmPolicy(); + Policy.HashAlgorithmPolicy sigHashAlgoPolicy = oldPolicy.getDataSignatureHashAlgorithmPolicy(); assertTrue(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA512)); // Per default, non-revocation signatures using SHA-1 are rejected assertFalse(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA1)); @@ -98,9 +66,9 @@ public class ManagePolicy { // List of acceptable hash algorithms Arrays.asList(HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256, HashAlgorithm.SHA224, HashAlgorithm.SHA1)); // Set the hash algo policy as policy for non-revocation signatures - policy.setDataSignatureHashAlgorithmPolicy(customPolicy); + api = new PGPainless(oldPolicy.copy().withDataSignatureHashAlgorithmPolicy(customPolicy).build()); - sigHashAlgoPolicy = policy.getDataSignatureHashAlgorithmPolicy(); + sigHashAlgoPolicy = api.getAlgorithmPolicy().getDataSignatureHashAlgorithmPolicy(); assertTrue(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA512)); // SHA-1 is now acceptable as well assertTrue(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA1)); @@ -111,13 +79,14 @@ public class ManagePolicy { * Per default, PGPainless will reject signatures made by keys of unacceptable algorithm or length. * See {@link Policy.PublicKeyAlgorithmPolicy#bsi2021PublicKeyAlgorithmPolicy()} * to inspect PGPainless' defaults. - * + *

* This example demonstrates how to set a custom public key algorithm policy. */ @Test public void setCustomPublicKeyAlgorithmPolicy() { - Policy policy = PGPainless.getPolicy(); - Policy.PublicKeyAlgorithmPolicy pkAlgorithmPolicy = policy.getPublicKeyAlgorithmPolicy(); + PGPainless api = PGPainless.getInstance(); + Policy oldPolicy = api.getAlgorithmPolicy(); + Policy.PublicKeyAlgorithmPolicy pkAlgorithmPolicy = oldPolicy.getPublicKeyAlgorithmPolicy(); assertTrue(pkAlgorithmPolicy.isAcceptable(PublicKeyAlgorithm.RSA_GENERAL, 4096)); assertTrue(pkAlgorithmPolicy.isAcceptable(PublicKeyAlgorithm.RSA_GENERAL, 2048)); assertFalse(pkAlgorithmPolicy.isAcceptable(PublicKeyAlgorithm.RSA_GENERAL, 1024)); @@ -131,9 +100,10 @@ public class ManagePolicy { put(PublicKeyAlgorithm.RSA_GENERAL, 3000); }} ); - policy.setPublicKeyAlgorithmPolicy(customPolicy); - pkAlgorithmPolicy = policy.getPublicKeyAlgorithmPolicy(); + api = new PGPainless(oldPolicy.copy().withPublicKeyAlgorithmPolicy(customPolicy).build()); + + pkAlgorithmPolicy = api.getAlgorithmPolicy().getPublicKeyAlgorithmPolicy(); assertTrue(pkAlgorithmPolicy.isAcceptable(PublicKeyAlgorithm.RSA_GENERAL, 4096)); // RSA 2048 is no longer acceptable assertFalse(pkAlgorithmPolicy.isAcceptable(PublicKeyAlgorithm.RSA_GENERAL, 2048)); @@ -144,14 +114,14 @@ public class ManagePolicy { /** * OpenPGP requires implementations to reject signatures which contain critical notation data subpackets * which are not known to the implementation. - * + *

* PGPainless allows the user to define which notations should be considered known notations. * The following example demonstrates how to mark the notation value 'unknown@pgpainless.org' as known, * such that signatures containing a critical notation with that name are no longer being invalidated because of it. */ @Test public void manageKnownNotations() { - Policy policy = PGPainless.getPolicy(); + Policy policy = PGPainless.getInstance().getAlgorithmPolicy(); NotationRegistry notationRegistry = policy.getNotationRegistry(); assertFalse(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 768064e7..ba424402 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ModifyKeys.java @@ -10,15 +10,13 @@ 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.Date; import java.util.List; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -36,27 +34,27 @@ import org.pgpainless.util.Passphrase; /** * PGPainless offers a simple API to modify keys by adding and replacing signatures and/or subkeys. - * The main entry point to this API is {@link PGPainless#modifyKeyRing(PGPSecretKeyRing)}. + * The main entry point to this API is {@link PGPainless#modify(OpenPGPKey)}. */ public class ModifyKeys { private final String userId = "alice@pgpainless.org"; private final String originalPassphrase = "p4ssw0rd"; - private PGPSecretKeyRing secretKey; - private long primaryKeyId; - private long encryptionSubkeyId; - private long signingSubkeyId; + private OpenPGPKey secretKey; + private KeyIdentifier primaryKeyId; + private KeyIdentifier encryptionSubkeyId; + private KeyIdentifier signingSubkeyId; @BeforeEach - public void generateKey() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - secretKey = PGPainless.generateKeyRing() + public void generateKey() { + PGPainless api = PGPainless.getInstance(); + secretKey = api.generateKey() .modernKeyRing(userId, originalPassphrase); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); - primaryKeyId = info.getKeyId(); - encryptionSubkeyId = info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID(); - signingSubkeyId = info.getSigningSubkeys().get(0).getKeyID(); + KeyRingInfo info = api.inspect(secretKey); + primaryKeyId = info.getKeyIdentifier(); + encryptionSubkeyId = info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyIdentifier(); + signingSubkeyId = info.getSigningSubkeys().get(0).getKeyIdentifier(); } /** @@ -65,9 +63,9 @@ public class ModifyKeys { @Test public void extractPublicKey() { // the certificate consists of only the public keys - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); + OpenPGPCertificate certificate = secretKey.toCertificate(); - KeyRingInfo info = PGPainless.inspectKeyRing(certificate); + KeyRingInfo info = PGPainless.getInstance().inspect(certificate); assertFalse(info.isSecretKey()); } @@ -76,10 +74,10 @@ public class ModifyKeys { */ @Test public void toAsciiArmoredString() throws IOException { - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); + OpenPGPCertificate certificate = secretKey.toCertificate(); - String asciiArmoredSecretKey = PGPainless.asciiArmor(secretKey); - String asciiArmoredCertificate = PGPainless.asciiArmor(certificate); + String asciiArmoredSecretKey = secretKey.toAsciiArmoredString(); + String asciiArmoredCertificate = certificate.toAsciiArmoredString(); assertTrue(asciiArmoredSecretKey.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----")); assertTrue(asciiArmoredCertificate.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")); @@ -90,7 +88,8 @@ public class ModifyKeys { */ @Test public void changePassphrase() throws PGPException { - secretKey = PGPainless.modifyKeyRing(secretKey) + PGPainless api = PGPainless.getInstance(); + secretKey = api.modify(secretKey) .changePassphraseFromOldPassphrase(Passphrase.fromPassword(originalPassphrase)) .withSecureDefaultSettings() .toNewPassphrase(Passphrase.fromPassword("n3wP4ssW0rD")) @@ -98,9 +97,9 @@ public class ModifyKeys { // Old passphrase no longer works assertThrows(WrongPassphraseException.class, () -> - UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(), Passphrase.fromPassword(originalPassphrase))); + UnlockSecretKey.unlockSecretKey(secretKey.getPGPSecretKeyRing().getSecretKey(), Passphrase.fromPassword(originalPassphrase))); // But the new one does - UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(), Passphrase.fromPassword("n3wP4ssW0rD")); + UnlockSecretKey.unlockSecretKey(secretKey.getPGPSecretKeyRing().getSecretKey(), Passphrase.fromPassword("n3wP4ssW0rD")); } /** @@ -109,7 +108,8 @@ public class ModifyKeys { */ @Test public void changeSingleSubkeyPassphrase() throws PGPException { - secretKey = PGPainless.modifyKeyRing(secretKey) + PGPainless api = PGPainless.getInstance(); + secretKey = api.modify(secretKey) // Here we change the passphrase of the encryption subkey .changeSubKeyPassphraseFromOldPassphrase(encryptionSubkeyId, Passphrase.fromPassword(originalPassphrase)) .withSecureDefaultSettings() @@ -119,12 +119,12 @@ public class ModifyKeys { // encryption key can now only be unlocked using the new passphrase assertThrows(WrongPassphraseException.class, () -> UnlockSecretKey.unlockSecretKey( - secretKey.getSecretKey(encryptionSubkeyId), Passphrase.fromPassword(originalPassphrase))); + secretKey.getSecretKey(encryptionSubkeyId).getPGPSecretKey(), Passphrase.fromPassword(originalPassphrase))); UnlockSecretKey.unlockSecretKey( - secretKey.getSecretKey(encryptionSubkeyId), Passphrase.fromPassword("cryptP4ssphr4s3")); + secretKey.getSecretKey(encryptionSubkeyId).getPGPSecretKey(), Passphrase.fromPassword("cryptP4ssphr4s3")); // primary key remains unchanged UnlockSecretKey.unlockSecretKey( - secretKey.getSecretKey(primaryKeyId), Passphrase.fromPassword(originalPassphrase)); + secretKey.getSecretKey(primaryKeyId).getPGPSecretKey(), Passphrase.fromPassword(originalPassphrase)); } /** @@ -132,13 +132,14 @@ public class ModifyKeys { */ @Test public void addUserId() throws PGPException { + PGPainless api = PGPainless.getInstance(); SecretKeyRingProtector protector = SecretKeyRingProtector.unlockEachKeyWith(Passphrase.fromPassword(originalPassphrase), secretKey); - secretKey = PGPainless.modifyKeyRing(secretKey) + secretKey = api.modify(secretKey) .addUserId("additional@user.id", protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + KeyRingInfo info = api.inspect(secretKey); assertTrue(info.isUserIdValid("additional@user.id")); assertFalse(info.isUserIdValid("another@user.id")); } @@ -147,22 +148,23 @@ public class ModifyKeys { * This example demonstrates how to add an additional subkey to an existing key. * Prerequisites are a {@link SecretKeyRingProtector} that is capable of unlocking the primary key of the existing key, * and a {@link Passphrase} for the new subkey. - * + *

* There are two ways to add a subkey into an existing key; * Either the subkey gets generated on the fly (see below), * or the subkey already exists. In the latter case, the user has to provide * {@link org.bouncycastle.openpgp.PGPSignatureSubpacketVector PGPSignatureSubpacketVectors} for the binding signature * manually. - * + *

* Once the subkey is added, it can be decrypted using the provided subkey passphrase. */ @Test - public void addSubkey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void addSubkey() { + PGPainless api = PGPainless.getInstance(); // Protector for unlocking the existing secret key SecretKeyRingProtector protector = SecretKeyRingProtector.unlockEachKeyWith(Passphrase.fromPassword(originalPassphrase), secretKey); Passphrase subkeyPassphrase = Passphrase.fromPassword("subk3yP4ssphr4s3"); - secretKey = PGPainless.modifyKeyRing(secretKey) + secretKey = api.modify(secretKey) .addSubKey( KeySpec.getBuilder(KeyType.ECDH(EllipticCurve._BRAINPOOLP512R1), KeyFlag.ENCRYPT_COMMS) .build(), @@ -170,12 +172,15 @@ public class ModifyKeys { protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + KeyRingInfo info = api.inspect(secretKey); assertEquals(4, info.getSecretKeys().size()); assertEquals(4, info.getPublicKeys().size()); - List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.COMMUNICATIONS); + List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.COMMUNICATIONS); assertEquals(2, encryptionSubkeys.size()); - UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(encryptionSubkeys.get(1).getKeyID()), subkeyPassphrase); + OpenPGPCertificate.OpenPGPComponentKey addedKey = encryptionSubkeys.stream() + .filter(it -> !it.getKeyIdentifier().matchesExplicit(encryptionSubkeyId)).findFirst() + .get(); + UnlockSecretKey.unlockSecretKey(secretKey.getSecretKey(addedKey.getKeyIdentifier()).getPGPSecretKey(), subkeyPassphrase); } /** @@ -183,16 +188,17 @@ public class ModifyKeys { * The provided expiration date will be set on each user-id certification signature. */ @Test - public void setKeyExpirationDate() throws PGPException { + public void setKeyExpirationDate() { + PGPainless api = PGPainless.getInstance(); Date expirationDate = DateUtil.parseUTCDate("2030-06-24 12:44:56 UTC"); SecretKeyRingProtector protector = SecretKeyRingProtector .unlockEachKeyWith(Passphrase.fromPassword(originalPassphrase), secretKey); - secretKey = PGPainless.modifyKeyRing(secretKey) + secretKey = api.modify(secretKey) .setExpirationDate(expirationDate, protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + KeyRingInfo info = api.inspect(secretKey); assertEquals(DateUtil.formatUTCDate(expirationDate), DateUtil.formatUTCDate(info.getPrimaryKeyExpirationDate())); assertEquals(DateUtil.formatUTCDate(expirationDate), @@ -206,19 +212,20 @@ public class ModifyKeys { */ @Test public void revokeUserId() throws PGPException { + PGPainless api = PGPainless.getInstance(); SecretKeyRingProtector protector = SecretKeyRingProtector.unlockEachKeyWith( Passphrase.fromPassword(originalPassphrase), secretKey); - secretKey = PGPainless.modifyKeyRing(secretKey) + secretKey = api.modify(secretKey) .addUserId("alcie@pgpainless.org", protector) .done(); // Initially the user-id is valid - assertTrue(PGPainless.inspectKeyRing(secretKey).isUserIdValid("alcie@pgpainless.org")); + assertTrue(api.inspect(secretKey).isUserIdValid("alcie@pgpainless.org")); // Revoke the second user-id - secretKey = PGPainless.modifyKeyRing(secretKey) + secretKey = api.modify(secretKey) .revokeUserId("alcie@pgpainless.org", protector) .done(); // Now the user-id is no longer valid - assertFalse(PGPainless.inspectKeyRing(secretKey).isUserIdValid("alcie@pgpainless.org")); + assertFalse(api.inspect(secretKey).isUserIdValid("alcie@pgpainless.org")); } } 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 54e00c26..bf67086c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ReadKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ReadKeys.java @@ -7,11 +7,10 @@ package org.pgpainless.example; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; +import java.util.List; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.OpenPgpFingerprint; @@ -25,6 +24,7 @@ public class ReadKeys { */ @Test public void readCertificate() throws IOException { + PGPainless api = PGPainless.getInstance(); String certificate = "" + "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "Comment: Alice's OpenPGP certificate\n" + @@ -41,10 +41,9 @@ public class ReadKeys { "=iIGO\n" + "-----END PGP PUBLIC KEY BLOCK-----"; - PGPPublicKeyRing publicKey = PGPainless.readKeyRing() - .publicKeyRing(certificate); + OpenPGPCertificate publicKey = api.readKey().parseCertificate(certificate); - KeyRingInfo keyInfo = new KeyRingInfo(publicKey); + KeyRingInfo keyInfo = api.inspect(publicKey); OpenPgpFingerprint fingerprint = new OpenPgpV4Fingerprint("EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E"); assertEquals(fingerprint, keyInfo.getFingerprint()); assertEquals("Alice Lovelace ", keyInfo.getPrimaryUserId()); @@ -55,6 +54,7 @@ public class ReadKeys { */ @Test public void readSecretKey() throws IOException { + PGPainless api = PGPainless.getInstance(); String key = "\n" + "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Comment: Alice's OpenPGP Transferable Secret Key\n" + @@ -73,10 +73,9 @@ public class ReadKeys { "=n8OM\n" + "-----END PGP PRIVATE KEY BLOCK-----"; - PGPSecretKeyRing secretKey = PGPainless.readKeyRing() - .secretKeyRing(key); + OpenPGPKey secretKey = api.readKey().parseKey(key); - KeyRingInfo keyInfo = new KeyRingInfo(secretKey); + KeyRingInfo keyInfo = api.inspect(secretKey); OpenPgpFingerprint fingerprint = new OpenPgpV4Fingerprint("EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E"); assertEquals(fingerprint, keyInfo.getFingerprint()); assertEquals("Alice Lovelace ", keyInfo.getPrimaryUserId()); @@ -84,12 +83,13 @@ public class ReadKeys { /** * This example demonstrates how to read a collection of multiple OpenPGP public keys (certificates) at once. - * + *

* Note, that a public key collection can both be a concatenation of public key blocks (like below), * and a single public key block containing multiple public key packets. */ @Test - public void readKeyRingCollection() throws PGPException, IOException { + public void readKeyRingCollection() throws IOException { + PGPainless api = PGPainless.getInstance(); String certificateCollection = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "Comment: Alice's OpenPGP certificate\n" + "\n" + @@ -148,8 +148,7 @@ public class ReadKeys { "=NXei\n" + "-----END PGP PUBLIC KEY BLOCK-----"; - PGPPublicKeyRingCollection collection = PGPainless.readKeyRing() - .publicKeyRingCollection(certificateCollection); + List collection = api.readKey().parseKeysOrCertificates(certificateCollection); assertEquals(2, collection.size()); } } 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 06228c75..fbef5b40 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Sign.java @@ -12,13 +12,11 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPSignature; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -28,18 +26,19 @@ 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.SubkeyIdentifier; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.ArmorUtils; public class Sign { - private static PGPSecretKeyRing secretKey; + private static OpenPGPKey key; private static SecretKeyRingProtector protector; + private static final PGPainless api = PGPainless.getInstance(); @BeforeAll - public static void prepare() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - secretKey = PGPainless.generateKeyRing().modernKeyRing("Emilia Example "); + public static void prepare() { + key = api.generateKey() + .modernKeyRing("Emilia Example "); protector = SecretKeyRingProtector.unprotectedKeys(); // no password } @@ -52,10 +51,10 @@ public class Sign { String message = "\"Derivative Works\" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof."; InputStream messageIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream signedOut = new ByteArrayOutputStream(); - EncryptionStream signingStream = PGPainless.encryptAndOrSign() + EncryptionStream signingStream = api.generateMessage() .onOutputStream(signedOut) - .withOptions(ProducerOptions.sign(SigningOptions.get() - .addSignature(protector, secretKey)) + .withOptions(ProducerOptions.sign(SigningOptions.get(api) + .addSignature(protector, key)) ); Streams.pipeAll(messageIn, signingStream); @@ -70,7 +69,7 @@ public class Sign { /** * Demonstration of how to create a detached signature for a message. * A detached signature can be distributed alongside the message/file itself. - * + *

* The message/file doesn't need to be altered for detached signature creation. */ @Test @@ -82,10 +81,10 @@ public class Sign { // After signing, you want to distribute the original value of 'message' along with the 'detachedSignature' // from below. ByteArrayOutputStream ignoreMe = new ByteArrayOutputStream(); - EncryptionStream signingStream = PGPainless.encryptAndOrSign() + EncryptionStream signingStream = api.generateMessage() .onOutputStream(ignoreMe) - .withOptions(ProducerOptions.sign(SigningOptions.get() - .addDetachedSignature(protector, secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) + .withOptions(ProducerOptions.sign(SigningOptions.get(api) + .addDetachedSignature(protector, key, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) .setAsciiArmor(false) ); @@ -94,9 +93,9 @@ public class Sign { EncryptionResult result = signingStream.getResult(); - PGPPublicKey signingKey = PGPainless.inspectKeyRing(secretKey).getSigningSubkeys().get(0); - PGPSignature signature = result.getDetachedSignatures().get(new SubkeyIdentifier(secretKey, signingKey.getKeyID())).iterator().next(); - String detachedSignature = ArmorUtils.toAsciiArmoredString(signature.getEncoded()); + OpenPGPCertificate.OpenPGPComponentKey signingKey = api.inspect(key).getSigningSubkeys().get(0); + OpenPGPSignature.OpenPGPDocumentSignature signature = result.getDetachedDocumentSignatures().getSignaturesBy(signingKey).get(0); + String detachedSignature = ArmorUtils.toAsciiArmoredString(signature.getSignature().getEncoded()); assertTrue(detachedSignature.startsWith("-----BEGIN PGP SIGNATURE-----")); @@ -126,10 +125,10 @@ public class Sign { "limitations under the License."; InputStream messageIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream signedOut = new ByteArrayOutputStream(); - EncryptionStream signingStream = PGPainless.encryptAndOrSign() + EncryptionStream signingStream = api.generateMessage() .onOutputStream(signedOut) - .withOptions(ProducerOptions.sign(SigningOptions.get() - .addDetachedSignature(protector, secretKey, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) // Human-readable text document + .withOptions(ProducerOptions.sign(SigningOptions.get(api) + .addDetachedSignature(protector, key, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)) // Human-readable text document .setCleartextSigned() // <- Explicitly use Cleartext Signature Framework!!! ); 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 92387978..cb2908dd 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/UnlockSecretKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/UnlockSecretKeys.java @@ -6,9 +6,10 @@ package org.pgpainless.example; import java.io.IOException; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.OpenPgpV4Fingerprint; @@ -22,11 +23,11 @@ import org.pgpainless.util.Passphrase; * {@link PGPSecretKey PGPSecretKeys} are often password protected to prevent unauthorized access. * To perform certain actions with secret keys, such as creating signatures or decrypting encrypted messages, * the secret key needs to be unlocked to access the underlying {@link org.bouncycastle.openpgp.PGPPrivateKey}. - * + *

* Providing the required {@link org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor}/{@link org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor} * is a task that needs to be performed by the {@link SecretKeyRingProtector}. * There are different implementations available that implement this interface. - * + *

* Below are some examples of how to use these implementations in different scenarios. */ public class UnlockSecretKeys { @@ -36,7 +37,7 @@ public class UnlockSecretKeys { */ @Test public void unlockUnprotectedKeys() throws PGPException, IOException { - PGPSecretKeyRing unprotectedKey = TestKeys.getJulietSecretKeyRing(); + OpenPGPKey unprotectedKey = PGPainless.getInstance().toKey(TestKeys.getJulietSecretKeyRing()); // This protector will only unlock unprotected keys SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); @@ -49,7 +50,7 @@ public class UnlockSecretKeys { */ @Test public void unlockWholeKeyWithSamePassphrase() throws PGPException, IOException { - PGPSecretKeyRing secretKey = TestKeys.getCryptieSecretKeyRing(); + OpenPGPKey secretKey = PGPainless.getInstance().toKey(TestKeys.getCryptieSecretKeyRing()); Passphrase passphrase = TestKeys.CRYPTIE_PASSPHRASE; // Unlock all subkeys in the secret key with the same passphrase @@ -91,14 +92,14 @@ public class UnlockSecretKeys { "UPPI6jsYqxEHzRGex8t971atnDAjvDiS31YN\n" + "=fTmB\n" + "-----END PGP PRIVATE KEY BLOCK-----"; - PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(pgpPrivateKeyBlock); + OpenPGPKey secretKey = PGPainless.getInstance().readKey().parseKey(pgpPrivateKeyBlock); CachingSecretKeyRingProtector protector = SecretKeyRingProtector.defaultSecretKeyRingProtector(null); // Add passphrases for subkeys via public key - protector.addPassphrase(secretKey.getPublicKey(), + protector.addPassphrase(secretKey.getPrimaryKey().getKeyIdentifier(), Passphrase.fromPassword("pr1maryK3y")); // or via subkey-id - protector.addPassphrase(3907509425258753406L, + protector.addPassphrase(new KeyIdentifier(3907509425258753406L), Passphrase.fromPassword("f1rs7subk3y")); // or via fingerprint protector.addPassphrase(new OpenPgpV4Fingerprint("DD8E1195E4B1720E7FB10EF7F60402708E75D941"), @@ -107,10 +108,10 @@ public class UnlockSecretKeys { assertProtectorUnlocksAllSecretKeys(secretKey, protector); } - private void assertProtectorUnlocksAllSecretKeys(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector) + private void assertProtectorUnlocksAllSecretKeys(OpenPGPKey key, SecretKeyRingProtector protector) throws PGPException { - for (PGPSecretKey key : secretKey) { - UnlockSecretKey.unlockSecretKey(key, protector); + for (OpenPGPKey.OpenPGPSecretKey componentKey : key.getSecretKeys().values()) { + UnlockSecretKey.unlockSecretKey(componentKey, protector); } } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/BouncycastleExportSubkeys.java b/pgpainless-core/src/test/java/org/pgpainless/key/BouncycastleExportSubkeys.java deleted file mode 100644 index f8ea991b..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/key/BouncycastleExportSubkeys.java +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -import java.security.InvalidAlgorithmParameterException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.util.Date; - -import org.bouncycastle.bcpg.CompressionAlgorithmTags; -import org.bouncycastle.bcpg.HashAlgorithmTags; -import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; -import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; -import org.bouncycastle.bcpg.sig.Features; -import org.bouncycastle.bcpg.sig.KeyFlags; -import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPKeyRingGenerator; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; -import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; -import org.pgpainless.provider.ProviderFactory; -import org.pgpainless.util.TestAllImplementations; - -public class BouncycastleExportSubkeys { - - @TestTemplate - @ExtendWith(TestAllImplementations.class) - public void testExportImport() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, PGPException { - KeyPairGenerator generator; - KeyPair pair; - - // Generate master key - - generator = KeyPairGenerator.getInstance("ECDSA", ProviderFactory.getProvider()); - generator.initialize(new ECNamedCurveGenParameterSpec("P-256")); - - pair = generator.generateKeyPair(); - PGPKeyPair pgpMasterKey = new JcaPGPKeyPair(PublicKeyAlgorithmTags.ECDSA, pair, new Date()); - - PGPSignatureSubpacketGenerator subPackets = new PGPSignatureSubpacketGenerator(); - subPackets.setKeyFlags(false, KeyFlags.AUTHENTICATION & KeyFlags.CERTIFY_OTHER & KeyFlags.SIGN_DATA); - subPackets.setPreferredCompressionAlgorithms(false, new int[] { - SymmetricKeyAlgorithmTags.AES_256, - SymmetricKeyAlgorithmTags.AES_128, - SymmetricKeyAlgorithmTags.AES_128}); - subPackets.setPreferredHashAlgorithms(false, new int[] { - HashAlgorithmTags.SHA512, - HashAlgorithmTags.SHA384, - HashAlgorithmTags.SHA256, - HashAlgorithmTags.SHA224}); - subPackets.setPreferredCompressionAlgorithms(false, new int[] { - CompressionAlgorithmTags.ZLIB, - CompressionAlgorithmTags.BZIP2, - CompressionAlgorithmTags.ZIP, - CompressionAlgorithmTags.UNCOMPRESSED}); - subPackets.setFeature(false, Features.FEATURE_MODIFICATION_DETECTION); - - // Generate sub key - - generator = KeyPairGenerator.getInstance("ECDH", ProviderFactory.getProvider()); - generator.initialize(new ECNamedCurveGenParameterSpec("P-256")); - - pair = generator.generateKeyPair(); - PGPKeyPair pgpSubKey = new JcaPGPKeyPair(PublicKeyAlgorithmTags.ECDH, pair, new Date()); - - // Assemble key - - PGPDigestCalculator calculator = new JcaPGPDigestCalculatorProviderBuilder() - .setProvider(ProviderFactory.getProvider()) - .build() - .get(HashAlgorithmTags.SHA1); - - PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder( - pgpMasterKey.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA512) - .setProvider(ProviderFactory.getProvider()); - - PGPKeyRingGenerator pgpGenerator = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION, - pgpMasterKey, "alice@wonderland.lit", calculator, subPackets.generate(), null, - signerBuilder, null); - - // Add sub key - - subPackets.setKeyFlags(false, KeyFlags.ENCRYPT_STORAGE & KeyFlags.ENCRYPT_COMMS); - - pgpGenerator.addSubKey(pgpSubKey, subPackets.generate(), null); - - // Generate SecretKeyRing - - PGPSecretKeyRing secretKeys = pgpGenerator.generateSecretKeyRing(); - PGPPublicKeyRing publicKeys = pgpGenerator.generatePublicKeyRing(); - - // Test - - /* - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(2048); - outputStream.write(secretKeys.getEncoded()); - - PGPPublicKeyRing publicKeys = new PGPPublicKeyRing(outputStream.toByteArray(), new BcKeyFingerprintCalculator()); - - Iterator iterator = secretKeys.getPublicKeys(); - while (iterator.hasNext()) { - assertNotNull(publicKeys.getPublicKey(iterator.next().getKeyID())); - } - */ - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/ImportExportKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/ImportExportKeyTest.java index 52dac288..90a3e2a4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/ImportExportKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/ImportExportKeyTest.java @@ -12,10 +12,10 @@ import java.io.IOException; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPImplementation; import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.util.TestAllImplementations; public class ImportExportKeyTest { @@ -29,7 +29,7 @@ public class ImportExportKeyTest { public void testExportImportPublicKeyRing() throws IOException { PGPPublicKeyRing publicKeys = TestKeys.getJulietPublicKeyRing(); - KeyFingerPrintCalculator calc = ImplementationFactory.getInstance().getKeyFingerprintCalculator(); + KeyFingerPrintCalculator calc = OpenPGPImplementation.getInstance().keyFingerPrintCalculator(); byte[] bytes = publicKeys.getEncoded(); PGPPublicKeyRing parsed = new PGPPublicKeyRing(bytes, calc); assertArrayEquals(publicKeys.getEncoded(), parsed.getEncoded()); @@ -40,7 +40,7 @@ public class ImportExportKeyTest { public void testExportImportSecretKeyRing() throws IOException, PGPException { PGPSecretKeyRing secretKeys = TestKeys.getRomeoSecretKeyRing(); - KeyFingerPrintCalculator calc = ImplementationFactory.getInstance().getKeyFingerprintCalculator(); + KeyFingerPrintCalculator calc = OpenPGPImplementation.getInstance().keyFingerPrintCalculator(); byte[] bytes = secretKeys.getEncoded(); PGPSecretKeyRing parsed = new PGPSecretKeyRing(bytes, calc); assertArrayEquals(secretKeys.getEncoded(), parsed.getEncoded()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/SelfCertifyingRevocationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/SelfCertifyingRevocationTest.java index 5923f352..0631526a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/SelfCertifyingRevocationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/SelfCertifyingRevocationTest.java @@ -10,7 +10,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; @@ -162,7 +161,7 @@ public class SelfCertifyingRevocationTest { } @Test - public void mergeCertificatesResultsInRevokedKey() throws IOException, PGPException { + public void mergeCertificatesResultsInRevokedKey() throws IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); assertNotNull(secretKeys); PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/TestKeys.java b/pgpainless-core/src/test/java/org/pgpainless/key/TestKeys.java index cd4c7319..38ba2a2d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/TestKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/TestKeys.java @@ -14,8 +14,11 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.pgpainless.PGPainless; import org.pgpainless.util.Passphrase; public class TestKeys { @@ -265,6 +268,10 @@ public class TestKeys { "=1d67\n" + "-----END PGP PRIVATE KEY BLOCK-----"; + public static OpenPGPKey getJulietKey() throws PGPException, IOException { + return PGPainless.getInstance().toKey(getJulietSecretKeyRing()); + } + public static PGPSecretKeyRing getJulietSecretKeyRing() throws IOException, PGPException { if (julietSecretKeyRing == null) { julietSecretKeyRing = new PGPSecretKeyRing( @@ -281,6 +288,10 @@ public class TestKeys { return julietSecretKeyRingCollection; } + public static OpenPGPCertificate getJulietCertificate() throws IOException { + return PGPainless.getInstance().toCertificate(getJulietPublicKeyRing()); + } + public static PGPPublicKeyRing getJulietPublicKeyRing() throws IOException { if (julietPublicKeyRing == null) { julietPublicKeyRing = new PGPPublicKeyRing( @@ -297,6 +308,10 @@ public class TestKeys { return julietPublicKeyRingCollection; } + public static OpenPGPKey getRomeoKey() throws PGPException, IOException { + return PGPainless.getInstance().toKey(getRomeoSecretKeyRing()); + } + public static PGPSecretKeyRing getRomeoSecretKeyRing() throws IOException, PGPException { if (romeoSecretKeyRing == null) { romeoSecretKeyRing = new PGPSecretKeyRing( @@ -313,6 +328,10 @@ public class TestKeys { return romeoSecretKeyRingCollection; } + public static OpenPGPCertificate getRomeoCertificate() throws IOException { + return PGPainless.getInstance().toCertificate(getRomeoPublicKeyRing()); + } + public static PGPPublicKeyRing getRomeoPublicKeyRing() throws IOException { if (romeoPublicKeyRing == null) { romeoPublicKeyRing = new PGPPublicKeyRing( @@ -329,6 +348,10 @@ public class TestKeys { return romeoPublicKeyRingCollection; } + public static OpenPGPKey getEmilKey() throws PGPException, IOException { + return PGPainless.getInstance().toKey(getEmilSecretKeyRing()); + } + public static PGPSecretKeyRing getEmilSecretKeyRing() throws IOException, PGPException { if (emilSecretKeyRing == null) { emilSecretKeyRing = new PGPSecretKeyRing( @@ -345,6 +368,10 @@ public class TestKeys { return emilSecretKeyRingCollection; } + public static OpenPGPCertificate getEmilCertificate() throws IOException { + return PGPainless.getInstance().toCertificate(getEmilPublicKeyRing()); + } + public static PGPPublicKeyRing getEmilPublicKeyRing() throws IOException { if (emilPublicKeyRing == null) { emilPublicKeyRing = new PGPPublicKeyRing( @@ -361,6 +388,10 @@ public class TestKeys { return emilPublicKeyRingCollection; } + public static OpenPGPKey getCryptieKey() throws PGPException, IOException { + return PGPainless.getInstance().toKey(getCryptieSecretKeyRing()); + } + public static PGPSecretKeyRing getCryptieSecretKeyRing() throws IOException, PGPException { if (cryptieSecretKeyRing == null) { cryptieSecretKeyRing = new PGPSecretKeyRing( @@ -377,6 +408,10 @@ public class TestKeys { return cryptieSecretKeyRingCollection; } + public static OpenPGPCertificate getCryptieCertificate() throws IOException { + return PGPainless.getInstance().toCertificate(getCryptiePublicKeyRing()); + } + public static PGPPublicKeyRing getCryptiePublicKeyRing() throws IOException { if (cryptiePublicKeyRing == null) { cryptiePublicKeyRing = new PGPPublicKeyRing( diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/TestMergeCertificate.java b/pgpainless-core/src/test/java/org/pgpainless/key/TestMergeCertificate.java index 34861a5c..eee86a1b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/TestMergeCertificate.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/TestMergeCertificate.java @@ -4,7 +4,6 @@ package org.pgpainless.key; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; @@ -68,7 +67,7 @@ public class TestMergeCertificate { "-----END PGP SIGNATURE-----"; @Test - public void testRevocationStateWithDifferentRevocationsMerged() throws IOException, PGPException { + public void testRevocationStateWithDifferentRevocationsMerged() throws IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); 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 1290ad9c..2b1bd895 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -19,7 +19,7 @@ public class UserIdTest { public void testFormatOnlyName() { assertEquals( "Juliet Capulet", - UserId.newBuilder().withName("Juliet Capulet") + UserId.builder().withName("Juliet Capulet") .build().toString()); } @@ -27,7 +27,7 @@ public class UserIdTest { public void testFormatNameAndComment() { assertEquals( "Juliet Capulet (from the play)", - UserId.newBuilder().withName("Juliet Capulet") + UserId.builder().withName("Juliet Capulet") .withComment("from the play") .noEmail().build().toString()); } @@ -35,7 +35,7 @@ public class UserIdTest { @Test public void testFormatNameCommentAndMail() { assertEquals("Juliet Capulet (from the play) ", - UserId.newBuilder().withName("Juliet Capulet") + UserId.builder().withName("Juliet Capulet") .withComment("from the play") .withEmail("juliet@capulet.lit") .build() @@ -45,7 +45,7 @@ public class UserIdTest { @Test public void testFormatNameAndEmail() { assertEquals("Juliet Capulet ", - UserId.newBuilder().withName("Juliet Capulet") + UserId.builder().withName("Juliet Capulet") .noComment() .withEmail("juliet@capulet.lit") .build() @@ -60,7 +60,7 @@ public class UserIdTest { @Test void testBuilderWithName() { - final UserId userId = UserId.newBuilder().withName("John Smith").build(); + final UserId userId = UserId.builder().withName("John Smith").build(); assertEquals("John Smith", userId.getName()); assertNull(userId.getComment()); assertNull(userId.getEmail()); @@ -68,7 +68,7 @@ public class UserIdTest { @Test void testBuilderWithComment() { - final UserId userId = UserId.newBuilder().withComment("Sales Dept.").build(); + final UserId userId = UserId.builder().withComment("Sales Dept.").build(); assertNull(userId.getName()); assertEquals("Sales Dept.", userId.getComment()); assertNull(userId.getEmail()); @@ -76,7 +76,7 @@ public class UserIdTest { @Test void testBuilderWithEmail() { - final UserId userId = UserId.newBuilder().withEmail("john.smith@example.com").build(); + final UserId userId = UserId.builder().withEmail("john.smith@example.com").build(); assertNull(userId.getName()); assertNull(userId.getComment()); assertEquals("john.smith@example.com", userId.getEmail()); @@ -84,7 +84,7 @@ public class UserIdTest { @Test void testBuilderWithAll() { - final UserId userId = UserId.newBuilder().withEmail("john.smith@example.com") + final UserId userId = UserId.builder().withEmail("john.smith@example.com") .withName("John Smith") .withEmail("john.smith@example.com") .withComment("Sales Dept.").build(); @@ -95,7 +95,7 @@ public class UserIdTest { @Test void testBuilderNoName() { - final UserId.Builder builder = UserId.newBuilder() + final UserId.Builder builder = UserId.builder() .withEmail("john.smith@example.com") .withName("John Smith") .withComment("Sales Dept.").build().toBuilder(); @@ -107,7 +107,7 @@ public class UserIdTest { @Test void testBuilderNoComment() { - final UserId.Builder builder = UserId.newBuilder() + final UserId.Builder builder = UserId.builder() .withEmail("john.smith@example.com") .withName("John Smith") .withComment("Sales Dept.").build().toBuilder(); @@ -119,7 +119,7 @@ public class UserIdTest { @Test void testBuilderNoEmail() { - final UserId.Builder builder = UserId.newBuilder() + final UserId.Builder builder = UserId.builder() .withEmail("john.smith@example.com") .withName("John Smith") .withComment("Sales Dept.").build().toBuilder(); @@ -143,7 +143,7 @@ public class UserIdTest { @Test void testEmptyNameAndEmptyCommentAndValidEmailFormatting() { - final UserId userId = UserId.newBuilder() + final UserId userId = UserId.builder() .withComment("") .withName("") .withEmail("john.smith@example.com") @@ -157,8 +157,8 @@ public class UserIdTest { final String comment = "Sales Dept."; final String email = "john.smith@example.com"; final String upperEmail = email.toUpperCase(); - final UserId userId1 = UserId.newBuilder().withComment(comment).withName(name).withEmail(email).build(); - final UserId userId2 = UserId.newBuilder().withComment(comment).withName(name).withEmail(upperEmail).build(); + final UserId userId1 = UserId.builder().withComment(comment).withName(name).withEmail(email).build(); + final UserId userId2 = UserId.builder().withComment(comment).withName(name).withEmail(upperEmail).build(); assertEquals(userId1, userId2); } @@ -168,8 +168,8 @@ public class UserIdTest { final String name2 = "Don Duck"; final String comment = "Sales Dept."; final String email = "john.smith@example.com"; - final UserId userId1 = UserId.newBuilder().withComment(comment).withName(name1).withEmail(email).build(); - final UserId userId2 = UserId.newBuilder().withComment(comment).withName(name2).withEmail(email).build(); + final UserId userId1 = UserId.builder().withComment(comment).withName(name1).withEmail(email).build(); + final UserId userId2 = UserId.builder().withComment(comment).withName(name2).withEmail(email).build(); assertNotEquals(userId1, userId2); } @@ -179,8 +179,8 @@ public class UserIdTest { final String comment1 = "Sales Dept."; final String comment2 = "Legal Dept."; final String email = "john.smith@example.com"; - final UserId userId1 = UserId.newBuilder().withComment(comment1).withName(name).withEmail(email).build(); - final UserId userId2 = UserId.newBuilder().withComment(comment2).withName(name).withEmail(email).build(); + final UserId userId1 = UserId.builder().withComment(comment1).withName(name).withEmail(email).build(); + final UserId userId2 = UserId.builder().withComment(comment2).withName(name).withEmail(email).build(); assertNotEquals(userId1, userId2); } @@ -220,8 +220,8 @@ public class UserIdTest { 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(); + UserId id4 = UserId.builder().withName("Alice").build(); + UserId id5 = UserId.builder().withName("Alice").withComment("Work Mail").withEmail("alice@pgpainless.org").build(); assertEquals(id3.hashCode(), id3_.hashCode()); assertNotEquals(id2.hashCode(), id3.hashCode()); @@ -250,8 +250,8 @@ public class UserIdTest { 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 id4 = UserId.builder().withName("Alice").withComment("Work Email").withEmail("Alice@Pgpainless.Org").build(); + UserId id5 = UserId.builder().withName("alice").withComment("work email").withEmail("alice@pgpainless.org").build(); UserId id6 = UserId.nameAndEmail("Bob", "bob@pgpainless.org"); Comparator c = new UserId.DefaultIgnoreCaseComparator(); 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 4b0bb9de..e5d96bc0 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java @@ -9,24 +9,22 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Collections; import org.bouncycastle.bcpg.HashAlgorithmTags; 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.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPImplementation; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.Strings; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.OpenPGPKeyVersion; import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; @@ -69,8 +67,8 @@ public class WeirdKeys { "=BlPm\n" + "-----END PGP PRIVATE KEY BLOCK-----\n"; - public static PGPSecretKeyRing getTwoCryptSubkeysKey() throws IOException { - return PGPainless.readKeyRing().secretKeyRing(TWO_CRYPT_SUBKEYS); + public static OpenPGPKey getTwoCryptSubkeysKey() throws IOException { + return PGPainless.getInstance().readKey().parseKey(TWO_CRYPT_SUBKEYS); } /** @@ -98,17 +96,18 @@ public class WeirdKeys { "=h6sT\n" + "-----END PGP PRIVATE KEY BLOCK-----\n"; - public static PGPSecretKeyRing getArchiveCommsSubkeysKey() throws IOException { - return PGPainless.readKeyRing().secretKeyRing(ARCHIVE_COMMS_SUBKEYS); + public static OpenPGPKey getArchiveCommsSubkeysKey() throws IOException { + return PGPainless.getInstance().readKey().parseKey(ARCHIVE_COMMS_SUBKEYS); } @Test public void generateCertAndTestWithNonUTF8UserId() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing nakedKey = PGPainless.generateKeyRing().modernKeyRing(null); - PGPPublicKey pubKey = nakedKey.getPublicKey(); - PGPSecretKey secKey = nakedKey.getSecretKey(); - PGPPrivateKey privKey = UnlockSecretKey.unlockSecretKey(secKey, Passphrase.emptyPassphrase()); + throws PGPException, IOException { + OpenPGPKey nakedKey = PGPainless.getInstance() + .generateKey(OpenPGPKeyVersion.v4) // v4, since we manually craft the binding sig later on + .modernKeyRing(null); + OpenPGPKey.OpenPGPSecretKey primaryKey = nakedKey.getPrimarySecretKey(); + OpenPGPKey.OpenPGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(primaryKey, Passphrase.emptyPassphrase()); // Non-UTF8 User-ID ByteArrayOutputStream bOut = new ByteArrayOutputStream(); @@ -122,15 +121,16 @@ public class WeirdKeys { assertThrows(IllegalArgumentException.class, () -> Strings.fromUTF8ByteArray(idBytes)); PGPSignatureGenerator sigGen = new PGPSignatureGenerator( - ImplementationFactory.getInstance().getPGPContentSignerBuilder( - pubKey.getAlgorithm(), - HashAlgorithmTags.SHA512)); - sigGen.init(SignatureType.GENERIC_CERTIFICATION.getCode(), privKey); + OpenPGPImplementation.getInstance().pgpContentSignerBuilder( + primaryKey.getAlgorithm(), + HashAlgorithmTags.SHA512), + primaryKey.getPGPPublicKey()); + sigGen.init(SignatureType.GENERIC_CERTIFICATION.getCode(), privateKey.getKeyPair().getPrivateKey()); // We have to manually generate the signature over the user-ID // updateWithKey() - byte[] keyBytes = pubKey.getPublicKeyPacket().getEncodedContents(); - sigGen.update((byte) 0x99); + byte[] keyBytes = primaryKey.getPGPPublicKey().getPublicKeyPacket().getEncodedContents(); + sigGen.update((byte) 0x99); // 0x99 means v4 key sigGen.update((byte) (keyBytes.length >> 8)); sigGen.update((byte) (keyBytes.length)); sigGen.update(keyBytes); @@ -144,12 +144,13 @@ public class WeirdKeys { sigGen.update(idBytes); PGPSignature signature = sigGen.generate(); - pubKey = PGPPublicKey.addCertification(pubKey, idBytes, signature); + PGPPublicKey pubKey = PGPPublicKey.addCertification(primaryKey.getPGPPublicKey(), idBytes, signature); - PGPPublicKeyRing cert = new PGPPublicKeyRing(Collections.singletonList(pubKey)); + PGPPublicKeyRing pubRing = new PGPPublicKeyRing(Collections.singletonList(pubKey)); + OpenPGPCertificate cert = PGPainless.getInstance().toCertificate(pubRing); // This might fail - KeyRingInfo info = PGPainless.inspectKeyRing(cert); + KeyRingInfo info = PGPainless.getInstance().inspect(cert); assertTrue(info.getUserIds().isEmpty()); // Malformed ID is ignored } } 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 a38fa61d..370a0157 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/_64DigitFingerprintTest.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.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -19,7 +20,7 @@ public class _64DigitFingerprintTest { String prettyPrint = "76543210 ABCDEFAB 01AB23CD 1C0FFEE1 1EEFF0C1 DC32BA10 BAFEDCBA 01234567"; OpenPgpFingerprint parsed = OpenPgpFingerprint.parse(prettyPrint); - assertTrue(parsed instanceof _64DigitFingerprint); + assertInstanceOf(_64DigitFingerprint.class, parsed); assertEquals(prettyPrint, parsed.prettyPrint()); assertEquals(-1, parsed.getVersion()); } @@ -30,7 +31,7 @@ public class _64DigitFingerprintTest { byte[] binary = Hex.decode(hex); OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); - assertTrue(fingerprint instanceof _64DigitFingerprint); + assertInstanceOf(_64DigitFingerprint.class, fingerprint); assertEquals(hex, fingerprint.toString()); OpenPgpV5Fingerprint v5 = new OpenPgpV5Fingerprint(binary); 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 eb0069f5..c0361f89 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 @@ -10,16 +10,15 @@ 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.bcpg.sig.TrustSignature; 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.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPSignature; import org.bouncycastle.util.Arrays; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -28,138 +27,143 @@ 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; + +import javax.annotation.Nonnull; public class CertifyCertificateTest { @Test - public void testUserIdCertification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void testUserIdCertification() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice "); + OpenPGPKey alice = api.generateKey().modernKeyRing("Alice "); String bobUserId = "Bob "; - PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing(bobUserId); + OpenPGPKey bob = api.generateKey().modernKeyRing(bobUserId); - PGPPublicKeyRing bobCertificate = PGPainless.extractCertificate(bob); + OpenPGPCertificate bobCertificate = bob.toCertificate(); - CertifyCertificate.CertificationResult result = PGPainless.certify() - .userIdOnCertificate(bobUserId, bobCertificate) + CertifyCertificate.CertificationResult result = api.generateCertification() + .certifyUserId(bobUserId, bobCertificate) .withKey(alice, protector) .build(); assertNotNull(result); - PGPSignature signature = result.getCertification(); + PGPSignature signature = result.getPgpSignature(); assertNotNull(signature); - assertEquals(SignatureType.GENERIC_CERTIFICATION, SignatureType.valueOf(signature.getSignatureType())); - assertEquals(alice.getPublicKey().getKeyID(), signature.getKeyID()); + assertEquals(SignatureType.GENERIC_CERTIFICATION, SignatureType.requireFromCode(signature.getSignatureType())); + assertEquals(alice.getPrimaryKey().getPGPPublicKey().getKeyID(), signature.getKeyID()); - assertTrue(SignatureVerifier.verifyUserIdCertification( - bobUserId, signature, alice.getPublicKey(), bob.getPublicKey(), PGPainless.getPolicy(), DateUtil.now())); + assertTrue(result.getCertifiedCertificate().getUserId("Bob ").getCertificationBy(alice).isValid()); - PGPPublicKeyRing bobCertified = result.getCertifiedCertificate(); - PGPPublicKey bobCertifiedKey = bobCertified.getPublicKey(); + OpenPGPCertificate bobCertified = result.getCertifiedCertificate(); + PGPPublicKey bobCertifiedKey = bobCertified.getPrimaryKey().getPGPPublicKey(); // 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())); + bobCertifiedKey.getSignaturesForKeyID(alice.getPrimaryKey().getPGPPublicKey().getKeyID())); assertEquals(1, sigsByAlice.size()); assertEquals(signature, sigsByAlice.get(0)); - assertFalse(Arrays.areEqual(bobCertificate.getEncoded(), bobCertified.getEncoded())); + assertFalse(Arrays.areEqual(bobCertificate.getPGPPublicKeyRing().getEncoded(), bobCertified.getPGPPublicKeyRing().getEncoded())); } @Test - public void testKeyDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void testKeyDelegation() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice "); - PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("Bob "); + OpenPGPKey alice = api.generateKey().modernKeyRing("Alice "); + OpenPGPKey bob = api.generateKey().modernKeyRing("Bob "); - PGPPublicKeyRing bobCertificate = PGPainless.extractCertificate(bob); + OpenPGPCertificate bobCertificate = bob.toCertificate(); - CertifyCertificate.CertificationResult result = PGPainless.certify() - .certificate(bobCertificate, Trustworthiness.fullyTrusted().introducer()) + CertifyCertificate.CertificationResult result = api.generateCertification() + .delegateTrust(bobCertificate, Trustworthiness.fullyTrusted().introducer()) .withKey(alice, protector) .build(); assertNotNull(result); - PGPSignature signature = result.getCertification(); + OpenPGPSignature signature = result.getCertification(); + PGPSignature pgpSignature = signature.getSignature(); assertNotNull(signature); - assertEquals(SignatureType.DIRECT_KEY, SignatureType.valueOf(signature.getSignatureType())); - assertEquals(alice.getPublicKey().getKeyID(), signature.getKeyID()); - TrustSignature trustSignaturePacket = signature.getHashedSubPackets().getTrust(); + assertEquals(SignatureType.DIRECT_KEY, SignatureType.requireFromCode(pgpSignature.getSignatureType())); + assertEquals(alice.getPrimaryKey().getPGPPublicKey().getKeyID(), pgpSignature.getKeyID()); + TrustSignature trustSignaturePacket = pgpSignature.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())); + assertTrue(result.getCertifiedCertificate().getDelegationBy(alice).isValid()); - PGPPublicKeyRing bobCertified = result.getCertifiedCertificate(); - PGPPublicKey bobCertifiedKey = bobCertified.getPublicKey(); + OpenPGPCertificate bobCertified = result.getCertifiedCertificate(); + PGPPublicKey bobCertifiedKey = bobCertified.getPrimaryKey().getPGPPublicKey(); List sigsByAlice = CollectionUtils.iteratorToList( - bobCertifiedKey.getSignaturesForKeyID(alice.getPublicKey().getKeyID())); + bobCertifiedKey.getSignaturesForKeyID(alice.getPrimaryKey().getPGPPublicKey().getKeyID())); assertEquals(1, sigsByAlice.size()); - assertEquals(signature, sigsByAlice.get(0)); + assertEquals(signature.getSignature(), sigsByAlice.get(0)); - assertFalse(Arrays.areEqual(bobCertificate.getEncoded(), bobCertified.getEncoded())); + assertFalse(Arrays.areEqual(bobCertificate.getPGPPublicKeyRing().getEncoded(), bobCertified.getPGPPublicKeyRing().getEncoded())); } @Test - public void testPetNameCertification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing aliceKey = PGPainless.generateKeyRing() + public void testPetNameCertification() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey aliceKey = api.generateKey() .modernKeyRing("Alice "); - PGPSecretKeyRing bobKey = PGPainless.generateKeyRing() + OpenPGPKey bobKey = api.generateKey() .modernKeyRing("Bob "); - PGPPublicKeyRing bobCert = PGPainless.extractCertificate(bobKey); + OpenPGPCertificate bobCert = bobKey.toCertificate(); String petName = "Bobby"; - CertifyCertificate.CertificationResult result = PGPainless.certify() - .userIdOnCertificate(petName, bobCert) + CertifyCertificate.CertificationResult result = api.generateCertification() + .certifyUserId(petName, bobCert) .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) .buildWithSubpackets(new CertificationSubpackets.Callback() { @Override - public void modifyHashedSubpackets(CertificationSubpackets hashedSubpackets) { + public void modifyHashedSubpackets(@Nonnull CertificationSubpackets hashedSubpackets) { hashedSubpackets.setExportable(false); } }); - PGPSignature certification = result.getCertification(); - assertEquals(aliceKey.getPublicKey().getKeyID(), certification.getKeyID()); - assertEquals(CertificationType.GENERIC.asSignatureType().getCode(), certification.getSignatureType()); + OpenPGPSignature certification = result.getCertification(); + PGPSignature signature = certification.getSignature(); + assertEquals(aliceKey.getPrimaryKey().getPGPPublicKey().getKeyID(), signature.getKeyID()); + assertEquals(CertificationType.GENERIC.asSignatureType().getCode(), signature.getSignatureType()); - PGPPublicKeyRing certWithPetName = result.getCertifiedCertificate(); - KeyRingInfo info = PGPainless.inspectKeyRing(certWithPetName); + OpenPGPCertificate certWithPetName = result.getCertifiedCertificate(); + KeyRingInfo info = api.inspect(certWithPetName); assertTrue(info.getUserIds().contains(petName)); assertFalse(info.getValidUserIds().contains(petName)); } @Test - public void testScopedDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing aliceKey = PGPainless.generateKeyRing() + public void testScopedDelegation() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey aliceKey = api.generateKey() .modernKeyRing("Alice "); - PGPSecretKeyRing caKey = PGPainless.generateKeyRing() + OpenPGPKey caKey = api.generateKey() .modernKeyRing("CA "); - PGPPublicKeyRing caCert = PGPainless.extractCertificate(caKey); + OpenPGPCertificate caCert = caKey.toCertificate(); - CertifyCertificate.CertificationResult result = PGPainless.certify() - .certificate(caCert, Trustworthiness.fullyTrusted().introducer()) + CertifyCertificate.CertificationResult result = api.generateCertification() + .delegateTrust(caCert, Trustworthiness.fullyTrusted().introducer()) .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) .buildWithSubpackets(new CertificationSubpackets.Callback() { @Override - public void modifyHashedSubpackets(CertificationSubpackets hashedSubpackets) { + public void modifyHashedSubpackets(@Nonnull CertificationSubpackets hashedSubpackets) { hashedSubpackets.setRegularExpression("^.*<.+@example.com>.*$"); } }); - PGPSignature certification = result.getCertification(); - assertEquals(SignatureType.DIRECT_KEY.getCode(), certification.getSignatureType()); + OpenPGPSignature certification = result.getCertification(); + PGPSignature signature = certification.getSignature(); + assertEquals(SignatureType.DIRECT_KEY.getCode(), signature.getSignatureType()); assertEquals("^.*<.+@example.com>.*$", - certification.getHashedSubPackets().getRegularExpression().getRegex()); + signature.getHashedSubPackets().getRegularExpression().getRegex()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyV6CertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyV6CertificateTest.java new file mode 100644 index 00000000..a0b717f9 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyV6CertificateTest.java @@ -0,0 +1,348 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.certification; + +import org.bouncycastle.bcpg.SignaturePacket; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureException; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPSignature; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CertificationType; +import org.pgpainless.algorithm.OpenPGPKeyVersion; +import org.pgpainless.key.protection.SecretKeyRingProtector; + +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.assertTrue; + +public class CertifyV6CertificateTest { + + @Test + public void testCertifyV6UIDWithV6Key() throws PGPException { + // Alice (6) certifies Bob (6) + PGPainless api = PGPainless.getInstance(); + + OpenPGPKey aliceKey = api.generateKey(OpenPGPKeyVersion.v6) + .modernKeyRing("Alice "); + + OpenPGPKey bobKey = api.generateKey(OpenPGPKeyVersion.v6) + .modernKeyRing("Bob "); + OpenPGPCertificate bobCert = bobKey.toCertificate(); + + // Create a certification on Bobs certificate + OpenPGPCertificate bobCertified = api.generateCertification() + .certifyUserId("Bob ", bobCert, CertificationType.POSITIVE) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .build().getCertifiedCertificate(); + + // Check that there is a valid certification chain from Alice to Bobs UID + OpenPGPCertificate.OpenPGPSignatureChain certification = + bobCertified.getUserId("Bob ") + .getCertificationBy(aliceKey.toCertificate()); + assertNotNull(certification); + assertTrue(certification.isValid()); + OpenPGPSignature certificationSignature = certification.getSignature(); + assertEquals(SignaturePacket.VERSION_6, certificationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.POSITIVE_CERTIFICATION, certificationSignature.getSignature().getSignatureType()); + + + // Revoke Alice' key and... + OpenPGPKey aliceRevoked = api.modify(aliceKey) + .revoke(SecretKeyRingProtector.unprotectedKeys()) + .done(); + + // ...verify we no longer have a valid certification chain + OpenPGPCertificate.OpenPGPSignatureChain missingChain = + bobCertified.getUserId("Bob ") + .getCertificationBy(aliceRevoked.toCertificate()); + assertNull(missingChain); + + // ...but DO have a revocation chain + OpenPGPCertificate.OpenPGPSignatureChain revokedChain = + bobCertified.getUserId("Bob ") + .getRevocationBy(aliceRevoked); + assertNotNull(revokedChain); + assertTrue(revokedChain.isValid()); + OpenPGPSignature revocationSignature = revokedChain.getRevocation(); + assertEquals(SignaturePacket.VERSION_6, revocationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.KEY_REVOCATION, revocationSignature.getSignature().getSignatureType()); + + + // Instead, revoke the certification itself and... + bobCertified = api.generateCertification() + .revokeCertifiedUserId("Bob ", bobCertified) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .build().getCertifiedCertificate(); + + // ...verify we now have a valid certification chain + OpenPGPCertificate.OpenPGPSignatureChain brokenChain = + bobCertified.getUserId("Bob ") + .getRevocationBy(aliceKey.toCertificate()); + assertNotNull(brokenChain); + assertTrue(brokenChain.isValid()); + revocationSignature = brokenChain.getSignature(); + assertEquals(SignaturePacket.VERSION_6, revocationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.CERTIFICATION_REVOCATION, revocationSignature.getSignature().getSignatureType()); + } + + @Test + public void testDelegateV6CertWithV6Key() throws PGPSignatureException { + // Alice (6) delegates Bob (6) + PGPainless api = PGPainless.getInstance(); + + OpenPGPKey aliceKey = api.generateKey(OpenPGPKeyVersion.v6) + .modernKeyRing("Alice "); + OpenPGPKey bobKey = api.generateKey(OpenPGPKeyVersion.v6) + .modernKeyRing("Bob "); + OpenPGPCertificate bobCert = bobKey.toCertificate(); + + // Alice delegates trust to Bob + OpenPGPCertificate bobDelegated = api.generateCertification() + .delegateTrust(bobCert) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .build().getCertifiedCertificate(); + + // Check that Bob is actually delegated to by Alice + OpenPGPCertificate.OpenPGPSignatureChain delegation = bobDelegated.getDelegationBy(aliceKey.toCertificate()); + assertNotNull(delegation); + assertTrue(delegation.isValid()); + OpenPGPSignature delegationSignature = delegation.getSignature(); + assertEquals(SignaturePacket.VERSION_6, delegationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.DIRECT_KEY, delegationSignature.getSignature().getSignatureType()); + + // Alice revokes the delegation + OpenPGPCertificate bobRevoked = api.generateCertification() + .revokeDelegatedTrust(bobDelegated) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .build().getCertifiedCertificate(); + + OpenPGPCertificate.OpenPGPSignatureChain revocation = bobRevoked.getRevocationBy(aliceKey.toCertificate()); + assertNotNull(revocation); + assertTrue(revocation.isValid()); + OpenPGPSignature revocationSignature = revocation.getSignature(); + assertEquals(SignaturePacket.VERSION_6, revocationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.KEY_REVOCATION, revocationSignature.getSignature().getSignatureType()); + } + + @Test + public void testCertifyV4UIDWithV6Key() throws PGPException { + // Alice (6) certifies Bob (4) + PGPainless api = PGPainless.getInstance(); + + OpenPGPKey aliceKey = api.generateKey(OpenPGPKeyVersion.v6) + .modernKeyRing("Alice "); + + OpenPGPKey bobKey = api.generateKey(OpenPGPKeyVersion.v4) + .modernKeyRing("Bob "); + OpenPGPCertificate bobCert = bobKey.toCertificate(); + + // Create a certification on Bobs certificate + // Alice => "Bob" (Bob) + OpenPGPCertificate bobCertified = api.generateCertification() + .certifyUserId("Bob ", bobCert, CertificationType.CASUAL) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .build().getCertifiedCertificate(); + + // Check that there is a valid certification chain from Alice to Bobs UID + OpenPGPCertificate.OpenPGPSignatureChain signatureChain = + bobCertified.getUserId("Bob ") + .getCertificationBy(aliceKey.toCertificate()); + assertNotNull(signatureChain); + assertTrue(signatureChain.isValid()); + OpenPGPSignature certificationSignature = signatureChain.getSignature(); + assertEquals(SignaturePacket.VERSION_6, certificationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.CASUAL_CERTIFICATION, certificationSignature.getSignature().getSignatureType()); + + // Revoke Alice' key and... + OpenPGPKey aliceRevoked = api.modify(aliceKey) + .revoke(SecretKeyRingProtector.unprotectedKeys()) + .done(); + + // ...verify we no longer have a valid certification chain + OpenPGPCertificate.OpenPGPSignatureChain missingChain = + bobCertified.getUserId("Bob ") + .getCertificationBy(aliceRevoked.toCertificate()); + assertNull(missingChain); + + + // ...but DO have a revocation chain + OpenPGPCertificate.OpenPGPSignatureChain revokedChain = + bobCertified.getUserId("Bob ") + .getRevocationBy(aliceRevoked); + assertNotNull(revokedChain); + assertTrue(revokedChain.isValid()); + OpenPGPSignature revocationSignature = revokedChain.getRevocation(); + assertEquals(SignaturePacket.VERSION_6, revocationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.KEY_REVOCATION, revocationSignature.getSignature().getSignatureType()); + + + // Instead, revoke the certification itself and... + bobCertified = api.generateCertification() + .revokeCertifiedUserId("Bob ", bobCertified) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .build().getCertifiedCertificate(); + + // ...verify we now have a valid certification chain + OpenPGPCertificate.OpenPGPSignatureChain brokenChain = + bobCertified.getUserId("Bob ") + .getRevocationBy(aliceKey.toCertificate()); + assertNotNull(brokenChain); + assertTrue(brokenChain.isValid()); + revocationSignature = brokenChain.getSignature(); + assertEquals(SignaturePacket.VERSION_6, revocationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.CERTIFICATION_REVOCATION, revocationSignature.getSignature().getSignatureType()); + } + + @Test + public void testDelegateV4CertWithV6Key() throws PGPSignatureException { + PGPainless api = PGPainless.getInstance(); + + OpenPGPKey aliceKey = api.generateKey(OpenPGPKeyVersion.v6) + .modernKeyRing("Alice "); + OpenPGPKey bobKey = api.generateKey(OpenPGPKeyVersion.v6) + .modernKeyRing("Bob "); + OpenPGPCertificate bobCert = bobKey.toCertificate(); + + // Alice delegates trust to Bob + OpenPGPCertificate bobDelegated = api.generateCertification() + .delegateTrust(bobCert) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .build().getCertifiedCertificate(); + + // Check that Bob is actually delegated to by Alice + OpenPGPCertificate.OpenPGPSignatureChain delegation = bobDelegated.getDelegationBy(aliceKey.toCertificate()); + assertNotNull(delegation); + assertTrue(delegation.isValid()); + OpenPGPSignature delegationSignature = delegation.getSignature(); + assertEquals(SignaturePacket.VERSION_6, delegationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.DIRECT_KEY, delegationSignature.getSignature().getSignatureType()); + + // Alice revokes the delegation + OpenPGPCertificate bobRevoked = api.generateCertification() + .revokeDelegatedTrust(bobDelegated) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .build().getCertifiedCertificate(); + + OpenPGPCertificate.OpenPGPSignatureChain revocation = bobRevoked.getRevocationBy(aliceKey.toCertificate()); + assertNotNull(revocation); + assertTrue(revocation.isValid()); + OpenPGPSignature revocationSignature = revocation.getSignature(); + assertEquals(SignaturePacket.VERSION_6, revocationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.KEY_REVOCATION, revocationSignature.getSignature().getSignatureType()); + } + + @Test + public void testCertifyV6UIDWithV4Key() throws PGPException { + // Alice (4) certifies Bob (6) + PGPainless api = PGPainless.getInstance(); + + OpenPGPKey aliceKey = api.generateKey(OpenPGPKeyVersion.v4) + .modernKeyRing("Alice "); + + OpenPGPKey bobKey = api.generateKey(OpenPGPKeyVersion.v6) + .modernKeyRing("Bob "); + OpenPGPCertificate bobCert = bobKey.toCertificate(); + + // Create a certification on Bobs certificate + // Alice => "Bob" (Bob) + OpenPGPCertificate bobCertified = api.generateCertification() + .certifyUserId("Bob ", bobCert, CertificationType.CASUAL) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .build().getCertifiedCertificate(); + + // Check that there is a valid certification chain from Alice to Bobs UID + OpenPGPCertificate.OpenPGPSignatureChain signatureChain = + bobCertified.getUserId("Bob ") + .getCertificationBy(aliceKey.toCertificate()); + assertNotNull(signatureChain); + assertTrue(signatureChain.isValid()); + OpenPGPSignature certificationSignature = signatureChain.getSignature(); + assertEquals(SignaturePacket.VERSION_4, certificationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.CASUAL_CERTIFICATION, certificationSignature.getSignature().getSignatureType()); + + // Revoke Alice' key and... + OpenPGPKey aliceRevoked = api.modify(aliceKey) + .revoke(SecretKeyRingProtector.unprotectedKeys()) + .done(); + + // ...verify we no longer have a valid certification chain + OpenPGPCertificate.OpenPGPSignatureChain missingChain = + bobCertified.getUserId("Bob ") + .getCertificationBy(aliceRevoked.toCertificate()); + assertNull(missingChain); + + + // ...but DO have a revocation chain + OpenPGPCertificate.OpenPGPSignatureChain revokedChain = + bobCertified.getUserId("Bob ") + .getRevocationBy(aliceRevoked); + assertNotNull(revokedChain); + assertTrue(revokedChain.isValid()); + OpenPGPSignature revocationSignature = revokedChain.getRevocation(); + assertEquals(SignaturePacket.VERSION_4, revocationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.KEY_REVOCATION, revocationSignature.getSignature().getSignatureType()); + + + // Instead, revoke the certification itself and... + bobCertified = api.generateCertification() + .revokeCertifiedUserId("Bob ", bobCertified) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .build().getCertifiedCertificate(); + + // ...verify we now have a valid certification chain + OpenPGPCertificate.OpenPGPSignatureChain brokenChain = + bobCertified.getUserId("Bob ") + .getRevocationBy(aliceKey.toCertificate()); + assertNotNull(brokenChain); + assertTrue(brokenChain.isValid()); + revocationSignature = brokenChain.getSignature(); + assertEquals(SignaturePacket.VERSION_4, revocationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.CERTIFICATION_REVOCATION, revocationSignature.getSignature().getSignatureType()); + } + + @Test + public void testDelegateV6CertWithV4Key() throws PGPSignatureException { + // Alice (4) delegates Bob (6) + PGPainless api = PGPainless.getInstance(); + + OpenPGPKey aliceKey = api.generateKey(OpenPGPKeyVersion.v4) + .modernKeyRing("Alice "); + OpenPGPKey bobKey = api.generateKey(OpenPGPKeyVersion.v6) + .modernKeyRing("Bob "); + OpenPGPCertificate bobCert = bobKey.toCertificate(); + + // Alice delegates trust to Bob + OpenPGPCertificate bobDelegated = api.generateCertification() + .delegateTrust(bobCert) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .build().getCertifiedCertificate(); + + // Check that Bob is actually delegated to by Alice + OpenPGPCertificate.OpenPGPSignatureChain delegation = bobDelegated.getDelegationBy(aliceKey.toCertificate()); + assertNotNull(delegation); + assertTrue(delegation.isValid()); + OpenPGPSignature delegationSignature = delegation.getSignature(); + assertEquals(SignaturePacket.VERSION_4, delegationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.DIRECT_KEY, delegationSignature.getSignature().getSignatureType()); + + // Alice revokes the delegation + OpenPGPCertificate bobRevoked = api.generateCertification() + .revokeDelegatedTrust(bobDelegated) + .withKey(aliceKey, SecretKeyRingProtector.unprotectedKeys()) + .build().getCertifiedCertificate(); + + OpenPGPCertificate.OpenPGPSignatureChain revocation = bobRevoked.getRevocationBy(aliceKey.toCertificate()); + assertNotNull(revocation); + assertTrue(revocation.isValid()); + OpenPGPSignature revocationSignature = revocation.getSignature(); + assertEquals(SignaturePacket.VERSION_4, revocationSignature.getSignature().getVersion()); + assertEquals(PGPSignature.KEY_REVOCATION, revocationSignature.getSignature().getSignatureType()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/collection/PGPKeyRingCollectionTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/collection/PGPKeyRingCollectionTest.java index fd5530ba..769ead35 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/collection/PGPKeyRingCollectionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/collection/PGPKeyRingCollectionTest.java @@ -7,10 +7,7 @@ package org.pgpainless.key.collection; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collection; @@ -25,7 +22,7 @@ import org.pgpainless.key.util.KeyRingUtils; public class PGPKeyRingCollectionTest { @Test - public void constructorThrowsForInvalidInput() throws PGPException, IOException { + public void constructorThrowsForInvalidInput() { // This is neither a public key, nor a private key String invalidKeyRing = "-----BEGIN PGP SIGNATURE-----\n" + "\n" + @@ -56,9 +53,15 @@ public class PGPKeyRingCollectionTest { } @Test - public void testConstructorFromCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing first = PGPainless.generateKeyRing().simpleEcKeyRing("alice@wonderland.lit"); - PGPSecretKeyRing second = PGPainless.generateKeyRing().simpleEcKeyRing("bob@the-builder.tv"); + public void testConstructorFromCollection() { + PGPainless api = PGPainless.getInstance(); + PGPSecretKeyRing first = api.generateKey() + .simpleEcKeyRing("alice@wonderland.lit") + .getPGPSecretKeyRing(); + PGPSecretKeyRing second = api.generateKey() + .simpleEcKeyRing("bob@the-builder.tv") + .getPGPSecretKeyRing(); + // noinspection deprecation PGPPublicKeyRing secondPub = KeyRingUtils.publicKeyRingFrom(second); Collection keys = Arrays.asList(first, second, secondPub); 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 77023908..0192e395 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 @@ -9,12 +9,8 @@ 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.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Iterator; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.TestTemplate; @@ -22,12 +18,13 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.bouncycastle.extensions.PGPPublicKeyExtensionsKt; +import org.pgpainless.bouncycastle.extensions.PGPSecretKeyExtensionsKt; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; -import org.pgpainless.key.info.KeyInfo; import org.pgpainless.key.util.UserId; import org.pgpainless.util.Passphrase; import org.pgpainless.util.TestAllImplementations; @@ -36,8 +33,7 @@ public class BrainpoolKeyGenerationTest { @TestTemplate @ExtendWith(TestAllImplementations.class) - public void generateEcKeysTest() - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public void generateEcKeysTest() { for (EllipticCurve curve : EllipticCurve.values()) { PGPSecretKeyRing secretKeys = generateKey( @@ -51,22 +47,19 @@ public class BrainpoolKeyGenerationTest { Iterator secretKeyIterator = secretKeys.iterator(); PGPSecretKey primaryKey = secretKeyIterator.next(); - KeyInfo primaryInfo = new KeyInfo(primaryKey); - assertEquals(curve.getName(), primaryInfo.getCurveName()); - assertFalse(primaryInfo.isEncrypted()); - assertTrue(primaryInfo.isDecrypted()); - assertFalse(primaryInfo.hasDummyS2K()); + assertEquals(curve.getName(), PGPPublicKeyExtensionsKt.getCurveName(primaryKey.getPublicKey())); + assertFalse(PGPSecretKeyExtensionsKt.isEncrypted(primaryKey)); + assertTrue(PGPSecretKeyExtensionsKt.isDecrypted(primaryKey)); + assertFalse(PGPSecretKeyExtensionsKt.hasDummyS2K(primaryKey)); PGPSecretKey subKey = secretKeyIterator.next(); - KeyInfo subInfo = new KeyInfo(subKey); - assertEquals(curve.getName(), subInfo.getCurveName()); + assertEquals(curve.getName(), PGPPublicKeyExtensionsKt.getCurveName(subKey.getPublicKey())); } } @TestTemplate @ExtendWith(TestAllImplementations.class) - public void generateEdDSAKeyTest() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void generateEdDSAKeyTest() { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( @@ -78,47 +71,42 @@ public class BrainpoolKeyGenerationTest { KeyType.RSA(RsaLength._3072), KeyFlag.SIGN_DATA)) .addUserId(UserId.nameAndEmail("Alice", "alice@pgpainless.org")) .setPassphrase(Passphrase.fromPassword("passphrase")) - .build(); + .build() + .getPGPSecretKeyRing(); for (PGPSecretKey key : secretKeys) { - KeyInfo info = new KeyInfo(key); - assertTrue(info.isEncrypted()); - assertFalse(info.isDecrypted()); - - PGPPublicKey pubKey = key.getPublicKey(); - assertFalse(new KeyInfo(pubKey).isEncrypted()); - assertTrue(new KeyInfo(pubKey).isDecrypted()); - assertFalse(new KeyInfo(pubKey).hasDummyS2K()); + assertTrue(PGPSecretKeyExtensionsKt.isEncrypted(key)); + assertFalse(PGPSecretKeyExtensionsKt.isDecrypted(key)); + assertFalse(PGPSecretKeyExtensionsKt.hasDummyS2K(key)); } Iterator iterator = secretKeys.iterator(); PGPSecretKey ecdsaPrim = iterator.next(); - KeyInfo ecdsaInfo = new KeyInfo(ecdsaPrim); - assertEquals(EllipticCurve._BRAINPOOLP384R1.getName(), ecdsaInfo.getCurveName()); + assertEquals(EllipticCurve._BRAINPOOLP384R1.getName(), PGPPublicKeyExtensionsKt.getCurveName(ecdsaPrim.getPublicKey())); assertEquals(384, ecdsaPrim.getPublicKey().getBitStrength()); PGPSecretKey eddsaSub = iterator.next(); - KeyInfo eddsaInfo = new KeyInfo(eddsaSub); - assertEquals(EdDSALegacyCurve._Ed25519.getName(), eddsaInfo.getCurveName()); + assertEquals(EdDSALegacyCurve._Ed25519.getName(), PGPPublicKeyExtensionsKt.getCurveName(eddsaSub.getPublicKey())); assertEquals(256, eddsaSub.getPublicKey().getBitStrength()); PGPSecretKey xdhSub = iterator.next(); - KeyInfo xdhInfo = new KeyInfo(xdhSub); - assertEquals(XDHLegacySpec._X25519.getCurveName(), xdhInfo.getCurveName()); + assertEquals(XDHLegacySpec._X25519.getCurveName(), PGPPublicKeyExtensionsKt.getCurveName(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"); + assertThrows(IllegalArgumentException.class, + () -> PGPPublicKeyExtensionsKt.getCurveName(rsaSub.getPublicKey()), + "RSA is not a curve-based encryption system"); assertEquals(3072, rsaSub.getPublicKey().getBitStrength()); } - public PGPSecretKeyRing generateKey(KeySpec primaryKey, KeySpec subKey, String userId) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public PGPSecretKeyRing generateKey(KeySpec primaryKey, KeySpec subKey, String userId) { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(primaryKey) .addSubkey(subKey) .addUserId(userId) - .build(); + .build() + .getPGPSecretKeyRing(); return secretKeys; } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java index 4cb992db..0d63293b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java @@ -6,9 +6,6 @@ package org.pgpainless.key.generation; import static org.junit.jupiter.api.Assertions.assertEquals; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.TestTemplate; @@ -29,14 +26,15 @@ public class GenerateEllipticCurveKeyTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void generateEllipticCurveKeys() - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + throws PGPException { PGPSecretKeyRing keyRing = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS)) .addUserId(UserId.onlyEmail("alice@wonderland.lit").toString()) - .build(); + .build() + .getPGPSecretKeyRing(); assertEquals(PublicKeyAlgorithm.EDDSA_LEGACY.getAlgorithmId(), keyRing.getPublicKey().getAlgorithm()); UnlockSecretKey.unlockSecretKey(keyRing.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); 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 cf12ab57..2cdd4131 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,12 +7,9 @@ package org.pgpainless.key.generation; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.Iterator; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.JUtils; @@ -32,7 +29,7 @@ public class GenerateKeyWithAdditionalUserIdTest { @TestTemplate @ExtendWith(TestAllImplementations.class) - public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public void test() { Date now = DateUtil.now(); Date expiration = TestTimeFrameProvider.defaultExpirationForCreationDate(now); PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() @@ -45,7 +42,8 @@ public class GenerateKeyWithAdditionalUserIdTest { .addUserId(UserId.onlyEmail("additional2@user.id")) .addUserId("\ttrimThis@user.id ") .setExpirationDate(expiration) - .build(); + .build() + .getPGPSecretKeyRing(); PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys); JUtils.assertDateEquals(expiration, PGPainless.inspectKeyRing(publicKeys).getPrimaryKeyExpirationDate()); 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 0ad564db..342e060a 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 @@ -6,13 +6,10 @@ 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; import org.bouncycastle.openpgp.PGPSecretKeyRing; @@ -29,15 +26,15 @@ import org.pgpainless.util.DateUtil; public class GenerateKeyWithCustomCreationDateTest { @Test - public void generateKeyWithCustomCreationDateTest() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void generateKeyWithCustomCreationDateTest() { Date creationDate = DateUtil.parseUTCDate("2018-06-11 14:12:09 UTC"); PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) .setKeyCreationDate(creationDate)) // primary key with custom creation time .addUserId("Alice") - .build(); + .build() + .getPGPSecretKeyRing(); Iterator iterator = secretKeys.iterator(); PGPPublicKey primaryKey = iterator.next().getPublicKey(); @@ -49,7 +46,7 @@ public class GenerateKeyWithCustomCreationDateTest { } @Test - public void generateSubkeyWithFutureKeyCreationDate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void generateSubkeyWithFutureKeyCreationDate() { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.YEAR, 20); Date future = calendar.getTime(); @@ -58,7 +55,8 @@ public class GenerateKeyWithCustomCreationDateTest { .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(); + .build() + .getPGPSecretKeyRing(); // Subkey has future key creation date, so its binding will predate the key -> no usable encryption key left assertFalse(PGPainless.inspectKeyRing(secretKeys) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java index e477aeef..60d49db7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java @@ -12,12 +12,11 @@ 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.bcpg.KeyIdentifier; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -42,44 +41,45 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; public class GenerateKeyWithoutPrimaryKeyFlagsTest { @Test - public void generateKeyWithoutCertifyKeyFlag_cannotCertifyThirdParties() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing().setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519))) + public void generateKeyWithoutCertifyKeyFlag_cannotCertifyThirdParties() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey key = api.buildKey().setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519))) .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) .addUserId("Alice") .build(); - PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); + OpenPGPCertificate cert = key.toCertificate(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo info = api.inspect(key); assertTrue(info.getValidUserIds().contains("Alice")); - long primaryKeyId = info.getKeyId(); + KeyIdentifier primaryKeyIdentifier = info.getKeyIdentifier(); assertTrue(info.getKeyFlagsOf("Alice").isEmpty()); - assertTrue(info.getKeyFlagsOf(primaryKeyId).isEmpty()); + assertTrue(info.getKeyFlagsOf(primaryKeyIdentifier).isEmpty()); assertFalse(info.isUsableForThirdPartyCertification()); // Key without CERTIFY_OTHER flag cannot be used to certify other keys - PGPPublicKeyRing thirdPartyCert = TestKeys.getCryptiePublicKeyRing(); + OpenPGPCertificate thirdPartyCert = TestKeys.getCryptieCertificate(); assertThrows(KeyException.UnacceptableThirdPartyCertificationKeyException.class, () -> - PGPainless.certify().certificate(thirdPartyCert) - .withKey(secretKeys, SecretKeyRingProtector.unprotectedKeys())); + api.generateCertification().delegateTrust(thirdPartyCert) + .withKey(key, SecretKeyRingProtector.unprotectedKeys())); // Key without CERTIFY_OTHER flags is usable for encryption and signing ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(ciphertext) .withOptions(ProducerOptions.signAndEncrypt( EncryptionOptions.get().addRecipient(cert), - SigningOptions.get().addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT) + SigningOptions.get().addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), key, DocumentSignatureType.BINARY_DOCUMENT) )); encryptionStream.write("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); encryptionStream.close(); EncryptionResult result = encryptionStream.getResult(); assertTrue(result.isEncryptedFor(cert)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(new ByteArrayInputStream(ciphertext.toByteArray())) - .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeys) + .withOptions(ConsumerOptions.get().addDecryptionKey(key) .addVerificationCert(cert)); ByteArrayOutputStream plaintext = new ByteArrayOutputStream(); 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 e6a5c96a..8d0e1df1 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 @@ -5,8 +5,8 @@ package org.pgpainless.key.generation; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.JUtils; import org.junit.jupiter.api.Test; @@ -32,8 +32,6 @@ 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 java.util.List; @@ -43,17 +41,18 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class GenerateKeyWithoutUserIdTest { @Test - public void generateKeyWithoutUserId() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void generateKeyWithoutUserId() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); Date now = new Date(); Date expirationDate = TestTimeFrameProvider.defaultExpirationForCreationDate(now); - PGPSecretKeyRing secretKey = PGPainless.buildKeyRing() + OpenPGPKey secretKey = api.buildKey() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER).setKeyCreationDate(now)) .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).setKeyCreationDate(now)) .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE).setKeyCreationDate(now)) .setExpirationDate(expirationDate) .build(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + KeyRingInfo info = api.inspect(secretKey); assertNull(info.getPrimaryUserId()); assertTrue(info.getUserIds().isEmpty()); JUtils.assertDateEquals(expirationDate, info.getPrimaryKeyExpirationDate()); @@ -61,9 +60,9 @@ public class GenerateKeyWithoutUserIdTest { InputStream plaintextIn = new ByteArrayInputStream("Hello, World!\n".getBytes()); ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); + OpenPGPCertificate certificate = secretKey.toCertificate(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(ciphertextOut) .withOptions(ProducerOptions.signAndEncrypt( EncryptionOptions.get() @@ -79,7 +78,7 @@ public class GenerateKeyWithoutUserIdTest { ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray()); ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(ciphertextIn) .withOptions(ConsumerOptions.get() .addDecryptionKey(secretKey) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateV6KeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateV6KeyTest.java new file mode 100644 index 00000000..b387ef3e --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateV6KeyTest.java @@ -0,0 +1,178 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation; + +import org.bouncycastle.bcpg.SecretKeyPacket; +import org.bouncycastle.bcpg.SignatureSubpacketTags; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.SignatureParameters; +import org.bouncycastle.openpgp.operator.PGPKeyPairGenerator; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.Feature; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.OpenPGPKeyVersion; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.rsa.RsaLength; +import org.pgpainless.key.protection.KeyRingProtectionSettings; + +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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GenerateV6KeyTest { + + @Test + public void generateModernV6Key() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey key = api.generateKey(OpenPGPKeyVersion.v6) + .modernKeyRing("Alice "); + assertEquals(3, key.getKeys().size()); + + OpenPGPCertificate.OpenPGPPrimaryKey primaryKey = key.getPrimaryKey(); + assertEquals(primaryKey, key.getCertificationKeys().get(0)); + assertEquals(6, primaryKey.getVersion()); + assertEquals(PublicKeyAlgorithm.ED25519.getAlgorithmId(), + primaryKey.getAlgorithm()); + + OpenPGPCertificate.OpenPGPComponentKey signingKey = key.getSigningKeys().get(0); + assertEquals(6, signingKey.getVersion()); + assertEquals(PublicKeyAlgorithm.ED25519.getAlgorithmId(), + signingKey.getAlgorithm()); + + OpenPGPCertificate.OpenPGPComponentKey encryptionKey = key.getEncryptionKeys().get(0); + assertEquals(6, encryptionKey.getVersion()); + assertEquals(PublicKeyAlgorithm.X25519.getAlgorithmId(), + encryptionKey.getAlgorithm()); + } + + @Test + public void buildMinimalEd25519V6Key() throws PGPException { + OpenPGPKey key = PGPainless.getInstance()._buildKey(OpenPGPKeyVersion.v6) + .withPrimaryKey(PGPKeyPairGenerator::generateEd25519KeyPair, new SignatureParameters.Callback() { + @Override + public SignatureParameters apply(SignatureParameters parameters) { + return parameters.setHashedSubpacketsFunction(pgpSignatureSubpacketGenerator -> { + // TODO: Remove once https://github.com/bcgit/bc-java/pull/2013 lands + pgpSignatureSubpacketGenerator.removePacketsOfType(SignatureSubpacketTags.KEY_FLAGS); + pgpSignatureSubpacketGenerator.setKeyFlags(KeyFlag.SIGN_DATA.getFlag()); + return pgpSignatureSubpacketGenerator; + }); + } + }) + .build(); + + assertTrue(key.isSecretKey()); + + assertEquals(1, key.getKeys().size()); + OpenPGPCertificate.OpenPGPPrimaryKey primaryKey = key.getPrimaryKey(); + assertTrue(key.getCertificationKeys().isEmpty()); + assertEquals(6, primaryKey.getVersion()); + assertTrue(primaryKey.isPrimaryKey()); + assertEquals(PublicKeyAlgorithm.ED25519.getAlgorithmId(), + primaryKey.getAlgorithm()); + assertEquals(primaryKey, key.getSigningKeys().get(0)); + assertTrue(key.getEncryptionKeys().isEmpty()); + + OpenPGPKey.OpenPGPSecretKey primarySecretKey = key.getPrimarySecretKey(); + assertEquals(primarySecretKey, key.getSecretKey(primaryKey)); + } + + @Test + public void buildCompositeCurve25519V6Key() + throws PGPException, IOException { + OpenPGPKey key = PGPainless.getInstance()._buildKey(OpenPGPKeyVersion.v6) + .withPrimaryKey(PGPKeyPairGenerator::generateEd25519KeyPair) + .addSigningSubkey(PGPKeyPairGenerator::generateEd25519KeyPair) + .addEncryptionSubkey(PGPKeyPairGenerator::generateX25519KeyPair) + .addUserId("Alice ") + .build(); + + assertTrue(key.isSecretKey()); + assertEquals(3, key.getKeys().size()); + OpenPGPCertificate.OpenPGPPrimaryKey primaryKey = key.getPrimaryKey(); + assertEquals(primaryKey, key.getCertificationKeys().get(0)); + assertEquals(PublicKeyAlgorithm.ED25519.getAlgorithmId(), primaryKey.getAlgorithm()); + assertEquals(6, primaryKey.getVersion()); + assertTrue(primaryKey.isPrimaryKey()); + assertEquals(primaryKey, key.getKeys().get(0)); + + OpenPGPCertificate.OpenPGPComponentKey signingKey = key.getKeys().get(1); + assertTrue(key.getSigningKeys().contains(signingKey)); + assertEquals(6, signingKey.getVersion()); + assertEquals(PublicKeyAlgorithm.ED25519.getAlgorithmId(), signingKey.getAlgorithm()); + assertFalse(signingKey.isPrimaryKey()); + + OpenPGPCertificate.OpenPGPComponentKey encryptionKey = key.getKeys().get(2); + assertTrue(key.getEncryptionKeys().contains(encryptionKey)); + assertEquals(6, encryptionKey.getVersion()); + assertEquals(PublicKeyAlgorithm.X25519.getAlgorithmId(), encryptionKey.getAlgorithm()); + assertFalse(encryptionKey.isPrimaryKey()); + + OpenPGPCertificate certificate = key.toCertificate(); + assertFalse(certificate.isSecretKey()); + + // CHECKSTYLE:OFF + System.out.println(certificate.toAsciiArmoredString()); + // CHECKSTYLE:ON + } + + @Test + public void buildMonolithicRSAKey() { + OpenPGPKey key = PGPainless.getInstance().generateKey(OpenPGPKeyVersion.v6) + .simpleRsaKeyRing("Alice ", RsaLength._4096); + + OpenPGPCertificate.OpenPGPPrimaryKey primaryKey = key.getPrimaryKey(); + // Primary key is used for all purposes + assertEquals(primaryKey, key.getCertificationKeys().get(0)); + assertEquals(primaryKey, key.getSigningKeys().get(0)); + assertEquals(primaryKey, key.getEncryptionKeys().get(0)); + } + + @Test + public void generateAEADProtectedModernKey() + throws IOException, PGPException { + PGPainless api = PGPainless.getInstance(); + + // Change Policy to use AEAD for secret key protection + api = new PGPainless(api.getAlgorithmPolicy().copy() + .withKeyProtectionSettings(KeyRingProtectionSettings.aead()).build()); + + OpenPGPKey key = api + .generateKey(OpenPGPKeyVersion.v6) + .modernKeyRing("Alice ", "p455w0rd"); + + String armored = key.toAsciiArmoredString(); + + OpenPGPKey parsed = api.readKey().parseKey(armored); + + OpenPGPKey.OpenPGPSecretKey primaryKey = key.getPrimarySecretKey(); + assertEquals(SecretKeyPacket.USAGE_AEAD, primaryKey.getPGPSecretKey().getS2KUsage()); + + OpenPGPKey.OpenPGPPrivateKey privateKey = primaryKey.unlock("p455w0rd".toCharArray()); + assertNotNull(privateKey); + + assertEquals(armored, parsed.toAsciiArmoredString()); + } + + @Test + public void generateKeyOverrideFeatures() { + PGPainless api = PGPainless.getInstance(); + + OpenPGPKey key = api.buildKey(OpenPGPKeyVersion.v6) + .setPrimaryKey(KeySpec.getBuilder(KeyType.Ed25519(), KeyFlag.CERTIFY_OTHER) + .overrideFeatures(Feature.MODIFICATION_DETECTION, Feature.MODIFICATION_DETECTION_2)) + .build(); + + assertTrue(key.getPrimaryKey().getFeatures().supportsModificationDetection()); + assertTrue(key.getPrimaryKey().getFeatures().supportsSEIPDv2()); + } +} 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 c1375883..ef649d86 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 @@ -6,10 +6,6 @@ package org.pgpainless.key.generation; import static org.junit.jupiter.api.Assertions.assertNotNull; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - -import org.bouncycastle.openpgp.PGPException; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; @@ -30,8 +26,7 @@ public class GenerateWithEmptyPassphraseTest { @TestTemplate @ExtendWith(TestAllImplementations.class) - public void testGeneratingKeyWithEmptyPassphraseDoesNotThrow() - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public void testGeneratingKeyWithEmptyPassphraseDoesNotThrow() { assertNotNull(PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( 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 6fbf0572..d75a751c 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 @@ -6,12 +6,9 @@ package org.pgpainless.key.generation; import static org.junit.jupiter.api.Assertions.assertThrows; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Map; -import org.bouncycastle.openpgp.PGPException; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; @@ -24,23 +21,15 @@ public class GeneratingWeakKeyThrowsTest { @Test public void refuseToGenerateWeakPrimaryKeyTest() { - // ensure we have default public key algorithm policy set - PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( - Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); - assertThrows(IllegalArgumentException.class, () -> - PGPainless.buildKeyRing() + PGPainless.getInstance().buildKey() .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(RsaLength._1024), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA))); } @Test public void refuseToAddWeakSubkeyDuringGenerationTest() { - // ensure we have default public key algorithm policy set - PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( - Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); - - KeyRingBuilder kb = PGPainless.buildKeyRing() + KeyRingBuilder kb = PGPainless.getInstance().buildKey() .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)); @@ -50,25 +39,21 @@ public class GeneratingWeakKeyThrowsTest { } @Test - public void allowToAddWeakKeysWithWeakPolicy() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void allowToAddWeakKeysWithWeakPolicy() { // set a weak algorithm policy + PGPainless api = PGPainless.getInstance(); Map bitStrengths = new HashMap<>(); bitStrengths.put(PublicKeyAlgorithm.RSA_GENERAL, 512); - PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( - new Policy.PublicKeyAlgorithmPolicy(bitStrengths)); + Policy oldPolicy = api.getAlgorithmPolicy(); + api = new PGPainless(oldPolicy.copy().withPublicKeyAlgorithmPolicy(new Policy.PublicKeyAlgorithmPolicy(bitStrengths)).build()); - PGPainless.buildKeyRing() + api.buildKey() .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._1024), KeyFlag.ENCRYPT_COMMS)) .addUserId("Henry") .build(); - - // reset public key algorithm policy - PGPainless.getPolicy().setPublicKeyAlgorithmPolicy( - Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); } } 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 238d1a40..c8b86159 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 @@ -11,9 +11,6 @@ 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 java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -21,10 +18,10 @@ import java.util.List; import org.bouncycastle.bcpg.sig.IssuerFingerprint; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.HashAlgorithm; @@ -41,10 +38,11 @@ public class KeyGenerationSubpacketsTest { @Test public void verifyDefaultSubpacketsForUserIdSignatures() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); - - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + throws PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("Alice"); + Date plus1Sec = new Date(secretKeys.getPrimarySecretKey().getCreationTime().getTime() + 1000); + KeyRingInfo info = api.inspect(secretKeys); PGPSignature userIdSig = info.getLatestUserIdCertification("Alice"); assertNotNull(userIdSig); int keyFlags = userIdSig.getHashedSubPackets().getKeyFlags(); @@ -56,7 +54,7 @@ public class KeyGenerationSubpacketsTest { assertEquals("Alice", info.getPrimaryUserId()); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys, plus1Sec) .addUserId("Bob", new SelfSignatureSubpackets.Callback() { @Override @@ -68,7 +66,7 @@ public class KeyGenerationSubpacketsTest { .addUserId("Alice", SecretKeyRingProtector.unprotectedKeys()) .done(); - info = PGPainless.inspectKeyRing(secretKeys); + info = api.inspect(secretKeys, plus1Sec); userIdSig = info.getLatestUserIdCertification("Alice"); assertNotNull(userIdSig); @@ -89,9 +87,9 @@ public class KeyGenerationSubpacketsTest { assertEquals("Bob", info.getPrimaryUserId()); - Date now = new Date(); + Date now = plus1Sec; Date t1 = new Date(now.getTime() + 1000 * 60 * 60); - secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) + secretKeys = api.modify(secretKeys, t1) .addUserId("Alice", new SelfSignatureSubpackets.Callback() { @Override public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { @@ -100,36 +98,37 @@ public class KeyGenerationSubpacketsTest { } }, SecretKeyRingProtector.unprotectedKeys()) .done(); - info = PGPainless.inspectKeyRing(secretKeys, t1); + info = api.inspect(secretKeys, t1); assertEquals("Alice", info.getPrimaryUserId()); assertEquals(Collections.singleton(HashAlgorithm.SHA1), info.getPreferredHashAlgorithms("Alice")); } @Test public void verifyDefaultSubpacketsForSubkeyBindingSignatures() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - List keysBefore = info.getPublicKeys(); + throws PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("Alice"); + KeyRingInfo info = api.inspect(secretKeys); + List keysBefore = info.getPublicKeys(); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .addSubKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).build(), Passphrase.emptyPassphrase(), SecretKeyRingProtector.unprotectedKeys()) .done(); - info = PGPainless.inspectKeyRing(secretKeys); - List keysAfter = new ArrayList<>(info.getPublicKeys()); + info = api.inspect(secretKeys); + List keysAfter = new ArrayList<>(info.getPublicKeys()); keysAfter.removeAll(keysBefore); assertEquals(1, keysAfter.size()); - PGPPublicKey newSigningKey = keysAfter.get(0); + OpenPGPCertificate.OpenPGPComponentKey newSigningKey = keysAfter.get(0); - PGPSignature bindingSig = info.getCurrentSubkeyBindingSignature(newSigningKey.getKeyID()); + PGPSignature bindingSig = info.getCurrentSubkeyBindingSignature(newSigningKey.getKeyIdentifier()); assertNotNull(bindingSig); assureSignatureHasDefaultSubpackets(bindingSig, secretKeys, KeyFlag.SIGN_DATA); assertNotNull(bindingSig.getHashedSubPackets().getEmbeddedSignatures().get(0)); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .addSubKey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS).build(), Passphrase.emptyPassphrase(), new SelfSignatureSubpackets.Callback() { @@ -140,24 +139,24 @@ public class KeyGenerationSubpacketsTest { }, SecretKeyRingProtector.unprotectedKeys()) .done(); - info = PGPainless.inspectKeyRing(secretKeys); + info = api.inspect(secretKeys); keysAfter = new ArrayList<>(info.getPublicKeys()); keysAfter.removeAll(keysBefore); keysAfter.remove(newSigningKey); assertEquals(1, keysAfter.size()); - PGPPublicKey newEncryptionKey = keysAfter.get(0); - bindingSig = info.getCurrentSubkeyBindingSignature(newEncryptionKey.getKeyID()); + OpenPGPCertificate.OpenPGPComponentKey newEncryptionKey = keysAfter.get(0); + bindingSig = info.getCurrentSubkeyBindingSignature(newEncryptionKey.getKeyIdentifier()); assertNotNull(bindingSig); assertNull(bindingSig.getHashedSubPackets().getIssuerFingerprint()); assertEquals(KeyFlag.toBitmask(KeyFlag.ENCRYPT_COMMS), bindingSig.getHashedSubPackets().getKeyFlags()); } - private void assureSignatureHasDefaultSubpackets(PGPSignature signature, PGPSecretKeyRing secretKeys, KeyFlag... keyFlags) { + private void assureSignatureHasDefaultSubpackets(PGPSignature signature, OpenPGPKey secretKeys, KeyFlag... keyFlags) { PGPSignatureSubpacketVector hashedSubpackets = signature.getHashedSubPackets(); assertNotNull(hashedSubpackets.getIssuerFingerprint()); - assertEquals(secretKeys.getPublicKey().getKeyID(), hashedSubpackets.getIssuerKeyID()); + assertEquals(secretKeys.getKeyIdentifier().getKeyId(), hashedSubpackets.getIssuerKeyID()); assertArrayEquals( - secretKeys.getPublicKey().getFingerprint(), + secretKeys.getFingerprint(), hashedSubpackets.getIssuerFingerprint().getFingerprint()); assertEquals(hashedSubpackets.getKeyFlags(), KeyFlag.toBitmask(keyFlags)); } 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 19d5fdd1..bf93fe7a 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 @@ -5,8 +5,8 @@ package org.pgpainless.key.generation; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; @@ -99,14 +99,15 @@ public class StupidAlgorithmPreferenceEncryptionTest { @Test public void testEncryptionIsNotUnencrypted() throws PGPException, IOException { - PGPSecretKeyRing stupidKey = PGPainless.readKeyRing().secretKeyRing(STUPID_KEY); - PGPPublicKeyRing certificate = PGPainless.extractCertificate(stupidKey); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey stupidKey = api.readKey().parseKey(STUPID_KEY); + OpenPGPCertificate certificate = stupidKey.toCertificate(); ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(out) .withOptions(ProducerOptions.encrypt( - new EncryptionOptions().addRecipient(certificate) + EncryptionOptions.get(api).addRecipient(certificate) )); encryptionStream.write("Hello".getBytes(StandardCharsets.UTF_8)); @@ -114,7 +115,7 @@ public class StupidAlgorithmPreferenceEncryptionTest { EncryptionResult metadata = encryptionStream.getResult(); assertTrue(metadata.isEncryptedFor(certificate)); - assertEquals(PGPainless.getPolicy().getSymmetricKeyEncryptionAlgorithmPolicy().getDefaultSymmetricKeyAlgorithm(), + assertEquals(api.getAlgorithmPolicy().getSymmetricKeyEncryptionAlgorithmPolicy().getDefaultSymmetricKeyAlgorithm(), metadata.getEncryptionAlgorithm()); } } 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 34465bba..c7d20491 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 @@ -12,8 +12,6 @@ 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; import java.util.Calendar; import java.util.Collections; @@ -24,11 +22,11 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Set; +import org.bouncycastle.bcpg.KeyIdentifier; 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.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.JUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestTemplate; @@ -38,8 +36,10 @@ import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.OpenPGPKeyVersion; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.bouncycastle.extensions.PGPSecretKeyExtensionsKt; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; @@ -48,7 +48,6 @@ import org.pgpainless.key.generation.type.ecc.EllipticCurve; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; 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; @@ -60,14 +59,15 @@ public class KeyRingInfoTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void testWithEmilsKeys() throws IOException, PGPException { + PGPainless api = PGPainless.getInstance(); - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); - PGPPublicKeyRing publicKeys = TestKeys.getEmilPublicKeyRing(); - KeyRingInfo sInfo = PGPainless.inspectKeyRing(secretKeys); - KeyRingInfo pInfo = PGPainless.inspectKeyRing(publicKeys); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); + OpenPGPCertificate publicKeys = TestKeys.getEmilCertificate(); + KeyRingInfo sInfo = api.inspect(secretKeys); + KeyRingInfo pInfo = api.inspect(publicKeys); - assertEquals(TestKeys.EMIL_KEY_ID, sInfo.getKeyId()); - assertEquals(TestKeys.EMIL_KEY_ID, pInfo.getKeyId()); + assertEquals(TestKeys.EMIL_KEY_ID, sInfo.getKeyIdentifier().getKeyId()); + assertEquals(TestKeys.EMIL_KEY_ID, pInfo.getKeyIdentifier().getKeyId()); assertEquals(TestKeys.EMIL_FINGERPRINT, sInfo.getFingerprint()); assertEquals(TestKeys.EMIL_FINGERPRINT, pInfo.getFingerprint()); assertEquals(PublicKeyAlgorithm.ECDSA, sInfo.getAlgorithm()); @@ -88,8 +88,8 @@ public class KeyRingInfoTest { assertEquals(Collections.singletonList(""), pInfo.getUserIds()); assertEquals(Collections.singletonList("emil@email.user"), sInfo.getEmailAddresses()); assertEquals(Collections.singletonList("emil@email.user"), pInfo.getEmailAddresses()); - assertEquals(4, sInfo.getVersion()); - assertEquals(4, pInfo.getVersion()); + assertEquals(OpenPGPKeyVersion.v4, sInfo.getVersion()); + assertEquals(OpenPGPKeyVersion.v4, pInfo.getVersion()); assertTrue(sInfo.isSecretKey()); assertFalse(pInfo.isSecretKey()); @@ -106,49 +106,51 @@ public class KeyRingInfoTest { assertNull(sInfo.getRevocationDate()); assertNull(pInfo.getRevocationDate()); Date revocationDate = DateUtil.now(); - PGPSecretKeyRing revoked = PGPainless.modifyKeyRing(secretKeys).revoke( + OpenPGPKey revoked = api.modify(secretKeys).revoke( new UnprotectedKeysProtector(), RevocationAttributes.createKeyRevocation() .withReason(RevocationAttributes.Reason.KEY_RETIRED) .withoutDescription() ).done(); - KeyRingInfo rInfo = PGPainless.inspectKeyRing(revoked); + KeyRingInfo rInfo = api.inspect(revoked); assertNotNull(rInfo.getRevocationDate()); assertEquals(revocationDate.getTime(), rInfo.getRevocationDate().getTime(), 5); assertEquals(revocationDate.getTime(), rInfo.getLastModified().getTime(), 5); - assertFalse(pInfo.isKeyValidlyBound(1230)); - assertFalse(sInfo.isKeyValidlyBound(1230)); + assertFalse(pInfo.isKeyValidlyBound(new KeyIdentifier(1230))); + assertFalse(sInfo.isKeyValidlyBound(new KeyIdentifier(1230))); } @Test public void testIsFullyDecrypted() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); + KeyRingInfo info = api.inspect(secretKeys); assertTrue(info.isFullyDecrypted()); - secretKeys = encryptSecretKeys(secretKeys); - info = PGPainless.inspectKeyRing(secretKeys); + secretKeys = encryptSecretKeys(secretKeys, api); + info = api.inspect(secretKeys); assertFalse(info.isFullyDecrypted()); } @Test public void testIsFullyEncrypted() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); + KeyRingInfo info = api.inspect(secretKeys); assertFalse(info.isFullyEncrypted()); - secretKeys = encryptSecretKeys(secretKeys); - info = PGPainless.inspectKeyRing(secretKeys); + secretKeys = encryptSecretKeys(secretKeys, api); + info = api.inspect(secretKeys); assertTrue(info.isFullyEncrypted()); } - private static PGPSecretKeyRing encryptSecretKeys(PGPSecretKeyRing secretKeys) throws PGPException { - return PGPainless.modifyKeyRing(secretKeys) + private static OpenPGPKey encryptSecretKeys(OpenPGPKey secretKeys, PGPainless api) throws PGPException { + return api.modify(secretKeys) .changePassphraseFromOldPassphrase(Passphrase.emptyPassphrase()) .withSecureDefaultSettings() .toNewPassphrase(Passphrase.fromPassword("sw0rdf1sh")) @@ -158,25 +160,29 @@ public class KeyRingInfoTest { @Test public void testGetSecretKey() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); - PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getCryptieKey(); + OpenPGPCertificate publicKeys = secretKeys.toCertificate(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - assertEquals(KeyRingUtils.requirePrimarySecretKeyFrom(secretKeys), info.getSecretKey()); + KeyRingInfo info = api.inspect(secretKeys); + OpenPGPKey.OpenPGPSecretKey primarySecretKey = info.getSecretKey(); + assertNotNull(primarySecretKey); + assertEquals(secretKeys.getPrimarySecretKey().getPGPSecretKey(), primarySecretKey.getPGPSecretKey()); - info = PGPainless.inspectKeyRing(publicKeys); + info = api.inspect(publicKeys); assertNull(info.getSecretKey()); } @Test public void testGetPublicKey() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getCryptieKey(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - assertEquals(KeyRingUtils.requirePrimaryPublicKeyFrom(secretKeys), info.getPublicKey()); + KeyRingInfo info = api.inspect(secretKeys); + assertEquals(secretKeys.getPrimaryKey().getPGPPublicKey(), info.getPrimaryKey().getPGPPublicKey()); - assertEquals(KeyRingUtils.requirePrimarySecretKeyFrom(secretKeys), - KeyRingUtils.requireSecretKeyFrom(secretKeys, secretKeys.getPublicKey().getKeyID())); + assertEquals(secretKeys.getPrimarySecretKey().getPGPSecretKey(), + secretKeys.getPGPSecretKeyRing().getSecretKey(secretKeys.getKeyIdentifier())); } @TestTemplate @@ -213,16 +219,15 @@ public class KeyRingInfoTest { "=gU+0\n" + "-----END PGP PRIVATE KEY BLOCK-----\n"; - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(withDummyS2K); - assertTrue(new KeyInfo(secretKeys.getSecretKey()).hasDummyS2K()); + OpenPGPKey secretKeys = PGPainless.getInstance().readKey().parseKey(withDummyS2K); + assertTrue(PGPSecretKeyExtensionsKt.hasDummyS2K(secretKeys.getPrimarySecretKey().getPGPSecretKey())); } @TestTemplate @ExtendWith(TestAllImplementations.class) - public void testGetKeysWithFlagsAndExpiry() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + public void testGetKeysWithFlagsAndExpiry() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder( @@ -230,10 +235,10 @@ public class KeyRingInfoTest { KeyFlag.ENCRYPT_STORAGE)) .addSubkey(KeySpec.getBuilder( KeyType.ECDSA(EllipticCurve._BRAINPOOLP384R1), KeyFlag.SIGN_DATA)) - .addUserId(UserId.newBuilder().withName("Alice").withEmail("alice@pgpainless.org").build()) + .addUserId(UserId.builder().withName("Alice").withEmail("alice@pgpainless.org").build()) .build(); - Iterator keys = secretKeys.iterator(); + Iterator keys = secretKeys.getPGPSecretKeyRing().iterator(); Date now = DateUtil.now(); Calendar calendar = Calendar.getInstance(); @@ -251,23 +256,23 @@ public class KeyRingInfoTest { PGPSecretKey signingKey = keys.next(); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .setExpirationDate(primaryKeyExpiration, protector) .done(); - KeyRingInfo info = new KeyRingInfo(secretKeys); + KeyRingInfo info = api.inspect(secretKeys); - List encryptionKeys = info.getKeysWithKeyFlag(KeyFlag.ENCRYPT_STORAGE); + List encryptionKeys = info.getKeysWithKeyFlag(KeyFlag.ENCRYPT_STORAGE); assertEquals(1, encryptionKeys.size()); - assertEquals(encryptionKey.getKeyID(), encryptionKeys.get(0).getKeyID()); + assertEquals(encryptionKey.getKeyIdentifier(), encryptionKeys.get(0).getKeyIdentifier()); - List signingKeys = info.getKeysWithKeyFlag(KeyFlag.SIGN_DATA); + List signingKeys = info.getKeysWithKeyFlag(KeyFlag.SIGN_DATA); assertEquals(1, signingKeys.size()); - assertEquals(signingKey.getKeyID(), signingKeys.get(0).getKeyID()); + assertEquals(signingKey.getKeyIdentifier(), signingKeys.get(0).getKeyIdentifier()); - List certKeys = info.getKeysWithKeyFlag(KeyFlag.CERTIFY_OTHER); + List certKeys = info.getKeysWithKeyFlag(KeyFlag.CERTIFY_OTHER); assertEquals(1, certKeys.size()); - assertEquals(primaryKey.getKeyID(), certKeys.get(0).getKeyID()); + assertEquals(primaryKey.getKeyIdentifier(), certKeys.get(0).getKeyIdentifier()); assertNotNull(info.getPrimaryKeyExpirationDate()); assertEquals(primaryKeyExpiration.getTime(), info.getPrimaryKeyExpirationDate().getTime(), 5); @@ -350,11 +355,12 @@ public class KeyRingInfoTest { "crH02GDG8CotAnEHkLTz9GPO80q8mowzBV0EtHsXb4TeAFw5T5Qd0a5I+wk=\n" + "=Vcb3\n" + "-----END PGP ARMORED FILE-----\n"; - PGPPublicKeyRing keys = PGPainless.readKeyRing().publicKeyRing(KEY); + PGPainless api = PGPainless.getInstance(); + OpenPGPCertificate certificate = api.readKey().parseCertificate(KEY); - KeyRingInfo info = new KeyRingInfo(keys, DateUtil.parseUTCDate("2021-10-10 00:00:00 UTC")); + KeyRingInfo info = api.inspect(certificate, DateUtil.parseUTCDate("2021-10-10 00:00:00 UTC")); // Subkey is hard revoked - assertFalse(info.isKeyValidlyBound(5364407983539305061L)); + assertFalse(info.isKeyValidlyBound(new KeyIdentifier(5364407983539305061L))); } @Test @@ -430,14 +436,15 @@ public class KeyRingInfoTest { "=7Feh\n" + "-----END PGP ARMORED FILE-----\n"; - PGPPublicKeyRing keys = PGPainless.readKeyRing().publicKeyRing(KEY); - final long subkeyId = 5364407983539305061L; + PGPainless api = PGPainless.getInstance(); + OpenPGPCertificate certificate = api.readKey().parseCertificate(KEY); + final KeyIdentifier subkeyId = new KeyIdentifier(5364407983539305061L); - KeyRingInfo inspectDuringRevokedPeriod = new KeyRingInfo(keys, DateUtil.parseUTCDate("2019-01-02 00:00:00 UTC")); + KeyRingInfo inspectDuringRevokedPeriod = api.inspect(certificate, DateUtil.parseUTCDate("2019-01-02 00:00:00 UTC")); assertFalse(inspectDuringRevokedPeriod.isKeyValidlyBound(subkeyId)); assertNotNull(inspectDuringRevokedPeriod.getSubkeyRevocationSignature(subkeyId)); - KeyRingInfo inspectAfterRebinding = new KeyRingInfo(keys, DateUtil.parseUTCDate("2020-01-02 00:00:00 UTC")); + KeyRingInfo inspectAfterRebinding = api.inspect(certificate, DateUtil.parseUTCDate("2020-01-02 00:00:00 UTC")); assertTrue(inspectAfterRebinding.isKeyValidlyBound(subkeyId)); } @@ -514,56 +521,59 @@ public class KeyRingInfoTest { "=MhJL\n" + "-----END PGP ARMORED FILE-----\n"; - PGPPublicKeyRing keys = PGPainless.readKeyRing().publicKeyRing(KEY); + PGPainless api = PGPainless.getInstance(); + OpenPGPCertificate keys = api.readKey().parseCertificate(KEY); - KeyRingInfo info = PGPainless.inspectKeyRing(keys); + KeyRingInfo info = api.inspect(keys); // Primary key is hard revoked - assertFalse(info.isKeyValidlyBound(keys.getPublicKey().getKeyID())); + assertFalse(info.isKeyValidlyBound(keys.getKeyIdentifier())); assertFalse(info.isFullyEncrypted()); } @Test - public void getSecretKeyTest() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + public void getSecretKeyTest() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("Alice"); + KeyRingInfo info = api.inspect(secretKeys); OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(secretKeys); - PGPSecretKey primaryKey = info.getSecretKey(primaryKeyFingerprint); - - assertEquals(secretKeys.getSecretKey(), primaryKey); + OpenPGPKey.OpenPGPSecretKey primaryKey = info.getSecretKey(primaryKeyFingerprint); + assertNotNull(primaryKey); + assertEquals(secretKeys.getPrimarySecretKey().getKeyIdentifier(), primaryKey.getKeyIdentifier()); } @Test public void testGetLatestKeyCreationDate() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); Date latestCreationDate = DateUtil.parseUTCDate("2020-01-12 18:01:44 UTC"); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo info = PGPainless.getInstance().inspect(secretKeys); JUtils.assertDateEquals(latestCreationDate, info.getLatestKeyCreationDate()); } @Test public void testGetExpirationDateForUse_SPLIT() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); + KeyRingInfo info = PGPainless.getInstance().inspect(secretKeys); assertThrows(IllegalArgumentException.class, () -> info.getExpirationDateForUse(KeyFlag.SPLIT)); } @Test public void testGetExpirationDateForUse_SHARED() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); + KeyRingInfo info = PGPainless.getInstance().inspect(secretKeys); assertThrows(IllegalArgumentException.class, () -> info.getExpirationDateForUse(KeyFlag.SHARED)); } @Test - public void testGetExpirationDateForUse_NoSuchKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + public void testGetExpirationDateForUse_NoSuchKey() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = PGPainless.buildKeyRing() .addUserId("Alice") .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .build(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo info = api.inspect(secretKeys); assertThrows(NoSuchElementException.class, () -> info.getExpirationDateForUse(KeyFlag.ENCRYPT_COMMS)); } @@ -593,23 +603,23 @@ public class KeyRingInfoTest { "/+XL+qMMgLHaQ25aA11GVAkC\n" + "=7gbt\n" + "-----END PGP PRIVATE KEY BLOCK-----"; - - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - final long pkid = 6643807985200014832L; - final long skid1 = -2328413746552029063L; - final long skid2 = -3276877650571760552L; + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.readKey().parseKey(KEY); + final KeyIdentifier pkid = new KeyIdentifier(6643807985200014832L); + final KeyIdentifier skid1 = new KeyIdentifier(-2328413746552029063L); + final KeyIdentifier skid2 = new KeyIdentifier(-3276877650571760552L); Set preferredHashAlgorithms = new LinkedHashSet<>( Arrays.asList(HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256, HashAlgorithm.SHA224)); Set preferredCompressionAlgorithms = new LinkedHashSet<>( Arrays.asList(CompressionAlgorithm.ZLIB, CompressionAlgorithm.BZIP2, CompressionAlgorithm.ZIP, CompressionAlgorithm.UNCOMPRESSED)); Set preferredSymmetricAlgorithms = new LinkedHashSet<>( Arrays.asList(SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.AES_192, SymmetricKeyAlgorithm.AES_128)); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo info = api.inspect(secretKeys); // Bob is an invalid userId assertThrows(NoSuchElementException.class, () -> info.getPreferredSymmetricKeyAlgorithms("Bob")); // 123 is an invalid keyid - assertThrows(NoSuchElementException.class, () -> info.getPreferredSymmetricKeyAlgorithms(123L)); + assertThrows(NoSuchElementException.class, () -> info.getPreferredSymmetricKeyAlgorithms(new KeyIdentifier(123L))); assertEquals(preferredHashAlgorithms, info.getPreferredHashAlgorithms("Alice")); assertEquals(preferredHashAlgorithms, info.getPreferredHashAlgorithms(pkid)); @@ -619,7 +629,7 @@ public class KeyRingInfoTest { // Bob is an invalid userId assertThrows(NoSuchElementException.class, () -> info.getPreferredCompressionAlgorithms("Bob")); // 123 is an invalid keyid - assertThrows(NoSuchElementException.class, () -> info.getPreferredCompressionAlgorithms(123L)); + assertThrows(NoSuchElementException.class, () -> info.getPreferredCompressionAlgorithms(new KeyIdentifier(123L))); assertEquals(preferredCompressionAlgorithms, info.getPreferredCompressionAlgorithms("Alice")); assertEquals(preferredCompressionAlgorithms, info.getPreferredCompressionAlgorithms(pkid)); @@ -629,7 +639,7 @@ public class KeyRingInfoTest { // Bob is an invalid userId assertThrows(NoSuchElementException.class, () -> info.getPreferredSymmetricKeyAlgorithms("Bob")); // 123 is an invalid keyid - assertThrows(NoSuchElementException.class, () -> info.getPreferredSymmetricKeyAlgorithms(123L)); + assertThrows(NoSuchElementException.class, () -> info.getPreferredSymmetricKeyAlgorithms(new KeyIdentifier(123L))); assertEquals(preferredSymmetricAlgorithms, info.getPreferredSymmetricKeyAlgorithms("Alice")); assertEquals(preferredSymmetricAlgorithms, info.getPreferredSymmetricKeyAlgorithms(pkid)); @@ -687,26 +697,30 @@ public class KeyRingInfoTest { "C9h35EjDuD+1COXUOoW2B8LX6m2yf8cY72K70QgtGemj7UWhXL5u/wARAQAB\n" + "=A3B8\n" + "-----END PGP PUBLIC KEY BLOCK-----\n"; - - PGPPublicKeyRing certificate = PGPainless.readKeyRing().publicKeyRing(KEY); + PGPainless api = PGPainless.getInstance(); + OpenPGPCertificate certificate = api.readKey().parseCertificate(KEY); OpenPgpV4Fingerprint unboundKey = new OpenPgpV4Fingerprint("D622C916384E0F6D364907E55D918BBD521CCD10"); - KeyRingInfo info = PGPainless.inspectKeyRing(certificate); + KeyRingInfo info = api.inspect(certificate); - assertFalse(info.isKeyValidlyBound(unboundKey.getKeyId())); + assertFalse(info.isKeyValidlyBound(unboundKey.getKeyIdentifier())); - List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); - assertTrue(encryptionSubkeys.stream().map(OpenPgpV4Fingerprint::new).noneMatch(f -> f.equals(unboundKey)), + List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); + assertTrue(encryptionSubkeys.stream() + .map(it -> new OpenPgpV4Fingerprint(it.getPGPPublicKey())) + .noneMatch(f -> f.equals(unboundKey)), "Unbound subkey MUST NOT be considered a valid encryption subkey"); - List signingSubkeys = info.getSigningSubkeys(); - assertTrue(signingSubkeys.stream().map(OpenPgpV4Fingerprint::new).noneMatch(f -> f.equals(unboundKey)), + List signingSubkeys = info.getSigningSubkeys(); + assertTrue(signingSubkeys.stream() + .map(it -> new OpenPgpV4Fingerprint(it.getPGPPublicKey())) + .noneMatch(f -> f.equals(unboundKey)), "Unbound subkey MUST NOT be considered a valid signing subkey"); - assertTrue(info.getKeyFlagsOf(unboundKey.getKeyId()).isEmpty()); + assertTrue(info.getKeyFlagsOf(unboundKey.getKeyIdentifier()).isEmpty()); Date latestModification = info.getLastModified(); Date latestKeyCreation = info.getLatestKeyCreationDate(); - Date unboundKeyCreation = certificate.getPublicKey(unboundKey.getKeyId()).getCreationTime(); + Date unboundKeyCreation = certificate.getKey(unboundKey.getKeyIdentifier()).getCreationTime(); assertTrue(unboundKeyCreation.after(latestModification)); assertTrue(unboundKeyCreation.after(latestKeyCreation)); } @@ -762,9 +776,9 @@ public class KeyRingInfoTest { "qDzPRwEAhdVBeryRUcwjgwHX0xmMFK7vLkdonn8BR2++nXBO2g8=\n" + "=ZRAy\n" + "-----END PGP PRIVATE KEY BLOCK-----\n"; - - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.readKey().parseKey(KEY); + KeyRingInfo info = api.inspect(secretKeys); List emails = info.getEmailAddresses(); assertEquals(emails, Arrays.asList("alice@email.tld", "alice@pgpainless.org", "alice@openpgp.org", "alice@rfc4880.spec")); @@ -794,9 +808,9 @@ public class KeyRingInfoTest { "J5wP\n" + "=nFoO\n" + "-----END PGP PUBLIC KEY BLOCK-----"; - - PGPPublicKeyRing cert = PGPainless.readKeyRing().publicKeyRing(CERT); - KeyRingInfo info = PGPainless.inspectKeyRing(cert); + PGPainless api = PGPainless.getInstance(); + OpenPGPCertificate cert = api.readKey().parseCertificate(CERT); + KeyRingInfo info = api.inspect(cert); assertTrue(info.isUsableForEncryption()); } @@ -822,9 +836,9 @@ public class KeyRingInfoTest { "2XO/hpB2T8VXFfFKwj7U9LGkX+ciLg==\n" + "=etPP\n" + "-----END PGP PUBLIC KEY BLOCK-----"; - - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(CERT); - KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys); + PGPainless api = PGPainless.getInstance(); + OpenPGPCertificate publicKeys = api.readKey().parseCertificate(CERT); + KeyRingInfo info = api.inspect(publicKeys); assertTrue(info.isUsableForEncryption(EncryptionPurpose.COMMUNICATIONS)); assertTrue(info.isUsableForEncryption(EncryptionPurpose.ANY)); @@ -859,8 +873,9 @@ public class KeyRingInfoTest { "AQCjeV+3VT+u1movwIYv4XkzB6gB+B2C+DK9nvG5sXZhBg==\n" + "=uqmO\n" + "-----END PGP PUBLIC KEY BLOCK-----"; - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(CERT); - KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys); + PGPainless api = PGPainless.getInstance(); + OpenPGPCertificate publicKeys = api.readKey().parseCertificate(CERT); + KeyRingInfo info = api.inspect(publicKeys); assertFalse(info.isUsableForEncryption()); assertFalse(info.isUsableForEncryption(EncryptionPurpose.ANY)); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/PrimaryUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/PrimaryUserIdTest.java index d2ec8598..2a3895b5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/PrimaryUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/PrimaryUserIdTest.java @@ -6,11 +6,8 @@ package org.pgpainless.key.info; import static org.junit.jupiter.api.Assertions.assertEquals; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -18,13 +15,14 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; public class PrimaryUserIdTest { @Test - public void testGetPrimaryUserId() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("alice@wonderland.lit"); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + public void testGetPrimaryUserId() throws PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey().simpleEcKeyRing("alice@wonderland.lit"); + secretKeys = api.modify(secretKeys) .addUserId("mad_alice@wonderland.lit", SecretKeyRingProtector.unprotectedKeys()) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo info = api.inspect(secretKeys); assertEquals("alice@wonderland.lit", info.getPrimaryUserId()); } } 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 abb067d1..301daed5 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 @@ -11,17 +11,16 @@ 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; import java.util.List; import java.util.NoSuchElementException; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.bcpg.SignatureSubpacketTags; import org.bouncycastle.bcpg.sig.RevocationReason; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; @@ -38,8 +37,9 @@ import org.pgpainless.key.util.RevocationAttributes; public class UserIdRevocationTest { @Test - public void testRevocationWithoutRevocationAttributes() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + public void testRevocationWithoutRevocationAttributes() throws PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) @@ -50,24 +50,24 @@ public class UserIdRevocationTest { .build(); // make a copy with revoked subkey - PGPSecretKeyRing revoked = PGPainless.modifyKeyRing(secretKeys) + OpenPGPKey revoked = api.modify(secretKeys) .revokeUserId("secondary@key.id", new UnprotectedKeysProtector()) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(revoked); + KeyRingInfo info = api.inspect(revoked); List userIds = info.getUserIds(); assertEquals(Arrays.asList("primary@key.id", "secondary@key.id"), userIds); assertTrue(info.isUserIdValid("primary@key.id")); assertFalse(info.isUserIdValid("sedondary@key.id")); assertFalse(info.isUserIdValid("tertiary@key.id")); - info = PGPainless.inspectKeyRing(secretKeys); + info = api.inspect(secretKeys); assertTrue(info.isUserIdValid("secondary@key.id")); // key on original secret key ring is still valid - revoked = PGPainless.modifyKeyRing(secretKeys) + revoked = api.modify(secretKeys) .revokeUserId("secondary@key.id", new UnprotectedKeysProtector()) .done(); - info = PGPainless.inspectKeyRing(revoked); + info = api.inspect(revoked); userIds = info.getUserIds(); assertEquals(Arrays.asList("primary@key.id", "secondary@key.id"), userIds); assertTrue(info.isUserIdValid("primary@key.id")); @@ -76,8 +76,9 @@ public class UserIdRevocationTest { } @Test - public void testRevocationWithRevocationReason() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + public void testRevocationWithRevocationReason() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) @@ -86,13 +87,13 @@ public class UserIdRevocationTest { .addUserId("secondary@key.id") .build(); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .revokeUserId("secondary@key.id", new UnprotectedKeysProtector(), RevocationAttributes.createCertificateRevocation() .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) .withDescription("I lost my mail password")) .done(); - KeyRingInfo info = new KeyRingInfo(secretKeys); + KeyRingInfo info = api.inspect(secretKeys); PGPSignature signature = info.getUserIdRevocation("secondary@key.id"); assertNotNull(signature); @@ -104,31 +105,31 @@ public class UserIdRevocationTest { @Test public void unknownKeyThrowsIllegalArgumentException() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); + OpenPGPKey secretKeys = TestKeys.getCryptieKey(); SecretKeyRingProtector protector = PasswordBasedSecretKeyRingProtector - .forKey(secretKeys.getSecretKey(), TestKeys.CRYPTIE_PASSPHRASE); + .forKey(secretKeys, TestKeys.CRYPTIE_PASSPHRASE); - assertThrows(NoSuchElementException.class, () -> PGPainless.modifyKeyRing(secretKeys) - .revokeSubKey(1L, protector)); + assertThrows(NoSuchElementException.class, () -> PGPainless.getInstance().modify(secretKeys) + .revokeSubKey(new KeyIdentifier(1L), protector)); } @Test public void unknownUserIdThrowsNoSuchElementException() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); + OpenPGPKey secretKeys = TestKeys.getCryptieKey(); SecretKeyRingProtector protector = PasswordBasedSecretKeyRingProtector - .forKey(secretKeys.getSecretKey(), TestKeys.CRYPTIE_PASSPHRASE); + .forKey(secretKeys, TestKeys.CRYPTIE_PASSPHRASE); - assertThrows(NoSuchElementException.class, () -> PGPainless.modifyKeyRing(secretKeys) + assertThrows(NoSuchElementException.class, () -> PGPainless.getInstance().modify(secretKeys) .revokeUserId("invalid@user.id", protector)); } @Test public void invalidRevocationReasonThrowsIllegalArgumentException() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); + OpenPGPKey secretKeys = TestKeys.getCryptieKey(); SecretKeyRingProtector protector = PasswordBasedSecretKeyRingProtector - .forKey(secretKeys.getSecretKey(), TestKeys.CRYPTIE_PASSPHRASE); + .forKey(secretKeys, TestKeys.CRYPTIE_PASSPHRASE); - assertThrows(IllegalArgumentException.class, () -> PGPainless.modifyKeyRing(secretKeys) + assertThrows(IllegalArgumentException.class, () -> PGPainless.getInstance().modify(secretKeys) .revokeUserId("cryptie@encrypted.key", protector, RevocationAttributes.createKeyRevocation().withReason(RevocationAttributes.Reason.KEY_RETIRED) .withDescription("This is not a valid certification revocation reason."))); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubKeyTest.java index afcf9c98..7e2d8a38 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubKeyTest.java @@ -8,17 +8,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; @@ -39,36 +37,37 @@ public class AddSubKeyTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void testAddSubKey() - throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); + throws IOException, PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getCryptieKey(); - List keyIdsBefore = new ArrayList<>(); - for (Iterator it = secretKeys.getPublicKeys(); it.hasNext(); ) { - keyIdsBefore.add(it.next().getKeyID()); + List keyIdentifiersBefore = new ArrayList<>(); + for (Iterator it = secretKeys.getPGPSecretKeyRing().getPublicKeys(); it.hasNext(); ) { + keyIdentifiersBefore.add(it.next().getKeyIdentifier()); } - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .addSubKey( KeySpec.getBuilder(ECDSA.fromCurve(EllipticCurve._P256), KeyFlag.SIGN_DATA).build(), Passphrase.fromPassword("subKeyPassphrase"), PasswordBasedSecretKeyRingProtector.forKey(secretKeys, Passphrase.fromPassword("password123"))) .done(); - List keyIdsAfter = new ArrayList<>(); - for (Iterator it = secretKeys.getPublicKeys(); it.hasNext(); ) { - keyIdsAfter.add(it.next().getKeyID()); + List keyIdentifiersAfter = new ArrayList<>(); + for (Iterator it = secretKeys.getPGPSecretKeyRing().getPublicKeys(); it.hasNext(); ) { + keyIdentifiersAfter.add(it.next().getKeyIdentifier()); } - assertNotEquals(keyIdsAfter, keyIdsBefore); + assertNotEquals(keyIdentifiersAfter, keyIdentifiersBefore); - keyIdsAfter.removeAll(keyIdsBefore); - long subKeyId = keyIdsAfter.get(0); + keyIdentifiersAfter.removeAll(keyIdentifiersBefore); + KeyIdentifier subKeyIdentifier = keyIdentifiersAfter.get(0); - PGPSecretKey subKey = secretKeys.getSecretKey(subKeyId); + OpenPGPKey.OpenPGPSecretKey subKey = secretKeys.getSecretKey(subKeyIdentifier); SecretKeyRingProtector protector = SecretKeyRingProtector.unlockEachKeyWith( Passphrase.fromPassword("subKeyPassphrase"), secretKeys); UnlockSecretKey.unlockSecretKey(subKey, protector); - KeyRingInfo info = new KeyRingInfo(secretKeys); - assertEquals(Collections.singletonList(KeyFlag.SIGN_DATA), info.getKeyFlagsOf(subKeyId)); + KeyRingInfo info = api.inspect(secretKeys); + assertEquals(Collections.singletonList(KeyFlag.SIGN_DATA), info.getKeyFlagsOf(subKeyIdentifier)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpacketsTest.java similarity index 70% rename from pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java rename to pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpacketsTest.java index 85cddfd6..86bcd54e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpacketsTest.java @@ -9,22 +9,19 @@ 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.List; import org.bouncycastle.bcpg.sig.NotationData; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.JUtils; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.OpenPGPKeyVersion; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.generation.KeyRingBuilder; import org.pgpainless.key.generation.KeySpec; @@ -35,22 +32,25 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -public class AddSubkeyWithModifiedBindingSignatureSubpackets { +public class AddSubkeyWithModifiedBindingSignatureSubpacketsTest { public static final long MILLIS_IN_SEC = 1000; @Test - public void bindEncryptionSubkeyAndModifyBindingSignatureHashedSubpackets() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void bindEncryptionSubkeyAndModifyBindingSignatureHashedSubpackets() { + PGPainless api = PGPainless.getInstance(); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Alice "); - KeyRingInfo before = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo before = api.inspect(secretKeys); + List signingKeysBefore = before.getSigningSubkeys(); PGPKeyPair secretSubkey = KeyRingBuilder.generateKeyPair( - KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).build()); + KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).build(), + OpenPGPKeyVersion.v4); long secondsUntilExpiration = 1000; - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .addSubKey(secretSubkey, new SelfSignatureSubpackets.Callback() { @Override public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { @@ -60,20 +60,20 @@ public class AddSubkeyWithModifiedBindingSignatureSubpackets { }, SecretKeyRingProtector.unprotectedKeys(), protector, KeyFlag.SIGN_DATA) .done(); - KeyRingInfo after = PGPainless.inspectKeyRing(secretKeys); - List signingKeys = after.getSigningSubkeys(); - signingKeys.removeAll(before.getSigningSubkeys()); - assertFalse(signingKeys.isEmpty()); + KeyRingInfo after = api.inspect(secretKeys); + List signingKeysAfter = after.getSigningSubkeys(); + signingKeysAfter.removeAll(signingKeysBefore); + assertFalse(signingKeysAfter.isEmpty()); - PGPPublicKey newKey = signingKeys.get(0); - Date newExpirationDate = after.getSubkeyExpirationDate(new OpenPgpV4Fingerprint(newKey)); + OpenPGPCertificate.OpenPGPComponentKey newKey = signingKeysAfter.get(0); + Date newExpirationDate = after.getSubkeyExpirationDate(new OpenPgpV4Fingerprint(newKey.getPGPPublicKey())); assertNotNull(newExpirationDate); Date now = new Date(); JUtils.assertEquals( now.getTime() + MILLIS_IN_SEC * secondsUntilExpiration, newExpirationDate.getTime(), 2 * MILLIS_IN_SEC); - assertTrue(newKey.getSignatures().hasNext()); - PGPSignature binding = newKey.getSignatures().next(); + assertTrue(newKey.getPGPPublicKey().getSignatures().hasNext()); + PGPSignature binding = newKey.getPGPPublicKey().getSignatures().next(); List notations = SignatureSubpacketsUtil.getHashedNotationData(binding); assertEquals(1, notations.size()); assertEquals("test@test.test", notations.get(0).getNotationName()); 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 7e15c998..48875ebf 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 @@ -10,8 +10,6 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.Iterator; import java.util.NoSuchElementException; @@ -19,6 +17,7 @@ import java.util.NoSuchElementException; import openpgp.DateExtensionsKt; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @@ -37,30 +36,32 @@ public class AddUserIdTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void addUserIdToExistingKeyRing() - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("alice@wonderland.lit", "rabb1th0le"); + throws PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() + .simpleEcKeyRing("alice@wonderland.lit", "rabb1th0le"); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo info = api.inspect(secretKeys); Iterator userIds = info.getValidUserIds().iterator(); assertEquals("alice@wonderland.lit", userIds.next()); assertFalse(userIds.hasNext()); SecretKeyRingProtector protector = PasswordBasedSecretKeyRingProtector.forKey(secretKeys, Passphrase.fromPassword("rabb1th0le")); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .addUserId("cheshirecat@wonderland.lit", protector) .done(); - info = PGPainless.inspectKeyRing(secretKeys); + info = api.inspect(secretKeys); userIds = info.getValidUserIds().iterator(); assertEquals("alice@wonderland.lit", userIds.next()); assertEquals("cheshirecat@wonderland.lit", userIds.next()); assertFalse(userIds.hasNext()); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .revokeUserId("cheshirecat@wonderland.lit", protector) .done(); - info = PGPainless.inspectKeyRing(secretKeys); + info = api.inspect(secretKeys); userIds = info.getValidUserIds().iterator(); assertEquals("alice@wonderland.lit", userIds.next()); assertFalse(userIds.hasNext()); @@ -95,37 +96,40 @@ public class AddUserIdTest { "=bk4o\r\n" + "-----END PGP PRIVATE KEY BLOCK-----\r\n"; - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(ARMORED_PRIVATE_KEY); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + PGPainless api = PGPainless.getInstance(); + + OpenPGPKey key = api.readKey().parseKey(ARMORED_PRIVATE_KEY); + KeyRingInfo info = api.inspect(key); Iterator userIds = info.getValidUserIds().iterator(); assertEquals("", userIds.next()); assertFalse(userIds.hasNext()); SecretKeyRingProtector protector = new UnprotectedKeysProtector(); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + key = api.modify(key) .revokeUserId("", protector) .addUserId("cheshirecat@wonderland.lit", protector) .done(); - info = PGPainless.inspectKeyRing(secretKeys); + info = api.inspect(key); userIds = info.getValidUserIds().iterator(); assertEquals("cheshirecat@wonderland.lit", userIds.next()); assertFalse(userIds.hasNext()); } @Test - public void addNewPrimaryUserIdTest() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void addNewPrimaryUserIdTest() { Date now = new Date(); - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Alice"); - UserId bob = UserId.newBuilder().withName("Bob").noEmail().noComment().build(); + UserId bob = UserId.builder().withName("Bob").noEmail().noComment().build(); - assertNotEquals("Bob", PGPainless.inspectKeyRing(secretKeys).getPrimaryUserId()); + assertNotEquals("Bob", api.inspect(secretKeys).getPrimaryUserId()); - secretKeys = PGPainless.modifyKeyRing(secretKeys, DateExtensionsKt.plusSeconds(now, 1)) + secretKeys = api.modify(secretKeys, DateExtensionsKt.plusSeconds(now, 1)) .addPrimaryUserId(bob, SecretKeyRingProtector.unprotectedKeys()) .done(); - assertEquals("Bob", PGPainless.inspectKeyRing(secretKeys, DateExtensionsKt.plusSeconds(now, 2)).getPrimaryUserId()); + assertEquals("Bob", api.inspect(secretKeys, DateExtensionsKt.plusSeconds(now, 2)).getPrimaryUserId()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationOnKeyWithDifferentSignatureTypesTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationOnKeyWithDifferentSignatureTypesTest.java index c029a317..48bda2a2 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationOnKeyWithDifferentSignatureTypesTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationOnKeyWithDifferentSignatureTypesTest.java @@ -7,8 +7,7 @@ package org.pgpainless.key.modification; import java.io.IOException; import java.util.Date; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.JUtils; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @@ -138,8 +137,9 @@ public class ChangeExpirationOnKeyWithDifferentSignatureTypesTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void setExpirationDate_keyHasSigClass10() - throws PGPException, IOException { - PGPSecretKeyRing keys = PGPainless.readKeyRing().secretKeyRing(keyWithGenericCertification); + throws IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey keys = api.readKey().parseKey(keyWithGenericCertification); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); executeTestForKeys(keys, protector); } @@ -147,22 +147,24 @@ public class ChangeExpirationOnKeyWithDifferentSignatureTypesTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void setExpirationDate_keyHasSigClass12() - throws PGPException, IOException { - PGPSecretKeyRing keys = PGPainless.readKeyRing().secretKeyRing(keyWithCasualCertification); + throws IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey keys = api.readKey().parseKey(keyWithCasualCertification); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); executeTestForKeys(keys, protector); } - private void executeTestForKeys(PGPSecretKeyRing keys, SecretKeyRingProtector protector) - throws PGPException { + private void executeTestForKeys(OpenPGPKey keys, SecretKeyRingProtector protector) { + PGPainless api = PGPainless.getInstance(); + Date expirationDate = new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 14); // round date for test stability expirationDate = DateUtil.toSecondsPrecision(expirationDate); - PGPSecretKeyRing modded = PGPainless.modifyKeyRing(keys) + OpenPGPKey modded = api.modify(keys) .setExpirationDate(expirationDate, protector) .done(); - JUtils.assertDateEquals(expirationDate, PGPainless.inspectKeyRing(modded).getPrimaryKeyExpirationDate()); + JUtils.assertDateEquals(expirationDate, api.inspect(modded).getPrimaryKeyExpirationDate()); } } 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 6c0db287..ccef1d19 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 @@ -14,7 +14,7 @@ import java.util.Calendar; import java.util.Date; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.JUtils; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,28 +35,28 @@ public class ChangeExpirationTest { @ExtendWith(TestAllImplementations.class) public void setExpirationDateAndThenUnsetIt_OnPrimaryKey() throws PGPException, IOException { - - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); - KeyRingInfo sInfo = PGPainless.inspectKeyRing(secretKeys); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); + KeyRingInfo sInfo = api.inspect(secretKeys); assertNull(sInfo.getPrimaryKeyExpirationDate()); assertNull(sInfo.getSubkeyExpirationDate(subKeyFingerprint)); Date now = new Date(); Date date = DateUtil.parseUTCDate("2020-11-27 16:10:32 UTC"); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .setExpirationDate(date, new UnprotectedKeysProtector()).done(); - sInfo = PGPainless.inspectKeyRing(secretKeys); + sInfo = api.inspect(secretKeys); assertNotNull(sInfo.getPrimaryKeyExpirationDate()); assertEquals(date.getTime(), sInfo.getPrimaryKeyExpirationDate().getTime()); // subkey unchanged assertNull(sInfo.getSubkeyExpirationDate(subKeyFingerprint)); Date t1 = new Date(now.getTime() + 1000 * 60 * 60); - secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) + secretKeys = api.modify(secretKeys, t1) .setExpirationDate(null, new UnprotectedKeysProtector()).done(); - sInfo = PGPainless.inspectKeyRing(secretKeys, t1); + sInfo = api.inspect(secretKeys, t1); assertNull(sInfo.getPrimaryKeyExpirationDate()); assertNull(sInfo.getSubkeyExpirationDate(subKeyFingerprint)); } @@ -65,9 +65,9 @@ public class ChangeExpirationTest { @ExtendWith(TestAllImplementations.class) public void setExpirationDateAndThenUnsetIt_OnSubkey() throws PGPException, IOException { - - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); - KeyRingInfo sInfo = PGPainless.inspectKeyRing(secretKeys); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); + KeyRingInfo sInfo = api.inspect(secretKeys); assertNull(sInfo.getPrimaryKeyExpirationDate()); @@ -77,42 +77,43 @@ public class ChangeExpirationTest { calendar.add(Calendar.DATE, 5); Date expiration = calendar.getTime(); // in 5 days - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .setExpirationDate(expiration, new UnprotectedKeysProtector()).done(); - sInfo = PGPainless.inspectKeyRing(secretKeys); + sInfo = api.inspect(secretKeys); assertNotNull(sInfo.getPrimaryKeyExpirationDate()); JUtils.assertDateEquals(expiration, sInfo.getPrimaryKeyExpirationDate()); Date t1 = new Date(now.getTime() + 1000 * 60 * 60); - secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) + secretKeys = api.modify(secretKeys, t1) .setExpirationDate(null, new UnprotectedKeysProtector()).done(); - sInfo = PGPainless.inspectKeyRing(secretKeys, t1); + sInfo = api.inspect(secretKeys, t1); assertNull(sInfo.getPrimaryKeyExpirationDate()); } @TestTemplate @ExtendWith(TestAllImplementations.class) public void testExtremeExpirationDates() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); 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; + final OpenPGPKey finalKeys = secretKeys; assertThrows(IllegalArgumentException.class, () -> - PGPainless.modifyKeyRing(finalKeys) + api.modify(finalKeys) .setExpirationDate(farAwayExpiration, protector) .done()); Date notSoFarAwayExpiration = DateUtil.parseUTCDate("2100-01-01 00:00:00 UTC"); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .setExpirationDate(notSoFarAwayExpiration, protector) .done(); - Date actualExpiration = PGPainless.inspectKeyRing(secretKeys) + Date actualExpiration = api.inspect(secretKeys) .getPrimaryKeyExpirationDate(); JUtils.assertDateEquals(notSoFarAwayExpiration, actualExpiration); } 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 834497ac..a496594c 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 @@ -5,15 +5,13 @@ package org.pgpainless.key.modification; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Date; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -27,13 +25,15 @@ public class ChangePrimaryUserIdAndExpirationDatesTest { @Test public void generateA_primaryB_revokeA_cantSecondaryA() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + throws PGPException { + PGPainless api = PGPainless.getInstance(); + + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("A"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); Date now = new Date(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys, now); + KeyRingInfo info = api.inspect(secretKeys, now); assertFalse(info.isHardRevoked("A")); assertFalse(info.isHardRevoked("B")); assertIsPrimaryUserId("A", info); @@ -42,10 +42,10 @@ public class ChangePrimaryUserIdAndExpirationDatesTest { // One hour later Date oneHourLater = new Date(now.getTime() + millisInHour); - secretKeys = PGPainless.modifyKeyRing(secretKeys, oneHourLater) + secretKeys = api.modify(secretKeys, oneHourLater) .addPrimaryUserId("B", protector) .done(); - info = PGPainless.inspectKeyRing(secretKeys, oneHourLater); + info = api.inspect(secretKeys, oneHourLater); assertIsPrimaryUserId("B", info); assertIsNotPrimaryUserId("A", info); @@ -53,10 +53,10 @@ public class ChangePrimaryUserIdAndExpirationDatesTest { // Two hours later Date twoHoursLater = new Date(now.getTime() + 2 * millisInHour); - secretKeys = PGPainless.modifyKeyRing(secretKeys, twoHoursLater) + secretKeys = api.modify(secretKeys, twoHoursLater) .revokeUserId("A", protector) // hard revoke A .done(); - info = PGPainless.inspectKeyRing(secretKeys, twoHoursLater); + info = api.inspect(secretKeys, twoHoursLater); assertTrue(info.isHardRevoked("A")); assertFalse(info.isHardRevoked("B")); @@ -66,123 +66,124 @@ public class ChangePrimaryUserIdAndExpirationDatesTest { // Three hours later Date threeHoursLater = new Date(now.getTime() + 3 * millisInHour); - PGPSecretKeyRing finalSecretKeys = secretKeys; + OpenPGPKey finalSecretKeys = secretKeys; assertThrows(IllegalArgumentException.class, () -> - PGPainless.modifyKeyRing(finalSecretKeys, threeHoursLater).addUserId("A", protector)); + api.modify(finalSecretKeys, threeHoursLater).addUserId("A", protector)); } @Test - public void generateA_primaryExpire_isExpired() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void generateA_primaryExpire_isExpired() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("A"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo info = api.inspect(secretKeys); assertIsPrimaryUserId("A", info); Date now = new Date(); Date later = new Date(now.getTime() + millisInHour); - secretKeys = PGPainless.modifyKeyRing(secretKeys, now) + secretKeys = api.modify(secretKeys, now) .setExpirationDate(later, protector) // expire the whole key .done(); Date evenLater = new Date(now.getTime() + 2 * millisInHour); - info = PGPainless.inspectKeyRing(secretKeys, evenLater); + info = api.inspect(secretKeys, evenLater); assertFalse(info.isUserIdValid("A")); // is expired by now } @Test - public void generateA_primaryB_primaryExpire_bIsStillPrimary() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void generateA_primaryB_primaryExpire_bIsStillPrimary() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("A"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); Date now = new Date(); // Generate key with primary user-id A - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo info = api.inspect(secretKeys); assertIsPrimaryUserId("A", info); // later set primary user-id to B Date t1 = new Date(now.getTime() + millisInHour); - secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) + secretKeys = api.modify(secretKeys, t1) .addPrimaryUserId("B", protector) .done(); - info = PGPainless.inspectKeyRing(secretKeys, t1); + info = api.inspect(secretKeys, t1); assertIsPrimaryUserId("B", info); assertIsNotPrimaryUserId("A", info); // 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) + secretKeys = api.modify(secretKeys, t2) .setExpirationDate(expiration, protector) // expire the whole key in 1 hour .done(); Date t3 = new Date(now.getTime() + 3 * millisInHour); - info = PGPainless.inspectKeyRing(secretKeys, t3); + info = api.inspect(secretKeys, t3); assertIsValid("A", info); assertIsValid("B", info); assertIsPrimaryUserId("B", info); assertIsNotPrimaryUserId("A", info); - info = PGPainless.inspectKeyRing(secretKeys, expiration); + info = api.inspect(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 { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("A"); + public void generateA_expire_certify() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("A"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); Date now = new Date(); Date t1 = new Date(now.getTime() + millisInHour); - secretKeys = PGPainless.modifyKeyRing(secretKeys, now) + secretKeys = api.modify(secretKeys, now) .setExpirationDate(t1, protector) .done(); Date t2 = new Date(now.getTime() + 2 * millisInHour); Date t4 = new Date(now.getTime() + 4 * millisInHour); - secretKeys = PGPainless.modifyKeyRing(secretKeys, t2) + secretKeys = api.modify(secretKeys, t2) .setExpirationDate(t4, protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo info = api.inspect(secretKeys); assertIsValid("A", info); assertIsPrimaryUserId("A", info); } @Test public void generateA_expire_primaryB_expire_isPrimaryB() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("A"); + throws PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("A"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); Date now = new Date(); Date t1 = new Date(now.getTime() + millisInHour); - secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) + secretKeys = api.modify(secretKeys, t1) .setExpirationDate(t1, protector) .done(); Date t2 = new Date(now.getTime() + 2 * millisInHour); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys, t2); + KeyRingInfo info = api.inspect(secretKeys, t2); assertIsPrimaryUserId("A", info); assertIsNotValid("A", info); // A is expired - secretKeys = PGPainless.modifyKeyRing(secretKeys, t2) + secretKeys = api.modify(secretKeys, t2) .addPrimaryUserId("B", protector) .done(); Date t3 = new Date(now.getTime() + 3 * millisInHour); - info = PGPainless.inspectKeyRing(secretKeys, t3); + info = api.inspect(secretKeys, t3); assertIsPrimaryUserId("B", info); assertIsNotValid("B", info); // A and B are still expired @@ -190,19 +191,19 @@ public class ChangePrimaryUserIdAndExpirationDatesTest { Date t4 = new Date(now.getTime() + 4 * millisInHour); Date t5 = new Date(now.getTime() + 5 * millisInHour); - secretKeys = PGPainless.modifyKeyRing(secretKeys, t3) + secretKeys = api.modify(secretKeys, t3) .setExpirationDate(t5, protector) .done(); - info = PGPainless.inspectKeyRing(secretKeys, t4); + info = api.inspect(secretKeys, t4); assertIsValid("B", info); assertIsValid("A", info); // A got re-validated when changing exp date assertIsPrimaryUserId("B", info); - secretKeys = PGPainless.modifyKeyRing(secretKeys, t4) + secretKeys = api.modify(secretKeys, t4) .addUserId("A", protector) // re-certify A as non-primary user-id .done(); - info = PGPainless.inspectKeyRing(secretKeys, t4); + info = api.inspect(secretKeys, t4); assertIsValid("B", info); assertIsValid("A", info); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java index a0ea6984..8a2723db 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java @@ -11,13 +11,12 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Iterator; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPImplementation; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.TestTemplate; @@ -28,7 +27,6 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; 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.protection.KeyRingProtectionSettings; import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; @@ -37,74 +35,73 @@ import org.pgpainless.util.Passphrase; public class ChangeSecretKeyRingPassphraseTest { - private final PGPSecretKeyRing keyRing = PGPainless.generateKeyRing().simpleEcKeyRing("password@encryp.ted", "weakPassphrase"); + private final OpenPGPKey keyRing = PGPainless.getInstance() + .generateKey().simpleEcKeyRing("password@encryp.ted", "weakPassphrase"); - public ChangeSecretKeyRingPassphraseTest() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public ChangeSecretKeyRingPassphraseTest() { } @TestTemplate @ExtendWith(TestAllImplementations.class) public void changePassphraseOfWholeKeyRingTest() throws PGPException { - - PGPSecretKeyRing secretKeys = PGPainless.modifyKeyRing(keyRing) + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.modify(keyRing) .changePassphraseFromOldPassphrase(Passphrase.fromPassword("weakPassphrase")) .withSecureDefaultSettings() .toNewPassphrase(Passphrase.fromPassword("1337p455phr453")) .done(); - PGPSecretKeyRing changedPassphraseKeyRing = secretKeys; - assertEquals(KeyRingProtectionSettings.secureDefaultSettings().getEncryptionAlgorithm().getAlgorithmId(), - changedPassphraseKeyRing.getSecretKey().getKeyEncryptionAlgorithm()); + secretKeys.getPGPSecretKeyRing().getSecretKey().getKeyEncryptionAlgorithm()); assertThrows(PGPException.class, () -> - signDummyMessageWithKeysAndPassphrase(changedPassphraseKeyRing, Passphrase.emptyPassphrase()), + signDummyMessageWithKeysAndPassphrase(api, secretKeys, Passphrase.emptyPassphrase()), "Unlocking secret key ring with empty passphrase MUST fail."); assertThrows(PGPException.class, () -> - signDummyMessageWithKeysAndPassphrase(changedPassphraseKeyRing, Passphrase.fromPassword("weakPassphrase")), + signDummyMessageWithKeysAndPassphrase(api, secretKeys, Passphrase.fromPassword("weakPassphrase")), "Unlocking secret key ring with old passphrase MUST fail."); - assertDoesNotThrow(() -> signDummyMessageWithKeysAndPassphrase(changedPassphraseKeyRing, Passphrase.fromPassword("1337p455phr453")), + assertDoesNotThrow(() -> signDummyMessageWithKeysAndPassphrase(api, secretKeys, Passphrase.fromPassword("1337p455phr453")), "Unlocking the secret key ring with the new passphrase MUST succeed."); } @TestTemplate @ExtendWith(TestAllImplementations.class) public void changePassphraseOfWholeKeyRingToEmptyPassphrase() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.modifyKeyRing(keyRing) + PGPainless api = PGPainless.getInstance(); + + OpenPGPKey changedPassphraseKeyRing = api.modify(keyRing) .changePassphraseFromOldPassphrase(Passphrase.fromPassword("weakPassphrase")) .withSecureDefaultSettings() .toNoPassphrase() .done(); - PGPSecretKeyRing changedPassphraseKeyRing = secretKeys; - assertEquals(SymmetricKeyAlgorithm.NULL.getAlgorithmId(), - changedPassphraseKeyRing.getSecretKey().getKeyEncryptionAlgorithm()); + changedPassphraseKeyRing.getPGPSecretKeyRing().getSecretKey().getKeyEncryptionAlgorithm()); - signDummyMessageWithKeysAndPassphrase(changedPassphraseKeyRing, Passphrase.emptyPassphrase()); + signDummyMessageWithKeysAndPassphrase(api, changedPassphraseKeyRing, Passphrase.emptyPassphrase()); } @TestTemplate @ExtendWith(TestAllImplementations.class) public void changePassphraseOfSingleSubkeyToNewPassphrase() throws PGPException { - - Iterator keys = keyRing.getSecretKeys(); + PGPainless api = PGPainless.getInstance(); + Iterator keys = keyRing.getPGPSecretKeyRing().getSecretKeys(); PGPSecretKey primaryKey = keys.next(); PGPSecretKey subKey = keys.next(); extractPrivateKey(primaryKey, Passphrase.fromPassword("weakPassphrase")); extractPrivateKey(subKey, Passphrase.fromPassword("weakPassphrase")); - PGPSecretKeyRing secretKeys = PGPainless.modifyKeyRing(keyRing) - .changeSubKeyPassphraseFromOldPassphrase(subKey.getPublicKey().getKeyID(), + OpenPGPKey secretKeys = api.modify(keyRing) + .changeSubKeyPassphraseFromOldPassphrase(subKey.getPublicKey().getKeyIdentifier(), Passphrase.fromPassword("weakPassphrase")) .withSecureDefaultSettings() .toNewPassphrase(Passphrase.fromPassword("subKeyPassphrase")) .done(); - keys = secretKeys.getSecretKeys(); + keys = secretKeys.getPGPSecretKeyRing().getSecretKeys(); primaryKey = keys.next(); subKey = keys.next(); @@ -125,18 +122,18 @@ public class ChangeSecretKeyRingPassphraseTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void changePassphraseOfSingleSubkeyToEmptyPassphrase() throws PGPException { - - Iterator keys = keyRing.getSecretKeys(); + PGPainless api = PGPainless.getInstance(); + Iterator keys = keyRing.getPGPSecretKeyRing().getSecretKeys(); PGPSecretKey primaryKey = keys.next(); PGPSecretKey subKey = keys.next(); - PGPSecretKeyRing secretKeys = PGPainless.modifyKeyRing(keyRing) - .changeSubKeyPassphraseFromOldPassphrase(subKey.getKeyID(), Passphrase.fromPassword("weakPassphrase")) + OpenPGPKey secretKeys = api.modify(keyRing) + .changeSubKeyPassphraseFromOldPassphrase(subKey.getKeyIdentifier(), Passphrase.fromPassword("weakPassphrase")) .withSecureDefaultSettings() .toNoPassphrase() .done(); - keys = secretKeys.getSecretKeys(); + keys = secretKeys.getPGPSecretKeyRing().getSecretKeys(); primaryKey = keys.next(); subKey = keys.next(); @@ -167,18 +164,23 @@ public class ChangeSecretKeyRingPassphraseTest { } else if (!passphrase.isEmpty() && secretKey.getKeyEncryptionAlgorithm() == SymmetricKeyAlgorithm.NULL.getAlgorithmId()) { throw new PGPException("Cannot unlock unprotected private key with non-empty passphrase."); } - PBESecretKeyDecryptor decryptor = passphrase.isEmpty() ? null : ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); + PBESecretKeyDecryptor decryptor = passphrase.isEmpty() ? + null : + OpenPGPImplementation.getInstance() + .pbeSecretKeyDecryptorBuilderProvider() + .provide() + .build(passphrase.getChars()); UnlockSecretKey.unlockSecretKey(secretKey, decryptor); } - private void signDummyMessageWithKeysAndPassphrase(PGPSecretKeyRing keyRing, Passphrase passphrase) throws IOException, PGPException { + private void signDummyMessageWithKeysAndPassphrase(PGPainless api, OpenPGPKey key, Passphrase passphrase) throws IOException, PGPException { String dummyMessage = "dummy"; ByteArrayOutputStream dummy = new ByteArrayOutputStream(); - EncryptionStream stream = PGPainless.encryptAndOrSign().onOutputStream(dummy) + EncryptionStream stream = api.generateMessage().onOutputStream(dummy) .withOptions(ProducerOptions.sign(SigningOptions.get() - .addInlineSignature(PasswordBasedSecretKeyRingProtector.forKey(keyRing, passphrase), - keyRing, DocumentSignatureType.BINARY_DOCUMENT))); + .addInlineSignature(PasswordBasedSecretKeyRingProtector.forKey(key, passphrase), + key, DocumentSignatureType.BINARY_DOCUMENT))); Streams.pipeAll(new ByteArrayInputStream(dummyMessage.getBytes()), stream); stream.close(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java index e1926b67..9f1448d3 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java @@ -4,8 +4,8 @@ package org.pgpainless.key.modification; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.JUtils; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -25,25 +25,27 @@ public class ChangeSubkeyExpirationTimeTest { @Test public void changeExpirationTimeOfSubkey() { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); - Date now = secretKeys.getPublicKey().getCreationTime(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("Alice"); + Date now = secretKeys.getPrimaryKey().getCreationTime(); Date inAnHour = new Date(now.getTime() + 1000 * 60 * 60); - PGPPublicKey encryptionKey = PGPainless.inspectKeyRing(secretKeys) + OpenPGPCertificate.OpenPGPComponentKey encryptionKey = api.inspect(secretKeys) .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .setExpirationDateOfSubkey( inAnHour, - encryptionKey.getKeyID(), + encryptionKey.getKeyIdentifier(), SecretKeyRingProtector.unprotectedKeys()) .done(); - JUtils.assertDateEquals(inAnHour, PGPainless.inspectKeyRing(secretKeys) - .getSubkeyExpirationDate(OpenPgpFingerprint.of(encryptionKey))); + JUtils.assertDateEquals(inAnHour, api.inspect(secretKeys) + .getSubkeyExpirationDate(OpenPgpFingerprint.of(encryptionKey.getPGPPublicKey()))); } @Test public void changeExpirationTimeOfExpiredSubkey() throws IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing( + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.readKey().parseKey( "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: CA52 4D5D E3D8 9CD9 105B BA45 3761 076B C6B5 3000\n" + @@ -78,13 +80,13 @@ public class ChangeSubkeyExpirationTimeTest { OpenPgpFingerprint encryptionSubkey = new OpenPgpV4Fingerprint("2E541354A23C9943375EC27A3EF133ED8720D636"); JUtils.assertDateEquals( DateUtil.parseUTCDate("2023-12-07 16:29:46 UTC"), - PGPainless.inspectKeyRing(secretKeys).getSubkeyExpirationDate(encryptionSubkey)); + api.inspect(secretKeys).getSubkeyExpirationDate(encryptionSubkey)); // re-validate the subkey by setting its expiry to null (no expiry) - secretKeys = PGPainless.modifyKeyRing(secretKeys) - .setExpirationDateOfSubkey(null, encryptionSubkey.getKeyId(), SecretKeyRingProtector.unprotectedKeys()) + secretKeys = api.modify(secretKeys) + .setExpirationDateOfSubkey(null, encryptionSubkey.getKeyIdentifier(), SecretKeyRingProtector.unprotectedKeys()) .done(); - assertNull(PGPainless.inspectKeyRing(secretKeys).getSubkeyExpirationDate(encryptionSubkey)); + assertNull(api.inspect(secretKeys).getSubkeyExpirationDate(encryptionSubkey)); } } 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 d3fe1b2e..909c8cc8 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 @@ -17,9 +17,9 @@ import java.nio.charset.StandardCharsets; import java.util.NoSuchElementException; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -73,9 +73,10 @@ public class FixUserIdDoesNotBreakEncryptionCapabilityTest { @Test public void manualReplaceUserIdWithFixedVersionDoesNotHinderEncryptionCapability() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.readKey().parseKey(SECRET_KEY); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - PGPSecretKeyRing modified = PGPainless.modifyKeyRing(secretKeys) + OpenPGPKey modified = api.modify(secretKeys) .addUserId(userIdAfter, new SelfSignatureSubpackets.Callback() { @Override public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { @@ -85,9 +86,12 @@ public class FixUserIdDoesNotBreakEncryptionCapabilityTest { .removeUserId(userIdBefore, protector) .done(); - KeyRingInfo before = PGPainless.inspectKeyRing(secretKeys); - KeyRingInfo after = PGPainless.inspectKeyRing(modified); + KeyRingInfo before = api.inspect(secretKeys); + KeyRingInfo after = api.inspect(modified); + assertEquals(userIdBefore, before.getPrimaryUserId()); + assertEquals(userIdAfter, after.getPrimaryUserId()); + assertTrue(after.isKeyValidlyBound(after.getKeyIdentifier())); assertTrue(before.isUsableForEncryption()); assertTrue(before.isUsableForSigning()); assertTrue(before.isUserIdValid(userIdBefore)); @@ -101,34 +105,38 @@ public class FixUserIdDoesNotBreakEncryptionCapabilityTest { @Test public void testReplaceUserId_missingOldUserIdThrows() throws IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); - assertThrows(NoSuchElementException.class, () -> PGPainless.modifyKeyRing(secretKeys) + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.readKey().parseKey(SECRET_KEY); + assertThrows(NoSuchElementException.class, () -> api.modify(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) + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.readKey().parseKey(SECRET_KEY); + assertThrows(IllegalArgumentException.class, () -> api.modify(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) + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.readKey().parseKey(SECRET_KEY); + assertThrows(IllegalArgumentException.class, () -> api.modify(secretKeys) .replaceUserId(userIdBefore, " ", SecretKeyRingProtector.unprotectedKeys())); } @Test public void testReplaceImplicitUserIdDoesNotBreakStuff() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.readKey().parseKey(SECRET_KEY); - PGPSecretKeyRing edited = PGPainless.modifyKeyRing(secretKeys) + OpenPGPKey edited = api.modify(secretKeys) .replaceUserId(userIdBefore, userIdAfter, SecretKeyRingProtector.unprotectedKeys()) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(edited); + KeyRingInfo info = api.inspect(edited); assertTrue(info.isUserIdValid(userIdAfter)); assertEquals(userIdAfter, info.getPrimaryUserId()); @@ -136,25 +144,25 @@ public class FixUserIdDoesNotBreakEncryptionCapabilityTest { assertNotNull(latestCertification); assertTrue(latestCertification.getHashedSubPackets().isPrimaryUserID()); - PGPPublicKeyRing cert = PGPainless.extractCertificate(edited); + OpenPGPCertificate cert = edited.toCertificate(); ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(out) - .withOptions(ProducerOptions.encrypt(new EncryptionOptions() + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() .addRecipient(cert))); encryptionStream.write("Hello".getBytes(StandardCharsets.UTF_8)); encryptionStream.close(); EncryptionResult result = encryptionStream.getResult(); - assertTrue(result.isEncryptedFor(cert)); + assertTrue(result.isEncryptedFor(cert.getPGPPublicKeyRing())); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); ByteArrayOutputStream plain = new ByteArrayOutputStream(); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKey(edited)); Streams.pipeAll(decryptionStream, plain); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/GnuDummyS2KChangePassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/GnuDummyS2KChangePassphraseTest.java index 64439775..d9198e31 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/GnuDummyS2KChangePassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/GnuDummyS2KChangePassphraseTest.java @@ -10,7 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.util.Passphrase; @@ -166,17 +166,18 @@ public class GnuDummyS2KChangePassphraseTest { @Test public void testChangePassphraseToNoPassphraseIgnoresGnuDummyS2KKeys() throws PGPException, IOException { - PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(KEY_WITH_GNU_DUMMY_S2K_PRIMARY_KEY); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKey = api.readKey().parseKey(KEY_WITH_GNU_DUMMY_S2K_PRIMARY_KEY); - assertFalse(PGPainless.inspectKeyRing(secretKey).isFullyDecrypted()); + assertFalse(api.inspect(secretKey).isFullyDecrypted()); - secretKey = PGPainless.modifyKeyRing(secretKey) + secretKey = api.modify(secretKey) .changePassphraseFromOldPassphrase(passphrase) .withSecureDefaultSettings() .toNoPassphrase() .done(); - assertTrue(PGPainless.inspectKeyRing(secretKey).isFullyDecrypted()); + assertTrue(api.inspect(secretKey).isFullyDecrypted()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSigTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSigTest.java index b8c3244e..c99739b5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSigTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/OldSignatureSubpacketsArePreservedOnNewSigTest.java @@ -8,14 +8,11 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Date; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; @@ -28,12 +25,12 @@ public class OldSignatureSubpacketsArePreservedOnNewSigTest { @TestTemplate @ExtendWith(TestAllImplementations.class) - public void verifyOldSignatureSubpacketsArePreservedOnNewExpirationDateSig() - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void verifyOldSignatureSubpacketsArePreservedOnNewExpirationDateSig() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .simpleEcKeyRing("Alice "); - PGPSignature oldSignature = PGPainless.inspectKeyRing(secretKeys).getLatestUserIdCertification("Alice "); + PGPSignature oldSignature = api.inspect(secretKeys).getLatestUserIdCertification("Alice "); assertNotNull(oldSignature); PGPSignatureSubpacketVector oldPackets = oldSignature.getHashedSubPackets(); @@ -43,10 +40,10 @@ public class OldSignatureSubpacketsArePreservedOnNewSigTest { Date t1 = new Date(now.getTime() + millisInHour); Date expiration = new Date(now.getTime() + 5 * 24 * millisInHour); // in 5 days - secretKeys = PGPainless.modifyKeyRing(secretKeys, t1) + secretKeys = api.modify(secretKeys, t1) .setExpirationDate(expiration, new UnprotectedKeysProtector()) .done(); - PGPSignature newSignature = PGPainless.inspectKeyRing(secretKeys, t1).getLatestUserIdCertification("Alice "); + PGPSignature newSignature = api.inspect(secretKeys, t1).getLatestUserIdCertification("Alice "); assertNotNull(newSignature); PGPSignatureSubpacketVector newPackets = newSignature.getHashedSubPackets(); 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 04197d6f..cffdd063 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 @@ -7,14 +7,10 @@ package org.pgpainless.key.modification; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.EnumMap; import java.util.Map; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.EncryptionPurpose; @@ -31,14 +27,18 @@ import org.pgpainless.util.Passphrase; public class RefuseToAddWeakSubkeyTest { @Test - public void testEditorRefusesToAddWeakSubkey() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void testEditorRefusesToAddWeakSubkey() { + PGPainless api = PGPainless.getInstance(); // ensure default policy is set - PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); + Policy oldPolicy = api.getAlgorithmPolicy(); + Policy adjusted = oldPolicy.copy().withPublicKeyAlgorithmPolicy( + Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy() + ).build(); + api = new PGPainless(adjusted); - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Alice"); - SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); + SecretKeyRingEditorInterface editor = api.modify(secretKeys); KeySpec spec = KeySpec.getBuilder(KeyType.RSA(RsaLength._1024), KeyFlag.ENCRYPT_COMMS).build(); assertThrows(IllegalArgumentException.class, () -> @@ -46,11 +46,13 @@ public class RefuseToAddWeakSubkeyTest { } @Test - public void testEditorAllowsToAddWeakSubkeyIfCompliesToPublicKeyAlgorithmPolicy() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void testEditorAllowsToAddWeakSubkeyIfCompliesToPublicKeyAlgorithmPolicy() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Alice"); + Policy oldPolicy = api.getAlgorithmPolicy(); + // set weak policy Map minimalBitStrengths = new EnumMap<>(PublicKeyAlgorithm.class); // §5.4.1 @@ -72,9 +74,11 @@ public class RefuseToAddWeakSubkeyTest { minimalBitStrengths.put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000); // §7.2.2 minimalBitStrengths.put(PublicKeyAlgorithm.ECDH, 250); - PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(new Policy.PublicKeyAlgorithmPolicy(minimalBitStrengths)); + api = new PGPainless(oldPolicy.copy() + .withPublicKeyAlgorithmPolicy(new Policy.PublicKeyAlgorithmPolicy(minimalBitStrengths)) + .build()); - SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); + SecretKeyRingEditorInterface editor = api.modify(secretKeys); KeySpec spec = KeySpec.getBuilder(KeyType.RSA(RsaLength._1024), KeyFlag.ENCRYPT_COMMS) .setKeyCreationDate(editor.getReferenceTime()) // The key gets created after we instantiate the editor. .build(); @@ -82,9 +86,6 @@ public class RefuseToAddWeakSubkeyTest { secretKeys = editor.addSubKey(spec, Passphrase.emptyPassphrase(), SecretKeyRingProtector.unprotectedKeys()) .done(); - assertEquals(2, PGPainless.inspectKeyRing(secretKeys).getEncryptionSubkeys(EncryptionPurpose.ANY).size()); - - // reset default policy - PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); + assertEquals(2, api.inspect(secretKeys).getEncryptionSubkeys(EncryptionPurpose.ANY).size()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevocationCertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevocationCertificateTest.java index 48b5d5b7..2ac4c87f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevocationCertificateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevocationCertificateTest.java @@ -12,14 +12,13 @@ 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 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.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPSignature; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.TestKeys; @@ -32,9 +31,10 @@ public class RevocationCertificateTest { @Test public void createRevocationCertificateTest() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); - PGPSignature revocation = PGPainless.modifyKeyRing(secretKeys) + OpenPGPSignature revocation = api.modify(secretKeys) .createRevocation(SecretKeyRingProtector.unprotectedKeys(), RevocationAttributes.createKeyRevocation() .withReason(RevocationAttributes.Reason.KEY_RETIRED) @@ -42,66 +42,68 @@ public class RevocationCertificateTest { assertNotNull(revocation); - assertTrue(PGPainless.inspectKeyRing(secretKeys).isKeyValidlyBound(secretKeys.getPublicKey().getKeyID())); + assertTrue(api.inspect(secretKeys).isKeyValidlyBound(secretKeys.getKeyIdentifier())); // merge key and revocation certificate PGPSecretKeyRing revokedKey = KeyRingUtils.keysPlusSecretKey( - secretKeys, - KeyRingUtils.secretKeyPlusSignature(secretKeys.getSecretKey(), revocation)); + secretKeys.getPGPSecretKeyRing(), + KeyRingUtils.secretKeyPlusSignature(secretKeys.getPrimarySecretKey().getPGPSecretKey(), revocation.getSignature())); - assertFalse(PGPainless.inspectKeyRing(revokedKey).isKeyValidlyBound(secretKeys.getPublicKey().getKeyID())); + assertFalse(api.inspect(api.toKey(revokedKey)).isKeyValidlyBound(secretKeys.getKeyIdentifier())); } @Test public void createMinimalRevocationCertificateTest() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); - PGPPublicKeyRing minimalRevocationCert = PGPainless.modifyKeyRing(secretKeys).createMinimalRevocationCertificate( + OpenPGPCertificate minimalRevocationCert = api.modify(secretKeys).createMinimalRevocationCertificate( SecretKeyRingProtector.unprotectedKeys(), RevocationAttributes.createKeyRevocation().withReason(RevocationAttributes.Reason.KEY_RETIRED).withoutDescription()); - assertEquals(1, minimalRevocationCert.size()); - PGPPublicKey key = minimalRevocationCert.getPublicKey(); - assertEquals(secretKeys.getPublicKey().getKeyID(), key.getKeyID()); + assertEquals(1, minimalRevocationCert.getPGPKeyRing().size()); + PGPPublicKey key = minimalRevocationCert.getPrimaryKey().getPGPPublicKey(); + assertEquals(secretKeys.getKeyIdentifier(), key.getKeyIdentifier()); assertEquals(1, CollectionUtils.iteratorToList(key.getSignatures()).size()); assertFalse(key.getUserIDs().hasNext()); assertFalse(key.getUserAttributes().hasNext()); assertNull(key.getTrustData()); - PGPPublicKeyRing originalCert = PGPainless.extractCertificate(secretKeys); - PGPPublicKeyRing mergedCert = PGPainless.mergeCertificate(originalCert, minimalRevocationCert); + OpenPGPCertificate originalCert = secretKeys.toCertificate(); + OpenPGPCertificate mergedCert = api.mergeCertificate(originalCert, minimalRevocationCert); - assertTrue(PGPainless.inspectKeyRing(mergedCert).getRevocationState().isSoftRevocation()); + assertTrue(api.inspect(mergedCert).getRevocationState().isSoftRevocation()); } @Test - public void createMinimalRevocationCertificateForFreshKeyTest() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice "); + public void createMinimalRevocationCertificateForFreshKeyTest() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("Alice "); - PGPPublicKeyRing minimalRevocationCert = PGPainless.modifyKeyRing(secretKeys).createMinimalRevocationCertificate( + OpenPGPCertificate minimalRevocationCert = api.modify(secretKeys).createMinimalRevocationCertificate( SecretKeyRingProtector.unprotectedKeys(), RevocationAttributes.createKeyRevocation().withReason(RevocationAttributes.Reason.KEY_RETIRED).withoutDescription()); - assertEquals(1, minimalRevocationCert.size()); - PGPPublicKey key = minimalRevocationCert.getPublicKey(); - assertEquals(secretKeys.getPublicKey().getKeyID(), key.getKeyID()); + assertEquals(1, minimalRevocationCert.getKeys().size()); + PGPPublicKey key = minimalRevocationCert.getPGPPublicKeyRing().getPublicKey(); + assertEquals(secretKeys.getKeyIdentifier(), key.getKeyIdentifier()); assertEquals(1, CollectionUtils.iteratorToList(key.getSignatures()).size()); assertFalse(key.getUserIDs().hasNext()); assertFalse(key.getUserAttributes().hasNext()); assertNull(key.getTrustData()); - PGPPublicKeyRing originalCert = PGPainless.extractCertificate(secretKeys); - PGPPublicKeyRing mergedCert = PGPainless.mergeCertificate(originalCert, minimalRevocationCert); + OpenPGPCertificate originalCert = secretKeys.toCertificate(); + OpenPGPCertificate mergedCert = api.mergeCertificate(originalCert, minimalRevocationCert); - assertTrue(PGPainless.inspectKeyRing(mergedCert).getRevocationState().isSoftRevocation()); + assertTrue(api.inspect(mergedCert).getRevocationState().isSoftRevocation()); } @Test public void createMinimalRevocationCertificate_wrongReason() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); assertThrows(IllegalArgumentException.class, - () -> PGPainless.modifyKeyRing(secretKeys).createMinimalRevocationCertificate( + () -> api.modify(secretKeys).createMinimalRevocationCertificate( SecretKeyRingProtector.unprotectedKeys(), RevocationAttributes.createCertificateRevocation() .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithGenericCertificationSignatureTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithGenericCertificationSignatureTest.java index b2cd85fc..2d02dba2 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithGenericCertificationSignatureTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RevokeKeyWithGenericCertificationSignatureTest.java @@ -7,13 +7,14 @@ package org.pgpainless.key.modification; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Arrays; +import java.util.Collections; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; @@ -24,7 +25,7 @@ import org.pgpainless.util.TestAllImplementations; /** * Test that makes sure that PGPainless can deal with keys that carry a key * signature of type 0x10 (generic certification). - * + *

* Originally PGPainless would only handle keys with key signature type * 0x13 (positive certification) and would otherwise crash when negotiating * algorithms, esp. when revoking a key. @@ -70,23 +71,26 @@ public class RevokeKeyWithGenericCertificationSignatureTest { } private KeyPair revokeKey(String priv) throws IOException, PGPException { - byte[] armoredBytes = priv.getBytes(StandardCharsets.UTF_8); - PGPSecretKeyRing r = PGPainless.readKeyRing() - .secretKeyRing(armoredBytes); - PGPSecretKey secretKey = r.getSecretKey(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey key = api.readKey().parseKey(priv); + OpenPGPKey onlyPrimaryKey = api.toKey( + new PGPSecretKeyRing( + Collections.singletonList(key.getPrimarySecretKey().getPGPSecretKey()) + ) + ); // this is not ideal, but still valid usage - PGPSecretKeyRing secretKeyRing = - PGPainless.modifyKeyRing(new PGPSecretKeyRing(Arrays.asList(secretKey))) + OpenPGPKey revokedPrimaryKey = + api.modify(onlyPrimaryKey) .revoke(new UnprotectedKeysProtector()).done(); - PGPPublicKey pkr = secretKeyRing.getPublicKeys().next(); + PGPPublicKey pkr = revokedPrimaryKey.getPGPSecretKeyRing().getPublicKeys().next(); ByteArrayOutputStream pubOutBytes = new ByteArrayOutputStream(); try (ArmoredOutputStream pubOut = ArmoredOutputStreamFactory.get(pubOutBytes)) { pkr.encode(pubOut); } pubOutBytes.close(); - PGPSecretKey skr = secretKeyRing.getSecretKeys().next(); + PGPSecretKey skr = revokedPrimaryKey.getPGPSecretKeyRing().getSecretKeys().next(); ByteArrayOutputStream secOutBytes = new ByteArrayOutputStream(); try (ArmoredOutputStream privOut = ArmoredOutputStreamFactory.get(secOutBytes)) { skr.encode(privOut); 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 2cbbbe87..0460b319 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 @@ -7,8 +7,7 @@ package org.pgpainless.key.modification; import java.io.IOException; import java.util.Date; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.JUtils; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @@ -99,17 +98,18 @@ public class RevokeKeyWithoutPreferredAlgorithmsOnPrimaryKey { @TestTemplate @ExtendWith(TestAllImplementations.class) public void testChangingExpirationTimeWithKeyWithoutPrefAlgos() - throws IOException, PGPException { + throws IOException { + PGPainless api = PGPainless.getInstance(); Date expirationDate = DateUtil.now(); - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + OpenPGPKey secretKeys = api.readKey().parseKey(KEY); SecretKeyRingProtector protector = new UnprotectedKeysProtector(); - SecretKeyRingEditorInterface modify = PGPainless.modifyKeyRing(secretKeys) + SecretKeyRingEditorInterface modify = api.modify(secretKeys) .setExpirationDate(expirationDate, protector); secretKeys = modify.done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo info = api.inspect(secretKeys); JUtils.assertDateEquals(expirationDate, info.getPrimaryKeyExpirationDate()); } 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 ee6f0de3..0843e368 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 @@ -13,16 +13,15 @@ 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.Iterator; import org.bouncycastle.bcpg.sig.IssuerFingerprint; 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.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPSignature; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,7 +34,6 @@ import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterfac import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; import org.pgpainless.util.TestAllImplementations; @@ -45,10 +43,11 @@ public class RevokeSubKeyTest { @TestTemplate @ExtendWith(TestAllImplementations.class) - public void revokeSukeyTest() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); + public void revokeSubkeyTest() throws IOException, PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getCryptieKey(); - Iterator keysIterator = secretKeys.iterator(); + Iterator keysIterator = secretKeys.getPGPSecretKeyRing().iterator(); PGPSecretKey primaryKey = keysIterator.next(); PGPSecretKey subKey = keysIterator.next(); @@ -57,10 +56,10 @@ public class RevokeSubKeyTest { SecretKeyRingProtector protector = PasswordBasedSecretKeyRingProtector .forKey(secretKeys, Passphrase.fromPassword("password123")); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .revokeSubKey(new OpenPgpV4Fingerprint(subKey), protector) .done(); - keysIterator = secretKeys.iterator(); + keysIterator = secretKeys.getPGPSecretKeyRing().iterator(); primaryKey = keysIterator.next(); subKey = keysIterator.next(); @@ -70,19 +69,20 @@ public class RevokeSubKeyTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void detachedRevokeSubkeyTest() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getCryptieKey(); OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(secretKeys); SecretKeyRingProtector protector = PasswordBasedSecretKeyRingProtector.forKey(secretKeys, Passphrase.fromPassword("password123")); - PGPSignature revocationCertificate = PGPainless.modifyKeyRing(secretKeys) + OpenPGPSignature revocationCertificate = api.modify(secretKeys) .createRevocation(fingerprint, protector, RevocationAttributes.createKeyRevocation() .withReason(RevocationAttributes.Reason.KEY_RETIRED) .withDescription("Key no longer used.")); - PGPPublicKey publicKey = secretKeys.getPublicKey(); + PGPPublicKey publicKey = secretKeys.getPGPSecretKeyRing().getPublicKey(); assertFalse(publicKey.hasRevocation()); - publicKey = PGPPublicKey.addCertification(publicKey, revocationCertificate); + publicKey = PGPPublicKey.addCertification(publicKey, revocationCertificate.getSignature()); assertTrue(publicKey.hasRevocation()); } @@ -90,19 +90,20 @@ public class RevokeSubKeyTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void testRevocationSignatureTypeCorrect() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); - Iterator keysIterator = secretKeys.getPublicKeys(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getCryptieKey(); + Iterator keysIterator = secretKeys.getPGPKeyRing().getPublicKeys(); PGPPublicKey primaryKey = keysIterator.next(); PGPPublicKey subKey = keysIterator.next(); SecretKeyRingProtector protector = PasswordBasedSecretKeyRingProtector .forKey(secretKeys, Passphrase.fromPassword("password123")); - SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); - PGPSignature keyRevocation = editor.createRevocation(primaryKey.getKeyID(), protector, (RevocationAttributes) null); - PGPSignature subkeyRevocation = editor.createRevocation(subKey.getKeyID(), protector, (RevocationAttributes) null); + SecretKeyRingEditorInterface editor = api.modify(secretKeys); + OpenPGPSignature keyRevocation = editor.createRevocation(primaryKey.getKeyIdentifier(), protector, (RevocationAttributes) null); + OpenPGPSignature subkeyRevocation = editor.createRevocation(subKey.getKeyIdentifier(), protector, (RevocationAttributes) null); - assertEquals(SignatureType.KEY_REVOCATION.getCode(), keyRevocation.getSignatureType()); - assertEquals(SignatureType.SUBKEY_REVOCATION.getCode(), subkeyRevocation.getSignatureType()); + assertEquals(SignatureType.KEY_REVOCATION.getCode(), keyRevocation.getSignature().getSignatureType()); + assertEquals(SignatureType.SUBKEY_REVOCATION.getCode(), subkeyRevocation.getSignature().getSignatureType()); } @Test @@ -127,39 +128,40 @@ public class RevokeSubKeyTest { @Test public void inspectSubpacketsOnDefaultRevocationSignature() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); + throws PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("Alice"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - PGPPublicKey encryptionSubkey = PGPainless.inspectKeyRing(secretKeys) - .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); + PGPPublicKey encryptionSubkey = api.inspect(secretKeys) + .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getPGPPublicKey(); - secretKeys = PGPainless.modifyKeyRing(secretKeys) - .revokeSubKey(encryptionSubkey.getKeyID(), protector) + secretKeys = api.modify(secretKeys) + .revokeSubKey(encryptionSubkey.getKeyIdentifier(), protector) .done(); - encryptionSubkey = secretKeys.getPublicKey(encryptionSubkey.getKeyID()); + encryptionSubkey = secretKeys.getPGPSecretKeyRing().getPublicKey(encryptionSubkey.getKeyIdentifier()); PGPSignature revocation = encryptionSubkey.getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.getCode()).next(); assertNotNull(revocation); assertArrayEquals( - secretKeys.getPublicKey().getFingerprint(), + secretKeys.getPGPSecretKeyRing().getPublicKey().getFingerprint(), revocation.getHashedSubPackets().getIssuerFingerprint().getFingerprint()); - assertEquals(secretKeys.getPublicKey().getKeyID(), + assertEquals(secretKeys.getPGPSecretKeyRing().getPublicKey().getKeyID(), revocation.getHashedSubPackets().getIssuerKeyID()); assertNull(SignatureSubpacketsUtil.getRevocationReason(revocation)); - assertTrue(SignatureUtils.isHardRevocation(revocation)); + assertTrue(revocation.isHardRevocation()); } @Test - public void inspectSubpacketsOnModifiedRevocationSignature() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); + public void inspectSubpacketsOnModifiedRevocationSignature() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("Alice"); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - PGPPublicKey encryptionSubkey = PGPainless.inspectKeyRing(secretKeys) - .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); + PGPPublicKey encryptionSubkey = api.inspect(secretKeys) + .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getPGPPublicKey(); - secretKeys = PGPainless.modifyKeyRing(secretKeys) - .revokeSubKey(encryptionSubkey.getKeyID(), protector, new RevocationSignatureSubpackets.Callback() { + secretKeys = api.modify(secretKeys) + .revokeSubKey(encryptionSubkey.getKeyIdentifier(), protector, new RevocationSignatureSubpackets.Callback() { @Override public void modifyHashedSubpackets(RevocationSignatureSubpackets hashedSubpackets) { hashedSubpackets.setRevocationReason( @@ -172,14 +174,14 @@ public class RevokeSubKeyTest { }) .done(); - encryptionSubkey = secretKeys.getPublicKey(encryptionSubkey.getKeyID()); + encryptionSubkey = secretKeys.getPGPSecretKeyRing().getPublicKey(encryptionSubkey.getKeyIdentifier()); PGPSignature revocation = encryptionSubkey.getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.getCode()).next(); assertNotNull(revocation); assertNull(revocation.getHashedSubPackets().getIssuerFingerprint()); - assertEquals(secretKeys.getPublicKey().getKeyID(), + assertEquals(secretKeys.getKeyIdentifier().getKeyId(), revocation.getHashedSubPackets().getIssuerKeyID()); assertNotNull(SignatureSubpacketsUtil.getRevocationReason(revocation)); - assertFalse(SignatureUtils.isHardRevocation(revocation)); + assertFalse(revocation.isHardRevocation()); } } 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 fb6f1ec1..f39c030f 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 @@ -9,74 +9,78 @@ 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.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; +import java.util.Date; import java.util.NoSuchElementException; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPKey; 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.key.util.RevocationAttributes; -import org.pgpainless.util.selection.userid.SelectUserId; public class RevokeUserIdsTest { @Test - public void revokeWithSelectUserId() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void revokeWithSelectUserId() throws PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Alice "); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .addUserId("Allice ", protector) .addUserId("Alice ", protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo info = api.inspect(secretKeys); assertTrue(info.isUserIdValid("Alice ")); assertTrue(info.isUserIdValid("Allice ")); assertTrue(info.isUserIdValid("Alice ")); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + Date n1 = new Date(info.getCreationDate().getTime() + 1000); // 1 sec later + + secretKeys = api.modify(secretKeys, n1) .revokeUserIds( - SelectUserId.containsEmailAddress("alice@example.org"), protector, RevocationAttributes.createCertificateRevocation() .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) - .withoutDescription()) + .withoutDescription(), + uid -> uid.contains("alice@example.org")) .done(); - info = PGPainless.inspectKeyRing(secretKeys); + info = api.inspect(secretKeys, n1); assertTrue(info.isUserIdValid("Alice ")); assertFalse(info.isUserIdValid("Allice ")); assertFalse(info.isUserIdValid("Alice ")); } @Test - public void removeUserId() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void removeUserId() throws PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Alice "); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .addUserId("Allice ", protector) .addUserId("Alice ", protector) .done(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + KeyRingInfo info = api.inspect(secretKeys); assertTrue(info.isUserIdValid("Alice ")); assertTrue(info.isUserIdValid("Allice ")); assertTrue(info.isUserIdValid("Alice ")); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + Date n1 = new Date(info.getCreationDate().getTime() + 1000); + + secretKeys = api.modify(secretKeys, n1) .removeUserId("Allice ", protector) .done(); - info = PGPainless.inspectKeyRing(secretKeys); + info = api.inspect(secretKeys, n1); assertTrue(info.isUserIdValid("Alice ")); assertFalse(info.isUserIdValid("Allice ")); assertTrue(info.isUserIdValid("Alice ")); @@ -89,14 +93,15 @@ public class RevokeUserIdsTest { } @Test - public void emptySelectionYieldsNoSuchElementException() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void emptySelectionYieldsNoSuchElementException() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Alice "); assertThrows(NoSuchElementException.class, () -> - PGPainless.modifyKeyRing(secretKeys).revokeUserIds( - SelectUserId.containsEmailAddress("alice@example.org"), + api.modify(secretKeys).revokeUserIds( SecretKeyRingProtector.unprotectedKeys(), - (RevocationAttributes) null)); + (RevocationAttributes) null, + uid -> uid.contains("alice@example.org"))); } } 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 552ae25f..5d8c8027 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 @@ -7,11 +7,8 @@ package org.pgpainless.key.parsing; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Iterator; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; @@ -25,10 +22,15 @@ import org.pgpainless.util.ArmorUtils; public class KeyRingCollectionReaderTest { @Test - public void writeAndParseKeyRingCollections() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void writeAndParseKeyRingCollections() throws IOException { + PGPainless api = PGPainless.getInstance(); // secret keys - PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("Alice "); - PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("Bob "); + PGPSecretKeyRing alice = api.generateKey() + .modernKeyRing("Alice ") + .getPGPSecretKeyRing(); + PGPSecretKeyRing bob = api.generateKey() + .modernKeyRing("Bob ") + .getPGPSecretKeyRing(); PGPSecretKeyRingCollection collection = KeyRingUtils.keyRingsToKeyRingCollection(alice, bob); String ascii = ArmorUtils.toAsciiArmoredString(collection); @@ -37,8 +39,8 @@ public class KeyRingCollectionReaderTest { assertEquals(collection.size(), parsed.size()); // public keys - PGPPublicKeyRing pAlice = KeyRingUtils.publicKeyRingFrom(alice); - PGPPublicKeyRing pBob = KeyRingUtils.publicKeyRingFrom(bob); + PGPPublicKeyRing pAlice = alice.toCertificate(); + PGPPublicKeyRing pBob = bob.toCertificate(); PGPPublicKeyRingCollection pCollection = KeyRingUtils.keyRingsToKeyRingCollection(pAlice, pBob); ascii = ArmorUtils.toAsciiArmoredString(pCollection); @@ -48,7 +50,7 @@ public class KeyRingCollectionReaderTest { } @Test - public void parseSeparatedSecretKeyRingCollection() throws PGPException, IOException { + public void parseSeparatedSecretKeyRingCollection() throws IOException { String ascii = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: 58F2 0119 232F BBC0 B624 CCA7 7BED B6B3 2279 0657\n" + @@ -107,7 +109,7 @@ public class KeyRingCollectionReaderTest { } @Test - public void parseConcatenatedSecretKeyRingCollection() throws PGPException, IOException { + public void parseConcatenatedSecretKeyRingCollection() throws IOException { // same key ring collection as above, but concatenated in a single armor block String ascii = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + "Version: BCPG v1.68\n" + 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 5ae6d9b2..f868c6b7 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 @@ -6,6 +6,8 @@ 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.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -14,8 +16,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -31,20 +31,23 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPImplementation; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.opentest4j.TestAbortedException; import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.collection.PGPKeyRingCollection; -import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.ArmoredOutputStreamFactory; import org.pgpainless.util.TestUtils; class KeyRingReaderTest { + private final PGPainless api = PGPainless.getInstance(); + private InputStream requireResource(String resourceName) { InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourceName); if (inputStream == null) { @@ -65,25 +68,25 @@ class KeyRingReaderTest { InputStream possiblyArmored = PGPUtil.getDecoderStream(PGPUtil.getDecoderStream(inputStream)); PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection( - possiblyArmored, ImplementationFactory.getInstance().getKeyFingerprintCalculator()); + possiblyArmored, OpenPGPImplementation.getInstance().keyFingerPrintCalculator()); assertEquals(10, collection.size()); } @Test - void publicKeyRingCollectionFromStream() throws IOException, PGPException { + void publicKeyRingCollectionFromStream() throws IOException { InputStream inputStream = requireResource("pub_keys_10_pieces.asc"); PGPPublicKeyRingCollection rings = PGPainless.readKeyRing().publicKeyRingCollection(inputStream); assertEquals(10, rings.size()); } @Test - void publicKeyRingCollectionFromNotArmoredStream() throws IOException, PGPException, - InvalidAlgorithmParameterException, NoSuchAlgorithmException { + void publicKeyRingCollectionFromNotArmoredStream() throws IOException { Collection collection = new ArrayList<>(); for (int i = 0; i < 10; i++) { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("user_" + i + "@encrypted.key"); - collection.add(KeyRingUtils.publicKeyRingFrom(secretKeys)); + PGPSecretKeyRing secretKeys = api.generateKey().simpleEcKeyRing("user_" + i + "@encrypted.key") + .getPGPSecretKeyRing(); + collection.add(secretKeys.toCertificate()); } PGPPublicKeyRingCollection originalRings = new PGPPublicKeyRingCollection(collection); @@ -96,7 +99,7 @@ class KeyRingReaderTest { } @Test - void publicKeyRingCollectionFromString() throws IOException, PGPException { + void publicKeyRingCollectionFromString() throws IOException { 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); @@ -104,7 +107,7 @@ class KeyRingReaderTest { } @Test - void publicKeyRingCollectionFromBytes() throws IOException, PGPException { + void publicKeyRingCollectionFromBytes() throws IOException { byte[] bytes = readFromResource("pub_keys_10_pieces.asc"); InputStream byteArrayInputStream = new ByteArrayInputStream(bytes); PGPPublicKeyRingCollection rings = PGPainless.readKeyRing().publicKeyRingCollection(byteArrayInputStream); @@ -115,7 +118,7 @@ class KeyRingReaderTest { * One armored pub key. */ @Test - void parsePublicKeysSingleArmored() throws IOException, PGPException { + void parsePublicKeysSingleArmored() throws IOException { assertEquals(1, getPgpPublicKeyRingsFromResource("single_pub_key_armored.asc").size()); } @@ -123,7 +126,7 @@ class KeyRingReaderTest { * One binary pub key. */ @Test - void parsePublicKeysSingleBinary() throws IOException, PGPException { + void parsePublicKeysSingleBinary() throws IOException { assertEquals(1, getPgpPublicKeyRingsFromResource("single_pub_key_binary.key").size()); } @@ -131,7 +134,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 { + void parsePublicKeysMultiplyArmoredSingleHeader() throws IOException { assertEquals(10, getPgpPublicKeyRingsFromResource("10_pub_keys_armored_single_header.asc").size()); } @@ -139,7 +142,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 { + void parsePublicKeysMultiplyArmoredOwnHeader() throws IOException { assertEquals(10, getPgpPublicKeyRingsFromResource("10_pub_keys_armored_own_header.asc").size()); } @@ -148,7 +151,7 @@ class KeyRingReaderTest { * Each of those blocks can have a different count of keys. */ @Test - void parsePublicKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException { + void parsePublicKeysMultiplyArmoredOwnWithSingleHeader() throws IOException { assertEquals(10, getPgpPublicKeyRingsFromResource("10_pub_keys_armored_own_with_single_header.asc").size()); } @@ -156,7 +159,7 @@ class KeyRingReaderTest { * Many binary pub keys. */ @Test - void parsePublicKeysMultiplyBinary() throws IOException, PGPException { + void parsePublicKeysMultiplyBinary() throws IOException { assertEquals(10, getPgpPublicKeyRingsFromResource("10_pub_keys_binary.key").size()); } @@ -164,7 +167,7 @@ class KeyRingReaderTest { * One armored private key. */ @Test - void parseSecretKeysSingleArmored() throws IOException, PGPException { + void parseSecretKeysSingleArmored() throws IOException { assertEquals(1, getPgpSecretKeyRingsFromResource("single_prv_key_armored.asc").size()); } @@ -172,7 +175,7 @@ class KeyRingReaderTest { * One binary private key. */ @Test - void parseSecretKeysSingleBinary() throws IOException, PGPException { + void parseSecretKeysSingleBinary() throws IOException { assertEquals(1, getPgpSecretKeyRingsFromResource("single_prv_key_binary.key").size()); } @@ -181,7 +184,7 @@ class KeyRingReaderTest { * -----BEGIN PGP PRIVATE KEY BLOCK-----...-----END PGP PRIVATE KEY BLOCK----- */ @Test - void parseSecretKeysMultiplyArmoredSingleHeader() throws IOException, PGPException { + void parseSecretKeysMultiplyArmoredSingleHeader() throws IOException { assertEquals(10, getPgpSecretKeyRingsFromResource("10_prv_keys_armored_single_header.asc").size()); } @@ -189,7 +192,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 { + void parseSecretKeysMultiplyArmoredOwnHeader() throws IOException { assertEquals(10, getPgpSecretKeyRingsFromResource("10_prv_keys_armored_own_header.asc").size()); } @@ -198,7 +201,7 @@ class KeyRingReaderTest { * Each of those blocks can have a different count of keys. */ @Test - void parseSecretKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException { + void parseSecretKeysMultiplyArmoredOwnWithSingleHeader() throws IOException { assertEquals(10, getPgpSecretKeyRingsFromResource("10_prv_keys_armored_own_with_single_header.asc").size()); } @@ -206,7 +209,7 @@ class KeyRingReaderTest { * Many binary private keys. */ @Test - void parseSecretKeysMultiplyBinary() throws IOException, PGPException { + void parseSecretKeysMultiplyBinary() throws IOException { assertEquals(10, getPgpSecretKeyRingsFromResource("10_prv_keys_binary.key").size()); } @@ -214,7 +217,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 { + void parseKeysMultiplyArmoredOwnHeader() throws IOException { assertEquals(10, getPGPKeyRingsFromResource("10_prv_and_pub_keys_armored_own_header.asc").size()); } @@ -223,7 +226,7 @@ class KeyRingReaderTest { * Each of those blocks can have a different count of keys. */ @Test - void parseKeysMultiplyArmoredOwnWithSingleHeader() throws IOException, PGPException { + void parseKeysMultiplyArmoredOwnWithSingleHeader() throws IOException { assertEquals(10, getPGPKeyRingsFromResource("10_prv_and_pub_keys_armored_own_with_single_header.asc").size()); } @@ -231,22 +234,22 @@ class KeyRingReaderTest { * Many binary keys(private or pub). */ @Test - void parseKeysMultiplyBinary() throws IOException, PGPException { + void parseKeysMultiplyBinary() throws IOException { assertEquals(10, getPGPKeyRingsFromResource("10_prv_and_pub_keys_binary.key").size()); } private PGPKeyRingCollection getPGPKeyRingsFromResource(String fileName) - throws IOException, PGPException { + throws IOException { return PGPainless.readKeyRing().keyRingCollection(requireResource(fileName), true); } private PGPPublicKeyRingCollection getPgpPublicKeyRingsFromResource(String fileName) - throws IOException, PGPException { + throws IOException { return PGPainless.readKeyRing().publicKeyRingCollection(requireResource(fileName)); } private PGPSecretKeyRingCollection getPgpSecretKeyRingsFromResource(String fileName) - throws IOException, PGPException { + throws IOException { return PGPainless.readKeyRing().secretKeyRingCollection(requireResource(fileName)); } @@ -275,7 +278,8 @@ class KeyRingReaderTest { "=9jtR\n" + "-----END PGP PRIVATE KEY BLOCK-----"; - PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(markerAndKey); + OpenPGPKey secretKey = api.readKey().parseKey(markerAndKey); + assertNotNull(secretKey); assertEquals( new OpenPgpV4Fingerprint("562584F8730F39FCB02AACAE735E5EB1C541C0CE"), new OpenPgpV4Fingerprint(secretKey)); @@ -304,7 +308,7 @@ class KeyRingReaderTest { "=6XFh\n" + "-----END PGP PUBLIC KEY BLOCK-----\n"; - PGPPublicKeyRing certificate = PGPainless.readKeyRing().publicKeyRing(markerAndCert); + OpenPGPCertificate certificate = api.readKey().parseCertificate(markerAndCert); assertEquals( new OpenPgpV4Fingerprint("4291C2BEF9B9209DF11128E7F6F2BBD4F5D29793"), @@ -312,7 +316,7 @@ class KeyRingReaderTest { } @Test - public void testReadSecretKeyCollectionIgnoresMarkerPackets() throws PGPException, IOException { + public void testReadSecretKeyCollectionIgnoresMarkerPackets() throws IOException { // Marker // Alice // Marker @@ -380,7 +384,7 @@ class KeyRingReaderTest { } @Test - public void testReadCertificateCollectionIgnoresMarkerPackets() throws PGPException, IOException { + public void testReadCertificateCollectionIgnoresMarkerPackets() throws IOException { String markersAndCerts = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "Version: PGPainless\n" + "Comment: Certificates with injected Marker Packets\n" + @@ -434,7 +438,7 @@ class KeyRingReaderTest { } @Test - public void testReadSignatureIgnoresMarkerPacket() throws PGPException, IOException { + public void testReadSignatureIgnoresMarkerPacket() { String markerAndSignature = "-----BEGIN PGP SIGNATURE-----\n" + "Version: PGPainless\n" + "Comment: Signature with prepended Marker Packet\n" + @@ -449,9 +453,9 @@ class KeyRingReaderTest { } @Test - public void testReadSecretKeysIgnoresMultipleMarkers() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org"); - PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("bob@pgpainless.org"); + public void testReadSecretKeysIgnoresMultipleMarkers() throws IOException { + OpenPGPKey alice = api.generateKey().modernKeyRing("alice@pgpainless.org"); + OpenPGPKey bob = api.generateKey().modernKeyRing("bob@pgpainless.org"); MarkerPacket marker = TestUtils.getMarkerPacket(); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); @@ -462,11 +466,11 @@ class KeyRingReaderTest { for (int i = 0; i < 25; i++) { marker.encode(outputStream); } - alice.encode(outputStream); + outputStream.write(alice.getEncoded()); for (int i = 0; i < 53; i++) { marker.encode(outputStream); } - bob.encode(outputStream); + outputStream.write(bob.getEncoded()); for (int i = 0; i < 102; i++) { marker.encode(outputStream); } @@ -477,14 +481,14 @@ class KeyRingReaderTest { PGPSecretKeyRingCollection secretKeys = PGPainless.readKeyRing().secretKeyRingCollection(armoredMess); assertEquals(2, secretKeys.size()); - assertTrue(secretKeys.contains(alice.getSecretKey().getKeyID())); - assertTrue(secretKeys.contains(bob.getSecretKey().getKeyID())); + assertTrue(secretKeys.contains(alice.getKeyIdentifier().getKeyId())); + assertTrue(secretKeys.contains(bob.getKeyIdentifier().getKeyId())); } @Test public void testReadingSecretKeysExceedsIterationLimit() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org"); + throws IOException { + OpenPGPKey alice = api.generateKey().modernKeyRing("alice@pgpainless.org"); MarkerPacket marker = TestUtils.getMarkerPacket(); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); @@ -494,7 +498,7 @@ class KeyRingReaderTest { for (int i = 0; i < 600; i++) { marker.encode(outputStream); } - alice.encode(outputStream); + outputStream.write(alice.getEncoded()); assertThrows(IOException.class, () -> KeyRingReader.readSecretKeyRing(new ByteArrayInputStream(bytes.toByteArray()), 512)); @@ -502,9 +506,9 @@ class KeyRingReaderTest { @Test public void testReadingSecretKeyCollectionExceedsIterationLimit() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing alice = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org"); - PGPSecretKeyRing bob = PGPainless.generateKeyRing().modernKeyRing("bob@pgpainless.org"); + throws IOException { + OpenPGPKey alice = api.generateKey().modernKeyRing("alice@pgpainless.org"); + OpenPGPKey bob = api.generateKey().modernKeyRing("bob@pgpainless.org"); MarkerPacket marker = TestUtils.getMarkerPacket(); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); @@ -514,8 +518,8 @@ class KeyRingReaderTest { for (int i = 0; i < 600; i++) { marker.encode(outputStream); } - alice.encode(outputStream); - bob.encode(outputStream); + outputStream.write(alice.getEncoded()); + outputStream.write(bob.getEncoded()); assertThrows(IOException.class, () -> KeyRingReader.readSecretKeyRingCollection(new ByteArrayInputStream(bytes.toByteArray()), 512)); @@ -524,9 +528,9 @@ class KeyRingReaderTest { @Test public void testReadingPublicKeysExceedsIterationLimit() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("alice@pgpainless.org"); - PGPPublicKeyRing alice = PGPainless.extractCertificate(secretKeys); + throws IOException { + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("alice@pgpainless.org"); + OpenPGPCertificate alice = secretKeys.toCertificate(); MarkerPacket marker = TestUtils.getMarkerPacket(); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); @@ -536,7 +540,7 @@ class KeyRingReaderTest { for (int i = 0; i < 600; i++) { marker.encode(outputStream); } - alice.encode(outputStream); + outputStream.write(alice.getEncoded()); assertThrows(IOException.class, () -> KeyRingReader.readPublicKeyRing(new ByteArrayInputStream(bytes.toByteArray()), 512)); @@ -544,11 +548,11 @@ class KeyRingReaderTest { @Test public void testReadingPublicKeyCollectionExceedsIterationLimit() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - 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); + throws IOException { + OpenPGPKey sec1 = api.generateKey().modernKeyRing("alice@pgpainless.org"); + OpenPGPKey sec2 = api.generateKey().modernKeyRing("bob@pgpainless.org"); + OpenPGPCertificate alice = sec1.toCertificate(); + OpenPGPCertificate bob = sec2.toCertificate(); MarkerPacket marker = TestUtils.getMarkerPacket(); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); @@ -558,60 +562,60 @@ class KeyRingReaderTest { for (int i = 0; i < 600; i++) { marker.encode(outputStream); } - alice.encode(outputStream); - bob.encode(outputStream); + outputStream.write(alice.getEncoded()); + outputStream.write(bob.getEncoded()); 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); + public void testReadKeyRingWithBinaryPublicKey() throws IOException { + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("Alice "); + OpenPGPCertificate publicKeys = secretKeys.toCertificate(); byte[] bytes = publicKeys.getEncoded(); PGPKeyRing keyRing = PGPainless.readKeyRing() .keyRing(bytes); - assertTrue(keyRing instanceof PGPPublicKeyRing); + assertInstanceOf(PGPPublicKeyRing.class, keyRing); assertArrayEquals(keyRing.getEncoded(), publicKeys.getEncoded()); } @Test - public void testReadKeyRingWithBinarySecretKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice "); + public void testReadKeyRingWithBinarySecretKey() throws IOException { + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("Alice "); byte[] bytes = secretKeys.getEncoded(); PGPKeyRing keyRing = PGPainless.readKeyRing() .keyRing(bytes); - assertTrue(keyRing instanceof PGPSecretKeyRing); + assertInstanceOf(PGPSecretKeyRing.class, keyRing); 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); + public void testReadKeyRingWithArmoredPublicKey() throws IOException { + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("Alice "); + OpenPGPCertificate publicKeys = secretKeys.toCertificate(); + String armored = api.toAsciiArmor(publicKeys); PGPKeyRing keyRing = PGPainless.readKeyRing() .keyRing(armored); - assertTrue(keyRing instanceof PGPPublicKeyRing); + assertInstanceOf(PGPPublicKeyRing.class, keyRing); 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); + public void testReadKeyRingWithArmoredSecretKey() throws IOException { + OpenPGPKey secretKeys = api.generateKey().modernKeyRing("Alice "); + String armored = secretKeys.toAsciiArmoredString(); PGPKeyRing keyRing = PGPainless.readKeyRing() .keyRing(armored); - assertTrue(keyRing instanceof PGPSecretKeyRing); + assertInstanceOf(PGPSecretKeyRing.class, keyRing); assertArrayEquals(keyRing.getEncoded(), secretKeys.getEncoded()); } } 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 3f7a9e6e..90acf6d5 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 @@ -10,16 +10,16 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Iterator; import java.util.Random; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -32,13 +32,13 @@ public class CachingSecretKeyRingProtectorTest { // Dummy passphrase callback that returns the doubled key-id as passphrase private final SecretKeyPassphraseProvider dummyCallback = new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(Long keyId) { - long doubled = keyId * 2; + public Passphrase getPassphraseFor(@NotNull KeyIdentifier keyIdentifier) { + long doubled = keyIdentifier.getKeyId() * 2; return Passphrase.fromPassword(Long.toString(doubled)); } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(@NotNull KeyIdentifier keyIdentifier) { return true; } }; @@ -51,66 +51,66 @@ public class CachingSecretKeyRingProtectorTest { } @Test - public void noCallbackReturnsNullForUnknownKeyId() throws PGPException { - assertNull(protector.getDecryptor(123L)); - assertNull(protector.getEncryptor(123L)); + public void noCallbackReturnsNullForUnknownKeyId() { + assertNull(protector.getDecryptor(new KeyIdentifier(123L))); } @Test - public void testAddPassphrase() throws PGPException { + public void testAddPassphrase() { + KeyIdentifier k123 = new KeyIdentifier(123L); Passphrase passphrase = Passphrase.fromPassword("HelloWorld"); - protector.addPassphrase(123L, passphrase); - assertEquals(passphrase, protector.getPassphraseFor(123L)); - assertNotNull(protector.getEncryptor(123L)); - assertNotNull(protector.getDecryptor(123L)); + protector.addPassphrase(k123, passphrase); + assertEquals(passphrase, protector.getPassphraseFor(k123)); + assertNotNull(protector.getDecryptor(k123)); - assertNull(protector.getPassphraseFor(999L)); + assertNull(protector.getPassphraseFor(new KeyIdentifier(999L))); } @Test public void testForgetPassphrase() { + KeyIdentifier k123 = new KeyIdentifier(123L); Passphrase passphrase = Passphrase.fromPassword("amnesiac"); - protector.addPassphrase(123L, passphrase); - assertEquals(passphrase, protector.getPassphraseFor(123L)); - protector.forgetPassphrase(123L); - assertNull(protector.getPassphraseFor(123L)); + protector.addPassphrase(k123, passphrase); + assertEquals(passphrase, protector.getPassphraseFor(k123)); + protector.forgetPassphrase(k123); + assertNull(protector.getPassphraseFor(k123)); } @Test - public void testAddPassphraseForKeyRing() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing keys = PGPainless.generateKeyRing() + public void testAddPassphraseForKeyRing() throws PGPException { + OpenPGPKey keys = PGPainless.getInstance().generateKey() .modernKeyRing("test@test.test", "Passphrase123"); Passphrase passphrase = Passphrase.fromPassword("Passphrase123"); protector.addPassphrase(keys, passphrase); - Iterator it = keys.getSecretKeys(); + Iterator it = keys.getSecretKeys().values().iterator(); while (it.hasNext()) { - PGPSecretKey key = it.next(); + OpenPGPKey.OpenPGPSecretKey key = it.next(); assertEquals(passphrase, protector.getPassphraseFor(key)); - assertNotNull(protector.getEncryptor(key.getKeyID())); - assertNotNull(protector.getDecryptor(key.getKeyID())); + assertNotNull(protector.getEncryptor(key)); + assertNotNull(protector.getDecryptor(key)); } long nonMatching = findNonMatchingKeyId(keys); - assertNull(protector.getPassphraseFor(nonMatching)); + assertNull(protector.getPassphraseFor(new KeyIdentifier(nonMatching))); protector.forgetPassphrase(keys); - it = keys.getSecretKeys(); + it = keys.getSecretKeys().values().iterator(); while (it.hasNext()) { - PGPSecretKey key = it.next(); + OpenPGPKey.OpenPGPSecretKey key = it.next(); assertNull(protector.getPassphraseFor(key)); - assertNull(protector.getEncryptor(key.getKeyID())); - assertNull(protector.getDecryptor(key.getKeyID())); + assertNull(protector.getEncryptor(key.getPublicKey())); + assertNull(protector.getDecryptor(key.getKeyIdentifier())); } } - private static long findNonMatchingKeyId(PGPKeyRing keyRing) { + private static long findNonMatchingKeyId(OpenPGPCertificate cert) { Random random = new Random(); long nonMatchingKeyId = 123L; outerloop: while (true) { - Iterator pubKeys = keyRing.getPublicKeys(); + Iterator pubKeys = cert.getKeys().iterator(); while (pubKeys.hasNext()) { - if (pubKeys.next().getKeyID() == nonMatchingKeyId) { + if (pubKeys.next().getKeyIdentifier().getKeyId() == nonMatchingKeyId) { nonMatchingKeyId = random.nextLong(); continue outerloop; } @@ -124,13 +124,13 @@ public class CachingSecretKeyRingProtectorTest { CachingSecretKeyRingProtector withCallback = new CachingSecretKeyRingProtector(dummyCallback); for (int i = -5; i <= 5; i++) { - long x = i * 5; - long doubled = x * 2; + KeyIdentifier x = new KeyIdentifier(i * 5); + KeyIdentifier doubled = new KeyIdentifier(x.getKeyId() * 2); Passphrase passphrase = withCallback.getPassphraseFor(x); assertNotNull(passphrase); assertNotNull(passphrase.getChars()); - assertEquals(doubled, Long.parseLong(new String(passphrase.getChars()))); + assertEquals(doubled, new KeyIdentifier(Long.parseLong(new String(passphrase.getChars())))); } } 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 index b0746398..40eb776a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/InvalidProtectionSettingsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/InvalidProtectionSettingsTest.java @@ -15,6 +15,6 @@ public class InvalidProtectionSettingsTest { @Test public void unencryptedKeyRingProtectionSettingsThrows() { assertThrows(IllegalArgumentException.class, () -> - new KeyRingProtectionSettings(SymmetricKeyAlgorithm.NULL, HashAlgorithm.SHA256, 0x60)); + new KeyRingProtectionSettings(SymmetricKeyAlgorithm.NULL, HashAlgorithm.SHA256, 0x60, false)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/MapBasedPassphraseProviderTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/MapBasedPassphraseProviderTest.java index 3961a2be..6501095e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/MapBasedPassphraseProviderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/MapBasedPassphraseProviderTest.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; @@ -22,20 +23,20 @@ public class MapBasedPassphraseProviderTest { @Test public void testMapBasedProvider() throws IOException, PGPException { - Map passphraseMap = new ConcurrentHashMap<>(); - passphraseMap.put(1L, Passphrase.fromPassword("tiger")); - passphraseMap.put(123123123L, Passphrase.fromPassword("snake")); - passphraseMap.put(69696969L, Passphrase.emptyPassphrase()); + Map passphraseMap = new ConcurrentHashMap<>(); + passphraseMap.put(new KeyIdentifier(1L), Passphrase.fromPassword("tiger")); + passphraseMap.put(new KeyIdentifier(123123123L), Passphrase.fromPassword("snake")); + passphraseMap.put(new KeyIdentifier(69696969L), Passphrase.emptyPassphrase()); MapBasedPassphraseProvider provider = new MapBasedPassphraseProvider(passphraseMap); - assertEquals(Passphrase.fromPassword("tiger"), provider.getPassphraseFor(1L)); - assertEquals(Passphrase.fromPassword("snake"), provider.getPassphraseFor(123123123L)); - assertEquals(Passphrase.emptyPassphrase(), provider.getPassphraseFor(69696969L)); - assertNull(provider.getPassphraseFor(555L)); + assertEquals(Passphrase.fromPassword("tiger"), provider.getPassphraseFor(new KeyIdentifier(1L))); + assertEquals(Passphrase.fromPassword("snake"), provider.getPassphraseFor(new KeyIdentifier((123123123L)))); + assertEquals(Passphrase.emptyPassphrase(), provider.getPassphraseFor(new KeyIdentifier(69696969L))); + assertNull(provider.getPassphraseFor(new KeyIdentifier(555L))); PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); passphraseMap = new ConcurrentHashMap<>(); - passphraseMap.put(secretKeys.getSecretKey().getKeyID(), TestKeys.CRYPTIE_PASSPHRASE); + passphraseMap.put(secretKeys.getSecretKey().getKeyIdentifier(), TestKeys.CRYPTIE_PASSPHRASE); provider = new MapBasedPassphraseProvider(passphraseMap); assertEquals(TestKeys.CRYPTIE_PASSPHRASE, provider.getPassphraseFor(secretKeys.getSecretKey())); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java index 370cfd85..c8a062a3 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java @@ -7,14 +7,14 @@ package org.pgpainless.key.protection; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Iterator; +import java.io.IOException; import javax.annotation.Nullable; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.TestKeys; @@ -31,8 +31,8 @@ public class PassphraseProtectedKeyTest { new SecretKeyPassphraseProvider() { @Nullable @Override - public Passphrase getPassphraseFor(Long keyId) { - if (keyId == TestKeys.CRYPTIE_KEY_ID) { + public Passphrase getPassphraseFor(@NotNull KeyIdentifier keyIdentifier) { + if (keyIdentifier.getKeyId() == TestKeys.CRYPTIE_KEY_ID) { return new Passphrase(TestKeys.CRYPTIE_PASSWORD.toCharArray()); } else { return null; @@ -40,31 +40,33 @@ public class PassphraseProtectedKeyTest { } @Override - public boolean hasPassphrase(Long keyId) { - return keyId == TestKeys.CRYPTIE_KEY_ID; + public boolean hasPassphrase(@NotNull KeyIdentifier keyIdentifier) { + return keyIdentifier.getKeyId() == TestKeys.CRYPTIE_KEY_ID; } }); @Test - public void testReturnsNonNullDecryptorEncryptorForPassword() throws PGPException { - assertNotNull(protector.getEncryptor(TestKeys.CRYPTIE_KEY_ID)); + public void testReturnsNonNullDecryptorEncryptorForPassword() throws IOException { + assertNotNull(protector.getEncryptor(TestKeys.getCryptiePublicKeyRing().getPublicKey(TestKeys.CRYPTIE_KEY_ID))); assertNotNull(protector.getDecryptor(TestKeys.CRYPTIE_KEY_ID)); } @Test - public void testReturnsNullDecryptorEncryptorForNoPassword() throws PGPException { - assertNull(protector.getEncryptor(TestKeys.JULIET_KEY_ID)); + public void testReturnsNullDecryptorEncryptorForNoPassword() throws IOException { + assertNull(protector.getEncryptor(TestKeys.getJulietPublicKeyRing().getPublicKey(TestKeys.JULIET_KEY_ID))); assertNull(protector.getDecryptor(TestKeys.JULIET_KEY_ID)); } @Test - public void testReturnsNonNullDecryptorForSubkeys() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("alice", "passphrase"); - SecretKeyRingProtector protector = PasswordBasedSecretKeyRingProtector.forKey(secretKeys, Passphrase.fromPassword("passphrase")); - for (Iterator it = secretKeys.getPublicKeys(); it.hasNext(); ) { - PGPPublicKey subkey = it.next(); - assertNotNull(protector.getEncryptor(subkey.getKeyID())); - assertNotNull(protector.getDecryptor(subkey.getKeyID())); + public void testReturnsNonNullDecryptorForSubkeys() throws PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey key = api.generateKey() + .modernKeyRing("alice ", "passphrase"); + SecretKeyRingProtector protector = PasswordBasedSecretKeyRingProtector.forKey(key, Passphrase.fromPassword("passphrase")); + for (OpenPGPCertificate.OpenPGPComponentKey subkey : key.getPublicKeys().values()) { + assertNotNull(protector.getEncryptor(subkey)); + assertNotNull(protector.getDecryptor(subkey.getKeyIdentifier())); + assertNotNull(protector.getDecryptor(subkey.getKeyIdentifier().getKeyId())); } } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java index a5030f74..1b0e404c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java @@ -10,17 +10,15 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Iterator; import java.util.Map; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; +import org.bouncycastle.bcpg.KeyIdentifier; 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.api.OpenPGPKey; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,23 +33,20 @@ public class SecretKeyRingProtectorTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void testUnlockAllKeysWithSamePassword() - throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + throws IOException, PGPException { - PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); + OpenPGPKey key = TestKeys.getCryptieKey(); SecretKeyRingProtector protector = - SecretKeyRingProtector.unlockEachKeyWith(TestKeys.CRYPTIE_PASSPHRASE, secretKeys); - for (PGPSecretKey secretKey : secretKeys) { - PBESecretKeyDecryptor decryptor = protector.getDecryptor(secretKey.getKeyID()); - assertNotNull(decryptor); - secretKey.extractPrivateKey(decryptor); + SecretKeyRingProtector.unlockEachKeyWith(TestKeys.CRYPTIE_PASSPHRASE, key); + for (OpenPGPKey.OpenPGPSecretKey secretKey : key.getSecretKeys().values()) { + assertNotNull(secretKey.unlock(protector)); } - PGPSecretKeyRing unrelatedKeys = PGPainless.generateKeyRing().simpleEcKeyRing("unrelated", + + OpenPGPKey unrelatedKey = PGPainless.getInstance().generateKey() + .simpleEcKeyRing("unrelated", "SecurePassword"); - for (PGPSecretKey unrelatedKey : unrelatedKeys) { - PBESecretKeyDecryptor decryptor = protector.getDecryptor(unrelatedKey.getKeyID()); - assertNull(decryptor); - assertThrows(PGPException.class, - () -> unrelatedKey.extractPrivateKey(protector.getDecryptor(unrelatedKey.getKeyID()))); + for (OpenPGPKey.OpenPGPSecretKey k : unrelatedKey.getSecretKeys().values()) { + assertThrows(PGPException.class, () -> k.unlock(protector)); } } @@ -60,9 +55,8 @@ public class SecretKeyRingProtectorTest { Random random = new Random(); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); for (int i = 0; i < 10; i++) { - Long keyId = random.nextLong(); - assertNull(protector.getEncryptor(keyId)); - assertNull(protector.getDecryptor(keyId)); + KeyIdentifier keyIdentifier = new KeyIdentifier(random.nextLong()); + assertNull(protector.getDecryptor(keyIdentifier)); } } @@ -70,55 +64,56 @@ public class SecretKeyRingProtectorTest { @ExtendWith(TestAllImplementations.class) public void testUnlockSingleKeyWithPassphrase() throws IOException, PGPException { - - PGPSecretKeyRing secretKeys = TestKeys.getCryptieSecretKeyRing(); - Iterator iterator = secretKeys.iterator(); - PGPSecretKey secretKey = iterator.next(); - PGPSecretKey subKey = iterator.next(); + OpenPGPKey secretKeys = TestKeys.getCryptieKey(); + Iterator iterator = secretKeys.getSecretKeys().values().iterator(); + OpenPGPKey.OpenPGPSecretKey key = iterator.next(); + OpenPGPKey.OpenPGPSecretKey subKey = iterator.next(); SecretKeyRingProtector protector = - SecretKeyRingProtector.unlockSingleKeyWith(TestKeys.CRYPTIE_PASSPHRASE, secretKey); - assertNotNull(protector.getDecryptor(secretKey.getKeyID())); - assertNotNull(protector.getEncryptor(secretKey.getKeyID())); - assertNull(protector.getEncryptor(subKey.getKeyID())); - assertNull(protector.getDecryptor(subKey.getKeyID())); + SecretKeyRingProtector.unlockSingleKeyWith(TestKeys.CRYPTIE_PASSPHRASE, key); + assertNotNull(protector.getDecryptor(key.getKeyIdentifier())); + assertNotNull(protector.getEncryptor(key.getPublicKey())); + assertNull(protector.getEncryptor(subKey.getPublicKey())); + assertNull(protector.getDecryptor(subKey.getKeyIdentifier())); } @Test public void testFromPassphraseMap() { - Map passphraseMap = new ConcurrentHashMap<>(); - passphraseMap.put(1L, Passphrase.emptyPassphrase()); + Map passphraseMap = new ConcurrentHashMap<>(); + KeyIdentifier k1 = new KeyIdentifier(1L); + KeyIdentifier k5 = new KeyIdentifier(5L); + passphraseMap.put(k1, Passphrase.emptyPassphrase()); CachingSecretKeyRingProtector protector = (CachingSecretKeyRingProtector) SecretKeyRingProtector.fromPassphraseMap(passphraseMap); - assertNotNull(protector.getPassphraseFor(1L)); - assertNull(protector.getPassphraseFor(5L)); + assertNotNull(protector.getPassphraseFor(k1)); + assertNull(protector.getPassphraseFor(k5)); - protector.addPassphrase(5L, Passphrase.fromPassword("pa55w0rd")); - protector.forgetPassphrase(1L); + protector.addPassphrase(k5, Passphrase.fromPassword("pa55w0rd")); + protector.forgetPassphrase(k1); - assertNull(protector.getPassphraseFor(1L)); - assertNotNull(protector.getPassphraseFor(5L)); + assertNull(protector.getPassphraseFor(k1)); + assertNotNull(protector.getPassphraseFor(k5)); } @Test public void testMissingPassphraseCallback() { - Map passphraseMap = new ConcurrentHashMap<>(); - passphraseMap.put(1L, Passphrase.emptyPassphrase()); + Map passphraseMap = new ConcurrentHashMap<>(); + passphraseMap.put(new KeyIdentifier(1L), Passphrase.emptyPassphrase()); CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(@NotNull KeyIdentifier keyIdentifier) { return Passphrase.fromPassword("missingP455w0rd"); } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(@NotNull KeyIdentifier keyIdentifier) { return true; } }); - assertEquals(Passphrase.emptyPassphrase(), protector.getPassphraseFor(1L)); - assertEquals(Passphrase.fromPassword("missingP455w0rd"), protector.getPassphraseFor(3L)); + assertEquals(Passphrase.emptyPassphrase(), protector.getPassphraseFor(new KeyIdentifier(1L))); + assertEquals(Passphrase.fromPassword("missingP455w0rd"), protector.getPassphraseFor(new KeyIdentifier(3L))); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnlockSecretKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnlockSecretKeyTest.java index c6a35ea9..5aa940f1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnlockSecretKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnlockSecretKeyTest.java @@ -7,9 +7,6 @@ package org.pgpainless.key.protection; 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.PGPPrivateKey; import org.bouncycastle.openpgp.PGPSecretKey; @@ -22,9 +19,11 @@ import org.pgpainless.util.Passphrase; public class UnlockSecretKeyTest { @Test - public void testUnlockSecretKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeyRing = PGPainless.generateKeyRing() - .simpleEcKeyRing("alice@wonderland.lit", "heureka!"); + public void testUnlockSecretKey() throws PGPException { + PGPainless api = PGPainless.getInstance(); + PGPSecretKeyRing secretKeyRing = api.generateKey() + .simpleEcKeyRing("alice@wonderland.lit", "heureka!") + .getPGPSecretKeyRing(); PGPSecretKey secretKey = secretKeyRing.getSecretKey(); SecretKeyRingProtector correctPassphrase = SecretKeyRingProtector diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnprotectedKeysProtectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnprotectedKeysProtectorTest.java index 07f65a59..dcb35175 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnprotectedKeysProtectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/UnprotectedKeysProtectorTest.java @@ -6,6 +6,7 @@ package org.pgpainless.key.protection; import static org.junit.jupiter.api.Assertions.assertNull; +import org.bouncycastle.openpgp.PGPException; import org.junit.jupiter.api.Test; public class UnprotectedKeysProtectorTest { @@ -13,12 +14,7 @@ public class UnprotectedKeysProtectorTest { private final UnprotectedKeysProtector protector = new UnprotectedKeysProtector(); @Test - public void testKeyProtectorReturnsNullDecryptor() { + public void testKeyProtectorReturnsNullDecryptor() throws PGPException { assertNull(protector.getDecryptor(0L)); } - - @Test - public void testKeyProtectorReturnsNullEncryptor() { - assertNull(protector.getEncryptor(0L)); - } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java index ba6673e5..e8d73ddb 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java @@ -11,13 +11,12 @@ 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.bcpg.SecretKeyPacket; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -68,27 +67,28 @@ public class S2KUsageFixTest { @Test public void verifyOutFixInChangePassphraseWorks() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing before = PGPainless.generateKeyRing().modernKeyRing("Alice", "before"); - for (PGPSecretKey key : before) { + throws PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey before = api.generateKey().modernKeyRing("Alice", "before"); + for (PGPSecretKey key : before.getPGPSecretKeyRing()) { assertEquals(SecretKeyPacket.USAGE_SHA1, key.getS2KUsage()); } - PGPSecretKeyRing unprotected = PGPainless.modifyKeyRing(before) + OpenPGPKey unprotected = api.modify(before) .changePassphraseFromOldPassphrase(Passphrase.fromPassword("before")) .withSecureDefaultSettings() .toNoPassphrase() .done(); - for (PGPSecretKey key : unprotected) { + for (PGPSecretKey key : unprotected.getPGPSecretKeyRing()) { assertEquals(SecretKeyPacket.USAGE_NONE, key.getS2KUsage()); } - PGPSecretKeyRing after = PGPainless.modifyKeyRing(unprotected) + OpenPGPKey after = api.modify(unprotected) .changePassphraseFromOldPassphrase(Passphrase.emptyPassphrase()) .withSecureDefaultSettings() .toNewPassphrase(Passphrase.fromPassword("after")) .done(); - for (PGPSecretKey key : after) { + for (PGPSecretKey key : after.getPGPSecretKeyRing()) { assertEquals(SecretKeyPacket.USAGE_SHA1, key.getS2KUsage()); } } @@ -96,23 +96,24 @@ public class S2KUsageFixTest { @Test public void testFixS2KUsageFrom_USAGE_CHECKSUM_to_USAGE_SHA1() throws IOException, PGPException { - PGPSecretKeyRing keys = PGPainless.readKeyRing().secretKeyRing(KEY_WITH_USAGE_CHECKSUM); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey keys = api.readKey().parseKey(KEY_WITH_USAGE_CHECKSUM); SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword("after")); - PGPSecretKeyRing fixed = S2KUsageFix.replaceUsageChecksumWithUsageSha1(keys, protector); + PGPSecretKeyRing fixed = S2KUsageFix.replaceUsageChecksumWithUsageSha1(keys.getPGPSecretKeyRing(), protector); for (PGPSecretKey key : fixed) { assertEquals(SecretKeyPacket.USAGE_SHA1, key.getS2KUsage()); } - testCanStillDecrypt(keys, protector); + testCanStillDecrypt(api.toKey(fixed), protector); } - private void testCanStillDecrypt(PGPSecretKeyRing keys, SecretKeyRingProtector protector) + private void testCanStillDecrypt(OpenPGPKey keys, SecretKeyRingProtector protector) throws PGPException, IOException { ByteArrayInputStream in = new ByteArrayInputStream(MESSAGE.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(in) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKey(keys, protector)); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.pipeAll(decryptionStream, out); 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 11fd5cd3..f140b8ce 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 @@ -14,12 +14,14 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVectorGenerator; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.OpenPGPKeyVersion; 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; @@ -28,8 +30,6 @@ 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; @@ -40,13 +40,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class KeyRingUtilTest { @Test - public void testInjectCertification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Alice"); + public void testInjectCertification() throws PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey key = api.generateKey().modernKeyRing("Alice"); // test preconditions - assertFalse(secretKeys.getPublicKey().getUserAttributes().hasNext()); - int sigCount = CollectionUtils.iteratorToList(secretKeys.getPublicKey().getSignatures()).size(); + assertFalse(key.getPrimaryKey().getPGPPublicKey().getUserAttributes().hasNext()); + int sigCount = CollectionUtils.iteratorToList(key.getPrimaryKey().getPGPPublicKey().getSignatures()).size(); // Create "image" byte[] image = new byte[512]; @@ -57,15 +57,15 @@ public class KeyRingUtilTest { // create sig PGPSignatureGenerator sigGen = new PGPSignatureGenerator( - ImplementationFactory.getInstance().getPGPContentSignerBuilder( - secretKeys.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId() - )); + api.getImplementation().pgpContentSignerBuilder( + key.getPrimaryKey().getPGPPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId() + ), key.getPrimaryKey().getPGPPublicKey()); sigGen.init( SignatureType.POSITIVE_CERTIFICATION.getCode(), - UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys())); - PGPSignature signature = sigGen.generateCertification(userAttr, secretKeys.getPublicKey()); + UnlockSecretKey.unlockSecretKey(key.getPrimarySecretKey().getPGPSecretKey(), SecretKeyRingProtector.unprotectedKeys())); + PGPSignature signature = sigGen.generateCertification(userAttr, key.getPrimaryKey().getPGPPublicKey()); // inject sig - secretKeys = KeyRingUtils.injectCertification(secretKeys, userAttr, signature); + PGPSecretKeyRing secretKeys = KeyRingUtils.injectCertification(key.getPGPSecretKeyRing(), userAttr, signature); assertTrue(secretKeys.getPublicKey().getUserAttributes().hasNext()); assertEquals(userAttr, secretKeys.getPublicKey().getUserAttributes().next()); @@ -73,19 +73,21 @@ public class KeyRingUtilTest { } @Test - public void testKeysPlusPublicKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); - PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); + public void testKeysPlusPublicKey() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey key = api.generateKey().modernKeyRing("Alice"); + OpenPGPCertificate certificate = key.toCertificate(); PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(KeySpec.getBuilder( - KeyType.ECDH(EllipticCurve._P256), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE).build()); + KeyType.ECDH(EllipticCurve._P256), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE).build(), + OpenPGPKeyVersion.v4); PGPPublicKey pubkey = keyPair.getPublicKey(); assertFalse(pubkey.isMasterKey()); - PGPSecretKeyRing secretKeysPlus = KeyRingUtils.keysPlusPublicKey(secretKeys, pubkey); + PGPSecretKeyRing secretKeysPlus = KeyRingUtils.keysPlusPublicKey(key.getPGPSecretKeyRing(), pubkey); assertNotNull(secretKeysPlus.getPublicKey(pubkey.getKeyID())); - PGPPublicKeyRing publicKeysPlus = KeyRingUtils.keysPlusPublicKey(publicKeys, pubkey); + PGPPublicKeyRing publicKeysPlus = KeyRingUtils.keysPlusPublicKey(certificate.getPGPPublicKeyRing(), pubkey); assertNotNull(publicKeysPlus.getPublicKey(pubkey.getKeyID())); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicySetterTest.java b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicySetterTest.java index 6e90847d..42d7e7b5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicySetterTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicySetterTest.java @@ -17,44 +17,44 @@ public class PolicySetterTest { @Test public void testSetCertificationSignatureHashAlgorithmPolicy_NullFails() { - Policy policy = Policy.getInstance(); - assertThrows(NullPointerException.class, () -> policy.setCertificationSignatureHashAlgorithmPolicy(null)); + Policy policy = new Policy(); + assertThrows(NullPointerException.class, () -> policy.copy().withCertificationSignatureHashAlgorithmPolicy(null)); } @Test public void testSetDataSignatureHashAlgorithmPolicy_NullFails() { - Policy policy = Policy.getInstance(); - assertThrows(NullPointerException.class, () -> policy.setDataSignatureHashAlgorithmPolicy(null)); + Policy policy = new Policy(); + assertThrows(NullPointerException.class, () -> policy.copy().withDataSignatureHashAlgorithmPolicy(null)); } @Test public void testSetRevocationSignatureHashAlgorithmPolicy_NullFails() { - Policy policy = Policy.getInstance(); - assertThrows(NullPointerException.class, () -> policy.setRevocationSignatureHashAlgorithmPolicy(null)); + Policy policy = new Policy(); + assertThrows(NullPointerException.class, () -> policy.copy().withRevocationSignatureHashAlgorithmPolicy(null)); } @Test public void testSetSymmetricKeyEncryptionAlgorithmPolicy_NullFails() { - Policy policy = Policy.getInstance(); - assertThrows(NullPointerException.class, () -> policy.setSymmetricKeyEncryptionAlgorithmPolicy(null)); + Policy policy = new Policy(); + assertThrows(NullPointerException.class, () -> policy.copy().withSymmetricKeyEncryptionAlgorithmPolicy(null)); } @Test public void testSetSymmetricKeyDecryptionAlgorithmPolicy_NullFails() { - Policy policy = Policy.getInstance(); - assertThrows(NullPointerException.class, () -> policy.setSymmetricKeyDecryptionAlgorithmPolicy(null)); + Policy policy = new Policy(); + assertThrows(NullPointerException.class, () -> policy.copy().withSymmetricKeyDecryptionAlgorithmPolicy(null)); } @Test public void testSetCompressionAlgorithmPolicy_NullFails() { - Policy policy = Policy.getInstance(); - assertThrows(NullPointerException.class, () -> policy.setCompressionAlgorithmPolicy(null)); + Policy policy = new Policy(); + assertThrows(NullPointerException.class, () -> policy.copy().withCompressionAlgorithmPolicy(null)); } @Test public void testSetPublicKeyAlgorithmPolicy_NullFails() { - Policy policy = Policy.getInstance(); - assertThrows(NullPointerException.class, () -> policy.setPublicKeyAlgorithmPolicy(null)); + Policy policy = new Policy(); + assertThrows(NullPointerException.class, () -> policy.copy().withPublicKeyAlgorithmPolicy(null)); } @Test @@ -62,7 +62,7 @@ public class PolicySetterTest { Policy policy = new Policy(); Map acceptableAlgorithms = new HashMap<>(); acceptableAlgorithms.put(PublicKeyAlgorithm.RSA_GENERAL, 2000); - policy.setPublicKeyAlgorithmPolicy(new Policy.PublicKeyAlgorithmPolicy(acceptableAlgorithms)); + policy = policy.copy().withPublicKeyAlgorithmPolicy(new Policy.PublicKeyAlgorithmPolicy(acceptableAlgorithms)).build(); // Policy does not contain ECDSA assertFalse(policy.getPublicKeyAlgorithmPolicy().isAcceptable(PublicKeyAlgorithm.ECDSA, 256)); 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 9ff4df85..440b8165 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java @@ -6,7 +6,6 @@ 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; @@ -28,23 +27,12 @@ public class PolicyTest { @BeforeAll public static void setup() { - policy = new Policy(); - policy.setCompressionAlgorithmPolicy(new Policy.CompressionAlgorithmPolicy(CompressionAlgorithm.UNCOMPRESSED, - Arrays.asList(CompressionAlgorithm.ZIP, CompressionAlgorithm.ZLIB, CompressionAlgorithm.UNCOMPRESSED))); - - policy.setSymmetricKeyEncryptionAlgorithmPolicy(new Policy.SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_256, - Arrays.asList(SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.AES_192, SymmetricKeyAlgorithm.AES_128))); - - policy.setSymmetricKeyDecryptionAlgorithmPolicy(new Policy.SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_256, - Arrays.asList(SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.AES_192, SymmetricKeyAlgorithm.AES_128, SymmetricKeyAlgorithm.BLOWFISH))); - 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.setCertificationSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, sigHashAlgoMap)); Map revHashAlgoMap = new HashMap<>(); revHashAlgoMap.put(HashAlgorithm.SHA512, null); @@ -53,10 +41,26 @@ public class PolicyTest { 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, - revHashAlgoMap)); - policy.setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()); + policy = new Policy().copy() + + .withCompressionAlgorithmPolicy(new Policy.CompressionAlgorithmPolicy(CompressionAlgorithm.UNCOMPRESSED, + Arrays.asList(CompressionAlgorithm.ZIP, CompressionAlgorithm.ZLIB, CompressionAlgorithm.UNCOMPRESSED))) + + .withSymmetricKeyEncryptionAlgorithmPolicy(new Policy.SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_256, + Arrays.asList(SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.AES_192, SymmetricKeyAlgorithm.AES_128))) + + .withSymmetricKeyDecryptionAlgorithmPolicy(new Policy.SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_256, + Arrays.asList(SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.AES_192, SymmetricKeyAlgorithm.AES_128, SymmetricKeyAlgorithm.BLOWFISH))) + + .withCertificationSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, sigHashAlgoMap)) + + .withRevocationSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, + revHashAlgoMap)) + + .withPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()) + + .build(); } @Test @@ -200,9 +204,4 @@ public class PolicyTest { public void testUnknownPublicKeyAlgorithmIsNotAcceptable() { assertFalse(policy.getPublicKeyAlgorithmPolicy().isAcceptable(-1, 4096)); } - - @Test - public void setNullSignerUserIdValidationLevelThrows() { - assertThrows(NullPointerException.class, () -> policy.setSignerUserIdValidationLevel(null)); - } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/policy/WeakRSAKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/policy/WeakRSAKeyTest.java index a2813402..52c60c2e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/WeakRSAKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/WeakRSAKeyTest.java @@ -13,8 +13,8 @@ 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.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -149,13 +149,14 @@ public class WeakRSAKeyTest { public void cannotGenerateWeakKeyWithDefaultPolicyTest() { String userId = "Alice "; assertThrows(IllegalArgumentException.class, () -> - PGPainless.generateKeyRing() + PGPainless.getInstance().generateKey() .rsaKeyRing(userId, RsaLength._1024, Passphrase.emptyPassphrase())); } @Test public void cannotSignWithWeakKey() throws IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(WEAK_RSA_KEY); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.readKey().parseKey(WEAK_RSA_KEY); SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); SigningOptions signingOptions = SigningOptions.get(); @@ -167,14 +168,15 @@ public class WeakRSAKeyTest { @Test public void encryptDecryptRoundTripWithWeakRSAKey() throws IOException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(WEAK_RSA_KEY); - PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.readKey().parseKey(WEAK_RSA_KEY); + OpenPGPCertificate publicKeys = secretKeys.toCertificate(); ByteArrayOutputStream encryptOut = new ByteArrayOutputStream(); - EncryptionOptions encryptionOptions = EncryptionOptions.encryptCommunications() + EncryptionOptions encryptionOptions = EncryptionOptions.encryptCommunications(api) .addRecipient(publicKeys); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(encryptOut) .withOptions(ProducerOptions.encrypt(encryptionOptions)); @@ -183,7 +185,7 @@ public class WeakRSAKeyTest { ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(encryptOut.toByteArray()); ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(ciphertextIn) .withOptions(ConsumerOptions.get() .addDecryptionKey(secretKeys)); diff --git a/pgpainless-core/src/test/java/org/pgpainless/provider/ProviderFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/provider/ProviderFactoryTest.java deleted file mode 100644 index 5489a11c..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/provider/ProviderFactoryTest.java +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.provider; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.security.Provider; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -public class ProviderFactoryTest { - - private final ProviderFactory customProviderFactory = new ProviderFactory() { - - @SuppressWarnings("deprecation") - final Provider provider = new Provider("PL", 1L, "PGPainlessTestProvider") { - - }; - - @Override - protected Provider getSecurityProvider() { - return provider; - } - - }; - - @Test - public void providerFactoryDefaultIsBouncyCastleTest() { - assertEquals("BC", ProviderFactory.getProviderName()); - } - - @Test - public void setCustomProviderTest() { - ProviderFactory.setFactory(customProviderFactory); - assertEquals("PL", ProviderFactory.getProviderName()); - } - - @AfterEach - public void resetToDefault() { - // Reset back to BouncyCastle - ProviderFactory.setFactory(new BouncyCastleProviderFactory()); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/BindingSignatureSubpacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/BindingSignatureSubpacketsTest.java index 331ca07a..5155861d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/BindingSignatureSubpacketsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/BindingSignatureSubpacketsTest.java @@ -4,24 +4,23 @@ package org.pgpainless.signature; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.util.Date; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +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.SignatureValidationException; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.consumer.CertificateValidator; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.util.TestAllImplementations; /** @@ -48,9 +47,6 @@ public class BindingSignatureSubpacketsTest { "-----END PGP SIGNATURE-----\n"; private static final String data = "Hello World :)"; - private Date validationDate = new Date(); - private Policy policy = PGPainless.getPolicy(); - @TestTemplate @ExtendWith(TestAllImplementations.class) public void baseCase() throws IOException, PGPException { @@ -1971,26 +1967,37 @@ public class BindingSignatureSubpacketsTest { } private void expectSignatureValidationSucceeds(String key, String message) throws IOException, PGPException { - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); - PGPSignature signature = SignatureUtils.readSignatures(sig).get(0); + PGPainless api = PGPainless.getInstance(); + OpenPGPCertificate certificate = api.readKey().parseCertificate(key); - try { - CertificateValidator.validateCertificateAndVerifyUninitializedSignature(signature, getSignedData(data), publicKeys, policy, validationDate); - } catch (SignatureValidationException e) { - // CHECKSTYLE:OFF - e.printStackTrace(); - // CHECKSTYLE:ON - fail(message + ": " + e.getMessage()); + DecryptionStream decryptionStream = api.processMessage().onInputStream(getSignedData(data)) + .withOptions(ConsumerOptions.get(api) + .addVerificationCert(certificate) + .addVerificationOfDetachedSignatures(SignatureUtils.readSignatures(sig))); + + Streams.drain(decryptionStream); + decryptionStream.close(); + MessageMetadata metadata = decryptionStream.getMetadata(); + + if (!metadata.getRejectedSignatures().isEmpty()) { + throw metadata.getRejectedSignatures().get(0).getValidationException(); } + assertTrue(decryptionStream.getMetadata().isVerifiedSignedBy(certificate), + message); } private void expectSignatureValidationFails(String key, String message) throws IOException, PGPException { - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); - PGPSignature signature = SignatureUtils.readSignatures(sig).get(0); + PGPainless api = PGPainless.getInstance(); + OpenPGPCertificate certificate = api.readKey().parseCertificate(key); - assertThrows(SignatureValidationException.class, () -> - CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - signature, getSignedData(data), publicKeys, policy, validationDate), + DecryptionStream decryptionStream = api.processMessage().onInputStream(getSignedData(data)) + .withOptions(ConsumerOptions.get(api) + .addVerificationCert(certificate) + .addVerificationOfDetachedSignatures(SignatureUtils.readSignatures(sig))); + + Streams.drain(decryptionStream); + decryptionStream.close(); + assertFalse(decryptionStream.getMetadata().isVerifiedSignedBy(certificate), message); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateValidatorTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateValidatorTest.java index 175ce101..63c2be55 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateValidatorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/CertificateValidatorTest.java @@ -12,11 +12,11 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.util.Date; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestTemplate; @@ -26,8 +26,6 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.consumer.CertificateValidator; import org.pgpainless.util.TestAllImplementations; public class CertificateValidatorTest { @@ -39,7 +37,7 @@ public class CertificateValidatorTest { */ @TestTemplate @ExtendWith(TestAllImplementations.class) - public void testPrimaryKeySignsAndIsHardRevokedUnknown() throws IOException, PGPException { + public void testPrimaryKeySignsAndIsHardRevokedUnknown() throws IOException { String key = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + @@ -166,21 +164,19 @@ public class CertificateValidatorTest { PGPSignature primaryKeyRevoked = SignatureUtils.readSignatures(sigPrimaryKeyRevoked).get(0); PGPSignature primaryKeyRevalidated = SignatureUtils.readSignatures(sigPrimaryKeyRevalidated).get(0); - Policy policy = PGPainless.getPolicy(); - Date validationDate = new Date(); String data = "Hello, World"; - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - predatesPrimaryKey, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + predatesPrimaryKey, getSignedData(data), publicKeys), "Signature predates primary key"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - unboundSubkey, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + unboundSubkey, getSignedData(data), publicKeys), "Primary key hard revoked"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - primaryKeyRevoked, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + primaryKeyRevoked, getSignedData(data), publicKeys), "Primary key hard revoked"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - primaryKeyRevalidated, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + primaryKeyRevalidated, getSignedData(data), publicKeys), "Primary key hard revoked"); } @@ -190,7 +186,7 @@ public class CertificateValidatorTest { */ @TestTemplate @ExtendWith(TestAllImplementations.class) - public void testSubkeySignsPrimaryKeyIsHardRevokedUnknown() throws IOException, PGPException { + public void testSubkeySignsPrimaryKeyIsHardRevokedUnknown() throws IOException { String key = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + @@ -317,21 +313,19 @@ public class CertificateValidatorTest { PGPSignature revokedSubkey = SignatureUtils.readSignatures(sigSubkeyRevoked).get(0); PGPSignature revalidatedSubkey = SignatureUtils.readSignatures(sigSubkeyRevalidated).get(0); - Policy policy = PGPainless.getPolicy(); - Date validationDate = new Date(); String data = "Hello, World"; - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - predatesPrimaryKey, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + predatesPrimaryKey, getSignedData(data), publicKeys), "Signature predates primary key"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - unboundSubkey, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + unboundSubkey, getSignedData(data), publicKeys), "Signing key unbound + hard revocation"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - revokedSubkey, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + revokedSubkey, getSignedData(data), publicKeys), "Primary key is hard revoked"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - revalidatedSubkey, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + revalidatedSubkey, getSignedData(data), publicKeys), "Primary key is hard revoked"); } @@ -342,7 +336,7 @@ public class CertificateValidatorTest { */ @TestTemplate @ExtendWith(TestAllImplementations.class) - public void testSubkeySignsAndIsHardRevokedUnknown() throws IOException, PGPException { + public void testSubkeySignsAndIsHardRevokedUnknown() throws IOException { String keyWithHardRev = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + @@ -469,21 +463,19 @@ public class CertificateValidatorTest { PGPSignature afterHardRevocation = SignatureUtils.readSignatures(sigAfterHardRevocation).get(0); PGPSignature afterRevalidation = SignatureUtils.readSignatures(sigAfterRevalidation).get(0); - Policy policy = PGPainless.getPolicy(); - Date validationDate = new Date(); String data = "Hello World :)"; - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - predatesPrimaryKey, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + predatesPrimaryKey, getSignedData(data), publicKeys), "Signature predates primary key"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - unboundKey, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + unboundKey, getSignedData(data), publicKeys), "Signing key unbound + hard revocation"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - afterHardRevocation, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + afterHardRevocation, getSignedData(data), publicKeys), "Hard revocation invalidates key at all times"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - afterRevalidation, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + afterRevalidation, getSignedData(data), publicKeys), "Hard revocation invalidates key at all times"); } @@ -494,7 +486,7 @@ public class CertificateValidatorTest { */ @TestTemplate @ExtendWith(TestAllImplementations.class) - public void testPrimaryKeySignsAndIsSoftRevokedSuperseded() throws IOException, PGPException { + public void testPrimaryKeySignsAndIsSoftRevokedSuperseded() throws IOException { String keyWithSoftRev = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + @@ -620,27 +612,26 @@ public class CertificateValidatorTest { PGPSignature keyIsValid = SignatureUtils.readSignatures(sigKeyIsValid).get(0); PGPSignature keyIsRevoked = SignatureUtils.readSignatures(sigKeyIsRevoked).get(0); PGPSignature keyIsRevalidated = SignatureUtils.readSignatures(sigKeyIsRevalidated).get(0); - Policy policy = PGPainless.getPolicy(); String data = "Hello, World"; // Sig not valid, as it predates the signing key creation time - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - predatesPrimaryKey, getSignedData(data), publicKeys, policy, predatesPrimaryKey.getCreationTime()), + assertThrows(SignatureValidationException.class, () -> verify( + predatesPrimaryKey, getSignedData(data), publicKeys), "Signature predates primary key creation date"); // Sig valid - assertDoesNotThrow(() -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - keyIsValid, getSignedData(data), publicKeys, policy, keyIsValid.getCreationTime()), + assertDoesNotThrow(() -> verify( + keyIsValid, getSignedData(data), publicKeys), "Signature is valid"); // Sig not valid, as the signing key is revoked - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - keyIsRevoked, getSignedData(data), publicKeys, policy, keyIsRevoked.getCreationTime()), + assertThrows(SignatureValidationException.class, () -> verify( + keyIsRevoked, getSignedData(data), publicKeys), "Signing key is revoked at this point"); // Sig valid, as the signing key is revalidated - assertDoesNotThrow(() -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - keyIsRevalidated, getSignedData(data), publicKeys, policy, keyIsRevalidated.getCreationTime()), + assertDoesNotThrow(() -> verify( + keyIsRevalidated, getSignedData(data), publicKeys), "Signature is valid, as signing key is revalidated"); } @@ -651,7 +642,7 @@ public class CertificateValidatorTest { */ @TestTemplate @ExtendWith(TestAllImplementations.class) - public void testSubkeySignsPrimaryKeyIsSoftRevokedSuperseded() throws IOException, PGPException { + public void testSubkeySignsPrimaryKeyIsSoftRevokedSuperseded() throws IOException { String key = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + @@ -778,22 +769,20 @@ public class CertificateValidatorTest { PGPSignature keyRevoked = SignatureUtils.readSignatures(sigKeyRevoked).get(0); PGPSignature valid = SignatureUtils.readSignatures(sigKeyValid).get(0); - Policy policy = PGPainless.getPolicy(); String data = "Hello, World"; - Date validationDate = new Date(); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - predatesPrimaryKey, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + predatesPrimaryKey, getSignedData(data), publicKeys), "Signature predates primary key creation date"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - keyNotBound, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + keyNotBound, getSignedData(data), publicKeys), "Signing key is not bound at this point"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - keyRevoked, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + keyRevoked, getSignedData(data), publicKeys), "Signing key is revoked at this point"); assertDoesNotThrow(() -> - CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - valid, getSignedData(data), publicKeys, policy, validationDate), + verify( + valid, getSignedData(data), publicKeys), "Signing key is revalidated"); } @@ -804,7 +793,7 @@ public class CertificateValidatorTest { */ @TestTemplate @ExtendWith(TestAllImplementations.class) - public void testPrimaryKeySignsAndIsSoftRevokedRetired() throws IOException, PGPException { + public void testPrimaryKeySignsAndIsSoftRevokedRetired() throws IOException { String key = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + @@ -931,22 +920,20 @@ public class CertificateValidatorTest { PGPSignature revoked = SignatureUtils.readSignatures(sigRevoked).get(0); PGPSignature revalidated = SignatureUtils.readSignatures(sigReLegitimized).get(0); - Policy policy = PGPainless.getPolicy(); - Date validationDate = new Date(); String data = "Hello, World"; - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - predatesPrimaryKey, getSignedData(data), publicKeys, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + predatesPrimaryKey, getSignedData(data), publicKeys), "Signature predates primary key creation date"); - assertDoesNotThrow(() -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - valid, getSignedData(data), publicKeys, policy, validationDate), + assertDoesNotThrow(() -> verify( + valid, getSignedData(data), publicKeys), "Signature is valid"); assertThrows(SignatureValidationException.class, () -> - CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - revoked, getSignedData(data), publicKeys, policy, validationDate), + verify( + revoked, getSignedData(data), publicKeys), "Primary key is revoked"); - assertDoesNotThrow(() -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - revalidated, getSignedData(data), publicKeys, policy, validationDate), + assertDoesNotThrow(() -> verify( + revalidated, getSignedData(data), publicKeys), "Primary key is re-legitimized"); } @@ -957,7 +944,7 @@ public class CertificateValidatorTest { */ @TestTemplate @ExtendWith(TestAllImplementations.class) - public void testTemporaryValidity() throws IOException, PGPException { + public void testTemporaryValidity() throws IOException { String keyA = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + "Comment: D1A6 6E1A 23B1 82C9 980F 788C FBFC C82A 015E 7330\n" + @@ -1271,51 +1258,68 @@ public class CertificateValidatorTest { PGPSignature sigCT2_T3 = SignatureUtils.readSignatures(keyCSigT2_T3).get(0); PGPSignature sigCT3_now = SignatureUtils.readSignatures(keyCSigT3_now).get(0); - Policy policy = PGPainless.getPolicy(); - Date validationDate = new Date(); String data = "Hello World :)"; - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - sigAT0, getSignedData(data), keysA, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + sigAT0, getSignedData(data), keysA), "Signature predates key creation time"); - assertDoesNotThrow(() -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - sigAT1_T2, getSignedData(data), keysA, policy, validationDate), + assertDoesNotThrow(() -> verify( + sigAT1_T2, getSignedData(data), keysA), "Key valid"); assertThrows(SignatureValidationException.class, () -> - CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - sigAT2_T3, getSignedData(data), keysA, policy, validationDate), + verify( + sigAT2_T3, getSignedData(data), keysA), "Key is not valid, as subkey binding expired"); - assertDoesNotThrow(() -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - sigAT3_now, getSignedData(data), keysA, policy, validationDate), + assertDoesNotThrow(() -> verify( + sigAT3_now, getSignedData(data), keysA), "Key is valid again"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - sigBT0, getSignedData(data), keysB, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + sigBT0, getSignedData(data), keysB), "Signature predates key creation time"); - assertDoesNotThrow(() -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - sigBT1_T2, getSignedData(data), keysB, policy, validationDate), + assertDoesNotThrow(() -> verify( + sigBT1_T2, getSignedData(data), keysB), "Key is valid"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - sigBT2_T3, getSignedData(data), keysB, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + sigBT2_T3, getSignedData(data), keysB), "Primary key is not signing-capable"); - assertDoesNotThrow(() -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - sigBT3_now, getSignedData(data), keysB, policy, validationDate), + assertDoesNotThrow(() -> verify( + sigBT3_now, getSignedData(data), keysB), "Key is valid again"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - sigCT0, getSignedData(data), keysC, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + sigCT0, getSignedData(data), keysC), "Signature predates key creation time"); - assertDoesNotThrow(() -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - sigCT1_T2, getSignedData(data), keysC, policy, validationDate), + assertDoesNotThrow(() -> verify( + sigCT1_T2, getSignedData(data), keysC), "Key is valid"); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - sigCT2_T3, getSignedData(data), keysC, policy, validationDate), + assertThrows(SignatureValidationException.class, () -> verify( + sigCT2_T3, getSignedData(data), keysC), "Key is revoked"); - assertDoesNotThrow(() -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature( - sigCT3_now, getSignedData(data), keysC, policy, validationDate), + assertDoesNotThrow(() -> verify( + sigCT3_now, getSignedData(data), keysC), "Key is valid again"); } + private void verify(PGPSignature signature, InputStream dataIn, PGPPublicKeyRing cert) throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPCertificate certificate = api.toCertificate(cert); + + DecryptionStream decryptionStream = api.processMessage() + .onInputStream(dataIn) + .withOptions(ConsumerOptions.get(api) + .addVerificationOfDetachedSignature(signature) + .addVerificationCert(certificate)); + + Streams.drain(decryptionStream); + decryptionStream.close(); + MessageMetadata metadata = decryptionStream.getMetadata(); + + if (metadata.hasRejectedSignatures()) { + throw metadata.getRejectedSignatures().get(0).getValidationException(); + } + } + private static InputStream getSignedData(String data) { return new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); } @@ -1381,11 +1385,12 @@ public class CertificateValidatorTest { "=NXei\n" + "-----END PGP PUBLIC KEY BLOCK-----\n"; String DATA = "Hello World :)"; + PGPainless api = PGPainless.getInstance(); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = api.processMessage() .onInputStream(new ByteArrayInputStream(DATA.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions() - .addVerificationCert(PGPainless.readKeyRing().publicKeyRing(CERT)) + .withOptions(ConsumerOptions.get() + .addVerificationCert(api.readKey().parseCertificate(CERT)) .addVerificationOfDetachedSignatures(new ByteArrayInputStream(SIG.getBytes(StandardCharsets.UTF_8)))); Streams.drain(decryptionStream); diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java index ff26506d..8b7bff74 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java @@ -142,9 +142,9 @@ public class IgnoreMarkerPacketsTest { PGPSignature signature = SignatureUtils.readSignatures(sig).get(0); InputStream messageIn = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(messageIn) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addVerificationCert(publicKeys) .addVerificationOfDetachedSignature(signature) ); @@ -191,9 +191,9 @@ public class IgnoreMarkerPacketsTest { String data = "Marker + Encrypted Message"; InputStream messageIn = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + DecryptionStream decryptionStream = PGPainless.getInstance().processMessage() .onInputStream(messageIn) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKey(secretKeys) .addVerificationCert(publicKeys) ); diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/KeyRevocationTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/KeyRevocationTest.java index 67f5cf4c..9d21e9a8 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/KeyRevocationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/KeyRevocationTest.java @@ -8,17 +8,20 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.util.Date; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +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.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.signature.consumer.CertificateValidator; import org.pgpainless.util.TestAllImplementations; public class KeyRevocationTest { @@ -27,7 +30,7 @@ public class KeyRevocationTest { @TestTemplate @ExtendWith(TestAllImplementations.class) - public void subkeySignsPrimaryKeyRevokedNoReason() throws IOException, PGPException { + public void subkeySignsPrimaryKeyRevokedNoReason() throws IOException { String key = "-----BEGIN PGP ARMORED FILE-----\n" + "Comment: ASCII Armor added by openpgp-interoperability-test-suite\n" + @@ -146,25 +149,26 @@ public class KeyRevocationTest { "u5SfXaTsbMeVQJNdjCNsHq2bOXPGLw==\n" + "=2BW4\n" + "-----END PGP ARMORED FILE-----\n"; + PGPainless api = PGPainless.getInstance(); - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); + OpenPGPCertificate publicKeys = api.readKey().parseCertificate(key); PGPSignature t0 = SignatureUtils.readSignatures(sigT0).get(0); PGPSignature t1t2 = SignatureUtils.readSignatures(sigT1T2).get(0); PGPSignature t2t3 = SignatureUtils.readSignatures(sigT2T3).get(0); PGPSignature t3now = SignatureUtils.readSignatures(sigT3Now).get(0); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature(t0, + assertThrows(SignatureValidationException.class, () -> verify(t0, new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), - publicKeys, PGPainless.getPolicy(), new Date())); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature(t1t2, + publicKeys, api)); + assertThrows(SignatureValidationException.class, () -> verify(t1t2, new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), - publicKeys, PGPainless.getPolicy(), new Date())); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature(t2t3, + publicKeys, api)); + assertThrows(SignatureValidationException.class, () -> verify(t2t3, new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), - publicKeys, PGPainless.getPolicy(), new Date())); - assertThrows(SignatureValidationException.class, () -> CertificateValidator.validateCertificateAndVerifyUninitializedSignature(t3now, + publicKeys, api)); + assertThrows(SignatureValidationException.class, () -> verify(t3now, new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), - publicKeys, PGPainless.getPolicy(), new Date())); + publicKeys, api)); } /** @@ -252,11 +256,32 @@ public class KeyRevocationTest { "=MOaJ\n" + "-----END PGP ARMORED FILE-----\n"; - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(key); + PGPainless api = PGPainless.getInstance(); + + OpenPGPCertificate publicKeys = api.readKey().parseCertificate(key); PGPSignature signature = SignatureUtils.readSignatures(sig).get(0); - CertificateValidator.validateCertificateAndVerifyUninitializedSignature(signature, + verify(signature, new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), - publicKeys, PGPainless.getPolicy(), new Date()); + publicKeys, api); } + + + private void verify(PGPSignature signature, InputStream dataIn, OpenPGPCertificate certificate, PGPainless api) + throws PGPException, IOException { + DecryptionStream decryptionStream = api.processMessage() + .onInputStream(dataIn) + .withOptions(ConsumerOptions.get(api) + .addVerificationOfDetachedSignature(signature) + .addVerificationCert(certificate)); + + Streams.drain(decryptionStream); + decryptionStream.close(); + MessageMetadata metadata = decryptionStream.getMetadata(); + + if (metadata.hasRejectedSignatures()) { + throw metadata.getRejectedSignatures().get(0).getValidationException(); + } + } + } 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 fd7c53e9..578094bb 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java @@ -6,14 +6,13 @@ package org.pgpainless.signature; 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.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; @@ -23,14 +22,12 @@ import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPOnePassSignature; import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPPrivateKey; 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.PGPUtil; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.TestTemplate; @@ -44,7 +41,6 @@ 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.implementation.ImplementationFactory; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.TestAllImplementations; @@ -54,19 +50,20 @@ public class OnePassSignatureBracketingTest { @TestTemplate @ExtendWith(TestAllImplementations.class) public void onePassSignaturePacketsAndSignaturesAreBracketedTest() - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); - PGPSecretKeyRing key1 = PGPainless.generateKeyRing().modernKeyRing("Alice"); - PGPSecretKeyRing key2 = PGPainless.generateKeyRing().modernKeyRing("Bob"); - PGPPublicKeyRing cert1 = PGPainless.extractCertificate(key1); + OpenPGPKey key1 = api.generateKey().modernKeyRing("Alice"); + OpenPGPKey key2 = api.generateKey().modernKeyRing("Bob"); + OpenPGPCertificate cert1 = key1.toCertificate(); ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = api.generateMessage() .onOutputStream(out) .withOptions(ProducerOptions.signAndEncrypt( - EncryptionOptions.encryptCommunications() + EncryptionOptions.encryptCommunications(api) .addRecipient(cert1), - SigningOptions.get() + SigningOptions.get(api) .addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), key1, DocumentSignatureType.BINARY_DOCUMENT) .addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), key2, DocumentSignatureType.BINARY_DOCUMENT) ).setAsciiArmor(true)); @@ -78,7 +75,7 @@ public class OnePassSignatureBracketingTest { ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(out.toByteArray()); InputStream inputStream = PGPUtil.getDecoderStream(ciphertextIn); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(inputStream); + PGPObjectFactory objectFactory = api.getImplementation().pgpObjectFactory(inputStream); PGPOnePassSignatureList onePassSignatures = null; PGPSignatureList signatures = null; @@ -93,11 +90,12 @@ public class OnePassSignatureBracketingTest { for (PGPEncryptedData encryptedData : encryptedDataList) { if (encryptedData instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData; - PGPSecretKey secretKey = key1.getSecretKey(publicKeyEncryptedData.getKeyID()); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, SecretKeyRingProtector.unprotectedKeys()); - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey); + OpenPGPKey.OpenPGPSecretKey secretKey = key1.getSecretKey(publicKeyEncryptedData.getKeyIdentifier()); + OpenPGPKey.OpenPGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, SecretKeyRingProtector.unprotectedKeys()); + PublicKeyDataDecryptorFactory decryptorFactory = api.getImplementation() + .publicKeyDataDecryptorFactory(privateKey.getKeyPair().getPrivateKey()); InputStream decryptionStream = publicKeyEncryptedData.getDataStream(decryptorFactory); - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decryptionStream); + objectFactory = api.getImplementation().pgpObjectFactory(decryptionStream); continue outerloop; } } @@ -107,7 +105,7 @@ public class OnePassSignatureBracketingTest { } else if (next instanceof PGPCompressedData) { PGPCompressedData compressed = (PGPCompressedData) next; InputStream decompressor = compressed.getDataStream(); - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decompressor); + objectFactory = api.getImplementation().pgpObjectFactory(decompressor); continue outerloop; } else if (next instanceof PGPLiteralData) { continue outerloop; @@ -131,6 +129,7 @@ public class OnePassSignatureBracketingTest { // eg. (OPS1, OPS2, LiteralData, Sig2, Sig1) PGPOnePassSignature onePassSignature = onePassSignatures.get(i); PGPSignature signature = signatures.get(signatures.size() - 1 - i); + assertTrue(signature.hasKeyIdentifier(onePassSignature.getKeyIdentifier())); assertEquals(onePassSignature.getKeyID(), signature.getKeyID()); byte[] encoded = onePassSignature.getEncoded(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureOverUserAttributesTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureOverUserAttributesTest.java index 16b241b4..19e4ef52 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureOverUserAttributesTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureOverUserAttributesTest.java @@ -4,37 +4,37 @@ package org.pgpainless.signature; -import static org.junit.jupiter.api.Assertions.assertThrows; - import java.io.IOException; import java.util.Date; +import java.util.List; 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.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVectorGenerator; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPImplementation; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.signature.consumer.SignatureVerifier; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; public class SignatureOverUserAttributesTest { private static final byte[] image = new byte[] {(byte) -1, (byte) -40, (byte) -1, (byte) -32, (byte) 0, (byte) 16, (byte) 74, (byte) 70, (byte) 73, (byte) 70, (byte) 0, (byte) 1, (byte) 1, (byte) 1, (byte) 1, (byte) 44, (byte) 1, (byte) 44, (byte) 0, (byte) 0, (byte) -1, (byte) -2, (byte) 0, (byte) 19, (byte) 67, (byte) 114, (byte) 101, (byte) 97, (byte) 116, (byte) 101, (byte) 100, (byte) 32, (byte) 119, (byte) 105, (byte) 116, (byte) 104, (byte) 32, (byte) 71, (byte) 73, (byte) 77, (byte) 80, (byte) -1, (byte) -30, (byte) 2, (byte) -80, (byte) 73, (byte) 67, (byte) 67, (byte) 95, (byte) 80, (byte) 82, (byte) 79, (byte) 70, (byte) 73, (byte) 76, (byte) 69, (byte) 0, (byte) 1, (byte) 1, (byte) 0, (byte) 0, (byte) 2, (byte) -96, (byte) 108, (byte) 99, (byte) 109, (byte) 115, (byte) 4, (byte) 48, (byte) 0, (byte) 0, (byte) 109, (byte) 110, (byte) 116, (byte) 114, (byte) 82, (byte) 71, (byte) 66, (byte) 32, (byte) 88, (byte) 89, (byte) 90, (byte) 32, (byte) 7, (byte) -27, (byte) 0, (byte) 10, (byte) 0, (byte) 4, (byte) 0, (byte) 12, (byte) 0, (byte) 27, (byte) 0, (byte) 19, (byte) 97, (byte) 99, (byte) 115, (byte) 112, (byte) 65, (byte) 80, (byte) 80, (byte) 76, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) -10, (byte) -42, (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) -45, (byte) 45, (byte) 108, (byte) 99, (byte) 109, (byte) 115, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 13, (byte) 100, (byte) 101, (byte) 115, (byte) 99, (byte) 0, (byte) 0, (byte) 1, (byte) 32, (byte) 0, (byte) 0, (byte) 0, (byte) 64, (byte) 99, (byte) 112, (byte) 114, (byte) 116, (byte) 0, (byte) 0, (byte) 1, (byte) 96, (byte) 0, (byte) 0, (byte) 0, (byte) 54, (byte) 119, (byte) 116, (byte) 112, (byte) 116, (byte) 0, (byte) 0, (byte) 1, (byte) -104, (byte) 0, (byte) 0, (byte) 0, (byte) 20, (byte) 99, (byte) 104, (byte) 97, (byte) 100, (byte) 0, (byte) 0, (byte) 1, (byte) -84, (byte) 0, (byte) 0, (byte) 0, (byte) 44, (byte) 114, (byte) 88, (byte) 89, (byte) 90, (byte) 0, (byte) 0, (byte) 1, (byte) -40, (byte) 0, (byte) 0, (byte) 0, (byte) 20, (byte) 98, (byte) 88, (byte) 89, (byte) 90, (byte) 0, (byte) 0, (byte) 1, (byte) -20, (byte) 0, (byte) 0, (byte) 0, (byte) 20, (byte) 103, (byte) 88, (byte) 89, (byte) 90, (byte) 0, (byte) 0, (byte) 2, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 20, (byte) 114, (byte) 84, (byte) 82, (byte) 67, (byte) 0, (byte) 0, (byte) 2, (byte) 20, (byte) 0, (byte) 0, (byte) 0, (byte) 32, (byte) 103, (byte) 84, (byte) 82, (byte) 67, (byte) 0, (byte) 0, (byte) 2, (byte) 20, (byte) 0, (byte) 0, (byte) 0, (byte) 32, (byte) 98, (byte) 84, (byte) 82, (byte) 67, (byte) 0, (byte) 0, (byte) 2, (byte) 20, (byte) 0, (byte) 0, (byte) 0, (byte) 32, (byte) 99, (byte) 104, (byte) 114, (byte) 109, (byte) 0, (byte) 0, (byte) 2, (byte) 52, (byte) 0, (byte) 0, (byte) 0, (byte) 36, (byte) 100, (byte) 109, (byte) 110, (byte) 100, (byte) 0, (byte) 0, (byte) 2, (byte) 88, (byte) 0, (byte) 0, (byte) 0, (byte) 36, (byte) 100, (byte) 109, (byte) 100, (byte) 100, (byte) 0, (byte) 0, (byte) 2, (byte) 124, (byte) 0, (byte) 0, (byte) 0, (byte) 36, (byte) 109, (byte) 108, (byte) 117, (byte) 99, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 12, (byte) 101, (byte) 110, (byte) 85, (byte) 83, (byte) 0, (byte) 0, (byte) 0, (byte) 36, (byte) 0, (byte) 0, (byte) 0, (byte) 28, (byte) 0, (byte) 71, (byte) 0, (byte) 73, (byte) 0, (byte) 77, (byte) 0, (byte) 80, (byte) 0, (byte) 32, (byte) 0, (byte) 98, (byte) 0, (byte) 117, (byte) 0, (byte) 105, (byte) 0, (byte) 108, (byte) 0, (byte) 116, (byte) 0, (byte) 45, (byte) 0, (byte) 105, (byte) 0, (byte) 110, (byte) 0, (byte) 32, (byte) 0, (byte) 115, (byte) 0, (byte) 82, (byte) 0, (byte) 71, (byte) 0, (byte) 66, (byte) 109, (byte) 108, (byte) 117, (byte) 99, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 12, (byte) 101, (byte) 110, (byte) 85, (byte) 83, (byte) 0, (byte) 0, (byte) 0, (byte) 26, (byte) 0, (byte) 0, (byte) 0, (byte) 28, (byte) 0, (byte) 80, (byte) 0, (byte) 117, (byte) 0, (byte) 98, (byte) 0, (byte) 108, (byte) 0, (byte) 105, (byte) 0, (byte) 99, (byte) 0, (byte) 32, (byte) 0, (byte) 68, (byte) 0, (byte) 111, (byte) 0, (byte) 109, (byte) 0, (byte) 97, (byte) 0, (byte) 105, (byte) 0, (byte) 110, (byte) 0, (byte) 0, (byte) 88, (byte) 89, (byte) 90, (byte) 32, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) -10, (byte) -42, (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) -45, (byte) 45, (byte) 115, (byte) 102, (byte) 51, (byte) 50, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 12, (byte) 66, (byte) 0, (byte) 0, (byte) 5, (byte) -34, (byte) -1, (byte) -1, (byte) -13, (byte) 37, (byte) 0, (byte) 0, (byte) 7, (byte) -109, (byte) 0, (byte) 0, (byte) -3, (byte) -112, (byte) -1, (byte) -1, (byte) -5, (byte) -95, (byte) -1, (byte) -1, (byte) -3, (byte) -94, (byte) 0, (byte) 0, (byte) 3, (byte) -36, (byte) 0, (byte) 0, (byte) -64, (byte) 110, (byte) 88, (byte) 89, (byte) 90, (byte) 32, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 111, (byte) -96, (byte) 0, (byte) 0, (byte) 56, (byte) -11, (byte) 0, (byte) 0, (byte) 3, (byte) -112, (byte) 88, (byte) 89, (byte) 90, (byte) 32, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 36, (byte) -97, (byte) 0, (byte) 0, (byte) 15, (byte) -124, (byte) 0, (byte) 0, (byte) -74, (byte) -60, (byte) 88, (byte) 89, (byte) 90, (byte) 32, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 98, (byte) -105, (byte) 0, (byte) 0, (byte) -73, (byte) -121, (byte) 0, (byte) 0, (byte) 24, (byte) -39, (byte) 112, (byte) 97, (byte) 114, (byte) 97, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 3, (byte) 0, (byte) 0, (byte) 0, (byte) 2, (byte) 102, (byte) 102, (byte) 0, (byte) 0, (byte) -14, (byte) -89, (byte) 0, (byte) 0, (byte) 13, (byte) 89, (byte) 0, (byte) 0, (byte) 19, (byte) -48, (byte) 0, (byte) 0, (byte) 10, (byte) 91, (byte) 99, (byte) 104, (byte) 114, (byte) 109, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 3, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) -93, (byte) -41, (byte) 0, (byte) 0, (byte) 84, (byte) 124, (byte) 0, (byte) 0, (byte) 76, (byte) -51, (byte) 0, (byte) 0, (byte) -103, (byte) -102, (byte) 0, (byte) 0, (byte) 38, (byte) 103, (byte) 0, (byte) 0, (byte) 15, (byte) 92, (byte) 109, (byte) 108, (byte) 117, (byte) 99, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 12, (byte) 101, (byte) 110, (byte) 85, (byte) 83, (byte) 0, (byte) 0, (byte) 0, (byte) 8, (byte) 0, (byte) 0, (byte) 0, (byte) 28, (byte) 0, (byte) 71, (byte) 0, (byte) 73, (byte) 0, (byte) 77, (byte) 0, (byte) 80, (byte) 109, (byte) 108, (byte) 117, (byte) 99, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 12, (byte) 101, (byte) 110, (byte) 85, (byte) 83, (byte) 0, (byte) 0, (byte) 0, (byte) 8, (byte) 0, (byte) 0, (byte) 0, (byte) 28, (byte) 0, (byte) 115, (byte) 0, (byte) 82, (byte) 0, (byte) 71, (byte) 0, (byte) 66, (byte) -1, (byte) -37, (byte) 0, (byte) 67, (byte) 0, (byte) 16, (byte) 11, (byte) 12, (byte) 14, (byte) 12, (byte) 10, (byte) 16, (byte) 14, (byte) 13, (byte) 14, (byte) 18, (byte) 17, (byte) 16, (byte) 19, (byte) 24, (byte) 40, (byte) 26, (byte) 24, (byte) 22, (byte) 22, (byte) 24, (byte) 49, (byte) 35, (byte) 37, (byte) 29, (byte) 40, (byte) 58, (byte) 51, (byte) 61, (byte) 60, (byte) 57, (byte) 51, (byte) 56, (byte) 55, (byte) 64, (byte) 72, (byte) 92, (byte) 78, (byte) 64, (byte) 68, (byte) 87, (byte) 69, (byte) 55, (byte) 56, (byte) 80, (byte) 109, (byte) 81, (byte) 87, (byte) 95, (byte) 98, (byte) 103, (byte) 104, (byte) 103, (byte) 62, (byte) 77, (byte) 113, (byte) 121, (byte) 112, (byte) 100, (byte) 120, (byte) 92, (byte) 101, (byte) 103, (byte) 99, (byte) -1, (byte) -37, (byte) 0, (byte) 67, (byte) 1, (byte) 17, (byte) 18, (byte) 18, (byte) 24, (byte) 21, (byte) 24, (byte) 47, (byte) 26, (byte) 26, (byte) 47, (byte) 99, (byte) 66, (byte) 56, (byte) 66, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) 99, (byte) -1, (byte) -62, (byte) 0, (byte) 17, (byte) 8, (byte) 0, (byte) 16, (byte) 0, (byte) 16, (byte) 3, (byte) 1, (byte) 17, (byte) 0, (byte) 2, (byte) 17, (byte) 1, (byte) 3, (byte) 17, (byte) 1, (byte) -1, (byte) -60, (byte) 0, (byte) 22, (byte) 0, (byte) 1, (byte) 1, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 3, (byte) 5, (byte) -1, (byte) -60, (byte) 0, (byte) 20, (byte) 1, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) -1, (byte) -38, (byte) 0, (byte) 12, (byte) 3, (byte) 1, (byte) 0, (byte) 2, (byte) 16, (byte) 3, (byte) 16, (byte) 0, (byte) 0, (byte) 1, (byte) -46, (byte) 4, (byte) -127, (byte) -1, (byte) -60, (byte) 0, (byte) 23, (byte) 16, (byte) 1, (byte) 1, (byte) 1, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 3, (byte) 1, (byte) 2, (byte) 18, (byte) -1, (byte) -38, (byte) 0, (byte) 8, (byte) 1, (byte) 1, (byte) 0, (byte) 1, (byte) 5, (byte) 2, (byte) 100, (byte) -99, (byte) -118, (byte) 78, (byte) -44, (byte) -18, (byte) -100, (byte) -114, (byte) -27, (byte) -1, (byte) 0, (byte) -1, (byte) -60, (byte) 0, (byte) 20, (byte) 17, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 32, (byte) -1, (byte) -38, (byte) 0, (byte) 8, (byte) 1, (byte) 3, (byte) 1, (byte) 1, (byte) 63, (byte) 1, (byte) 31, (byte) -1, (byte) -60, (byte) 0, (byte) 20, (byte) 17, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 32, (byte) -1, (byte) -38, (byte) 0, (byte) 8, (byte) 1, (byte) 2, (byte) 1, (byte) 1, (byte) 63, (byte) 1, (byte) 31, (byte) -1, (byte) -60, (byte) 0, (byte) 31, (byte) 16, (byte) 0, (byte) 1, (byte) 1, (byte) 9, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 2, (byte) 17, (byte) 18, (byte) 33, (byte) 49, (byte) 81, (byte) 113, (byte) -111, (byte) -47, (byte) 3, (byte) -1, (byte) -38, (byte) 0, (byte) 8, (byte) 1, (byte) 1, (byte) 0, (byte) 6, (byte) 63, (byte) 2, (byte) 30, (byte) 111, (byte) 55, (byte) 107, (byte) 8, (byte) -80, (byte) -13, (byte) 118, (byte) 112, (byte) -88, (byte) 97, (byte) 32, (byte) 79, (byte) 125, (byte) 84, (byte) 48, (byte) -128, (byte) 103, (byte) -82, (byte) 47, (byte) -1, (byte) -60, (byte) 0, (byte) 28, (byte) 16, (byte) 1, (byte) 0, (byte) 2, (byte) 1, (byte) 5, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 17, (byte) 33, (byte) 49, (byte) 65, (byte) 81, (byte) 113, (byte) -127, (byte) -1, (byte) -38, (byte) 0, (byte) 8, (byte) 1, (byte) 1, (byte) 0, (byte) 1, (byte) 63, (byte) 33, (byte) 50, (byte) -128, (byte) -43, (byte) 26, (byte) -84, (byte) -73, (byte) -18, (byte) 56, (byte) 104, (byte) 106, (byte) -83, (byte) -34, (byte) 27, (byte) -9, (byte) 26, (byte) 113, (byte) -125, (byte) -59, (byte) 65, (byte) 78, (byte) 112, (byte) 120, (byte) -88, (byte) -1, (byte) -38, (byte) 0, (byte) 12, (byte) 3, (byte) 1, (byte) 0, (byte) 2, (byte) 0, (byte) 3, (byte) 0, (byte) 0, (byte) 0, (byte) 16, (byte) 0, (byte) 15, (byte) -1, (byte) -60, (byte) 0, (byte) 20, (byte) 17, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 32, (byte) -1, (byte) -38, (byte) 0, (byte) 8, (byte) 1, (byte) 3, (byte) 1, (byte) 1, (byte) 63, (byte) 16, (byte) 31, (byte) -1, (byte) -60, (byte) 0, (byte) 20, (byte) 17, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 32, (byte) -1, (byte) -38, (byte) 0, (byte) 8, (byte) 1, (byte) 2, (byte) 1, (byte) 1, (byte) 63, (byte) 16, (byte) 31, (byte) -1, (byte) -60, (byte) 0, (byte) 25, (byte) 16, (byte) 1, (byte) 1, (byte) 0, (byte) 3, (byte) 1, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 1, (byte) 17, (byte) 0, (byte) 33, (byte) 49, (byte) 65, (byte) -1, (byte) -38, (byte) 0, (byte) 8, (byte) 1, (byte) 1, (byte) 0, (byte) 1, (byte) 63, (byte) 16, (byte) -107, (byte) 3, (byte) 101, (byte) -86, (byte) 14, (byte) -55, (byte) 65, (byte) -18, (byte) 74, (byte) -95, (byte) -78, (byte) -43, (byte) 15, (byte) 109, (byte) -119, (byte) -9, (byte) 27, (byte) -42, (byte) -76, (byte) -70, (byte) 80, (byte) 69, (byte) -91, (byte) -27, (byte) 115, (byte) -61, (byte) 27, (byte) -62, (byte) -108, (byte) -70, (byte) 20, (byte) 1, (byte) -95, (byte) -27, (byte) 115, (byte) -41, (byte) 63, (byte) -1, (byte) -39}; - private static PGPUserAttributeSubpacketVector attribute; - private static PGPUserAttributeSubpacketVector invalidAttribute; + private static final PGPUserAttributeSubpacketVector attribute; + private static final PGPUserAttributeSubpacketVector invalidAttribute; static { PGPUserAttributeSubpacketVectorGenerator attrGen = new PGPUserAttributeSubpacketVectorGenerator(); @@ -52,40 +52,50 @@ public class SignatureOverUserAttributesTest { @Test public void createAndVerifyUserAttributeCertification() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); - PGPSecretKey secretKey = secretKeys.getSecretKey(); - PGPPublicKey publicKey = secretKey.getPublicKey(); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, SecretKeyRingProtector.unprotectedKeys()); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); + + OpenPGPKey.OpenPGPSecretKey secretKey = secretKeys.getPrimarySecretKey(); + OpenPGPCertificate.OpenPGPComponentKey publicKey = secretKey.getPublicKey(); + OpenPGPKey.OpenPGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, SecretKeyRingProtector.unprotectedKeys()); PGPSignatureGenerator generator = new PGPSignatureGenerator( - ImplementationFactory.getInstance() - .getPGPContentSignerBuilder(secretKey.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId())); - generator.init(SignatureType.CASUAL_CERTIFICATION.getCode(), privateKey); + OpenPGPImplementation.getInstance() + .pgpContentSignerBuilder(secretKey.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId()), + secretKey.getPublicKey().getPGPPublicKey()); + generator.init(SignatureType.CASUAL_CERTIFICATION.getCode(), privateKey.getKeyPair().getPrivateKey()); - PGPSignature signature = generator.generateCertification(attribute, publicKey); - publicKey = PGPPublicKey.addCertification(publicKey, attribute, signature); - SignatureVerifier.verifyUserAttributesCertification(attribute, signature, publicKey, PGPainless.getPolicy(), new Date()); + PGPSignature signature = generator.generateCertification(attribute, publicKey.getPGPPublicKey()); + PGPPublicKey pgpPublicKey = PGPPublicKey.addCertification(publicKey.getPGPPublicKey(), attribute, signature); + pgpPublicKey = PGPPublicKey.addCertification(pgpPublicKey, invalidAttribute, signature); + OpenPGPCertificate withUserAttribute = api.toCertificate(PGPPublicKeyRing.insertPublicKey(secretKeys.getPGPPublicKeyRing(), pgpPublicKey)); + List identities = withUserAttribute.getIdentities(); - PGPPublicKey finalPublicKey = publicKey; - assertThrows(SignatureValidationException.class, () -> SignatureVerifier.verifyUserAttributesCertification(invalidAttribute, signature, finalPublicKey, PGPainless.getPolicy(), new Date())); + assertTrue(identities.get(1).isBound()); // valid + assertFalse(identities.get(2).isBound()); // invalid } @Test public void createAndVerifyUserAttributeRevocation() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); - PGPSecretKey secretKey = secretKeys.getSecretKey(); - PGPPublicKey publicKey = secretKey.getPublicKey(); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, SecretKeyRingProtector.unprotectedKeys()); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); + OpenPGPKey.OpenPGPSecretKey secretKey = secretKeys.getPrimarySecretKey(); + OpenPGPCertificate.OpenPGPComponentKey publicKey = secretKey.getPublicKey(); + OpenPGPKey.OpenPGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, SecretKeyRingProtector.unprotectedKeys()); PGPSignatureGenerator generator = new PGPSignatureGenerator( - ImplementationFactory.getInstance() - .getPGPContentSignerBuilder(secretKey.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId())); - generator.init(SignatureType.CERTIFICATION_REVOCATION.getCode(), privateKey); + api.getImplementation() + .pgpContentSignerBuilder(secretKey.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId()), + publicKey.getPGPPublicKey()); + generator.init(SignatureType.CERTIFICATION_REVOCATION.getCode(), privateKey.getKeyPair().getPrivateKey()); - PGPSignature signature = generator.generateCertification(attribute, publicKey); - publicKey = PGPPublicKey.addCertification(publicKey, attribute, signature); - SignatureVerifier.verifyUserAttributesRevocation(attribute, signature, publicKey, PGPainless.getPolicy(), new Date()); - PGPPublicKey finalPublicKey = publicKey; - assertThrows(SignatureValidationException.class, () -> SignatureVerifier.verifyUserAttributesCertification(invalidAttribute, signature, finalPublicKey, PGPainless.getPolicy(), new Date())); + PGPSignature signature = generator.generateCertification(attribute, publicKey.getPGPPublicKey()); + PGPPublicKey pgpPublicKey = PGPPublicKey.addCertification(publicKey.getPGPPublicKey(), attribute, signature); + OpenPGPCertificate withRevocation = api.toCertificate(PGPPublicKeyRing.insertPublicKey(secretKeys.getPGPPublicKeyRing(), pgpPublicKey)); + List identities = withRevocation.getIdentities(); + OpenPGPCertificate.OpenPGPComponentSignature revocation = identities.get(1).getRevocation(new Date()); + revocation.verify(api.getImplementation()); + assertTrue(revocation.isRevocation()); + assertTrue(revocation.isTestedCorrect()); } } 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 0d957309..83c7c2a8 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureStructureTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureStructureTest.java @@ -7,12 +7,10 @@ package org.pgpainless.signature; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import java.io.IOException; import java.util.List; import org.bouncycastle.bcpg.sig.IssuerKeyID; import org.bouncycastle.bcpg.sig.NotationData; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.util.encoders.Hex; import org.junit.jupiter.api.BeforeAll; @@ -30,7 +28,7 @@ public class SignatureStructureTest { private static PGPSignature signature; @BeforeAll - public static void parseSignature() throws IOException, PGPException { + public static void parseSignature() { // see https://tests.sequoia-pgp.org/#Detached_signature_with_Subpackets (base case) signature = SignatureUtils.readSignatures("-----BEGIN PGP SIGNATURE-----\n" + "\n" + 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 1caeb9e9..475aa8c6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java @@ -9,15 +9,10 @@ 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.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.time.temporal.ChronoUnit; import java.util.Arrays; -import java.util.Date; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -30,58 +25,39 @@ import org.bouncycastle.bcpg.sig.RevocationKey; import org.bouncycastle.bcpg.sig.TrustSignature; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.bouncycastle.openpgp.api.OpenPGPImplementation; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.Feature; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.consumer.SignaturePicker; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; public class SignatureSubpacketsUtilTest { - @Test - public void testGetKeyExpirationTimeAsDate() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() - .modernKeyRing("Expire"); - Date expiration = Date.from(new Date().toInstant().plus(365, ChronoUnit.DAYS)); - secretKeys = PGPainless.modifyKeyRing(secretKeys) - .setExpirationDate(expiration, SecretKeyRingProtector.unprotectedKeys()) - .done(); - - PGPSignature expirationSig = SignaturePicker.pickCurrentUserIdCertificationSignature( - secretKeys, "Expire", Policy.getInstance(), new Date()); - PGPPublicKey notTheRightKey = PGPainless.inspectKeyRing(secretKeys).getSigningSubkeys().get(0); - - assertThrows(IllegalArgumentException.class, () -> - SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(expirationSig, notTheRightKey)); - } - @Test public void testGetRevocable() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); - PGPPrivateKey certKey = UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); + OpenPGPKey secretKeys = TestKeys.getEmilKey(); + PGPPrivateKey certKey = UnlockSecretKey.unlockSecretKey(secretKeys.getPrimarySecretKey().getPGPSecretKey(), + SecretKeyRingProtector.unprotectedKeys()); PGPSignatureGenerator generator = getSignatureGenerator(certKey, SignatureType.CASUAL_CERTIFICATION); - PGPSignature withoutRevocable = generator.generateCertification(secretKeys.getPublicKey()); + PGPSignature withoutRevocable = generator.generateCertification(secretKeys.getPrimaryKey().getPGPPublicKey()); assertNull(SignatureSubpacketsUtil.getRevocable(withoutRevocable)); generator = getSignatureGenerator(certKey, SignatureType.CASUAL_CERTIFICATION); PGPSignatureSubpacketGenerator hashed = new PGPSignatureSubpacketGenerator(); hashed.setRevocable(true, true); generator.setHashedSubpackets(hashed.generate()); - PGPSignature withRevocable = generator.generateCertification(secretKeys.getPublicKey()); + PGPSignature withRevocable = generator.generateCertification(secretKeys.getPrimaryKey().getPGPPublicKey()); assertNotNull(SignatureSubpacketsUtil.getRevocable(withRevocable)); } @@ -287,7 +263,7 @@ public class SignatureSubpacketsUtilTest { private PGPSignatureGenerator getSignatureGenerator(PGPPrivateKey signingKey, SignatureType signatureType) throws PGPException { PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( - ImplementationFactory.getInstance().getPGPContentSignerBuilder( + OpenPGPImplementation.getInstance().pgpContentSignerBuilder( signingKey.getPublicKeyPacket().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId())); signatureGenerator.init(signatureType.getCode(), signingKey); 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 f57ed2c4..1415eff9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureUtilsTest.java @@ -9,7 +9,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; 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; @@ -19,7 +18,7 @@ import org.pgpainless.PGPainless; public class SignatureUtilsTest { @Test - public void readSignaturesFromCompressedDataDoesNotAttemptDecompression() throws PGPException, IOException { + public void readSignaturesFromCompressedDataDoesNotAttemptDecompression() { String compressed = "-----BEGIN PGP MESSAGE-----\n" + "Version: PGPainless\n" + "\n" + @@ -35,7 +34,7 @@ public class SignatureUtilsTest { } @Test - public void noIssuerResultsInKeyId0() throws PGPException, IOException { + public void noIssuerResultsInKeyId0() { String sig = "-----BEGIN PGP SIGNATURE-----\n" + "\n" + "wsEaBAABCABOBYJhVBVcRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEt\n" + @@ -55,7 +54,7 @@ public class SignatureUtilsTest { } @Test - public void skipInvalidSignatures() throws PGPException, IOException { + public void skipInvalidSignatures() { // Sig version 23 (invalid), sig version 4 String sigs = "-----BEGIN PGP SIGNATURE-----\n" + "\n" + diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureWasPossiblyMadeByKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureWasPossiblyMadeByKeyTest.java deleted file mode 100644 index c7424af1..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureWasPossiblyMadeByKeyTest.java +++ /dev/null @@ -1,213 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.IOException; - -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; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.signature.consumer.SignatureValidator; - -public class SignatureWasPossiblyMadeByKeyTest { - - public static PGPPublicKeyRing CERT; - public static PGPPublicKey SIGKEY; - public static PGPPublicKey NOSIGKEY; - static { - try { - CERT = PGPainless.readKeyRing().publicKeyRing("-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "Comment: Bob's OpenPGP certificate\n" + - "\n" + - "mQGNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\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" + - "vLIwa3T4CyshfT0AEQEAAbQhQm9iIEJhYmJhZ2UgPGJvYkBvcGVucGdwLmV4YW1w\n" + - "bGU+iQHOBBMBCgA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE0aZuGiOx\n" + - "gsmYD3iM+/zIKgFeczAFAl2lnvoACgkQ+/zIKgFeczBvbAv/VNk90a6hG8Od9xTz\n" + - "XxH5YRFUSGfIA1yjPIVOnKqhMwps2U+sWE3urL+MvjyQRlyRV8oY9IOhQ5Esm6DO\n" + - "ZYrTnE7qVETm1ajIAP2OFChEc55uH88x/anpPOXOJY7S8jbn3naC9qad75BrZ+3g\n" + - "9EBUWiy5p8TykP05WSnSxNRt7vFKLfEB4nGkehpwHXOVF0CRNwYle42bg8lpmdXF\n" + - "DcCZCi+qEbafmTQzkAqyzS3nCh3IAqq6Y0kBuaKLm2tSNUOlZbD+OHYQNZ5Jix7c\n" + - "ZUzs6Xh4+I55NRWl5smrLq66yOQoFPy9jot/Qxikx/wP3MsAzeGaZSEPc0fHp5G1\n" + - "6rlGbxQ3vl8/usUV7W+TMEMljgwd5x8POR6HC8EaCDfVnUBCPi/Gv+egLjsIbPJZ\n" + - "ZEroiE40e6/UoCiQtlpQB5exPJYSd1Q1txCwueih99PHepsDhmUQKiACszNU+RRo\n" + - "zAYau2VdHqnRJ7QYdxHDiH49jPK4NTMyb/tJh2TiIwcmsIpGuQGNBF2lnPIBDADW\n" + - "ML9cbGMrp12CtF9b2P6z9TTT74S8iyBOzaSvdGDQY/sUtZXRg21HWamXnn9sSXvI\n" + - "DEINOQ6A9QxdxoqWdCHrOuW3ofneYXoG+zeKc4dC86wa1TR2q9vW+RMXSO4uImA+\n" + - "Uzula/6k1DogDf28qhCxMwG/i/m9g1c/0aApuDyKdQ1PXsHHNlgd/Dn6rrd5y2AO\n" + - "baifV7wIhEJnvqgFXDN2RXGjLeCOHV4Q2WTYPg/S4k1nMXVDwZXrvIsA0YwIMgIT\n" + - "86Rafp1qKlgPNbiIlC1g9RY/iFaGN2b4Ir6GDohBQSfZW2+LXoPZuVE/wGlQ01rh\n" + - "827KVZW4lXvqsge+wtnWlszcselGATyzqOK9LdHPdZGzROZYI2e8c+paLNDdVPL6\n" + - "vdRBUnkCaEkOtl1mr2JpQi5nTU+gTX4IeInC7E+1a9UDF/Y85ybUz8XV8rUnR76U\n" + - "qVC7KidNepdHbZjjXCt8/Zo+Tec9JNbYNQB/e9ExmDntmlHEsSEQzFwzj8sxH48A\n" + - "EQEAAYkBtgQYAQoAIBYhBNGmbhojsYLJmA94jPv8yCoBXnMwBQJdpZzyAhsMAAoJ\n" + - "EPv8yCoBXnMw6f8L/26C34dkjBffTzMj5Bdzm8MtF67OYneJ4TQMw7+41IL4rVcS\n" + - "KhIhk/3Ud5knaRtP2ef1+5F66h9/RPQOJ5+tvBwhBAcUWSupKnUrdVaZQanYmtSx\n" + - "cVV2PL9+QEiNN3tzluhaWO//rACxJ+K/ZXQlIzwQVTpNhfGzAaMVV9zpf3u0k14i\n" + - "tcv6alKY8+rLZvO1wIIeRZLmU0tZDD5HtWDvUV7rIFI1WuoLb+KZgbYn3OWjCPHV\n" + - "dTrdZ2CqnZbG3SXw6awH9bzRLV9EXkbhIMez0deCVdeo+wFFklh8/5VK2b0vk/+w\n" + - "qMJxfpa1lHvJLobzOP9fvrswsr92MA2+k901WeISR7qEzcI0Fdg8AyFAExaEK6Vy\n" + - "jP7SXGLwvfisw34OxuZr3qmx1Sufu4toH3XrB7QJN8XyqqbsGxUCBqWif9RSK4xj\n" + - "zRTe56iPeiSJJOIciMP9i2ldI+KgLycyeDvGoBj0HCLO3gVaBe4ubVrj5KjhX2PV\n" + - "NEJd3XZRzaXZE2aAMQ==\n" + - "=NXei\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"); - SIGKEY = CERT.getPublicKey(new OpenPgpV4Fingerprint("D1A66E1A23B182C9980F788CFBFCC82A015E7330").getKeyId()); - NOSIGKEY = CERT.getPublicKey(new OpenPgpV4Fingerprint("1DDCE15F09217CEE2F3B37607C2FAA4DF93C37B2").getKeyId()); - } catch (IOException e) { - fail("Cannot parse certificate"); - } - } - - @Test - public void issuer() throws PGPException, IOException { - String sigWithIssuer = "-----BEGIN PGP SIGNATURE-----\n" + - "\n" + - "wsE7BAABCABlBYJgyf21RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEt\n" + - "cGdwLm9yZ41vMHyr0Q9WbEh89cDcZt1LU9wR1Li+3wXFW0I0Lv4qFiEE0aZuGiOx\n" + - "gsmYD3iM+/zIKgFeczAACgkQ+/zIKgFeczCN/Av+P/RqTj8hMDTsoQWggQS3EPmx\n" + - "5u43yp8JCNuIKiDwTy+civQpAsfWLKhwmHZqokPonMtVSvxH9RFry9x8MpaaQzag\n" + - "gNO2XwsFFpYa3ce/vjOHv+b9JGfPsSak6RvcPKV99AjqAnxDj93Q9od5DzmWo4jp\n" + - "i9zt1Kj1AGEgqg/tp9jmmIEJ6ZgjM1sAysyE2YFU0hc0xySKI8+pBk8YG3fj8Twq\n" + - "d6FDQ3CTvpApdrL5EKW3qW1K/vBvmck15GZOxAsiXaPoiIPDJPxBCy0koK7z/Z+0\n" + - "vCft+isreOB9B1b68iGdaET9W+bd0ODdZHTfi7KmtG1D8+Ep4oVL8IRuWmf2M1Z9\n" + - "qI93KxIYqanw0I5HDsfd/5IQ4X1ZD5hoMy+ICLKHQirQzyXL1tjYcw6NPJt0jAHR\n" + - "LNlerP+KD288SPzu7jymsRXfxp91F1n+UT8n7kG16YARBGhc7hen858EJXn9dtWi\n" + - "cqP71SMuOwD+JNuWQCd4e1WaTWNXrB1xerzmuWFc\n" + - "=4ZFC\n" + - "-----END PGP SIGNATURE-----"; - assertWasPossiblyMadeByKey(SIGKEY, get(sigWithIssuer)); - } - - @Test - public void hashedIssuer() throws PGPException, IOException { - String sigWithHashedIssuer = "-----BEGIN PGP SIGNATURE-----\n" + - "\n" + - "wsE7BAABCABvBYJgyf21CRD7/MgqAV5zMEcUAAAAAAAeACBzYWx0QG5vdGF0aW9u\n" + - "cy5zZXF1b2lhLXBncC5vcmcQXJVlUI2e8ug0H1ekty9QUCLzgzv/H/U243WfBpJP\n" + - "PBYhBNGmbhojsYLJmA94jPv8yCoBXnMwAADK5Av/XIuS13XrduVk3V7a28Uz9ARz\n" + - "l2eHgOuSyM8IovCaWvO+L8KaCsFXVYUB0cHEHzH0QfyMapkVymsjLmqT5ULdwdKf\n" + - "HsPluhZQlgEzeS043/uoikBgGOF+u0hdsibVpW0TVp0vZgBpuD7raLQQ9eWymRUE\n" + - "dZ1dwWPc5OD3OV6jVwjNPoLy8yQaYLhfRser/h+pIFTL8XSqjPMDklN+oLnzZkvo\n" + - "A0OCupqpiDtrREmeTVKDkL0DJ0DM7qMky7oI1qB4i3Ryt7gMUzpy7nK6APosi5Rg\n" + - "vPuofxHle32pzaMDbBFQFcFYsXFdzmQdCcIx1myo6yrdkq4RMYXR20+cE+4R0pcQ\n" + - "JZhDFyx3d/7vloWXeXM9HT+asPVfub+HXPFkqvsFulogpo/Pr66Og6+fRc2FPPSO\n" + - "HmamWg1mMpzca8F35zZie1ICT7Qdef+aBcUb/7gwlv0Fd4FYWaIcleve4YtEacE/\n" + - "Q0Quxar3DOTtNNQVrXeoeIlVGue0pNCwg6abDj5N\n" + - "=IcX1\n" + - "-----END PGP SIGNATURE-----\n"; - assertWasPossiblyMadeByKey(SIGKEY, get(sigWithHashedIssuer)); - } - - @Test - public void noIssuerNoFingerprint() throws PGPException, IOException { - String sigWithNoIssuerNoFingerprint = "-----BEGIN PGP SIGNATURE-----\n" + - "\n" + - "wsEaBAABCABOBYJgyf21RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEt\n" + - "cGdwLm9yZ2T4siBf1CQ8wqT+oTCmxVm6OC+KC/kvqQdQ4AAGC68JAAC3Zwv/d70q\n" + - "hUeTJrMXGej1/FkNOSKyjVRnJusDEojc4eQ/+Ov8jdByj2Rcr44UH7ZICtWuR4gc\n" + - "ZrG9+DBFKgeuY6TawyBbTj3NU4IEOqihtn8RDBEXKIc91cW9BuqLyxvoUr432g6y\n" + - "7l7nyXf17kPx8E62BjOhUz0NuwRQ5c2pnIRDe37xX0519DMf9PaywTAgs4eZaKXd\n" + - "e1JLYvkd7BuMaT17VEggdRLM3GJGAJfZQ4+eoOmAzGRs1xGZrvcs2AH+OOzslU5l\n" + - "t2nR9N7BCLX0NZVIP5KpRzw/puIFBiFj7zrPb7CJqKb0UEK8qngukASlvzYZTjHA\n" + - "03qAeYqUj6LXTPNYlobPsGB0Srt2j7ycpeOYh6c3l7pKkvyaQL4QVawECMxsymu0\n" + - "iMrLtyuWclsBcRDezIHQqKHOhSeCLt67SJj2+fCa+7WgQdvBT//3McFVsWnLQJsq\n" + - "zVflI4b3E2kyhRgYK7f6jaa0OZ7BJRpQ3RRNk0Oq3rIYjysrwkbBG9N6tnCk\n" + - "=NdKQ\n" + - "-----END PGP SIGNATURE-----"; - - - assertWasPossiblyMadeByKey(SIGKEY, get(sigWithNoIssuerNoFingerprint)); - } - - @Test - public void noIssuerUnhashedFingerprint() throws PGPException, IOException { - String sigWithNoIssuerUnhashedFingerprint = "-----BEGIN PGP SIGNATURE-----\n" + - "\n" + - "wsExBAABCABOBYJgyf21RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEt\n" + - "cGdwLm9yZ3rfvSuqnA0bf1EQnEstGhA5DCtJi6DDcXnosObaXtDLABcWIQTRpm4a\n" + - "I7GCyZgPeIz7/MgqAV5zMHc4C/0b582atQFBFrSn+YAtDMPChnu/p4DKDXu3Ytxf\n" + - "X04TYV2+31MtndB4OMs0IMijWBpLkBFp57ozwIlos+y2gWAiJJ/uyLOzJNYPsEzA\n" + - "WVbEOrTgmelkc0sYFZlL0JcNsaCmpWjXuhNNulTj2svmwyNrD/3sO2G2hZcGqDDd\n" + - "zREX0Z8rEAssk4UxJVwOqmvhspWRDT3/UYpAA7sMQa3NtoLB0BM/+/mPG78fmSsP\n" + - "CmquP71TF3VdbW3zDdeq71apJbGgLdbEKVbwqU7IHtMk3DA469rT0NHdNVbQu0Mv\n" + - "nbNA43fNfaBbT7ApFQgnzBMF+nBc+HLCJQxq4uRBRX0i2eh+hgFM8VxX8miV1iCT\n" + - "o6NkMerueuXGFSGU37wGQQMdzOK13cW/Rp1DyFu3L0BSnFpykowdADmjAWhZYCMX\n" + - "6HAbz8mWRfNbNOahOtCVO3pojI8UiJ9ru7efTA/k3n06WYLndLcI3uW3Bn1F6/we\n" + - "7IQfGLcjtGngm993hPuCHrg/dnc=\n" + - "=LBou\n" + - "-----END PGP SIGNATURE-----\n"; - - assertWasPossiblyMadeByKey(SIGKEY, get(sigWithNoIssuerUnhashedFingerprint)); - } - - @Test - public void issuerMismatch() throws PGPException, IOException { - String sig = "-----BEGIN PGP SIGNATURE-----\n" + - "\n" + - "wsE7BAABCABlBYJgyf21RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEt\n" + - "cGdwLm9yZ41vMHyr0Q9WbEh89cDcZt1LU9wR1Li+3wXFW0I0Lv4qFiEE0aZuGiOx\n" + - "gsmYD3iM+/zIKgFeczAACgkQ+/zIKgFeczCN/Av+P/RqTj8hMDTsoQWggQS3EPmx\n" + - "5u43yp8JCNuIKiDwTy+civQpAsfWLKhwmHZqokPonMtVSvxH9RFry9x8MpaaQzag\n" + - "gNO2XwsFFpYa3ce/vjOHv+b9JGfPsSak6RvcPKV99AjqAnxDj93Q9od5DzmWo4jp\n" + - "i9zt1Kj1AGEgqg/tp9jmmIEJ6ZgjM1sAysyE2YFU0hc0xySKI8+pBk8YG3fj8Twq\n" + - "d6FDQ3CTvpApdrL5EKW3qW1K/vBvmck15GZOxAsiXaPoiIPDJPxBCy0koK7z/Z+0\n" + - "vCft+isreOB9B1b68iGdaET9W+bd0ODdZHTfi7KmtG1D8+Ep4oVL8IRuWmf2M1Z9\n" + - "qI93KxIYqanw0I5HDsfd/5IQ4X1ZD5hoMy+ICLKHQirQzyXL1tjYcw6NPJt0jAHR\n" + - "LNlerP+KD288SPzu7jymsRXfxp91F1n+UT8n7kG16YARBGhc7hen858EJXn9dtWi\n" + - "cqP71SMuOwD+JNuWQCd4e1WaTWNXrB1xerzmuWFc\n" + - "=4ZFC\n" + - "-----END PGP SIGNATURE-----"; - assertWasNotPossiblyMadeByKey(NOSIGKEY, get(sig)); - } - - @Test - public void noIssuer_fingerprintMismatch() throws PGPException, IOException { - String sigWithNoIssuerAndWrongFingerprint = "-----BEGIN PGP SIGNATURE-----\n" + - "\n" + - "wsExBAABCABlBYJgyf21RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEt\n" + - "cGdwLm9yZ4LlavMh1EAlex0cmzIH6jHzRv9iaqLPdHi1pM7J65EzFiEE0aZuGiOx\n" + - "gsmYD3iM+/zIKgFeczAAAIMIC/0QkkD7RcvKUogLhENpeGrQnkGmVEBupHz6V8LR\n" + - "i/DtlIBNjTRAEwDHcDDfn0JkY9Zp3E4IkNN6cJ9o8vsZvOMu0v9qQKVDwhy6N0SM\n" + - "BAOCJ+rNkZlXIWM8wyRzt52TWG6CStU2bbLJAq3EeEkZ2+WupCAdsVax0qrWJxQf\n" + - "tcm2lLQrtCa3gvRtaGCnmW1jrpvkkNZyC/bOBAazr4aD5lgeVtP8Oq3SI32xGV6f\n" + - "zCSfctIxGz9ZxQGe/VGHmgExkQ6SCaF3JhHHgZt/FCmquIK/IV5WIYAidWmFtQYI\n" + - "26jVUVUgNHU7Oxagx/55ZXUAMPIspO+J0HOLCpVQUTABBumhgwF6JkVnIn8ZO+vn\n" + - "GXIkZXQIK1Hx7M4xFYgJjva2ZwxCsENmtDp8FKyeTjq5QTU4Q1WSpJH6KVSpqCVM\n" + - "hyYvz7nf+kWf5Gm/Z0yGlkDhFnj3th4tUyytvypKgWeZu/1/0+Lfs293OrjjygCW\n" + - "lMirZ5N3oGYyNH4DQMJ1jeMwdbg=\n" + - "=A/zE\n" + - "-----END PGP SIGNATURE-----\n"; - - assertWasNotPossiblyMadeByKey(NOSIGKEY, get(sigWithNoIssuerAndWrongFingerprint)); - } - - private PGPSignature get(String encoded) throws PGPException, IOException { - return SignatureUtils.readSignatures(encoded).get(0); - } - - private void assertWasPossiblyMadeByKey(PGPPublicKey signatureKey, PGPSignature signature) throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signatureKey).verify(signature); - } - - private void assertWasNotPossiblyMadeByKey(PGPPublicKey signatureKey, PGPSignature signature) { - assertThrows(SignatureValidationException.class, () -> assertWasPossiblyMadeByKey(signatureKey, signature)); - } - -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/SubkeyAndPrimaryKeyBindingSignatureTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/SubkeyAndPrimaryKeyBindingSignatureTest.java index e44af522..f1b87884 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/SubkeyAndPrimaryKeyBindingSignatureTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/SubkeyAndPrimaryKeyBindingSignatureTest.java @@ -14,10 +14,10 @@ import java.util.HashSet; import java.util.Set; 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.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.EncryptionPurpose; @@ -33,20 +33,22 @@ public class SubkeyAndPrimaryKeyBindingSignatureTest { @Test public void testRebindSubkey() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.toKey(TestKeys.getEmilSecretKeyRing()); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - PGPSecretKey primaryKey = secretKeys.getSecretKey(); - PGPPublicKey encryptionSubkey = info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); + OpenPGPKey.OpenPGPSecretKey primaryKey = secretKeys.getPrimarySecretKey(); + OpenPGPCertificate.OpenPGPComponentKey encryptionSubkey = + info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); assertNotNull(encryptionSubkey); - Set hashAlgorithmSet = info.getPreferredHashAlgorithms(encryptionSubkey.getKeyID()); + Set hashAlgorithmSet = info.getPreferredHashAlgorithms(encryptionSubkey.getKeyIdentifier()); assertEquals( new HashSet<>(Arrays.asList( HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256, HashAlgorithm.SHA224)), hashAlgorithmSet); - SubkeyBindingSignatureBuilder sbb = new SubkeyBindingSignatureBuilder(primaryKey, SecretKeyRingProtector.unprotectedKeys()); + SubkeyBindingSignatureBuilder sbb = new SubkeyBindingSignatureBuilder(primaryKey, SecretKeyRingProtector.unprotectedKeys(), api); sbb.applyCallback(new SelfSignatureSubpackets.Callback() { @Override public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { @@ -55,10 +57,10 @@ public class SubkeyAndPrimaryKeyBindingSignatureTest { } }); - PGPSignature binding = sbb.build(encryptionSubkey); - secretKeys = KeyRingUtils.injectCertification(secretKeys, encryptionSubkey, binding); + PGPSignature binding = sbb.build(encryptionSubkey.getPGPPublicKey()); + PGPSecretKeyRing secretKeyRing = KeyRingUtils.injectCertification(secretKeys.getPGPKeyRing(), encryptionSubkey.getPGPPublicKey(), binding); - info = PGPainless.inspectKeyRing(secretKeys); - assertEquals(Collections.singleton(HashAlgorithm.SHA512), info.getPreferredHashAlgorithms(encryptionSubkey.getKeyID())); + info = PGPainless.inspectKeyRing(secretKeyRing); + assertEquals(Collections.singleton(HashAlgorithm.SHA512), info.getPreferredHashAlgorithms(encryptionSubkey.getKeyIdentifier())); } } 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 2b0f4d35..931c36b7 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,22 +4,20 @@ package org.pgpainless.signature.builder; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.bcpg.sig.Exportable; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPSignature; import org.junit.jupiter.api.Test; 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.CertificationSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import java.security.InvalidAlgorithmParameterException; -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; @@ -30,27 +28,31 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class ThirdPartyCertificationSignatureBuilderTest { @Test - public void testInvalidSignatureTypeThrows() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void testInvalidSignatureTypeThrows() { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Alice"); assertThrows(IllegalArgumentException.class, () -> new ThirdPartyCertificationSignatureBuilder( SignatureType.BINARY_DOCUMENT, // invalid type - secretKeys.getSecretKey(), - SecretKeyRingProtector.unprotectedKeys())); + secretKeys.getPrimarySecretKey(), + SecretKeyRingProtector.unprotectedKeys(), + api)); } @Test - public void testUserIdCertification() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + public void testUserIdCertification() throws PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Alice"); - PGPPublicKeyRing bobsPublicKeys = PGPainless.extractCertificate( - PGPainless.generateKeyRing().modernKeyRing("Bob")); + OpenPGPCertificate bobsPublicKeys = api.generateKey().modernKeyRing("Bob") + .toCertificate(); ThirdPartyCertificationSignatureBuilder signatureBuilder = new ThirdPartyCertificationSignatureBuilder( - secretKeys.getSecretKey(), - SecretKeyRingProtector.unprotectedKeys()); + secretKeys.getPrimarySecretKey(), + SecretKeyRingProtector.unprotectedKeys(), + api); signatureBuilder.applyCallback(new CertificationSubpackets.Callback() { @Override @@ -59,16 +61,20 @@ public class ThirdPartyCertificationSignatureBuilderTest { } }); - PGPSignature certification = signatureBuilder.build(bobsPublicKeys, "Bob"); - assertEquals(SignatureType.GENERIC_CERTIFICATION, SignatureType.valueOf(certification.getSignatureType())); - assertEquals(secretKeys.getPublicKey().getKeyID(), certification.getKeyID()); - assertArrayEquals(secretKeys.getPublicKey().getFingerprint(), certification.getHashedSubPackets().getIssuerFingerprint().getFingerprint()); - Exportable exportable = SignatureSubpacketsUtil.getExportableCertification(certification); + OpenPGPSignature certification = signatureBuilder.build(bobsPublicKeys, "Bob"); + PGPSignature signature = certification.getSignature(); + assertEquals(SignatureType.GENERIC_CERTIFICATION, SignatureType.requireFromCode(signature.getSignatureType())); + assertTrue(KeyIdentifier.matches(signature.getKeyIdentifiers(), secretKeys.getKeyIdentifier(), true)); + assertArrayEquals( + secretKeys.getPrimaryKey().getPGPPublicKey().getFingerprint(), + signature.getHashedSubPackets().getIssuerFingerprint().getFingerprint()); + Exportable exportable = SignatureSubpacketsUtil.getExportableCertification(signature); assertNotNull(exportable); assertFalse(exportable.isExportable()); // test sig correctness - certification.init(ImplementationFactory.getInstance().getPgpContentVerifierBuilderProvider(), secretKeys.getPublicKey()); - assertTrue(certification.verifyCertification("Bob", bobsPublicKeys.getPublicKey())); + signature.init(api.getImplementation().pgpContentVerifierBuilderProvider(), + secretKeys.getPrimaryKey().getPGPPublicKey()); + assertTrue(signature.verifyCertification("Bob", bobsPublicKeys.getPrimaryKey().getPGPPublicKey())); } } 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 56605f83..c71e3fc7 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 @@ -14,6 +14,8 @@ import java.util.Date; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.bouncycastle.openpgp.api.OpenPGPSignature; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; @@ -32,12 +34,14 @@ public class ThirdPartyDirectKeySignatureBuilderTest { @Test public void testDirectKeySignatureBuilding() throws PGPException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .modernKeyRing("Alice"); DirectKeySelfSignatureBuilder dsb = new DirectKeySelfSignatureBuilder( - secretKeys.getSecretKey(), - SecretKeyRingProtector.unprotectedKeys()); + secretKeys.getPrimarySecretKey(), + SecretKeyRingProtector.unprotectedKeys(), + api); Date now = new Date(); Date t1 = new Date(now.getTime() + 1000 * 60 * 60); @@ -53,22 +57,25 @@ public class ThirdPartyDirectKeySignatureBuilderTest { } }); - PGPSignature directKeySig = dsb.build(); + OpenPGPSignature directKeySig = dsb.build(); assertNotNull(directKeySig); - secretKeys = KeyRingUtils.injectCertification(secretKeys, secretKeys.getPublicKey(), directKeySig); + PGPSecretKeyRing secretKeyRing = KeyRingUtils.injectCertification( + secretKeys.getPGPSecretKeyRing(), + secretKeys.getPrimaryKey().getPGPPublicKey(), + directKeySig.getSignature()); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys, t1); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, t1); PGPSignature signature = info.getLatestDirectKeySelfSignature(); assertNotNull(signature); - assertEquals(directKeySig, signature); + assertEquals(directKeySig.getSignature(), signature); - assertEquals(SignatureType.DIRECT_KEY, SignatureType.valueOf(signature.getSignatureType())); + assertEquals(SignatureType.DIRECT_KEY, SignatureType.requireFromCode(signature.getSignatureType())); assertEquals(Collections.singletonList(KeyFlag.CERTIFY_OTHER), SignatureSubpacketsUtil.parseKeyFlags(signature)); assertEquals(Collections.singleton(HashAlgorithm.SHA512), SignatureSubpacketsUtil.parsePreferredHashAlgorithms(signature)); assertEquals(Collections.singleton(CompressionAlgorithm.ZIP), SignatureSubpacketsUtil.parsePreferredCompressionAlgorithms(signature)); assertEquals(Collections.singleton(SymmetricKeyAlgorithm.AES_256), SignatureSubpacketsUtil.parsePreferredSymmetricKeyAlgorithms(signature)); - assertEquals(secretKeys.getPublicKey().getKeyID(), signature.getKeyID()); - assertArrayEquals(secretKeys.getPublicKey().getFingerprint(), signature.getHashedSubPackets().getIssuerFingerprint().getFingerprint()); + assertEquals(secretKeyRing.getPublicKey().getKeyID(), signature.getKeyID()); + assertArrayEquals(secretKeyRing.getPublicKey().getFingerprint(), signature.getHashedSubPackets().getIssuerFingerprint().getFingerprint()); } } 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 index 37bc6fd3..c308877a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/UniversalSignatureBuilderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/UniversalSignatureBuilderTest.java @@ -6,15 +6,16 @@ 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.assertTrue; import java.io.IOException; +import org.bouncycastle.bcpg.KeyIdentifier; 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.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -51,20 +52,21 @@ public class UniversalSignatureBuilderTest { "=Dqbd\n" + "-----END PGP PRIVATE KEY BLOCK-----"; - private PGPSecretKeyRing secretKeys; + private OpenPGPKey secretKeys; private final SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); @BeforeEach public void parseKey() throws IOException { - secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + secretKeys = PGPainless.getInstance().readKey().parseKey(KEY); } @Test public void createPetNameSignature() throws PGPException { - PGPSecretKey signingKey = secretKeys.getSecretKey(); - PGPSignature archetype = signingKey.getPublicKey().getSignatures().next(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey.OpenPGPSecretKey signingKey = secretKeys.getPrimarySecretKey(); + PGPSignature archetype = signingKey.getPublicKey().getPGPPublicKey().getSignatures().next(); UniversalSignatureBuilder builder = new UniversalSignatureBuilder( - signingKey, protector, archetype); + signingKey, protector, archetype, api); builder.applyCallback(new SignatureSubpackets.Callback() { @Override @@ -77,11 +79,11 @@ public class UniversalSignatureBuilderTest { PGPSignatureGenerator generator = builder.getSignatureGenerator(); String petName = "mykey"; - PGPSignature petNameSig = generator.generateCertification(petName, secretKeys.getPublicKey()); + PGPSignature petNameSig = generator.generateCertification(petName, secretKeys.getPrimarySecretKey().getPublicKey().getPGPPublicKey()); assertEquals(SignatureType.POSITIVE_CERTIFICATION.getCode(), petNameSig.getSignatureType()); assertEquals(4, petNameSig.getVersion()); - assertEquals(signingKey.getKeyID(), petNameSig.getKeyID()); + assertTrue(KeyIdentifier.matches(petNameSig.getKeyIdentifiers(), signingKey.getKeyIdentifier(), true)); assertEquals(HashAlgorithm.SHA512.getAlgorithmId(), petNameSig.getHashAlgorithm()); assertEquals(KeyFlag.toBitmask(KeyFlag.CERTIFY_OTHER), petNameSig.getHashedSubPackets().getKeyFlags()); assertFalse(petNameSig.getHashedSubPackets().isExportable()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/CertificationSubpacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/CertificationSubpacketsTest.java new file mode 100644 index 00000000..4394ee47 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/CertificationSubpacketsTest.java @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets; + +import kotlin.Unit; +import org.bouncycastle.bcpg.SignatureSubpacket; +import org.bouncycastle.bcpg.sig.IssuerFingerprint; +import org.bouncycastle.bcpg.sig.NotationData; +import org.junit.jupiter.api.Test; +import org.pgpainless.key.TestKeys; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.pgpainless.signature.subpackets.SignatureSubpacketsTest.toArray; + +public class CertificationSubpacketsTest { + + @Test + public void testNopDoesNothing() { + SignatureSubpackets subpackets = new SignatureSubpackets(); + CertificationSubpackets.Callback cb = CertificationSubpackets.nop(); + + cb.modifyHashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + + cb.modifyUnhashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + } + + @Test + public void testApplyHashed() { + SignatureSubpackets subpackets = new SignatureSubpackets(); + CertificationSubpackets.Callback cb = CertificationSubpackets.applyHashed( + selfSignatureSubpackets -> { + selfSignatureSubpackets.setIssuerFingerprint(new IssuerFingerprint(false, 4, TestKeys.ROMEO_FINGERPRINT.getBytes())); + return Unit.INSTANCE; + }); + + assertEquals(0, toArray(subpackets).length); + + // The callback only applies to hashed subpackets, so modifying unhashed area does nothing + cb.modifyUnhashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + + cb.modifyHashedSubpackets(subpackets); + assertEquals(1, toArray(subpackets).length); + } + + @Test + public void testApplyUnhashed() { + SignatureSubpackets subpackets = new SignatureSubpackets(); + CertificationSubpackets.Callback cb = CertificationSubpackets.applyUnhashed( + selfSignatureSubpackets -> { + selfSignatureSubpackets.setIssuerKeyId(123L); + return Unit.INSTANCE; + }); + + assertEquals(0, toArray(subpackets).length); + + // The callback only applies to unhashed subpackets, so modifying hashed area does nothing + cb.modifyHashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + + cb.modifyUnhashedSubpackets(subpackets); + assertEquals(1, toArray(subpackets).length); + } + + @Test + public void testThen() { + SignatureSubpackets subpackets = new SignatureSubpackets(); + + CertificationSubpackets.Callback first = CertificationSubpackets.applyHashed(selfSignatureSubpackets -> { + selfSignatureSubpackets.setIssuerFingerprint(new IssuerFingerprint(false, 4, TestKeys.ROMEO_FINGERPRINT.getBytes())); + selfSignatureSubpackets.addNotationData(false, "test@pgpainless.org", "foo"); + return Unit.INSTANCE; + }); + + CertificationSubpackets.Callback second = CertificationSubpackets.applyHashed(selfSignatureSubpackets -> { + selfSignatureSubpackets.setIssuerFingerprint(new IssuerFingerprint(true, 4, TestKeys.ROMEO_FINGERPRINT.getBytes())); + selfSignatureSubpackets.addNotationData(false, "test@pgpainless.org", "bar"); + return Unit.INSTANCE; + }); + + CertificationSubpackets.Callback both = first.then(second); + both.modifyUnhashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + + both.modifyHashedSubpackets(subpackets); + + SignatureSubpacket[] array = toArray(subpackets); + assertEquals(3, array.length); + NotationData n1 = (NotationData) array[0]; + assertEquals("foo", n1.getNotationValue()); + IssuerFingerprint fingerprint = (IssuerFingerprint) array[1]; + assertTrue(fingerprint.isCritical()); + NotationData n2 = (NotationData) array[2]; + assertEquals("bar", n2.getNotationValue()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/RevocationSignatureSubpacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/RevocationSignatureSubpacketsTest.java new file mode 100644 index 00000000..8d4d2d96 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/RevocationSignatureSubpacketsTest.java @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets; + +import kotlin.Unit; +import org.bouncycastle.bcpg.SignatureSubpacket; +import org.bouncycastle.bcpg.sig.NotationData; +import org.bouncycastle.bcpg.sig.RevocationReason; +import org.junit.jupiter.api.Test; +import org.pgpainless.key.util.RevocationAttributes; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.pgpainless.signature.subpackets.SignatureSubpacketsTest.toArray; + +public class RevocationSignatureSubpacketsTest { + + @Test + public void testNopDoesNothing() { + SignatureSubpackets subpackets = new SignatureSubpackets(); + RevocationSignatureSubpackets.Callback cb = RevocationSignatureSubpackets.nop(); + + cb.modifyHashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + + cb.modifyUnhashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + } + + + @Test + public void testApplyHashed() { + SignatureSubpackets subpackets = new SignatureSubpackets(); + RevocationSignatureSubpackets.Callback cb = RevocationSignatureSubpackets.applyHashed( + selfSignatureSubpackets -> { + selfSignatureSubpackets.setRevocationReason(true, RevocationAttributes.Reason.KEY_COMPROMISED, "Leaked"); + return Unit.INSTANCE; + }); + + assertEquals(0, toArray(subpackets).length); + + // The callback only applies to hashed subpackets, so modifying unhashed area does nothing + cb.modifyUnhashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + + cb.modifyHashedSubpackets(subpackets); + assertEquals(1, toArray(subpackets).length); + } + + @Test + public void testApplyUnhashed() { + SignatureSubpackets subpackets = new SignatureSubpackets(); + RevocationSignatureSubpackets.Callback cb = RevocationSignatureSubpackets.applyUnhashed( + selfSignatureSubpackets -> { + selfSignatureSubpackets.setIssuerKeyId(123L); + return Unit.INSTANCE; + }); + + assertEquals(0, toArray(subpackets).length); + + // The callback only applies to unhashed subpackets, so modifying hashed area does nothing + cb.modifyHashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + + cb.modifyUnhashedSubpackets(subpackets); + assertEquals(1, toArray(subpackets).length); + } + + @Test + public void testThen() { + SignatureSubpackets subpackets = new SignatureSubpackets(); + + RevocationSignatureSubpackets.Callback first = RevocationSignatureSubpackets.applyHashed(selfSignatureSubpackets -> { + selfSignatureSubpackets.setRevocationReason(true, RevocationAttributes.Reason.KEY_COMPROMISED, "Leakett (typo)"); + selfSignatureSubpackets.addNotationData(false, "test@pgpainless.org", "foo"); + return Unit.INSTANCE; + }); + + RevocationSignatureSubpackets.Callback second = RevocationSignatureSubpackets.applyHashed(selfSignatureSubpackets -> { + selfSignatureSubpackets.setRevocationReason(true, RevocationAttributes.Reason.KEY_COMPROMISED, "Leaked"); + selfSignatureSubpackets.addNotationData(false, "test@pgpainless.org", "bar"); + return Unit.INSTANCE; + }); + + RevocationSignatureSubpackets.Callback both = first.then(second); + both.modifyUnhashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + + both.modifyHashedSubpackets(subpackets); + + SignatureSubpacket[] array = toArray(subpackets); + assertEquals(3, array.length); + NotationData n1 = (NotationData) array[0]; + assertEquals("foo", n1.getNotationValue()); + RevocationReason reason = (RevocationReason) array[1]; + assertEquals(RevocationAttributes.Reason.KEY_COMPROMISED.code(), reason.getRevocationReason()); + NotationData n2 = (NotationData) array[2]; + assertEquals("bar", n2.getNotationValue()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SelfSignatureSubpacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SelfSignatureSubpacketsTest.java new file mode 100644 index 00000000..6a19a3ff --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SelfSignatureSubpacketsTest.java @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets; + +import kotlin.Unit; +import org.bouncycastle.bcpg.SignatureSubpacket; +import org.bouncycastle.bcpg.sig.KeyFlags; +import org.bouncycastle.bcpg.sig.NotationData; +import org.bouncycastle.bcpg.sig.PreferredAlgorithms; +import org.junit.jupiter.api.Test; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.pgpainless.signature.subpackets.SignatureSubpacketsTest.toArray; + +public class SelfSignatureSubpacketsTest { + + @Test + public void testNopDoesNothing() { + SignatureSubpackets subpackets = new SignatureSubpackets(); + SelfSignatureSubpackets.Callback cb = SelfSignatureSubpackets.nop(); + + cb.modifyHashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + + cb.modifyUnhashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + } + + @Test + public void testApplyHashed() { + SignatureSubpackets subpackets = new SignatureSubpackets(); + SelfSignatureSubpackets.Callback cb = SelfSignatureSubpackets.applyHashed( + selfSignatureSubpackets -> { + selfSignatureSubpackets.setKeyFlags(KeyFlag.CERTIFY_OTHER); + return Unit.INSTANCE; + }); + + assertEquals(0, toArray(subpackets).length); + + // The callback only applies to hashed subpackets, so modifying unhashed area does nothing + cb.modifyUnhashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + + cb.modifyHashedSubpackets(subpackets); + assertEquals(1, toArray(subpackets).length); + } + + @Test + public void testApplyUnhashed() { + SignatureSubpackets subpackets = new SignatureSubpackets(); + SelfSignatureSubpackets.Callback cb = SelfSignatureSubpackets.applyUnhashed( + selfSignatureSubpackets -> { + selfSignatureSubpackets.setIssuerKeyId(123L); + return Unit.INSTANCE; + }); + + assertEquals(0, toArray(subpackets).length); + + // The callback only applies to unhashed subpackets, so modifying hashed area does nothing + cb.modifyHashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + + cb.modifyUnhashedSubpackets(subpackets); + assertEquals(1, toArray(subpackets).length); + } + + @Test + public void testThen() { + SignatureSubpackets subpackets = new SignatureSubpackets(); + + SelfSignatureSubpackets.Callback first = SelfSignatureSubpackets.applyHashed(selfSignatureSubpackets -> { + selfSignatureSubpackets.setPreferredHashAlgorithms(HashAlgorithm.SHA256, HashAlgorithm.SHA512); + selfSignatureSubpackets.setKeyFlags(KeyFlag.CERTIFY_OTHER); + selfSignatureSubpackets.addNotationData(false, "test@pgpainless.org", "foo"); + return Unit.INSTANCE; + }); + + SelfSignatureSubpackets.Callback second = SelfSignatureSubpackets.applyHashed(selfSignatureSubpackets -> { + selfSignatureSubpackets.setKeyFlags(KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA); + selfSignatureSubpackets.addNotationData(false, "test@pgpainless.org", "bar"); + return Unit.INSTANCE; + }); + + SelfSignatureSubpackets.Callback both = first.then(second); + both.modifyUnhashedSubpackets(subpackets); + assertEquals(0, toArray(subpackets).length); + + both.modifyHashedSubpackets(subpackets); + + SignatureSubpacket[] array = toArray(subpackets); + assertEquals(4, array.length); + PreferredAlgorithms hashAlgs = (PreferredAlgorithms) array[0]; + assertArrayEquals( + new int[] {HashAlgorithm.SHA256.getAlgorithmId(), HashAlgorithm.SHA512.getAlgorithmId()}, + hashAlgs.getPreferences()); + NotationData n1 = (NotationData) array[1]; + assertEquals("foo", n1.getNotationValue()); + KeyFlags flags = (KeyFlags) array[2]; + assertEquals(KeyFlag.toBitmask(KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA), flags.getFlags()); + NotationData n2 = (NotationData) array[3]; + assertEquals("bar", n2.getNotationValue()); + } +} 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 14eed4de..733356d5 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 @@ -39,6 +39,7 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; +import org.bouncycastle.openpgp.api.OpenPGPImplementation; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,7 +50,6 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.TestKeys; import org.pgpainless.key.protection.UnlockSecretKey; @@ -414,8 +414,9 @@ public class SignatureSubpacketsTest { Iterator secretKeyIterator = secretKeys.iterator(); PGPSecretKey primaryKey = secretKeyIterator.next(); PGPSignatureGenerator generator = new PGPSignatureGenerator( - ImplementationFactory.getInstance().getPGPContentSignerBuilder(primaryKey.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId()) - ); + OpenPGPImplementation.getInstance().pgpContentSignerBuilder( + primaryKey.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId()), + primaryKey.getPublicKey()); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(primaryKey, (Passphrase) null); generator.init(SignatureType.DIRECT_KEY.getCode(), privateKey); @@ -533,4 +534,8 @@ public class SignatureSubpacketsTest { PreferredAlgorithms aeadAlgorithms = (PreferredAlgorithms) vector.getSubpacket(SignatureSubpacketTags.PREFERRED_AEAD_ALGORITHMS); assertArrayEquals(aead.getPreferences(), aeadAlgorithms.getPreferences()); } + + public static SignatureSubpacket[] toArray(SignatureSubpackets subpackets) { + return subpackets.getSubpacketsGenerator().generate().toArray(); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java index d0d37117..127eb521 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java @@ -31,7 +31,7 @@ public class MultiPassphraseSymmetricEncryptionTest { "the decryptor finds the session key encrypted for the right passphrase."; ByteArrayInputStream plaintextIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); - EncryptionStream encryptor = PGPainless.encryptAndOrSign() + EncryptionStream encryptor = PGPainless.getInstance().generateMessage() .onOutputStream(ciphertextOut) .withOptions(ProducerOptions.encrypt( EncryptionOptions.encryptCommunications() @@ -46,9 +46,9 @@ public class MultiPassphraseSymmetricEncryptionTest { // decrypting the p1 package with p2 first will not work. Test if it is handled correctly. for (Passphrase passphrase : new Passphrase[] {Passphrase.fromPassword("p2"), Passphrase.fromPassword("p1")}) { - DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + DecryptionStream decryptor = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(ciphertext)) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addMessagePassphrase(passphrase)); ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java index dbf7ca24..bf0c15f5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java @@ -63,9 +63,9 @@ public class SymmetricEncryptionTest { byte[] ciphertext = ciphertextOut.toByteArray(); // Test symmetric decryption - DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + DecryptionStream decryptor = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(ciphertext)) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addMessagePassphrase(encryptionPassphrase)); ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); @@ -80,9 +80,9 @@ public class SymmetricEncryptionTest { SecretKeyRingProtector protector = new PasswordBasedSecretKeyRingProtector( KeyRingProtectionSettings.secureDefaultSettings(), new SolitaryPassphraseProvider(Passphrase.fromPassword(TestKeys.CRYPTIE_PASSWORD))); - decryptor = PGPainless.decryptAndOrVerify() + decryptor = PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(ciphertext)) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .addDecryptionKeys(decryptionKeys, protector)); decrypted = new ByteArrayOutputStream(); @@ -100,7 +100,7 @@ public class SymmetricEncryptionTest { new Random().nextBytes(bytes); ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); - EncryptionStream encryptor = PGPainless.encryptAndOrSign().onOutputStream(ciphertextOut) + EncryptionStream encryptor = PGPainless.getInstance().generateMessage().onOutputStream(ciphertextOut) .withOptions(ProducerOptions.encrypt( EncryptionOptions.encryptCommunications() .addMessagePassphrase(Passphrase.fromPassword("mellon")))); @@ -108,9 +108,9 @@ public class SymmetricEncryptionTest { Streams.pipeAll(new ByteArrayInputStream(bytes), encryptor); encryptor.close(); - assertThrows(MissingDecryptionMethodException.class, () -> PGPainless.decryptAndOrVerify() + assertThrows(MissingDecryptionMethodException.class, () -> PGPainless.getInstance().processMessage() .onInputStream(new ByteArrayInputStream(ciphertextOut.toByteArray())) - .withOptions(new ConsumerOptions() + .withOptions(ConsumerOptions.get() .setMissingKeyPassphraseStrategy(MissingKeyPassphraseStrategy.THROW_EXCEPTION) .addMessagePassphrase(Passphrase.fromPassword("meldir")))); } 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 c0a6e00c..9d3ad35a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/ArmorUtilsTest.java @@ -26,6 +26,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.api.OpenPGPImplementation; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -35,7 +36,6 @@ 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; @@ -130,9 +130,9 @@ public class ArmorUtilsTest { } @Test - public void signatureToAsciiArmoredString() throws PGPException, IOException { + public void signatureToAsciiArmoredString() { String SIG = "-----BEGIN PGP SIGNATURE-----\n" + - "Version: PGPainless\n" + + "Comment: 4F66 5C4D C2C4 660B C642 5E41 5736 E693 1ACF 370C\n" + "\n" + "iHUEARMKAB0WIQRPZlxNwsRmC8ZCXkFXNuaTGs83DAUCYJ/x5gAKCRBXNuaTGs83\n" + "DFRwAP9/4wMvV3WcX59Clo7mkRce6iwW3VBdiN+yMu3tjmHB2wD/RfE28Q1v4+eo\n" + @@ -183,7 +183,8 @@ public class ArmorUtilsTest { .addUserId("Juliet ") .addUserId("xmpp:juliet@capulet.lit") .setPassphrase(Passphrase.fromPassword("test")) - .build(); + .build() + .getPGPSecretKeyRing(); PGPPublicKey publicKey = secretKeyRing.getPublicKey(); PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); String armored = PGPainless.asciiArmor(publicKeyRing); @@ -199,7 +200,8 @@ public class ArmorUtilsTest { .addUserId("xmpp:juliet@capulet.lit") .addUserId("Juliet Montague ") .setPassphrase(Passphrase.fromPassword("test")) - .build(); + .build() + .getPGPSecretKeyRing(); PGPPublicKey publicKey = secretKeyRing.getPublicKey(); PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); String armored = PGPainless.asciiArmor(publicKeyRing); @@ -214,7 +216,8 @@ public class ArmorUtilsTest { .setPrimaryKey(KeySpec.getBuilder(ECDSA.fromCurve(EllipticCurve._P256), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) .addUserId("Juliet ") .setPassphrase(Passphrase.fromPassword("test")) - .build(); + .build() + .getPGPSecretKeyRing(); PGPPublicKey publicKey = secretKeyRing.getPublicKey(); PGPPublicKeyRing publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKey.getEncoded()); String armored = PGPainless.asciiArmor(publicKeyRing); @@ -254,9 +257,9 @@ public class ArmorUtilsTest { "-----END PGP MESSAGE-----"; InputStream inputStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8))); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(inputStream); + PGPObjectFactory factory = OpenPGPImplementation.getInstance().pgpObjectFactory(inputStream); PGPCompressedData compressed = (PGPCompressedData) factory.nextObject(); - factory = ImplementationFactory.getInstance().getPGPObjectFactory(compressed.getDataStream()); + factory = OpenPGPImplementation.getInstance().pgpObjectFactory(compressed.getDataStream()); PGPLiteralData literal = (PGPLiteralData) factory.nextObject(); ByteArrayOutputStream out = new ByteArrayOutputStream(); assertEquals("_CONSOLE", literal.getFileName()); 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 821d9e30..5df91552 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/BCUtilTest.java @@ -8,11 +8,8 @@ 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.Iterator; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; @@ -34,15 +31,15 @@ public class BCUtilTest { private static final Logger LOGGER = LoggerFactory.getLogger(BCUtilTest.class); @Test - public void keyRingToCollectionTest() - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { + public void keyRingToCollectionTest() { PGPSecretKeyRing sec = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.RSA(RsaLength._3072), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._3072), KeyFlag.ENCRYPT_COMMS)) .addUserId("donald@duck.tails") - .build(); + .build() + .getPGPSecretKeyRing(); PGPPublicKeyRing pub = KeyRingUtils.publicKeyRingFrom(sec); diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java deleted file mode 100644 index e276ba8f..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java +++ /dev/null @@ -1,47 +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 java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Collections; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.key.generation.KeySpec; -import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; -import org.pgpainless.key.util.OpenPgpKeyAttributeUtil; - -public class GuessPreferredHashAlgorithmTest { - - @Test - public void guessPreferredHashAlgorithmsAssumesHashAlgoUsedBySelfSig() - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), - KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) - .overridePreferredHashAlgorithms(new HashAlgorithm[] {}) - .overridePreferredSymmetricKeyAlgorithms(new SymmetricKeyAlgorithm[] {}) - .overridePreferredCompressionAlgorithms(new CompressionAlgorithm[] {})) - .addUserId("test@test.test") - .build(); - - PGPPublicKey publicKey = secretKeys.getPublicKey(); - assertEquals(Collections.emptyList(), - OpenPgpKeyAttributeUtil.getPreferredHashAlgorithms(publicKey)); - assertEquals(Collections.singletonList(HashAlgorithm.SHA512), - OpenPgpKeyAttributeUtil.guessPreferredHashAlgorithms(publicKey)); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/OpenPGPCertificateUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/OpenPGPCertificateUtilTest.java new file mode 100644 index 00000000..520dc303 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/util/OpenPGPCertificateUtilTest.java @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util; + +import org.bouncycastle.bcpg.PacketFormat; +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.pgpainless.PGPainless; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; + +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 OpenPGPCertificateUtilTest { + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncodeSingleCert() { + PGPainless api = PGPainless.getInstance(); + + List certs = new ArrayList<>(); + certs.add(api.generateKey().modernKeyRing("Alice ").toCertificate()); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPCertificateUtil.armor(certs, bOut, PacketFormat.CURRENT); + String armor = bOut.toString(); + + assertTrue(armor.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: "), + "For a single cert, the ASCII armor MUST contain a comment with the fingerprint"); + } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncodeSingleKey() { + PGPainless api = PGPainless.getInstance(); + + List certs = new ArrayList<>(); + certs.add(api.generateKey().modernKeyRing("Alice ")); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPCertificateUtil.armor(certs, bOut, PacketFormat.CURRENT); + String armor = bOut.toString(); + + assertTrue(armor.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\nComment: "), + "For a single key, the ASCII armor MUST contain a comment with the fingerprint"); + } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncodeTwoCerts() { + PGPainless api = PGPainless.getInstance(); + + List certs = new ArrayList<>(); + certs.add(api.generateKey().modernKeyRing("Alice ").toCertificate()); + certs.add(api.generateKey().modernKeyRing("Bob ").toCertificate()); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPCertificateUtil.armor(certs, bOut, PacketFormat.CURRENT); + String armor = bOut.toString(); + + assertTrue(armor.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")); + assertEquals( + armor.indexOf("-----BEGIN PGP PUBLIC KEY BLOCK-----"), + armor.lastIndexOf("-----BEGIN PGP PUBLIC KEY BLOCK-----"), + "There MUST only be a single block in the armor."); + assertFalse(armor.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: "), + "For multiple certs, the ASCII armor MUST NOT contain a comment containing the fingerprint"); + } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncodeCertAndKey() { + PGPainless api = PGPainless.getInstance(); + + List certs = new ArrayList<>(); + certs.add(api.generateKey().modernKeyRing("Alice ").toCertificate()); + certs.add(api.generateKey().modernKeyRing("Bob ")); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPCertificateUtil.armor(certs, bOut, PacketFormat.CURRENT); + String armor = bOut.toString(); + + assertTrue(armor.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")); + assertEquals( + armor.indexOf("-----BEGIN PGP PUBLIC KEY BLOCK-----"), + armor.lastIndexOf("-----BEGIN PGP PUBLIC KEY BLOCK-----")); + assertFalse(armor.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\nComment: "), + "For multiple certs/keys, the ASCII armor MUST NOT contain a comment containing the fingerprint"); + } + + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void testEncodeKeyAndCert() { + PGPainless api = PGPainless.getInstance(); + + List certs = new ArrayList<>(); + certs.add(api.generateKey().modernKeyRing("Alice ")); + certs.add(api.generateKey().modernKeyRing("Bob ").toCertificate()); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OpenPGPCertificateUtil.armor(certs, bOut, PacketFormat.CURRENT); + String armor = bOut.toString(); + + assertTrue(armor.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----")); + assertEquals( + armor.indexOf("-----BEGIN PGP PRIVATE KEY BLOCK-----"), + armor.lastIndexOf("-----BEGIN PGP PRIVATE KEY BLOCK-----")); + assertFalse(armor.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\nComment: "), + "For multiple certs, the ASCII armor MUST NOT contain a comment containing the fingerprint"); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/TestAllImplementations.java b/pgpainless-core/src/test/java/org/pgpainless/util/TestAllImplementations.java index 2b874b6d..8467a0eb 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/TestAllImplementations.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/TestAllImplementations.java @@ -9,23 +9,23 @@ import java.util.Collections; import java.util.List; import java.util.stream.Stream; +import org.bouncycastle.openpgp.api.OpenPGPImplementation; +import org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation; +import org.bouncycastle.openpgp.api.jcajce.JcaOpenPGPImplementation; import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; -import org.pgpainless.implementation.BcImplementationFactory; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.implementation.JceImplementationFactory; /** - * InvocationContextProvider that sets different {@link ImplementationFactory} implementations before running annotated - * tests. + * InvocationContextProvider that sets different {@link org.bouncycastle.openpgp.api.OpenPGPImplementation} + * before running annotated tests. * * Example test annotation: * {@code * @TestTemplate - * @ExtendWith(ImplementationFactoryTestInvocationContextProvider.class) + * @ExtendWith(TestAllImplementations.class) * public void testAllImplementationFactories() { * ... * } @@ -35,9 +35,9 @@ import org.pgpainless.implementation.JceImplementationFactory; */ public class TestAllImplementations implements TestTemplateInvocationContextProvider { - private static final List IMPLEMENTATIONS = Arrays.asList( - new BcImplementationFactory(), - new JceImplementationFactory() + private static final List IMPLEMENTATIONS = Arrays.asList( + new BcOpenPGPImplementation(), + new JcaOpenPGPImplementation() ); @Override @@ -49,16 +49,16 @@ public class TestAllImplementations implements TestTemplateInvocationContextProv public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { return IMPLEMENTATIONS.stream() - .map(implementationFactory -> new TestTemplateInvocationContext() { + .map(implementation -> new TestTemplateInvocationContext() { @Override public String getDisplayName(int invocationIndex) { - return context.getDisplayName() + " with " + implementationFactory.getClass().getSimpleName(); + return context.getDisplayName() + " with " + implementation.getClass().getSimpleName(); } @Override public List getAdditionalExtensions() { return Collections.singletonList( - (BeforeTestExecutionCallback) ctx -> ImplementationFactory.setFactoryImplementation(implementationFactory) + (BeforeTestExecutionCallback) ctx -> OpenPGPImplementation.setInstance(implementation) ); } }); diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/Tuple.java b/pgpainless-core/src/test/java/org/pgpainless/util/Tuple.java deleted file mode 100644 index 84d7a370..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/util/Tuple.java +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -/** - * Helper class pairing together two values. - * @param type of the first value - * @param type of the second value - * @deprecated Scheduled for removal. - * TODO: Remove - */ -@Deprecated -public class Tuple { - - private final A a; - private final B b; - - public Tuple(A a, B b) { - this.a = a; - this.b = b; - } - - public A getA() { - return a; - } - - public B getB() { - return b; - } -} 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 deleted file mode 100644 index c7f6d722..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util.selection.keyring; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.Set; - -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.junit.jupiter.api.Test; -import org.pgpainless.key.TestKeys; -import org.pgpainless.util.MultiMap; -import org.pgpainless.util.selection.keyring.impl.ExactUserId; - -public class KeyRingsFromCollectionTest { - - @Test - public void selectSecretKeyRingFromSecretKeyRingCollectionTest() throws IOException, PGPException { - PGPSecretKeyRing emil = TestKeys.getEmilSecretKeyRing(); - PGPSecretKeyRing juliet = TestKeys.getJulietSecretKeyRing(); - PGPSecretKeyRingCollection collection = new PGPSecretKeyRingCollection(Arrays.asList(emil, juliet)); - - SecretKeyRingSelectionStrategy strategy = new ExactUserId.SecRingSelectionStrategy(); - Set secretKeyRings = strategy.selectKeyRingsFromCollection(TestKeys.JULIET_UID, collection); - assertEquals(1, secretKeyRings.size()); - assertEquals(juliet.getPublicKey().getKeyID(), secretKeyRings.iterator().next().getPublicKey().getKeyID()); - } - - @Test - public void selectSecretKeyRingMapFromSecretKeyRingCollectionMapTest() throws IOException, PGPException { - PGPSecretKeyRing emil = TestKeys.getEmilSecretKeyRing(); - PGPSecretKeyRing juliet = TestKeys.getJulietSecretKeyRing(); - MultiMap map = new MultiMap<>(); - PGPSecretKeyRingCollection julietCollection = new PGPSecretKeyRingCollection(Arrays.asList(emil, juliet)); - map.put(TestKeys.JULIET_UID, julietCollection); - PGPSecretKeyRingCollection emilCollection = new PGPSecretKeyRingCollection(Collections.singletonList(emil)); - map.put(TestKeys.EMIL_UID, emilCollection); - assertEquals(2, julietCollection.size()); - map.put("invalidId", emilCollection); - - SecretKeyRingSelectionStrategy strategy = new ExactUserId.SecRingSelectionStrategy(); - MultiMap selected = strategy.selectKeyRingsFromCollections(map); - assertEquals(1, selected.get(TestKeys.JULIET_UID).size()); - assertEquals(1, selected.get(TestKeys.EMIL_UID).size()); - assertTrue(selected.get("invalidId").isEmpty()); - } - - @Test - public void selectPublicKeyRingFromPublicKeyRingCollectionTest() throws IOException { - PGPPublicKeyRing emil = TestKeys.getEmilPublicKeyRing(); - PGPPublicKeyRing juliet = TestKeys.getJulietPublicKeyRing(); - PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(Arrays.asList(emil, juliet)); - - PublicKeyRingSelectionStrategy strategy = new ExactUserId.PubRingSelectionStrategy(); - Set publicKeyRings = strategy.selectKeyRingsFromCollection(TestKeys.JULIET_UID, collection); - assertEquals(1, publicKeyRings.size()); - assertEquals(juliet.getPublicKey().getKeyID(), publicKeyRings.iterator().next().getPublicKey().getKeyID()); - } - - @Test - public void selectPublicKeyRingMapFromPublicKeyRingCollectionMapTest() throws IOException { - PGPPublicKeyRing emil = TestKeys.getEmilPublicKeyRing(); - PGPPublicKeyRing juliet = TestKeys.getJulietPublicKeyRing(); - MultiMap map = new MultiMap<>(); - PGPPublicKeyRingCollection julietCollection = new PGPPublicKeyRingCollection(Arrays.asList(emil, juliet)); - map.plus(TestKeys.JULIET_UID, julietCollection); - PGPPublicKeyRingCollection emilCollection = new PGPPublicKeyRingCollection(Collections.singletonList(emil)); - map.plus(TestKeys.EMIL_UID, emilCollection); - assertEquals(2, julietCollection.size()); - map.plus("invalidId", emilCollection); - - PublicKeyRingSelectionStrategy strategy = new ExactUserId.PubRingSelectionStrategy(); - MultiMap selected = strategy.selectKeyRingsFromCollections(map); - assertEquals(1, selected.get(TestKeys.JULIET_UID).size()); - assertEquals(1, selected.get(TestKeys.EMIL_UID).size()); - assertTrue(selected.get("invalidId").isEmpty()); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/WhitelistKeyRingSelectionStrategyTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/WhitelistKeyRingSelectionStrategyTest.java deleted file mode 100644 index 43c19a6c..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/WhitelistKeyRingSelectionStrategyTest.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util.selection.keyring; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.util.Collections; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.Test; -import org.pgpainless.key.TestKeys; -import org.pgpainless.util.selection.keyring.impl.Whitelist; - -public class WhitelistKeyRingSelectionStrategyTest { - - @Test - public void testWithPublicKeys() throws IOException { - Map> ids = new ConcurrentHashMap<>(); - ids.put(TestKeys.JULIET_UID, Collections.singleton(TestKeys.JULIET_KEY_ID)); - Whitelist.PubRingSelectionStrategy selectionStrategy = new Whitelist.PubRingSelectionStrategy<>(ids); - - PGPPublicKeyRing julietsKeys = TestKeys.getJulietPublicKeyRing(); - PGPPublicKeyRing romeosKeys = TestKeys.getRomeoPublicKeyRing(); - - assertTrue(selectionStrategy.accept(TestKeys.JULIET_UID, julietsKeys)); - assertFalse(selectionStrategy.accept(TestKeys.JULIET_UID, romeosKeys)); - assertFalse(selectionStrategy.accept(TestKeys.ROMEO_UID, julietsKeys)); - } - - @Test - public void testWithSecretKeys() throws IOException, PGPException { - Map> ids = new ConcurrentHashMap<>(); - ids.put(TestKeys.JULIET_UID, Collections.singleton(TestKeys.JULIET_KEY_ID)); - Whitelist.SecRingSelectionStrategy selectionStrategy = new Whitelist.SecRingSelectionStrategy<>(ids); - - PGPSecretKeyRing julietsKeys = TestKeys.getJulietSecretKeyRing(); - PGPSecretKeyRing romeosKeys = TestKeys.getRomeoSecretKeyRing(); - - assertTrue(selectionStrategy.accept(TestKeys.JULIET_UID, julietsKeys)); - assertFalse(selectionStrategy.accept(TestKeys.JULIET_UID, romeosKeys)); - assertFalse(selectionStrategy.accept(TestKeys.ROMEO_UID, julietsKeys)); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/WildcardKeyRingSelectionStrategyTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/WildcardKeyRingSelectionStrategyTest.java deleted file mode 100644 index 10907eca..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/WildcardKeyRingSelectionStrategyTest.java +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util.selection.keyring; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.Test; -import org.pgpainless.key.TestKeys; -import org.pgpainless.util.selection.keyring.impl.Wildcard; - -public class WildcardKeyRingSelectionStrategyTest { - - - private static final Wildcard.PubRingSelectionStrategy pubKeySelectionStrategy - = new Wildcard.PubRingSelectionStrategy<>(); - private static final Wildcard.SecRingSelectionStrategy secKeySelectionStrategy - = new Wildcard.SecRingSelectionStrategy<>(); - - @Test - public void testStratAcceptsMatchingUIDsOnPubKey() throws IOException { - String uid = TestKeys.EMIL_UID; - PGPPublicKeyRing key = TestKeys.getEmilPublicKeyRing(); - - assertTrue(pubKeySelectionStrategy.accept(uid, key)); - } - - @Test - public void testStratAcceptsMismatchingUIDsOnPubKey() throws IOException { - String uid = "blabla@bla.bla"; - PGPPublicKeyRing key = TestKeys.getEmilPublicKeyRing(); - - assertTrue(pubKeySelectionStrategy.accept(uid, key)); - } - - @Test - public void testStratAcceptsMatchingUIDsOnSecKey() throws IOException, PGPException { - String uid = TestKeys.EMIL_UID; - PGPSecretKeyRing key = TestKeys.getEmilSecretKeyRing(); - - assertTrue(secKeySelectionStrategy.accept(uid, key)); - } - - @Test - public void testStratAcceptsMismatchingUIDsOnSecKey() throws IOException, PGPException { - String uid = "blabla@bla.bla"; - PGPSecretKeyRing key = TestKeys.getEmilSecretKeyRing(); - - assertTrue(secKeySelectionStrategy.accept(uid, key)); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/XmppKeyRingSelectionStrategyTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/XmppKeyRingSelectionStrategyTest.java deleted file mode 100644 index 2b5f8ebb..00000000 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/XmppKeyRingSelectionStrategyTest.java +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util.selection.keyring; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.junit.jupiter.api.Test; -import org.pgpainless.key.TestKeys; -import org.pgpainless.util.selection.keyring.impl.XMPP; - -public class XmppKeyRingSelectionStrategyTest { - - private static final XMPP.PubRingSelectionStrategy pubKeySelectionStrategy = - new XMPP.PubRingSelectionStrategy(); - private static final XMPP.SecRingSelectionStrategy secKeySelectionStrategy = - new XMPP.SecRingSelectionStrategy(); - - @Test - public void testMatchingXmppUIDAcceptedOnPubKey() throws IOException { - String uid = "xmpp:juliet@capulet.lit"; - PGPPublicKeyRing key = TestKeys.getJulietPublicKeyRing(); - - assertTrue(pubKeySelectionStrategy.accept(uid, key)); - } - - @Test - public void testAddressIsFormattedToMatchOnPubKey() throws IOException { - String uid = "juliet@capulet.lit"; - PGPPublicKeyRing key = TestKeys.getJulietPublicKeyRing(); - - assertTrue(pubKeySelectionStrategy.accept(uid, key)); - } - - @Test - public void testPubKeyWithDifferentUIDIsRejected() throws IOException { - String wrongUid = "romeo@montague.lit"; - PGPPublicKeyRing key = TestKeys.getJulietPublicKeyRing(); - assertFalse(pubKeySelectionStrategy.accept(wrongUid, key)); - } - - @Test - public void testMatchingEmailUIDAcceptedOnSecKey() throws IOException, PGPException { - String uid = "xmpp:juliet@capulet.lit"; - PGPSecretKeyRing key = TestKeys.getJulietSecretKeyRing(); - - assertTrue(secKeySelectionStrategy.accept(uid, key)); - } - - @Test - public void testAddressIsFormattedToMatchOnSecKey() throws IOException, PGPException { - String uid = "juliet@capulet.lit"; - PGPSecretKeyRing key = TestKeys.getJulietSecretKeyRing(); - - assertTrue(secKeySelectionStrategy.accept(uid, key)); - } - - @Test - public void testSecKeyWithDifferentUIDIsRejected() throws IOException, PGPException { - String wrongUid = "romeo@montague.lit"; - - PGPSecretKeyRing key = TestKeys.getJulietSecretKeyRing(); - assertFalse(secKeySelectionStrategy.accept(wrongUid, key)); - } -} diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/selection/userid/SelectUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/userid/SelectUserIdTest.java index 99a0c87c..b8eccd95 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/userid/SelectUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/selection/userid/SelectUserIdTest.java @@ -14,7 +14,7 @@ import java.util.List; import java.util.stream.Collectors; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -24,16 +24,17 @@ public class SelectUserIdTest { @Test public void testSelectUserIds() throws PGPException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey() .simpleEcKeyRing(""); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .addUserId( - UserId.newBuilder().withName("Alice Liddell").noComment() + UserId.builder().withName("Alice Liddell").noComment() .withEmail("crazy@the-rabbit.hole").build(), SecretKeyRingProtector.unprotectedKeys()) .done(); - List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); + List userIds = api.inspect(secretKeys).getValidUserIds(); List validEmail = userIds.stream().filter(SelectUserId.and( SelectUserId.validUserId(secretKeys), SelectUserId.containsEmailAddress("alice@wonderland.lit") @@ -53,13 +54,14 @@ public class SelectUserIdTest { @Test public void testContainsSubstring() throws PGPException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("wine drinker"); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey().simpleEcKeyRing("wine drinker"); + secretKeys = api.modify(secretKeys) .addUserId("this is not a quine", SecretKeyRingProtector.unprotectedKeys()) .addUserId("this is not a crime", SecretKeyRingProtector.unprotectedKeys()) .done(); - List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); + List userIds = api.inspect(secretKeys).getValidUserIds(); List containSubstring = userIds.stream().filter(SelectUserId.containsSubstring("ine")).collect(Collectors.toList()); assertEquals(Arrays.asList("wine drinker", "this is not a quine"), containSubstring); @@ -67,8 +69,9 @@ public class SelectUserIdTest { @Test public void testContainsEmailAddress() { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("Alice "); - List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey().simpleEcKeyRing("Alice "); + List userIds = api.inspect(secretKeys).getValidUserIds(); assertEquals("Alice ", userIds.stream().filter( SelectUserId.containsEmailAddress("alice@wonderland.lit")).findFirst().get()); @@ -80,14 +83,15 @@ public class SelectUserIdTest { @Test public void testAndOrNot() throws PGPException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("Alice "); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey().simpleEcKeyRing("Alice "); + secretKeys = api.modify(secretKeys) .addUserId("Alice ", SecretKeyRingProtector.unprotectedKeys()) .addUserId("", SecretKeyRingProtector.unprotectedKeys()) .addUserId("Crazy Girl ", SecretKeyRingProtector.unprotectedKeys()) .done(); - List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); + List userIds = api.inspect(secretKeys).getValidUserIds(); List or = userIds.stream().filter(SelectUserId.or( SelectUserId.containsEmailAddress("alice@wonderland.lit"), @@ -106,11 +110,12 @@ public class SelectUserIdTest { @Test public void testFirstMatch() throws PGPException { - PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("First UserID"); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = api.generateKey().simpleEcKeyRing("First UserID"); + secretKeys = api.modify(secretKeys) .addUserId("Second UserID", SecretKeyRingProtector.unprotectedKeys()) .done(); - List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); + List userIds = api.inspect(secretKeys).getValidUserIds(); assertEquals("First UserID", userIds.stream().filter(SelectUserId.validUserId(secretKeys)).findFirst().get()); assertEquals("Second UserID", userIds.stream().filter(SelectUserId.containsSubstring("Second")).findFirst().get()); } 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 b96d95e5..4c50a297 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 @@ -6,10 +6,6 @@ package org.pgpainless.weird_keys; 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.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; @@ -25,8 +21,7 @@ import org.pgpainless.key.util.KeyRingUtils; public class TestEncryptCommsStorageFlagsDifferentiated { @Test - public void testThatEncryptionDifferentiatesBetweenPurposeKeyFlags() - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public void testThatEncryptionDifferentiatesBetweenPurposeKeyFlags() { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.RSA(RsaLength._3072), @@ -35,7 +30,8 @@ public class TestEncryptCommsStorageFlagsDifferentiated { KeyFlag.ENCRYPT_STORAGE // no ENCRYPT_COMMS )) .addUserId("cannot@encrypt.comms") - .build(); + .build() + .getPGPSecretKeyRing(); PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys); diff --git a/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestTwoSubkeysEncryption.java b/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestTwoSubkeysEncryption.java index 4b715a76..f09f1445 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestTwoSubkeysEncryption.java +++ b/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestTwoSubkeysEncryption.java @@ -12,18 +12,16 @@ 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.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; 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.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.key.WeirdKeys; -import org.pgpainless.key.util.KeyRingUtils; public class TestTwoSubkeysEncryption { @@ -36,8 +34,8 @@ public class TestTwoSubkeysEncryption { /** * {@link WeirdKeys#TWO_CRYPT_SUBKEYS} is a key that has two subkeys which both carry the key flags * {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS} and {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}. - * - * This test verifies that {@link EncryptionOptions#addRecipient(PGPPublicKeyRing, EncryptionOptions.EncryptionKeySelector)} + *

+ * This test verifies that {@link EncryptionOptions#addRecipient(OpenPGPCertificate, EncryptionOptions.EncryptionKeySelector)} * works properly, if {@link EncryptionOptions#encryptToAllCapableSubkeys()} is provided as argument. * * @throws IOException not expected @@ -45,13 +43,13 @@ public class TestTwoSubkeysEncryption { */ @Test public void testEncryptsToBothSubkeys() throws IOException, PGPException { - PGPSecretKeyRing twoSuitableSubkeysKeyRing = WeirdKeys.getTwoCryptSubkeysKey(); - PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(twoSuitableSubkeysKeyRing); + OpenPGPKey twoSuitableSubkeysKeyRing = WeirdKeys.getTwoCryptSubkeysKey(); + OpenPGPCertificate publicKeys = twoSuitableSubkeysKeyRing.toCertificate(); ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + EncryptionStream encryptionStream = PGPainless.getInstance().generateMessage() .onOutputStream(out) .withOptions( - ProducerOptions.encrypt(new EncryptionOptions(EncryptionPurpose.ANY) + ProducerOptions.encrypt(EncryptionOptions.get() .addRecipient(publicKeys, EncryptionOptions.encryptToAllCapableSubkeys()) ) .setAsciiArmor(false) diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt index d9c99c47..da642e4c 100644 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt @@ -24,6 +24,7 @@ class PGPPublicKeyExtensionsTest { PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.ECDSA(curve))) .build() + .pgpSecretKeyRing .publicKey assertEquals(curve.curveName, key.getCurveName()) @@ -37,6 +38,7 @@ class PGPPublicKeyExtensionsTest { PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(curve))) .build() + .pgpSecretKeyRing .publicKey assertEquals(curve.curveName, key.getCurveName()) diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt index 90641aff..cb3004ca 100644 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt @@ -4,56 +4,40 @@ package org.pgpainless.bouncycastle.extensions -import java.io.ByteArrayOutputStream import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import org.pgpainless.PGPainless -import org.pgpainless.encryption_signing.ProducerOptions -import org.pgpainless.encryption_signing.SigningOptions import org.pgpainless.key.TestKeys -import org.pgpainless.key.protection.SecretKeyRingProtector class PGPSecretKeyRingExtensionsTest { @Test fun testHasPgpSecretKeyRing() { val key = TestKeys.getEmilSecretKeyRing() - assertTrue(key.hasSecretKey(TestKeys.EMIL_KEY_ID)) + assertTrue(key.hasSecretKey(TestKeys.EMIL_FINGERPRINT.keyIdentifier)) + assertTrue(key.hasSecretKey(TestKeys.EMIL_FINGERPRINT.keyId)) assertTrue(key.hasSecretKey(TestKeys.EMIL_FINGERPRINT)) - assertFalse(key.hasSecretKey(TestKeys.ROMEO_KEY_ID)) + assertFalse(key.hasSecretKey(TestKeys.ROMEO_FINGERPRINT.keyIdentifier)) + assertFalse(key.hasSecretKey(TestKeys.ROMEO_FINGERPRINT.keyId)) assertFalse(key.hasSecretKey(TestKeys.ROMEO_FINGERPRINT)) } @Test fun testRequireSecretKey() { val key = TestKeys.getEmilSecretKeyRing() - assertNotNull(key.requireSecretKey(TestKeys.EMIL_KEY_ID)) + assertNotNull(key.requireSecretKey(TestKeys.EMIL_FINGERPRINT.keyIdentifier)) + assertNotNull(key.requireSecretKey(TestKeys.EMIL_FINGERPRINT.keyId)) assertNotNull(key.requireSecretKey(TestKeys.EMIL_FINGERPRINT)) - assertThrows { key.requireSecretKey(TestKeys.ROMEO_KEY_ID) } + assertThrows { + key.requireSecretKey(TestKeys.ROMEO_FINGERPRINT.keyIdentifier) + } + assertThrows { + key.requireSecretKey(TestKeys.ROMEO_FINGERPRINT.keyId) + } assertThrows { key.requireSecretKey(TestKeys.ROMEO_FINGERPRINT) } } - - @Test - fun testGetSecretKeyForSignature() { - val key = TestKeys.getEmilSecretKeyRing() - val signer = - PGPainless.encryptAndOrSign() - .onOutputStream(ByteArrayOutputStream()) - .withOptions( - ProducerOptions.sign( - SigningOptions.get() - .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key))) - signer.write("Hello, World!\n".toByteArray()) - signer.close() - val sig = signer.result.detachedSignatures.first().value.first() - - assertNotNull(key.getSecretKeyFor(sig)) - assertNull(TestKeys.getRomeoSecretKeyRing().getSecretKeyFor(sig)) - } } diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnknownSecretKeyEncryptionMethodTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnknownSecretKeyEncryptionMethodTest.kt index 253c0f19..b45bfa39 100644 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnknownSecretKeyEncryptionMethodTest.kt +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnknownSecretKeyEncryptionMethodTest.kt @@ -31,13 +31,11 @@ DhxJVTgA/1WaFrKdP3AgL0Ffdooc5XXbjQsj0uHo6FZSHRI4pchMAQCyJnKQ3RvW @Test @Disabled("Disabled since BC 1.77 chokes on the test key") fun testExtractCertificate() { - val key = PGPainless.readKeyRing().secretKeyRing(KEY)!! - val cert = PGPainless.extractCertificate(key) + val key = PGPainless.getInstance().readKey().parseKey(KEY)!! + val cert = key.toCertificate() assertNotNull(cert) // Each secret key got its public key component extracted - assertEquals( - key.secretKeys.asSequence().map { it.keyID }.toSet(), - cert.publicKeys.asSequence().map { it.keyID }.toSet()) + assertEquals(key.secretKeys.keys.toSet(), cert.publicKeys.keys.toSet()) } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt index 40ac811d..be2f272f 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt @@ -9,14 +9,15 @@ import java.io.InputStream import java.io.OutputStream import kotlin.jvm.Throws import org.bouncycastle.util.io.Streams -import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.PGPainless +import org.pgpainless.decryption_verification.OpenPGPAnimalSnifferInputStream import org.pgpainless.util.ArmoredOutputStreamFactory import sop.Ready import sop.exception.SOPGPException import sop.operation.Armor /** Implementation of the `armor` operation using PGPainless. */ -class ArmorImpl : Armor { +class ArmorImpl(private val api: PGPainless) : Armor { @Throws(SOPGPException.BadData::class) override fun data(data: InputStream): Ready { @@ -26,7 +27,7 @@ class ArmorImpl : Armor { val bufferedOutputStream = BufferedOutputStream(outputStream) // Determine the nature of the given data - val openPgpIn = OpenPgpInputStream(data) + val openPgpIn = OpenPGPAnimalSnifferInputStream(data) openPgpIn.reset() if (openPgpIn.isAsciiArmored) { diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/CertifyUserIdImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/CertifyUserIdImpl.kt new file mode 100644 index 00000000..cbd97934 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/CertifyUserIdImpl.kt @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.pgpainless.PGPainless +import org.pgpainless.exception.KeyException +import org.pgpainless.util.OpenPGPCertificateUtil +import org.pgpainless.util.Passphrase +import sop.Ready +import sop.exception.SOPGPException +import sop.operation.CertifyUserId + +class CertifyUserIdImpl(private val api: PGPainless) : CertifyUserId { + + private var armor: Boolean = true + private val keys: MutableList = mutableListOf() + private var requireSelfSig = true + private val userIds: MutableSet = mutableSetOf() + private var protector: MatchMakingSecretKeyRingProtector = MatchMakingSecretKeyRingProtector() + + override fun certs(certs: InputStream): Ready { + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + + val certificates = + api.readKey() + .parseCertificates(certs) + .onEach { cert -> + if (requireSelfSig) { + // Check for non-bound user-ids + userIds + .find { cert.getUserId(it)?.isBound != true } + ?.let { + throw SOPGPException.CertUserIdNoMatch(cert.fingerprint) + } + } + } + .map { cert -> + var certificate = cert + keys.forEach { key -> + userIds.forEach { userId -> + try { + certificate = + api.generateCertification() + .certifyUserId(userId, certificate) + .withKey(key, protector) + .build() + .certifiedCertificate + } catch (e: KeyException) { + throw SOPGPException.KeyCannotCertify(e) + } + } + } + certificate + } + + if (armor) { + OpenPGPCertificateUtil.armor(certificates, outputStream) + } else { + OpenPGPCertificateUtil.encode(certificates, outputStream) + } + } + } + } + + override fun keys(keys: InputStream): CertifyUserId = apply { + this.keys.addAll(api.readKey().parseKeys(keys).onEach { protector.addSecretKey(it) }) + } + + override fun noArmor(): CertifyUserId = apply { armor = false } + + override fun noRequireSelfSig(): CertifyUserId = apply { requireSelfSig = false } + + override fun userId(userId: String): CertifyUserId = apply { this.userIds.add(userId) } + + override fun withKeyPassword(password: ByteArray): CertifyUserId = apply { + protector.addPassphrase(Passphrase.fromPassword(String(password))) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt index a9aaf1e4..a7d0c530 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt @@ -8,19 +8,18 @@ import java.io.IOException import java.io.InputStream import java.io.OutputStream import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection -import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.PGPainless import org.pgpainless.exception.MissingPassphraseException import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.key.util.KeyRingUtils -import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.OpenPGPCertificateUtil import org.pgpainless.util.Passphrase import sop.Ready import sop.exception.SOPGPException import sop.operation.ChangeKeyPassword /** Implementation of the `change-key-password` operation using PGPainless. */ -class ChangeKeyPasswordImpl : ChangeKeyPassword { +class ChangeKeyPasswordImpl(private val api: PGPainless) : ChangeKeyPassword { private val oldProtector = MatchMakingSecretKeyRingProtector() private var newPassphrase = Passphrase.emptyPassphrase() @@ -28,42 +27,40 @@ class ChangeKeyPasswordImpl : ChangeKeyPassword { override fun keys(keys: InputStream): Ready { val newProtector = SecretKeyRingProtector.unlockAnyKeyWith(newPassphrase) - val secretKeysCollection = + val secretKeys = try { - KeyReader.readSecretKeys(keys, true) + KeyReader(api).readSecretKeys(keys, true) } catch (e: IOException) { throw SOPGPException.BadData(e) } val updatedSecretKeys = - secretKeysCollection - .map { secretKeys -> - oldProtector.addSecretKey(secretKeys) + secretKeys + .map { + oldProtector.addSecretKey(it) try { return@map KeyRingUtils.changePassphrase( - null, secretKeys, oldProtector, newProtector) + null, it.pgpSecretKeyRing, oldProtector, newProtector) } catch (e: MissingPassphraseException) { throw SOPGPException.KeyIsProtected( - "Cannot unlock key ${secretKeys.openPgpFingerprint}", e) + "Cannot unlock key ${it.keyIdentifier}", e) } catch (e: PGPException) { if (e.message?.contains("Exception decrypting key") == true) { throw SOPGPException.KeyIsProtected( - "Cannot unlock key ${secretKeys.openPgpFingerprint}", e) + "Cannot unlock key ${it.keyIdentifier}", e) } throw RuntimeException( - "Cannot change passphrase of key ${secretKeys.openPgpFingerprint}", e) + "Cannot change passphrase of key ${it.keyIdentifier}", e) } } - .let { PGPSecretKeyRingCollection(it) } + .map { api.toKey(it) } return object : Ready() { override fun writeTo(outputStream: OutputStream) { if (armor) { - ArmoredOutputStreamFactory.get(outputStream).use { - updatedSecretKeys.encode(it) - } + OpenPGPCertificateUtil.armor(updatedSecretKeys, outputStream) } else { - updatedSecretKeys.encode(outputStream) + OpenPGPCertificateUtil.encode(updatedSecretKeys, outputStream) } } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt index 9d196004..2d6e02d9 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt @@ -10,12 +10,13 @@ import java.io.InputStream import java.io.OutputStream import org.bouncycastle.openpgp.PGPUtil import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless import sop.Ready import sop.exception.SOPGPException import sop.operation.Dearmor /** Implementation of the `dearmor` operation using PGPainless. */ -class DearmorImpl : Dearmor { +class DearmorImpl(private val api: PGPainless) : Dearmor { override fun data(data: InputStream): Ready { val decoder = diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt index de2b2b3c..ddd11d50 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt @@ -25,9 +25,9 @@ import sop.operation.Decrypt import sop.util.UTF8Util /** Implementation of the `decrypt` operation using PGPainless. */ -class DecryptImpl : Decrypt { +class DecryptImpl(private val api: PGPainless) : Decrypt { - private val consumerOptions = ConsumerOptions.get() + private val consumerOptions = ConsumerOptions.get(api) private val protector = MatchMakingSecretKeyRingProtector() override fun ciphertext(ciphertext: InputStream): ReadyWithResult { @@ -39,9 +39,7 @@ class DecryptImpl : Decrypt { val decryptionStream = try { - PGPainless.decryptAndOrVerify() - .onInputStream(ciphertext) - .withOptions(consumerOptions) + api.processMessage().onInputStream(ciphertext).withOptions(consumerOptions) } catch (e: MissingDecryptionMethodException) { throw SOPGPException.CannotDecrypt( "No usable decryption key or password provided.", e) @@ -71,13 +69,10 @@ class DecryptImpl : Decrypt { val verificationList = metadata.verifiedInlineSignatures.map { VerificationHelper.mapVerification(it) } - var sessionKey: SessionKey? = null - if (metadata.sessionKey != null) { - sessionKey = - SessionKey( - metadata.sessionKey!!.algorithm.algorithmId.toByte(), - metadata.sessionKey!!.key) - } + val sessionKey: SessionKey? = + metadata.sessionKey?.let { + SessionKey(it.algorithm.algorithmId.toByte(), it.key) + } return DecryptionResult(sessionKey, verificationList) } } @@ -92,11 +87,11 @@ class DecryptImpl : Decrypt { } override fun verifyWithCert(cert: InputStream): Decrypt = apply { - KeyReader.readPublicKeys(cert, true).let { consumerOptions.addVerificationCerts(it) } + consumerOptions.addVerificationCerts(KeyReader(api).readPublicKeys(cert, true)) } override fun withKey(key: InputStream): Decrypt = apply { - KeyReader.readSecretKeys(key, true).forEach { + KeyReader(api).readSecretKeys(key, true).forEach { protector.addSecretKey(it) consumerOptions.addDecryptionKey(it, protector) } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt index 19bc782b..e23ca1c3 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt @@ -7,14 +7,13 @@ package org.pgpainless.sop import java.io.InputStream import java.io.OutputStream import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPSecretKeyRing import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.api.OpenPGPKey import org.bouncycastle.util.io.Streams import org.pgpainless.PGPainless import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.DocumentSignatureType import org.pgpainless.algorithm.HashAlgorithm -import org.pgpainless.bouncycastle.extensions.openPgpFingerprint import org.pgpainless.encryption_signing.ProducerOptions import org.pgpainless.encryption_signing.SigningOptions import org.pgpainless.exception.KeyException.MissingSecretKeyException @@ -30,11 +29,11 @@ import sop.operation.DetachedSign import sop.util.UTF8Util /** Implementation of the `sign` operation using PGPainless. */ -class DetachedSignImpl : DetachedSign { +class DetachedSignImpl(private val api: PGPainless) : DetachedSign { - private val signingOptions = SigningOptions.get() + private val signingOptions = SigningOptions.get(api) private val protector = MatchMakingSecretKeyRingProtector() - private val signingKeys = mutableListOf() + private val signingKeys = mutableListOf() private var armor = true private var mode = SignAs.binary @@ -44,19 +43,19 @@ class DetachedSignImpl : DetachedSign { try { signingOptions.addDetachedSignature(protector, it, modeToSigType(mode)) } catch (e: UnacceptableSigningKeyException) { - throw SOPGPException.KeyCannotSign("Key ${it.openPgpFingerprint} cannot sign.", e) + throw SOPGPException.KeyCannotSign("Key ${it.keyIdentifier} cannot sign.", e) } catch (e: MissingSecretKeyException) { throw SOPGPException.KeyCannotSign( - "Key ${it.openPgpFingerprint} cannot sign. Missing secret key.", e) + "Key ${it.keyIdentifier} cannot sign. Missing secret key.", e) } catch (e: PGPException) { throw SOPGPException.KeyIsProtected( - "Key ${it.openPgpFingerprint} cannot be unlocked.", e) + "Key ${it.keyIdentifier} cannot be unlocked.", e) } } try { val signingStream = - PGPainless.encryptAndOrSign() + api.generateMessage() .discardOutput() .withOptions( ProducerOptions.sign(signingOptions) @@ -74,16 +73,16 @@ class DetachedSignImpl : DetachedSign { // forget passphrases protector.clear() - val signatures = result.detachedSignatures.map { it.value }.flatten() + val signatures = result.detachedDocumentSignatures val out = if (armor) ArmoredOutputStreamFactory.get(outputStream) else outputStream - signatures.forEach { it.encode(out) } + signatures.forEach { it.signature.encode(out) } out.close() outputStream.close() return SigningResult.builder() - .setMicAlg(micAlgFromSignatures(signatures)) + .setMicAlg(micAlgFromSignatures(signatures.map { it.signature })) .build() } } @@ -93,8 +92,8 @@ class DetachedSignImpl : DetachedSign { } override fun key(key: InputStream): DetachedSign = apply { - KeyReader.readSecretKeys(key, true).forEach { - val info = PGPainless.inspectKeyRing(it) + KeyReader(api).readSecretKeys(key, true).forEach { + val info = api.inspect(it) if (!info.isUsableForSigning) { throw SOPGPException.KeyCannotSign( "Key ${info.fingerprint} does not have valid, signing capable subkeys.") diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt index 08472144..d9837def 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt @@ -18,18 +18,17 @@ import sop.operation.DetachedVerify import sop.operation.VerifySignatures /** Implementation of the `verify` operation using PGPainless. */ -class DetachedVerifyImpl : DetachedVerify { +class DetachedVerifyImpl(private val api: PGPainless) : DetachedVerify { - private val options = ConsumerOptions.get().forceNonOpenPgpData() + private val options = ConsumerOptions.get(api).forceNonOpenPgpData() override fun cert(cert: InputStream): DetachedVerify = apply { - options.addVerificationCerts(KeyReader.readPublicKeys(cert, true)) + options.addVerificationCerts(KeyReader(api).readPublicKeys(cert, true)) } override fun data(data: InputStream): List { try { - val verificationStream = - PGPainless.decryptAndOrVerify().onInputStream(data).withOptions(options) + val verificationStream = api.processMessage().onInputStream(data).withOptions(options) Streams.drain(verificationStream) verificationStream.close() diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt index b227561e..c8a71c24 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt @@ -8,12 +8,14 @@ import java.io.IOException import java.io.InputStream import java.io.OutputStream import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism +import org.bouncycastle.openpgp.api.OpenPGPKey import org.bouncycastle.util.io.Streams import org.pgpainless.PGPainless +import org.pgpainless.algorithm.AEADAlgorithm import org.pgpainless.algorithm.DocumentSignatureType import org.pgpainless.algorithm.StreamEncoding -import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.encryption_signing.EncryptionOptions import org.pgpainless.encryption_signing.ProducerOptions import org.pgpainless.encryption_signing.SigningOptions @@ -24,23 +26,29 @@ import org.pgpainless.util.Passphrase import sop.EncryptionResult import sop.Profile import sop.ReadyWithResult +import sop.SessionKey import sop.enums.EncryptAs import sop.exception.SOPGPException import sop.operation.Encrypt import sop.util.UTF8Util /** Implementation of the `encrypt` operation using PGPainless. */ -class EncryptImpl : Encrypt { +class EncryptImpl(private val api: PGPainless) : Encrypt { companion object { @JvmField val RFC4880_PROFILE = Profile("rfc4880", "Follow the packet format of rfc4880") + @JvmField val RFC9580_PROFILE = Profile("rfc9580", "Follow the packet format of rfc9580") - @JvmField val SUPPORTED_PROFILES = listOf(RFC4880_PROFILE) + @JvmField + val SUPPORTED_PROFILES = + listOf( + RFC4880_PROFILE.withAliases("default", "compatibility"), + RFC9580_PROFILE.withAliases("security", "performance")) } - private val encryptionOptions = EncryptionOptions.get() + private val encryptionOptions = EncryptionOptions.get(api) private var signingOptions: SigningOptions? = null - private val signingKeys = mutableListOf() + private val signingKeys = mutableListOf() private val protector = MatchMakingSecretKeyRingProtector() private var profile = RFC4880_PROFILE.name @@ -56,6 +64,13 @@ class EncryptImpl : Encrypt { throw SOPGPException.MissingArg("Missing encryption method.") } + if (encryptionOptions.usesOnlyPasswordBasedEncryption() && + profile == RFC9580_PROFILE.name) { + encryptionOptions.overrideEncryptionMechanism( + MessageEncryptionMechanism.aead( + SymmetricKeyAlgorithm.AES_128.algorithmId, AEADAlgorithm.OCB.algorithmId)) + } + val options = if (signingOptions != null) { ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions!!) @@ -69,9 +84,9 @@ class EncryptImpl : Encrypt { try { signingOptions!!.addInlineSignature(protector, it, modeToSignatureType(mode)) } catch (e: UnacceptableSigningKeyException) { - throw SOPGPException.KeyCannotSign("Key ${it.openPgpFingerprint} cannot sign", e) + throw SOPGPException.KeyCannotSign("Key ${it.keyIdentifier} cannot sign", e) } catch (e: WrongPassphraseException) { - throw SOPGPException.KeyIsProtected("Cannot unlock key ${it.openPgpFingerprint}", e) + throw SOPGPException.KeyIsProtected("Cannot unlock key ${it.keyIdentifier}", e) } catch (e: PGPException) { throw SOPGPException.BadData(e) } @@ -81,13 +96,13 @@ class EncryptImpl : Encrypt { return object : ReadyWithResult() { override fun writeTo(outputStream: OutputStream): EncryptionResult { val encryptionStream = - PGPainless.encryptAndOrSign() - .onOutputStream(outputStream) - .withOptions(options) + api.generateMessage().onOutputStream(outputStream).withOptions(options) Streams.pipeAll(plaintext, encryptionStream) encryptionStream.close() - // TODO: Extract and emit session key once BC supports that - return EncryptionResult(null) + return EncryptionResult( + encryptionStream.result.sessionKey?.let { + SessionKey(it.algorithm.algorithmId.toByte(), it.key) + }) } } } catch (e: PGPException) { @@ -97,24 +112,25 @@ class EncryptImpl : Encrypt { override fun profile(profileName: String): Encrypt = apply { profile = - SUPPORTED_PROFILES.find { it.name == profileName }?.name + SUPPORTED_PROFILES.find { it.name == profileName || it.aliases.contains(profileName) } + ?.name ?: throw SOPGPException.UnsupportedProfile("encrypt", profileName) } override fun signWith(key: InputStream): Encrypt = apply { if (signingOptions == null) { - signingOptions = SigningOptions.get() + signingOptions = SigningOptions.get(api) } val signingKey = - KeyReader.readSecretKeys(key, true).singleOrNull() + KeyReader(api).readSecretKeys(key, true).singleOrNull() ?: throw SOPGPException.BadData( AssertionError( "Exactly one secret key at a time expected. Got zero or multiple instead.")) - val info = PGPainless.inspectKeyRing(signingKey) + val info = api.inspect(signingKey) if (info.signingSubkeys.isEmpty()) { - throw SOPGPException.KeyCannotSign("Key ${info.fingerprint} cannot sign.") + throw SOPGPException.KeyCannotSign("Key ${info.keyIdentifier} cannot sign.") } protector.addSecretKey(signingKey) @@ -123,7 +139,7 @@ class EncryptImpl : Encrypt { override fun withCert(cert: InputStream): Encrypt = apply { try { - encryptionOptions.addRecipients(KeyReader.readPublicKeys(cert, true)) + KeyReader(api).readPublicKeys(cert, true).forEach { encryptionOptions.addRecipient(it) } } catch (e: UnacceptableEncryptionKeyException) { throw SOPGPException.CertCannotEncrypt(e.message ?: "Cert cannot encrypt", e) } catch (e: IOException) { diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt index 7fe66ee5..fc72d994 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt @@ -7,37 +7,24 @@ package org.pgpainless.sop import java.io.InputStream import java.io.OutputStream import org.pgpainless.PGPainless -import org.pgpainless.util.ArmorUtils -import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.OpenPGPCertificateUtil import sop.Ready import sop.operation.ExtractCert /** Implementation of the `extract-cert` operation using PGPainless. */ -class ExtractCertImpl : ExtractCert { +class ExtractCertImpl(private val api: PGPainless) : ExtractCert { private var armor = true override fun key(keyInputStream: InputStream): Ready { - val certs = - KeyReader.readSecretKeys(keyInputStream, true).map { PGPainless.extractCertificate(it) } + val certs = KeyReader(api).readSecretKeys(keyInputStream, true).map { it.toCertificate() } return object : Ready() { override fun writeTo(outputStream: OutputStream) { if (armor) { - if (certs.size == 1) { - val cert = certs[0] - // This way we have a nice armor header with fingerprint and user-ids - val armorOut = ArmorUtils.toAsciiArmoredStream(cert, outputStream) - cert.encode(armorOut) - armorOut.close() - } else { - // for multiple certs, add no info headers to the ASCII armor - val armorOut = ArmoredOutputStreamFactory.get(outputStream) - certs.forEach { it.encode(armorOut) } - armorOut.close() - } + OpenPGPCertificateUtil.armor(certs, outputStream) } else { - certs.forEach { it.encode(outputStream) } + OpenPGPCertificateUtil.encode(certs, outputStream) } } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt index f8297c56..e220f1bf 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt @@ -8,17 +8,21 @@ import java.io.OutputStream import java.lang.RuntimeException import java.security.InvalidAlgorithmParameterException import java.security.NoSuchAlgorithmException +import org.bouncycastle.bcpg.PacketFormat import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.api.OpenPGPKey import org.pgpainless.PGPainless import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.OpenPGPKeyVersion +import org.pgpainless.bouncycastle.extensions.asciiArmor +import org.pgpainless.bouncycastle.extensions.encode 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.generation.type.eddsa_legacy.EdDSALegacyCurve import org.pgpainless.key.generation.type.rsa.RsaLength import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec -import org.pgpainless.util.ArmorUtils import org.pgpainless.util.Passphrase import sop.Profile import sop.Ready @@ -26,16 +30,33 @@ import sop.exception.SOPGPException import sop.operation.GenerateKey /** Implementation of the `generate-key` operation using PGPainless. */ -class GenerateKeyImpl : GenerateKey { +class GenerateKeyImpl(private val api: PGPainless) : GenerateKey { companion object { @JvmField val CURVE25519_PROFILE = - Profile( - "draft-koch-eddsa-for-openpgp-00", "Generate EdDSA / ECDH keys using Curve25519") - @JvmField val RSA4096_PROFILE = Profile("rfc4880", "Generate 4096-bit RSA keys") + Profile("draft-koch-eddsa-for-openpgp-00", "OpenPGP v4 keys over Curve25519") + @JvmField + val RFC4880_RSA4096_PROFILE = Profile("rfc4880-rsa4096", "OpenPGP v4 keys with RSA 4096") + @JvmField val RFC6637_NIST_P256_PROFILE = Profile("rfc6637-nist-p256") + @JvmField val RFC6637_NIST_P384_PROFILE = Profile("rfc6637-nist-p384") + @JvmField val RFC6637_NIST_P521_PROFILE = Profile("rfc6637-nist-p521") + @JvmField + val RFC9580_CURVE25519_PROFILE = + Profile("rfc9580-curve25519", "OpenPGP v6 keys over Curve25519") + @JvmField + val RFC9580_CURVE448_PROFILE = Profile("rfc9580-curve448", "OpenPGP v6 keys over Curve448") - @JvmField val SUPPORTED_PROFILES = listOf(CURVE25519_PROFILE, RSA4096_PROFILE) + @JvmField + val SUPPORTED_PROFILES = + listOf( + CURVE25519_PROFILE.withAliases("default", "compatibility"), + RFC4880_RSA4096_PROFILE, + RFC6637_NIST_P256_PROFILE, + RFC6637_NIST_P384_PROFILE, + RFC6637_NIST_P521_PROFILE, + RFC9580_CURVE25519_PROFILE.withAliases("performance", "security"), + RFC9580_CURVE448_PROFILE) } private val userIds = mutableSetOf() @@ -50,11 +71,9 @@ class GenerateKeyImpl : GenerateKey { return object : Ready() { override fun writeTo(outputStream: OutputStream) { if (armor) { - val armorOut = ArmorUtils.toAsciiArmoredStream(key, outputStream) - key.encode(armorOut) - armorOut.close() + key.asciiArmor(outputStream, PacketFormat.CURRENT) } else { - key.encode(outputStream) + key.encode(outputStream, PacketFormat.CURRENT) } } } @@ -71,7 +90,7 @@ class GenerateKeyImpl : GenerateKey { override fun profile(profile: String): GenerateKey = apply { this.profile = - SUPPORTED_PROFILES.find { it.name == profile }?.name + SUPPORTED_PROFILES.find { it.name == profile || it.aliases.contains(profile) }?.name ?: throw SOPGPException.UnsupportedProfile("generate-key", profile) } @@ -88,11 +107,11 @@ class GenerateKeyImpl : GenerateKey { userIds: Set, passphrase: Passphrase, signingOnly: Boolean - ): PGPSecretKeyRing { + ): OpenPGPKey { val keyBuilder: KeyRingBuilder = when (profile) { CURVE25519_PROFILE.name -> - PGPainless.buildKeyRing() + api.buildKey(OpenPGPKeyVersion.v4) .setPrimaryKey( KeySpec.getBuilder( KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), @@ -109,8 +128,8 @@ class GenerateKeyImpl : GenerateKey { KeyFlag.ENCRYPT_STORAGE)) } } - RSA4096_PROFILE.name -> { - PGPainless.buildKeyRing() + RFC4880_RSA4096_PROFILE.name -> { + api.buildKey(OpenPGPKeyVersion.v4) .setPrimaryKey( KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.CERTIFY_OTHER)) .addSubkey( @@ -125,6 +144,88 @@ class GenerateKeyImpl : GenerateKey { } } } + RFC6637_NIST_P256_PROFILE.name -> { + api.buildKey(OpenPGPKeyVersion.v4) + .setPrimaryKey( + KeySpec.getBuilder( + KeyType.ECDSA(EllipticCurve._P256), KeyFlag.CERTIFY_OTHER)) + .addSubkey( + KeySpec.getBuilder( + KeyType.ECDSA(EllipticCurve._P256), KeyFlag.SIGN_DATA)) + .apply { + if (!signingOnly) { + addSubkey( + KeySpec.getBuilder( + KeyType.ECDH(EllipticCurve._P256), + KeyFlag.ENCRYPT_COMMS, + KeyFlag.ENCRYPT_STORAGE)) + } + } + } + RFC6637_NIST_P384_PROFILE.name -> { + api.buildKey(OpenPGPKeyVersion.v4) + .setPrimaryKey( + KeySpec.getBuilder( + KeyType.ECDSA(EllipticCurve._P384), KeyFlag.CERTIFY_OTHER)) + .addSubkey( + KeySpec.getBuilder( + KeyType.ECDSA(EllipticCurve._P384), KeyFlag.SIGN_DATA)) + .apply { + if (!signingOnly) { + addSubkey( + KeySpec.getBuilder( + KeyType.ECDH(EllipticCurve._P384), + KeyFlag.ENCRYPT_COMMS, + KeyFlag.ENCRYPT_STORAGE)) + } + } + } + RFC6637_NIST_P521_PROFILE.name -> { + api.buildKey(OpenPGPKeyVersion.v4) + .setPrimaryKey( + KeySpec.getBuilder( + KeyType.ECDSA(EllipticCurve._P521), KeyFlag.CERTIFY_OTHER)) + .addSubkey( + KeySpec.getBuilder( + KeyType.ECDSA(EllipticCurve._P521), KeyFlag.SIGN_DATA)) + .apply { + if (!signingOnly) { + addSubkey( + KeySpec.getBuilder( + KeyType.ECDH(EllipticCurve._P521), + KeyFlag.ENCRYPT_COMMS, + KeyFlag.ENCRYPT_STORAGE)) + } + } + } + RFC9580_CURVE25519_PROFILE.name -> { + api.buildKey(OpenPGPKeyVersion.v6) + .setPrimaryKey(KeySpec.getBuilder(KeyType.Ed25519(), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.Ed25519(), KeyFlag.SIGN_DATA)) + .apply { + if (!signingOnly) { + addSubkey( + KeySpec.getBuilder( + KeyType.X25519(), + KeyFlag.ENCRYPT_COMMS, + KeyFlag.ENCRYPT_STORAGE)) + } + } + } + RFC9580_CURVE448_PROFILE.name -> { + api.buildKey(OpenPGPKeyVersion.v6) + .setPrimaryKey(KeySpec.getBuilder(KeyType.Ed448(), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.Ed448(), KeyFlag.SIGN_DATA)) + .apply { + if (!signingOnly) { + addSubkey( + KeySpec.getBuilder( + KeyType.X448(), + KeyFlag.ENCRYPT_COMMS, + KeyFlag.ENCRYPT_STORAGE)) + } + } + } else -> throw SOPGPException.UnsupportedProfile("generate-key", profile) } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt index 88ca8c54..6c571163 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt @@ -14,10 +14,10 @@ import org.bouncycastle.openpgp.PGPLiteralData import org.bouncycastle.openpgp.PGPOnePassSignatureList import org.bouncycastle.openpgp.PGPSignatureList import org.bouncycastle.util.io.Streams -import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.PGPainless +import org.pgpainless.decryption_verification.OpenPGPAnimalSnifferInputStream 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 @@ -25,7 +25,7 @@ import sop.exception.SOPGPException import sop.operation.InlineDetach /** Implementation of the `inline-detach` operation using PGPainless. */ -class InlineDetachImpl : InlineDetach { +class InlineDetachImpl(private val api: PGPainless) : InlineDetach { private var armor = true @@ -35,7 +35,7 @@ class InlineDetachImpl : InlineDetach { private val sigOut = ByteArrayOutputStream() override fun writeTo(outputStream: OutputStream): Signatures { - var pgpIn = OpenPgpInputStream(messageInputStream) + var pgpIn = OpenPGPAnimalSnifferInputStream(messageInputStream) if (pgpIn.isNonOpenPgp) { throw SOPGPException.BadData("Data appears to be non-OpenPGP.") } @@ -61,7 +61,7 @@ class InlineDetachImpl : InlineDetach { } // else just dearmor - pgpIn = OpenPgpInputStream(armorIn) + pgpIn = OpenPGPAnimalSnifferInputStream(armorIn) } // If data was not using cleartext signature framework @@ -72,8 +72,7 @@ class InlineDetachImpl : InlineDetach { } // handle binary OpenPGP data - var objectFactory = - ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn) + var objectFactory = api.implementation.pgpObjectFactory(pgpIn) var next: Any? while (objectFactory.nextObject().also { next = it } != null) { @@ -95,8 +94,8 @@ class InlineDetachImpl : InlineDetach { // Decompress compressed data try { objectFactory = - ImplementationFactory.getInstance() - .getPGPObjectFactory((next as PGPCompressedData).dataStream) + api.implementation.pgpObjectFactory( + (next as PGPCompressedData).dataStream) } catch (e: PGPException) { throw SOPGPException.BadData( "Cannot decompress PGPCompressedData", e) diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt index bd77b553..fd7abfac 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt @@ -8,13 +8,12 @@ import java.io.InputStream import java.io.OutputStream import java.lang.RuntimeException import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.api.OpenPGPKey import org.bouncycastle.util.io.Streams import org.pgpainless.PGPainless import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.DocumentSignatureType import org.pgpainless.algorithm.StreamEncoding -import org.pgpainless.bouncycastle.extensions.openPgpFingerprint import org.pgpainless.encryption_signing.ProducerOptions import org.pgpainless.encryption_signing.SigningOptions import org.pgpainless.exception.KeyException.MissingSecretKeyException @@ -27,11 +26,11 @@ import sop.operation.InlineSign import sop.util.UTF8Util /** Implementation of the `inline-sign` operation using PGPainless. */ -class InlineSignImpl : InlineSign { +class InlineSignImpl(private val api: PGPainless) : InlineSign { - private val signingOptions = SigningOptions.get() + private val signingOptions = SigningOptions.get(api) private val protector = MatchMakingSecretKeyRingProtector() - private val signingKeys = mutableListOf() + private val signingKeys = mutableListOf() private var armor = true private var mode = InlineSignAs.binary @@ -45,14 +44,14 @@ class InlineSignImpl : InlineSign { signingOptions.addInlineSignature(protector, key, modeToSigType(mode)) } } catch (e: UnacceptableSigningKeyException) { - throw SOPGPException.KeyCannotSign("Key ${key.openPgpFingerprint} cannot sign.", e) + throw SOPGPException.KeyCannotSign("Key ${key.keyIdentifier} cannot sign.", e) } catch (e: MissingSecretKeyException) { throw SOPGPException.KeyCannotSign( - "Key ${key.openPgpFingerprint} does not have the secret signing key component available.", + "Key ${key.keyIdentifier} does not have the secret signing key component available.", e) } catch (e: PGPException) { throw SOPGPException.KeyIsProtected( - "Key ${key.openPgpFingerprint} cannot be unlocked.", e) + "Key ${key.keyIdentifier} cannot be unlocked.", e) } } @@ -80,7 +79,7 @@ class InlineSignImpl : InlineSign { override fun writeTo(outputStream: OutputStream) { try { val signingStream = - PGPainless.encryptAndOrSign() + api.generateMessage() .onOutputStream(outputStream) .withOptions(producerOptions) @@ -97,11 +96,11 @@ class InlineSignImpl : InlineSign { } override fun key(key: InputStream): InlineSign = apply { - KeyReader.readSecretKeys(key, true).forEach { - val info = PGPainless.inspectKeyRing(it) + KeyReader(api).readSecretKeys(key, true).forEach { + val info = api.inspect(it) if (!info.isUsableForSigning) { throw SOPGPException.KeyCannotSign( - "Key ${info.fingerprint} does not have valid, signing capable subkeys.") + "Key ${info.keyIdentifier} does not have valid, signing capable subkeys.") } protector.addSecretKey(it) signingKeys.add(it) diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt index 0b4e7d2f..416b53b3 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt @@ -19,12 +19,12 @@ import sop.exception.SOPGPException import sop.operation.InlineVerify /** Implementation of the `inline-verify` operation using PGPainless. */ -class InlineVerifyImpl : InlineVerify { +class InlineVerifyImpl(private val api: PGPainless) : InlineVerify { - private val options = ConsumerOptions.get() + private val options = ConsumerOptions.get(api) override fun cert(cert: InputStream): InlineVerify = apply { - options.addVerificationCerts(KeyReader.readPublicKeys(cert, true)) + options.addVerificationCerts(KeyReader(api).readPublicKeys(cert, true)) } override fun data(data: InputStream): ReadyWithResult> { @@ -32,7 +32,7 @@ class InlineVerifyImpl : InlineVerify { override fun writeTo(outputStream: OutputStream): List { try { val verificationStream = - PGPainless.decryptAndOrVerify().onInputStream(data).withOptions(options) + api.processMessage().onInputStream(data).withOptions(options) Streams.pipeAll(verificationStream, outputStream) verificationStream.close() diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt index 2ce608ca..2ea36137 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt @@ -7,71 +7,63 @@ package org.pgpainless.sop import java.io.IOException import java.io.InputStream import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection import org.bouncycastle.openpgp.PGPRuntimeOperationException -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.bouncycastle.openpgp.api.OpenPGPKey import org.pgpainless.PGPainless import sop.exception.SOPGPException /** Reader for OpenPGP keys and certificates with error matching according to the SOP spec. */ -class KeyReader { +class KeyReader(val api: PGPainless = PGPainless.getInstance()) { - companion object { - @JvmStatic - fun readSecretKeys( - keyInputStream: InputStream, - requireContent: Boolean - ): PGPSecretKeyRingCollection { - val keys = - try { - PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream) - } catch (e: IOException) { - if (e.message == null) { - throw e - } - if (e.message!!.startsWith("unknown object in stream:") || - e.message!!.startsWith("invalid header encountered")) { - throw SOPGPException.BadData(e) - } + fun readSecretKeys(keyInputStream: InputStream, requireContent: Boolean): List { + val keys = + try { + api.readKey().parseKeys(keyInputStream) + } catch (e: IOException) { + if (e.message == null) { throw e } - if (requireContent && keys.none()) { - throw SOPGPException.BadData(PGPException("No key data found.")) - } - - return keys - } - - @JvmStatic - fun readPublicKeys( - certIn: InputStream, - requireContent: Boolean - ): PGPPublicKeyRingCollection { - val certs = - try { - PGPainless.readKeyRing().keyRingCollection(certIn, true) - } catch (e: IOException) { - if (e.message == null) { - throw e - } - if (e.message!!.startsWith("unknown object in stream:") || - e.message!!.startsWith("invalid header encountered")) { - throw SOPGPException.BadData(e) - } - throw e - } catch (e: PGPRuntimeOperationException) { + if (e.message!!.startsWith("unknown object in stream:") || + e.message!!.startsWith("invalid header encountered") || + e.message!!.startsWith("Encountered unexpected packet:")) { throw SOPGPException.BadData(e) } - - if (certs.pgpSecretKeyRingCollection.any()) { - throw SOPGPException.BadData( - "Secret key components encountered, while certificates were expected.") + throw e } - - if (requireContent && certs.pgpPublicKeyRingCollection.none()) { - throw SOPGPException.BadData(PGPException("No cert data found.")) - } - return certs.pgpPublicKeyRingCollection + if (requireContent && keys.none()) { + throw SOPGPException.BadData(PGPException("No key data found.")) } + + return keys + } + + fun readPublicKeys(certIn: InputStream, requireContent: Boolean): List { + val certs = + try { + api.readKey().parseKeysOrCertificates(certIn) + } catch (e: IOException) { + if (e.message == null) { + throw e + } + if (e.message!!.startsWith("unknown object in stream:") || + e.message!!.startsWith("invalid header encountered")) { + throw SOPGPException.BadData(e) + } + throw e + } catch (e: PGPRuntimeOperationException) { + throw SOPGPException.BadData(e) + } + + if (certs.any { it.isSecretKey }) { + throw SOPGPException.BadData( + "Secret key components encountered, while certificates were expected.") + } + + if (requireContent && certs.isEmpty()) { + throw SOPGPException.BadData("No certificate data found.") + } + + return certs } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt index 39a5151d..a196fd01 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt @@ -4,12 +4,13 @@ package org.pgpainless.sop +import org.pgpainless.PGPainless import sop.Profile import sop.exception.SOPGPException import sop.operation.ListProfiles /** Implementation of the `list-profiles` operation using PGPainless. */ -class ListProfilesImpl : ListProfiles { +class ListProfilesImpl(private val api: PGPainless) : ListProfiles { override fun subcommand(command: String): List = when (command) { diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt index 13347721..21ed794b 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt @@ -4,9 +4,12 @@ package org.pgpainless.sop +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.api.OpenPGPKey import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor import org.pgpainless.bouncycastle.extensions.isDecrypted @@ -36,17 +39,19 @@ class MatchMakingSecretKeyRingProtector : SecretKeyRingProtector { keys.forEach { key -> for (subkey in key) { - if (protector.hasPassphrase(subkey.keyID)) { + if (protector.hasPassphrase(subkey.keyIdentifier)) { continue } if (testPassphrase(passphrase, subkey)) { - protector.addPassphrase(subkey.keyID, passphrase) + protector.addPassphrase(subkey.keyIdentifier, passphrase) } } } } + fun addSecretKey(key: OpenPGPKey) = addSecretKey(key.pgpSecretKeyRing) + fun addSecretKey(key: PGPSecretKeyRing) = apply { if (!keys.add(key)) { return@apply @@ -54,11 +59,11 @@ class MatchMakingSecretKeyRingProtector : SecretKeyRingProtector { key.forEach { subkey -> if (subkey.isDecrypted()) { - protector.addPassphrase(subkey.keyID, Passphrase.emptyPassphrase()) + protector.addPassphrase(subkey.keyIdentifier, Passphrase.emptyPassphrase()) } else { passphrases.forEach { passphrase -> if (testPassphrase(passphrase, subkey)) { - protector.addPassphrase(subkey.keyID, passphrase) + protector.addPassphrase(subkey.keyIdentifier, passphrase) } } } @@ -74,11 +79,17 @@ class MatchMakingSecretKeyRingProtector : SecretKeyRingProtector { false } - override fun hasPassphraseFor(keyId: Long): Boolean = protector.hasPassphrase(keyId) + override fun hasPassphraseFor(keyIdentifier: KeyIdentifier): Boolean = + protector.hasPassphrase(keyIdentifier) - override fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? = protector.getDecryptor(keyId) + override fun getDecryptor(keyIdentifier: KeyIdentifier): PBESecretKeyDecryptor? = + protector.getDecryptor(keyIdentifier) - override fun getEncryptor(keyId: Long): PBESecretKeyEncryptor? = protector.getEncryptor(keyId) + override fun getEncryptor(key: PGPPublicKey): PBESecretKeyEncryptor? = + protector.getEncryptor(key) + + override fun getKeyPassword(key: OpenPGPKey.OpenPGPSecretKey): CharArray? = + protector.getKeyPassword(key) /** Clear all known passphrases from the protector. */ fun clear() { diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MergeCertsImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MergeCertsImpl.kt new file mode 100644 index 00000000..dc429c9e --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MergeCertsImpl.kt @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.bcpg.KeyIdentifier +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.pgpainless.PGPainless +import org.pgpainless.util.OpenPGPCertificateUtil +import sop.Ready +import sop.operation.MergeCerts + +class MergeCertsImpl(private val api: PGPainless) : MergeCerts { + + private var armor = true + private val baseCerts: MutableMap = mutableMapOf() + private val updateCerts: MutableList = mutableListOf() + + // from standard input + override fun baseCertificates(certs: InputStream): Ready { + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + val baseCertsList = api.readKey().parseCertificates(certs) + + // Index and merge base certs + for (cert in baseCertsList) { + if (!baseCerts.contains(cert.keyIdentifier)) { + baseCerts[cert.keyIdentifier] = cert + } else { + val baseCert = baseCerts[cert.keyIdentifier]!! + baseCerts[cert.keyIdentifier] = api.mergeCertificate(baseCert, cert) + } + } + + // Merge updates with base certs + for (update in updateCerts) { + if (baseCerts[update.keyIdentifier] == null) { + // skip updates with missing base certs + continue + } + + val baseCert = baseCerts[update.keyIdentifier]!! + baseCerts[update.keyIdentifier] = api.mergeCertificate(baseCert, update) + } + + if (armor) { + OpenPGPCertificateUtil.armor(baseCerts.values, outputStream) + } else { + OpenPGPCertificateUtil.encode(baseCerts.values, outputStream) + } + } + } + } + + override fun noArmor(): MergeCerts = apply { armor = false } + + // from command line + override fun updates(updateCerts: InputStream): MergeCerts = apply { + this.updateCerts.addAll(api.readKey().parseCertificates(updateCerts)) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt index ecc87e62..0c7e3585 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt @@ -7,73 +7,69 @@ package org.pgpainless.sop import java.io.IOException import java.io.InputStream import java.io.OutputStream -import java.lang.RuntimeException import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPPublicKeyRing -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.bouncycastle.openpgp.api.OpenPGPCertificate import org.pgpainless.PGPainless -import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.bouncycastle.extensions.toOpenPGPCertificate import org.pgpainless.exception.WrongPassphraseException import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.key.util.RevocationAttributes -import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.OpenPGPCertificateUtil import org.pgpainless.util.Passphrase import sop.Ready import sop.exception.SOPGPException import sop.operation.RevokeKey import sop.util.UTF8Util -class RevokeKeyImpl : RevokeKey { +class RevokeKeyImpl(private val api: PGPainless) : RevokeKey { private val protector = MatchMakingSecretKeyRingProtector() private var armor = true override fun keys(keys: InputStream): Ready { - val secretKeyRings = + val secretKeys = try { - KeyReader.readSecretKeys(keys, true) + KeyReader(api).readSecretKeys(keys, true) } catch (e: IOException) { throw SOPGPException.BadData("Cannot decode secret keys.", e) } - secretKeyRings.forEach { protector.addSecretKey(it) } + secretKeys.forEach { protector.addSecretKey(it) } - val revocationCertificates = mutableListOf() - secretKeyRings.forEach { secretKeys -> - val editor = PGPainless.modifyKeyRing(secretKeys) + val revocationCertificates = mutableListOf() + secretKeys.forEach { + val editor = api.modify(it) try { val attributes = RevocationAttributes.createKeyRevocation() .withReason(RevocationAttributes.Reason.NO_REASON) .withoutDescription() - if (secretKeys.publicKey.version == 6) { + if (it.primaryKey.version == 6) { revocationCertificates.add( editor.createMinimalRevocationCertificate(protector, attributes)) } else { - val certificate = PGPainless.extractCertificate(secretKeys) + val certificate = it.toCertificate() val revocation = editor.createRevocation(protector, attributes) revocationCertificates.add( - KeyRingUtils.injectCertification(certificate, revocation)) + KeyRingUtils.injectCertification( + certificate.pgpKeyRing, revocation.signature) + .toOpenPGPCertificate(api.implementation)) } } catch (e: WrongPassphraseException) { throw SOPGPException.KeyIsProtected( - "Missing or wrong passphrase for key ${secretKeys.openPgpFingerprint}", e) + "Missing or wrong passphrase for key ${it.keyIdentifier}", e) } catch (e: PGPException) { throw RuntimeException( - "Cannot generate revocation certificate for key ${secretKeys.openPgpFingerprint}", - e) + "Cannot generate revocation certificate for key ${it.keyIdentifier}", e) } } return object : Ready() { override fun writeTo(outputStream: OutputStream) { - val collection = PGPPublicKeyRingCollection(revocationCertificates) if (armor) { - val armorOut = ArmoredOutputStreamFactory.get(outputStream) - collection.encode(armorOut) - armorOut.close() + OpenPGPCertificateUtil.armor(revocationCertificates, outputStream) } else { - collection.encode(outputStream) + OpenPGPCertificateUtil.encode(revocationCertificates, outputStream) } } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt index 16f54a22..9a1b1ad5 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt @@ -4,9 +4,11 @@ package org.pgpainless.sop +import org.pgpainless.PGPainless import sop.SOP import sop.SOPV import sop.operation.Armor +import sop.operation.CertifyUserId import sop.operation.ChangeKeyPassword import sop.operation.Dearmor import sop.operation.Decrypt @@ -19,38 +21,54 @@ import sop.operation.InlineDetach import sop.operation.InlineSign import sop.operation.InlineVerify import sop.operation.ListProfiles +import sop.operation.MergeCerts import sop.operation.RevokeKey +import sop.operation.UpdateKey +import sop.operation.ValidateUserId import sop.operation.Version -class SOPImpl(private val sopv: SOPV = SOPVImpl()) : SOP { +class SOPImpl( + private val api: PGPainless = PGPainless.getInstance(), + private val sopv: SOPV = SOPVImpl(api) +) : SOP { - override fun armor(): Armor = ArmorImpl() + constructor(api: PGPainless) : this(api, SOPVImpl(api)) - override fun changeKeyPassword(): ChangeKeyPassword = ChangeKeyPasswordImpl() + override fun armor(): Armor = ArmorImpl(api) - override fun dearmor(): Dearmor = DearmorImpl() + override fun certifyUserId(): CertifyUserId = CertifyUserIdImpl(api) - override fun decrypt(): Decrypt = DecryptImpl() + override fun changeKeyPassword(): ChangeKeyPassword = ChangeKeyPasswordImpl(api) - override fun detachedSign(): DetachedSign = DetachedSignImpl() + override fun dearmor(): Dearmor = DearmorImpl(api) - override fun detachedVerify(): DetachedVerify = sopv.detachedVerify() + override fun decrypt(): Decrypt = DecryptImpl(api) - override fun encrypt(): Encrypt = EncryptImpl() + override fun detachedSign(): DetachedSign = DetachedSignImpl(api) - override fun extractCert(): ExtractCert = ExtractCertImpl() + override fun detachedVerify(): DetachedVerify = sopv.detachedVerify()!! - override fun generateKey(): GenerateKey = GenerateKeyImpl() + override fun encrypt(): Encrypt = EncryptImpl(api) - override fun inlineDetach(): InlineDetach = InlineDetachImpl() + override fun extractCert(): ExtractCert = ExtractCertImpl(api) - override fun inlineSign(): InlineSign = InlineSignImpl() + override fun generateKey(): GenerateKey = GenerateKeyImpl(api) - override fun inlineVerify(): InlineVerify = sopv.inlineVerify() + override fun inlineDetach(): InlineDetach = InlineDetachImpl(api) - override fun listProfiles(): ListProfiles = ListProfilesImpl() + override fun inlineSign(): InlineSign = InlineSignImpl(api) - override fun revokeKey(): RevokeKey = RevokeKeyImpl() + override fun inlineVerify(): InlineVerify = sopv.inlineVerify()!! - override fun version(): Version = sopv.version() + override fun listProfiles(): ListProfiles = ListProfilesImpl(api) + + override fun mergeCerts(): MergeCerts = MergeCertsImpl(api) + + override fun revokeKey(): RevokeKey = RevokeKeyImpl(api) + + override fun updateKey(): UpdateKey = UpdateKeyImpl(api) + + override fun validateUserId(): ValidateUserId = sopv.validateUserId()!! + + override fun version(): Version = sopv.version()!! } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt index 43b4c64f..e5a18c97 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt @@ -4,21 +4,25 @@ package org.pgpainless.sop +import org.pgpainless.PGPainless import org.pgpainless.util.ArmoredOutputStreamFactory import sop.SOPV import sop.operation.DetachedVerify import sop.operation.InlineVerify +import sop.operation.ValidateUserId import sop.operation.Version -class SOPVImpl : SOPV { +class SOPVImpl(private val api: PGPainless) : SOPV { init { ArmoredOutputStreamFactory.setVersionInfo(null) } - override fun detachedVerify(): DetachedVerify = DetachedVerifyImpl() + override fun detachedVerify(): DetachedVerify = DetachedVerifyImpl(api) - override fun inlineVerify(): InlineVerify = InlineVerifyImpl() + override fun inlineVerify(): InlineVerify = InlineVerifyImpl(api) - override fun version(): Version = VersionImpl() + override fun version(): Version = VersionImpl(api) + + override fun validateUserId(): ValidateUserId = ValidateUserIdImpl(api) } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/UpdateKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/UpdateKeyImpl.kt new file mode 100644 index 00000000..37767424 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/UpdateKeyImpl.kt @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +import java.util.* +import org.bouncycastle.bcpg.KeyIdentifier +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.pgpainless.PGPainless +import org.pgpainless.key.modification.secretkeyring.OpenPGPKeyUpdater +import org.pgpainless.util.OpenPGPCertificateUtil +import org.pgpainless.util.Passphrase +import sop.Ready +import sop.operation.UpdateKey + +class UpdateKeyImpl(private val api: PGPainless) : UpdateKey { + + private var armor = true + private var addCapabilities = true + private var signingOnly = false + private val protector: MatchMakingSecretKeyRingProtector = MatchMakingSecretKeyRingProtector() + + private val mergeCerts: MutableMap = mutableMapOf() + + override fun key(key: InputStream): Ready { + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + var keyList = + api.readKey().parseKeys(key).map { + // Merge keys + if (mergeCerts[it.keyIdentifier] == null) { + it + } else { + val updatedCert: OpenPGPCertificate = + api.mergeCertificate( + it.toCertificate(), mergeCerts[it.keyIdentifier]!!) + api.toKey( + PGPSecretKeyRing.replacePublicKeys( + it.pgpSecretKeyRing, updatedCert.pgpPublicKeyRing)) + } + } + + // Update keys + keyList = + keyList.map { + OpenPGPKeyUpdater(it, protector, api) + .replaceRejectedAlgorithmPreferencesAndFeatures(addCapabilities) + .replaceWeakSubkeys(true, signingOnly) + .finish() + } + + if (armor) { + OpenPGPCertificateUtil.armor(keyList, outputStream) + } else { + OpenPGPCertificateUtil.encode(keyList, outputStream) + } + } + } + } + + override fun mergeCerts(certs: InputStream): UpdateKey = apply { + val certList = api.readKey().parseCertificates(certs) + for (cert in certList) { + if (mergeCerts[cert.keyIdentifier] == null) { + mergeCerts[cert.keyIdentifier] = cert + } else { + val existing = mergeCerts[cert.keyIdentifier]!! + mergeCerts[cert.keyIdentifier] = api.mergeCertificate(existing, cert) + } + } + } + + override fun noAddedCapabilities(): UpdateKey = apply { addCapabilities = false } + + override fun noArmor(): UpdateKey = apply { armor = false } + + override fun signingOnly(): UpdateKey = apply { signingOnly = true } + + override fun withKeyPassword(password: ByteArray): UpdateKey = apply { + protector.addPassphrase(Passphrase.fromPassword(String(password))) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ValidateUserIdImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ValidateUserIdImpl.kt new file mode 100644 index 00000000..1949f280 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ValidateUserIdImpl.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.util.* +import org.bouncycastle.openpgp.api.OpenPGPCertificate +import org.pgpainless.PGPainless +import sop.exception.SOPGPException +import sop.operation.ValidateUserId + +class ValidateUserIdImpl(private val api: PGPainless) : ValidateUserId { + + private var addSpecOnly = false + private var userId: String? = null + private val authorities: MutableList = mutableListOf() + private var validateAt: Date = Date() + + override fun addrSpecOnly(): ValidateUserId = apply { addSpecOnly = true } + + override fun authorities(certs: InputStream): ValidateUserId = apply { + authorities.addAll(api.readKey().parseCertificates(certs)) + } + + override fun subjects(certs: InputStream): Boolean { + requireNotNull(userId) { "Missing parameter USERID" } + return api.readKey().parseCertificates(certs).all { cert -> + authorities.all { authority -> + cert.getUserId(userId)?.getCertificationBy(authority, validateAt)?.isValid == true + } || + throw SOPGPException.CertUserIdNoMatch( + "${cert.keyIdentifier} does not carry valid user-id '$userId'") + } + } + + override fun userId(userId: String): ValidateUserId = apply { this.userId = userId } + + override fun validateAt(date: Date): ValidateUserId = apply { validateAt = date } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt index 9198e3b7..bc64ecd3 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt @@ -24,8 +24,8 @@ class VerificationHelper { fun mapVerification(sigVerification: SignatureVerification): Verification = Verification( sigVerification.signature.creationTime, - sigVerification.signingKey.subkeyFingerprint.toString(), - sigVerification.signingKey.primaryKeyFingerprint.toString(), + sigVerification.signingKey.componentKeyFingerprint.toString(), + sigVerification.signingKey.certificateFingerprint.toString(), getMode(sigVerification.signature), null) diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt index 94b9c016..1296fed8 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt @@ -8,22 +8,24 @@ import java.io.IOException import java.io.InputStream import java.util.* import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.pgpainless.PGPainless import sop.SOP import sop.operation.Version /** Implementation of the `version` operation using PGPainless. */ -class VersionImpl : Version { +class VersionImpl(private val api: PGPainless) : Version { companion object { - const val SOP_VERSION = 10 - const val SOPV_VERSION = "1.0" + const val SOP_VERSION = 14 + const val SOPV_VERSION = "1.2" } override fun getBackendVersion(): String = "PGPainless ${getVersion()}" override fun getExtendedVersion(): String { val bcVersion = - String.format(Locale.US, "Bouncy Castle %.2f", BouncyCastleProvider().version) + String.format( + Locale.US, "Bouncy Castle %.2f", BouncyCastleProvider().versionStr.toDouble()) val specVersion = String.format("%02d", SOP_VERSION) return """${getName()} ${getVersion()} https://codeberg.org/PGPainless/pgpainless/src/branch/main/pgpainless-sop 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 82688bbf..2fbb4f64 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java @@ -17,7 +17,10 @@ public class ArmorTest { @Test public void armor() throws IOException { - byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice").getEncoded(); + PGPainless api = PGPainless.getInstance(); + byte[] data = api.generateKey() + .modernKeyRing("Alice") + .getEncoded(); byte[] knownGoodArmor = ArmorUtils.toAsciiArmoredString(data) .replace("Version: PGPainless\n", "") // armor command does not add version anymore .getBytes(StandardCharsets.UTF_8); 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 83778106..9f25497d 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java @@ -9,6 +9,8 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import java.io.IOException; import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.policy.Policy; import sop.ByteArrayAndResult; import sop.DecryptionResult; import sop.EncryptionResult; @@ -275,6 +277,17 @@ public class CarolKeySignEncryptRoundtripTest { @Test public void regressionTest() throws IOException { + // PGPainless default API is strict + PGPainless strictAPI = PGPainless.getInstance(); + PGPainless relaxedAPI = new PGPainless( + strictAPI.getImplementation(), + // BSI policy allows DSA + strictAPI.getAlgorithmPolicy().copy() + .withPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()) + .build() + ); + PGPainless.setInstance(relaxedAPI); + SOPImpl sop = new SOPImpl(); byte[] msg = "Hello, World!\n".getBytes(); ReadyWithResult encryption = sop.encrypt() @@ -294,5 +307,7 @@ public class CarolKeySignEncryptRoundtripTest { VerificationListAssert.assertThatVerificationList(decryption.getResult().getVerifications()) .hasSingleItem() .issuedBy("71FFDA004409E5DDB0C3E8F19BA789DC76D6849A", "71FFDA004409E5DDB0C3E8F19BA789DC76D6849A"); + + PGPainless.setInstance(strictAPI); } } 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 aa366ef1..30616b22 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -568,7 +568,7 @@ public class EncryptDecryptRoundTripTest { public void encryptWithSupportedProfileTest() throws IOException { byte[] key = sop.generateKey() - .profile("rfc4880") + .profile(GenerateKeyImpl.RFC4880_RSA4096_PROFILE.getName()) .userId("Alice ") .generate() .getBytes(); @@ -578,7 +578,7 @@ public class EncryptDecryptRoundTripTest { .getBytes(); byte[] encrypted = sop.encrypt() - .profile("rfc4880") + .profile(EncryptImpl.RFC4880_PROFILE.getName()) .withCert(cert) .plaintext(message) .toByteArrayAndResult() 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 ca6df790..521cdfe0 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -100,4 +100,14 @@ public class GenerateKeyTest { assertThrows(SOPGPException.UnsupportedProfile.class, () -> sop.generateKey().profile("invalid")); } + + @Test + public void generateKeyWithNewlinesInUserId() throws IOException { + byte[] keyBytes = sop.generateKey() + .userId("Foo\n\nBar") + .generate() + .getBytes(); + + assertTrue(new String(keyBytes).contains("Foo\\n\\nBar")); + } } 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 efcd51c4..dd8fef77 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java @@ -4,7 +4,6 @@ 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; @@ -20,8 +19,6 @@ 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; @@ -35,12 +32,13 @@ public class IncapableKeysTest { private static final SOP sop = new SOPImpl(); @BeforeAll - public static void generateKeys() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public static void generateKeys() throws IOException { PGPSecretKeyRing key = PGPainless.buildKeyRing() .addSubkey(KeySpec.getBuilder(KeyType.ECDH(EllipticCurve._P256), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addUserId("Non Signing ") - .build(); + .build() + .getPGPSecretKeyRing(); nonSigningKey = ArmorUtils.toAsciiArmoredString(key).getBytes(StandardCharsets.UTF_8); nonSigningCert = sop.extractCert().key(nonSigningKey).getBytes(); @@ -48,7 +46,8 @@ public class IncapableKeysTest { .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addUserId("Non Encryption ") - .build(); + .build() + .getPGPSecretKeyRing(); nonEncryptionKey = ArmorUtils.toAsciiArmoredString(key).getBytes(StandardCharsets.UTF_8); nonEncryptionCert = sop.extractCert().key(nonEncryptionKey).getBytes(); } 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 98279e4f..ebb39694 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/InlineDetachTest.java @@ -5,6 +5,8 @@ package org.pgpainless.sop; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -22,11 +24,17 @@ import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.api.OpenPGPImplementation; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; +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.OpenPgpV4Fingerprint; +import org.pgpainless.key.protection.SecretKeyRingProtector; import sop.ByteArrayAndResult; import sop.SOP; import sop.Signatures; @@ -153,13 +161,13 @@ public class InlineDetachTest { ByteArrayOutputStream literalDataAndSignatures = new ByteArrayOutputStream(); ArmoredInputStream armorIn = new ArmoredInputStream(new ByteArrayInputStream(inlineSigned)); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(armorIn); + PGPObjectFactory objectFactory = OpenPGPImplementation.getInstance().pgpObjectFactory(armorIn); Object next; while ((next = objectFactory.nextObject()) != null) { if (next instanceof PGPCompressedData) { PGPCompressedData compressedData = (PGPCompressedData) next; try { - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(compressedData.getDataStream()); + objectFactory = OpenPGPImplementation.getInstance().pgpObjectFactory(compressedData.getDataStream()); } catch (PGPException e) { throw new SOPGPException.BadData("Cannot decompress compressed data", e); } @@ -201,4 +209,62 @@ public class InlineDetachTest { assertArrayEquals(data, message); } + + @Test + public void detachSignaturesFromCompressedMessage() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey key = api.generateKey() + .modernKeyRing("Alice "); + byte[] cert = key.toCertificate().getEncoded(); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + EncryptionStream eOut = api.generateMessage() + .onOutputStream(bOut) + .withOptions(ProducerOptions.sign( + SigningOptions.get().addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), key) + ).overrideCompressionAlgorithm(CompressionAlgorithm.ZIP)); + eOut.write("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + eOut.close(); + + ByteArrayAndResult result = sop.inlineDetach() + .message(bOut.toByteArray()) + .toByteArrayAndResult(); + + byte[] message = result.getBytes(); + byte[] signatures = result.getResult().getBytes(); + + List verifications = sop.detachedVerify() + .cert(cert) + .signatures(signatures) + .data(message); + + assertFalse(verifications.isEmpty()); + } + + @Test + public void detachMissingSignaturesFails() throws PGPException, IOException { + PGPainless api = PGPainless.getInstance(); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + EncryptionStream eOut = api.generateMessage() + .onOutputStream(bOut) + .withOptions(ProducerOptions.noEncryptionNoSigning() + .overrideCompressionAlgorithm(CompressionAlgorithm.ZIP)); + + eOut.write("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + eOut.close(); + + assertThrows(SOPGPException.BadData.class, () -> + sop.inlineDetach() + .message(bOut.toByteArray()) + .toByteArrayAndResult()); + } + + @Test + public void detachBadDataFails() { + byte[] bytes = "Hello, World\n".getBytes(StandardCharsets.UTF_8); + assertThrows(SOPGPException.BadData.class, () -> + sop.inlineDetach().message(bytes) + .toByteArrayAndResult()); + } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java index 9dc2b6c6..d7b5a419 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtectorTest.java @@ -96,13 +96,13 @@ public class MatchMakingSecretKeyRingProtectorTest { MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); protector.addSecretKey(unprotectedKey); assertTrue(protector.hasPassphraseFor(unprotectedKey.getPublicKey().getKeyID())); - assertNull(protector.getEncryptor(unprotectedKey.getPublicKey().getKeyID())); + assertNull(protector.getEncryptor(unprotectedKey.getPublicKey())); 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.getEncryptor(protectedKey.getPublicKey())); assertNotNull(protector.getDecryptor(protectedKey.getPublicKey().getKeyID())); } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/RevokeKeyTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/RevokeKeyTest.java new file mode 100644 index 00000000..0d77b952 --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/RevokeKeyTest.java @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import org.bouncycastle.openpgp.api.OpenPGPCertificate; +import org.bouncycastle.openpgp.api.OpenPGPKey; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.OpenPGPKeyVersion; +import sop.SOP; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RevokeKeyTest { + + private static SOP sop; + + @BeforeAll + public static void setup() { + sop = new SOPImpl(); + } + + @Test + public void revokeV6CertResultsInMinimalRevCert() throws IOException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey v6Key = api.generateKey(OpenPGPKeyVersion.v6) + .modernKeyRing("Alice "); + assertEquals(3, v6Key.getKeys().size()); + + byte[] revoked = sop.revokeKey() + .keys(v6Key.getEncoded()) + .getBytes(); + + OpenPGPCertificate revocationCert = api.readKey().parseCertificate(revoked); + assertEquals(1, revocationCert.getKeys().size(), + "V6 keys are revoked using a minimal revocation cert," + + " consisting only of the primary key and a rev sig."); + } +} diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/VerifyLegacySignatureTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/VerifyLegacySignatureTest.java index 23fd9840..a6096915 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/VerifyLegacySignatureTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VerifyLegacySignatureTest.java @@ -123,7 +123,8 @@ public class VerifyLegacySignatureTest { "=TtKx\n" + "-----END PGP MESSAGE-----"; - SOPImpl sop = new SOPImpl(); + PGPainless api = PGPainless.getInstance(); + SOPImpl sop = new SOPImpl(api); byte[] cert = sop.extractCert().key(KEY.getBytes(StandardCharsets.UTF_8)) .getBytes(); ByteArrayAndResult> result = sop.inlineVerify() @@ -134,8 +135,13 @@ public class VerifyLegacySignatureTest { assertFalse(result.getResult().isEmpty()); // Adjust data signature hash policy to accept new SHA-1 sigs - PGPainless.getPolicy().setDataSignatureHashAlgorithmPolicy( - Policy.HashAlgorithmPolicy.static2022RevocationSignatureHashAlgorithmPolicy()); + Policy policy = api.getAlgorithmPolicy(); + Policy adjusted = policy.copy() + .withDataSignatureHashAlgorithmPolicy( + Policy.HashAlgorithmPolicy.static2022RevocationSignatureHashAlgorithmPolicy() + ).build(); + api = new PGPainless(adjusted); + sop = new SOPImpl(api); // Sig generated in 2024 using SHA1 String newSig = "-----BEGIN PGP MESSAGE-----\n" + diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessCertifyValidateUserIdTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessCertifyValidateUserIdTest.java new file mode 100644 index 00000000..77bd0226 --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessCertifyValidateUserIdTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.CertifyValidateUserIdTest; + +public class PGPainlessCertifyValidateUserIdTest extends CertifyValidateUserIdTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java index baf595d3..6756300a 100644 --- a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java @@ -4,9 +4,10 @@ package sop.testsuite.pgpainless.operation; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.api.OpenPGPKey; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.pgpainless.PGPainless; @@ -22,8 +23,6 @@ import sop.testsuite.operation.ChangeKeyPasswordTest; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Iterator; import static org.junit.jupiter.api.Assertions.assertArrayEquals; @@ -32,22 +31,23 @@ public class PGPainlessChangeKeyPasswordTest extends ChangeKeyPasswordTest { @ParameterizedTest @MethodSource("provideInstances") - public void changePasswordOfKeyWithSeparateSubkeyPasswords(SOP sop) throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() + public void changePasswordOfKeyWithSeparateSubkeyPasswords(SOP sop) throws IOException, PGPException { + PGPainless api = PGPainless.getInstance(); + OpenPGPKey secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .build(); - Iterator keys = secretKeys.getPublicKeys(); - long primaryKeyId = keys.next().getKeyID(); - long signingKeyId = keys.next().getKeyID(); - long encryptKeyId = keys.next().getKeyID(); + Iterator keys = secretKeys.getPGPSecretKeyRing().getPublicKeys(); + KeyIdentifier primaryKeyId = keys.next().getKeyIdentifier(); + KeyIdentifier signingKeyId = keys.next().getKeyIdentifier(); + KeyIdentifier encryptKeyId = keys.next().getKeyIdentifier(); String p1 = "sw0rdf1sh"; String p2 = "0r4ng3"; String p3 = "dr4g0n"; - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = api.modify(secretKeys) .changeSubKeyPassphraseFromOldPassphrase(primaryKeyId, Passphrase.emptyPassphrase()) .withSecureDefaultSettings() .toNewPassphrase(Passphrase.fromPassword(p1)) 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 similarity index 78% rename from pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWIthSessionKeyTest.java rename to pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWithSessionKeyTest.java index 2825db5f..a046be68 100644 --- a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWIthSessionKeyTest.java +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDecryptWithSessionKeyTest.java @@ -6,6 +6,6 @@ package sop.testsuite.pgpainless.operation; import sop.testsuite.operation.DecryptWithSessionKeyTest; -public class PGPainlessDecryptWIthSessionKeyTest extends 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 index dff9e86f..28fc3d6a 100644 --- a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDetachedSignDetachedVerifyTest.java +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessDetachedSignDetachedVerifyTest.java @@ -8,6 +8,8 @@ import org.junit.jupiter.api.Disabled; import sop.SOP; import sop.testsuite.operation.DetachedSignDetachedVerifyTest; +import java.io.IOException; + public class PGPainlessDetachedSignDetachedVerifyTest extends DetachedSignDetachedVerifyTest { @Override @@ -15,4 +17,10 @@ public class PGPainlessDetachedSignDetachedVerifyTest extends DetachedSignDetach public void verifyMissingCertCausesMissingArg(SOP sop) { super.verifyMissingCertCausesMissingArg(sop); } + + @Override + @Disabled("Carol is an ElGamal key, which are no longer supported.") + public void signVerifyWithCarolKey(SOP sop) throws IOException { + super.signVerifyWithCarolKey(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 index b6264d3a..00fa3acc 100644 --- a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessEncryptDecryptTest.java +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessEncryptDecryptTest.java @@ -4,8 +4,30 @@ package sop.testsuite.pgpainless.operation; +import org.pgpainless.PGPainless; +import org.pgpainless.policy.Policy; +import sop.SOP; import sop.testsuite.operation.EncryptDecryptTest; +import java.io.IOException; + public class PGPainlessEncryptDecryptTest extends EncryptDecryptTest { + @Override + public void encryptDecryptRoundTripCarolTest(SOP sop) throws IOException { + // Carols key is DSA, which is rejected by PGPainless default policy now. + // Therefore, we need to set a relaxed PGPainless API instance, allowing DSA keys. + PGPainless strictAPI = PGPainless.getInstance(); + PGPainless relaxedAPI = new PGPainless( + strictAPI.getImplementation(), + strictAPI.getAlgorithmPolicy().copy() + .withPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy()) + .build()); + PGPainless.setInstance(relaxedAPI); + + super.encryptDecryptRoundTripCarolTest(sop); + + PGPainless.setInstance(strictAPI); + } + } 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 index 16166eb1..deb9d6b5 100644 --- a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineVerifyTest.java +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessInlineSignInlineVerifyTest.java @@ -4,8 +4,17 @@ package sop.testsuite.pgpainless.operation; +import org.junit.jupiter.api.Disabled; +import sop.SOP; import sop.testsuite.operation.InlineSignInlineVerifyTest; +import java.io.IOException; + public class PGPainlessInlineSignInlineVerifyTest extends InlineSignInlineVerifyTest { + @Override + @Disabled("Carol is an ElGamal key, which is no longer supported.") + public void inlineSignVerifyCarol(SOP sop) throws IOException { + super.inlineSignVerifyCarol(sop); + } } diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessMergeCertsTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessMergeCertsTest.java new file mode 100644 index 00000000..ba674c5a --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessMergeCertsTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.pgpainless.operation; + +import sop.testsuite.operation.MergeCertsTest; + +public class PGPainlessMergeCertsTest extends MergeCertsTest { + +} diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessRevokeKeyTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessRevokeKeyTest.java index b7590b7a..5567d3e0 100644 --- a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessRevokeKeyTest.java +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessRevokeKeyTest.java @@ -4,6 +4,7 @@ package sop.testsuite.pgpainless.operation; +import static java.lang.Thread.sleep; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -28,6 +29,11 @@ public class PGPainlessRevokeKeyTest extends RevokeKeyTest { super.revokeUnprotectedKey(sop); byte[] key = sop.generateKey().generate().getBytes(); + try { + sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } byte[] revokedKey = sop.revokeKey().keys(key).getBytes(); PGPKeyRing certificate = PGPainless.readKeyRing().keyRing(revokedKey); diff --git a/version.gradle b/version.gradle index bc2515a4..776f30f6 100644 --- a/version.gradle +++ b/version.gradle @@ -4,15 +4,15 @@ allprojects { ext { - shortVersion = '1.7.7' + shortVersion = '2.0.0' isSnapshot = true javaSourceCompatibility = 11 - bouncyCastleVersion = '1.81' + bouncyCastleVersion = '1.82-SNAPSHOT' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' logbackVersion = '1.5.13' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '10.1.1' + sopJavaVersion = '14.0.1-SNAPSHOT' } }