1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-09-09 18:29:39 +02:00

Compare commits

...

271 commits

Author SHA1 Message Date
3fd8b82c9b
Bump sop-java to 14.0.0 2025-06-18 12:16:48 +02:00
15d50bb4af
Bump sop-java to 14.0.0-SNAPSHOT 2025-06-17 13:32:57 +02:00
a49df00a9e
Bump BC to 1.81 + BC/#2105 2025-06-17 13:31:49 +02:00
98c48232f5
EncryptImpl: Emit session-key 2025-06-17 13:31:49 +02:00
9617b35703
Add test for PolicyAdapter properly adapting NotationRegistry implementations 2025-06-17 13:31:49 +02:00
aa1f99fe39
Add tests for SignatureSubpacketsCallback implementations 2025-06-17 13:31:49 +02:00
72ec1b1e06
setPreferredAEADCiphersuites(): Add missing method taking PreferredAEADCiphersuites object 2025-06-17 13:31:49 +02:00
7313c5e5a9
Add missing implementations of then() method 2025-06-17 13:31:48 +02:00
c054cb9705
Remove unused SignatureSubpackets callback related methods 2025-06-17 13:31:48 +02:00
0a639e1c2a
Implement update-key command properly 2025-06-17 13:31:48 +02:00
d789d3e0c4
Add test for CompressionAlgorithmNegotiator 2025-06-17 13:31:48 +02:00
21439854e3
Move SymmetricKeyAlgorithmNegotiatorTest to negotiation package 2025-06-17 13:31:48 +02:00
026be063f8
Swappable algorithm negotiation delegates 2025-06-17 13:31:47 +02:00
fd85f8e567
SOP encrypt --profile=rfc9580: Only override enc mechanism with seipd2 if exclusively symmetric encryption is used 2025-06-17 13:31:47 +02:00
24887e2521
EncryptionMechanismNegotiator: Allow producing AEADED/OED packets 2025-06-17 13:31:47 +02:00
df136adfab
ValidateUserIdImpl: throw CertUserIdNoMatch for unbound user-ids 2025-06-17 13:31:47 +02:00
8f24bcfb26
SOP encrypt: Add profile for rfc9580 2025-06-17 13:31:47 +02:00
76820b8cd5
Enable additional profiles 2025-06-17 13:31:47 +02:00
0027a3ed24
SOP generate-key: Implement additional profiles 2025-06-17 13:31:46 +02:00
e45b551ab3
SOP generate-key: Add rfc9580 profile 2025-06-17 13:31:46 +02:00
a575f46867
Move EncryptionMechanismNegotiator into own interface, improve negotiation 2025-06-17 13:31:46 +02:00
65e2de8186
Replace usage of KeyIdentifier.matches() with matchesExplicitly() 2025-06-17 13:31:46 +02:00
46367aff93
Remove SignerUserIdValidation enum 2025-06-17 13:31:46 +02:00
18a49d0afd
Add deprecation notices 2025-06-17 13:31:45 +02:00
45a79a0e65
WIP: EncryptionMechanismPolicy 2025-06-17 13:31:45 +02:00
5b39aea421
Improve GnuPGDummyKeyUtilTest 2025-06-17 13:31:45 +02:00
4e5eff6113
Test v6 key revocation 2025-06-17 13:31:45 +02:00
946d8aace0
Test edge-cases in inline-detach operation 2025-06-17 13:31:45 +02:00
bfd67abab7
Document KOpenPGP mitigations 2025-06-17 13:31:44 +02:00
aa4ffbaba5
Simplify SessionKey conversion 2025-06-17 13:31:44 +02:00
7b32da722f
Document KOpenPGP mitigations 2025-06-17 13:31:44 +02:00
c914a43853
Fix more javadoc references 2025-06-17 13:31:44 +02:00
4405c579a1
Fix references in javadoc 2025-06-17 13:31:44 +02:00
4462abce9f
Add OpenPGPCertificateUtil and unify the way, SOP encodes/armors certificates/keys 2025-06-17 13:31:44 +02:00
4d8179edc1
KeyRingReaderTest: Remove unused import 2025-06-17 13:31:43 +02:00
f786de4c54
TestAllImplementations: Fix javadoc 2025-06-17 13:31:43 +02:00
eaeb0e1ab2
Fix test 2025-06-17 13:31:43 +02:00
ed92f321dd
Generate-Key: Use new packet tags 2025-06-17 13:31:43 +02:00
f97591a509
Add missing license headers 2025-06-17 13:31:43 +02:00
8c7e9e1b54
Add documentation 2025-06-17 13:31:42 +02:00
f3b5664d95
Update documentation of AEADAlgorithm 2025-06-17 13:31:42 +02:00
82db3a9ea6
Port CertificateAuthority to KeyIdentifier, add tests for authenticated cert selection 2025-06-17 13:31:42 +02:00
06d0b90ff6
Add tests for LongExtension methods 2025-06-17 13:31:42 +02:00
88d9fae2fc
Add test and documentation to DateExtensions 2025-06-17 13:31:42 +02:00
2714c9770b
Some updates to the README file 2025-06-17 13:31:41 +02:00
e44e97844c
Add AEADAlkgorithm.toMechanism(SymAlg) shortcut method 2025-06-17 13:31:41 +02:00
48ba9dbe98
Update README 2025-06-17 13:31:41 +02:00
ab34413fa8
Port GnuPGDummyExtension implementation 2025-06-17 13:31:41 +02:00
a76128cf79
Port Exception classes to Kotlin 2025-06-17 13:31:41 +02:00
65f341f687
Remove usage of OpenPgpKeyAttributeUtil 2025-06-17 13:31:40 +02:00
a0ef949bb4
Port OpenPGPInputStream to Kotlin as OpenPGPAnimalSnifferInputStream 2025-06-17 13:31:40 +02:00
21246138aa
Typo 2025-06-17 13:31:40 +02:00
01c112770a
Clean up OnePassSignatureCheck 2025-06-17 13:31:40 +02:00
7c22d32a11
Remove unused SignatureComparator classes 2025-06-17 13:31:40 +02:00
3e867be780
Fix comment block layout 2025-06-17 13:31:40 +02:00
2d0e4b4fc0
Update documentation of SignatureVerification 2025-06-17 13:31:39 +02:00
1b6601cc19
Rework ASCII armor API 2025-06-17 13:31:39 +02:00
02d72c2691
Add documentation to PGPainless class 2025-06-17 13:31:39 +02:00
244113bc2f
Replace static decryptAndOrVerify() method with non-static processMessage() function 2025-06-17 13:31:39 +02:00
3bc07f045c
Prevent NULL encryption algorithm 2025-06-17 13:31:39 +02:00
76efbf2e45
Test encryptionMechanismOverride for symmetric and asymmetric encryption 2025-06-17 13:31:38 +02:00
f7dd72dd79
Respect encryptionMechanismOverride 2025-06-17 13:31:38 +02:00
6e8982df59
Remove debugging prints 2025-06-17 13:31:38 +02:00
ab6ab04bcb
Add documentation 2025-06-17 13:31:38 +02:00
dc2fe5d65a
Rework OpenPGPInputStream to rely on BCPGInputStream for packet parsing 2025-06-17 13:31:38 +02:00
05ea7bd94f
Workaround for OpenPGPInputStream to recognize PKESKv6 packets 2025-06-17 13:31:38 +02:00
c2f7a8b2fd
Fix checkstyle issues 2025-06-17 13:31:37 +02:00
333addf262
Move negotiation tests to dedicated test class 2025-06-17 13:31:37 +02:00
cc4928ab22
First draft for SEIPD2 negotiation 2025-06-17 13:31:37 +02:00
0266d14594
Rework KeyAccessor 2025-06-17 13:31:37 +02:00
94febc33df
Expose encryption mechanism during decryption 2025-06-17 13:31:37 +02:00
3cef99d256
Add BUILD.md 2025-06-17 13:31:36 +02:00
48f000f6f4
Update README 2025-06-17 13:31:36 +02:00
bdd5a9e26e
Raise kotlin lib version 2025-06-17 13:31:36 +02:00
9343e1e0f2
Remove duplicate line in build.gradle 2025-06-17 13:31:36 +02:00
1dd666d32b
Implement crude update-key command (only merges certs for now) 2025-06-17 13:31:36 +02:00
b7dedbd619
SOP certify-userid: Properly throw KeyCannotCertify exception 2025-06-17 13:31:36 +02:00
d540febc7f
Add PGPainlessCertifyValidateUserIdTest 2025-06-17 13:31:35 +02:00
168c884f27
Certify-UserId: Throw proper exception on unbound user-id 2025-06-17 13:31:35 +02:00
148af79794
Set relaxed PK policies for tests with weak DSA keys 2025-06-17 13:31:35 +02:00
85856567dd
Fix checkstyle issues 2025-06-17 13:31:35 +02:00
4797ce34c3
Add comments 2025-06-17 13:31:35 +02:00
68be1ffc5f
SOP: Implement merge-certs subcommand 2025-06-17 13:31:34 +02:00
9f2371932e
Update SOP version in VersionImpl 2025-06-17 13:31:34 +02:00
24cef79831
Add PublicKeyAlgorithmPolicy based on rfc9580 2025-06-17 13:31:34 +02:00
3080e8bdd3
Implement SOPs validate-userid command 2025-06-17 13:31:34 +02:00
2d1c2d2737
Implement SOPs certify-userid command 2025-06-17 13:31:34 +02:00
1b19634415
SOP-Java: These go to 11 2025-06-17 13:31:32 +02:00
c7c3d5b3ab
HardwareSecurity: Replace usage of Long KeyId with KeyIdentifier 2025-06-17 13:31:10 +02:00
f3257d9405
Remove unused test 2025-06-17 13:31:10 +02:00
b8f41b6212
Port ReadKeys example 2025-06-17 13:31:10 +02:00
96fa3af08c
Port Encrypt example 2025-06-17 13:31:10 +02:00
ff62a39dc8
Port DecryptOrVerify example 2025-06-17 13:31:10 +02:00
187416bbe1
Port EncryptDecryptTest 2025-06-17 13:31:09 +02:00
d1861e51cd
Improve API for signatures in results 2025-06-17 13:31:09 +02:00
654756c919
Replace all remaining usages of PGPainless.generateKeyRing() 2025-06-17 13:31:09 +02:00
2d6675ec06
Add tests for v6<->v4 certificate certification 2025-06-17 13:31:09 +02:00
7281ce530a
Port KeyWithUnknownSecretKeyEncryptionMethodTest 2025-06-17 13:31:09 +02:00
8aaa042087
Port a bunch of more tests 2025-06-17 13:31:09 +02:00
bab5a4b0bf
Add missing methods for SecretKeyRing protection 2025-06-17 13:31:08 +02:00
a8a09b7db7
Add OpenPGPSecretKey.unlock(Passphrase) extension method 2025-06-17 13:31:08 +02:00
e2d8db6796
Port BcHashContextSigner and test 2025-06-17 13:31:08 +02:00
bd24db9cc6
Port TryDecryptWithUnavailableGnuDummyKeyTest 2025-06-17 13:31:08 +02:00
9f35be1b0e
Port more tests 2025-06-17 13:31:08 +02:00
bb64188473
Port some more tests 2025-06-17 13:31:07 +02:00
54d83daee5
Port UnlockSecretKey method 2025-06-17 13:31:07 +02:00
cad89b9bde
Small javadoc fixes 2025-06-17 13:31:07 +02:00
c22a2e4fcf
Add test for overriding features during key generation 2025-06-17 13:31:07 +02:00
2dea73c584
KeySpecBuilder: Expose API for overriding default AEAD algorithms and features 2025-06-17 13:31:07 +02:00
47ec445ef7
Add missing javadoc to SigningOptions 2025-06-17 13:31:06 +02:00
ca22446f1c
Remove API instance parameter from ProducerOptions 2025-06-17 13:31:06 +02:00
41251296ce
Port ConvertKeys example 2025-06-17 13:31:06 +02:00
a37f6dfce9
Port GenerateKeys examples 2025-06-17 13:31:06 +02:00
69b0b2d371
Port PGPPublicKeyRingTest 2025-06-17 13:31:06 +02:00
1e67447efd
Port ExtractCertCmdTest 2025-06-17 13:31:06 +02:00
5f3e1b4da3
generate-key: Use API instance when generating keys 2025-06-17 13:31:05 +02:00
53b44e2817
Migrate GenerateKeyWithoutUserIdTest 2025-06-17 13:31:05 +02:00
c8694840d8
Migrate some tests to new API 2025-06-17 13:31:05 +02:00
c7ce79a5af
IntegrityProtectedInputStream: remove useless logger 2025-06-17 13:31:05 +02:00
e2832249cb
Remove SignatureValidator methods 2025-06-17 13:31:05 +02:00
2c1d89a249
Remove unused SignatureValidator methods 2025-06-17 13:31:04 +02:00
cb7c61cf10
Replace SignatureVerifier usage with BC API 2025-06-17 13:31:04 +02:00
053eb2c830
Remove usage of deprecated methods in SOP implementations 2025-06-17 13:31:04 +02:00
7db10432fe
Port MessageInspector 2025-06-17 13:31:04 +02:00
e2d79e00cc
KeyRingUtils: Use KeyIdentifier instead of keyId 2025-06-17 13:31:04 +02:00
793ee40290
KeyRingReader: Replace usage of deprecated PGPainless method with BC method 2025-06-17 13:31:04 +02:00
3b9858f9ef
Improve readability of OpenPGPMessageInputStream 2025-06-17 13:31:03 +02:00
c88d1573d7
Remove duplicate Padding parser branch 2025-06-17 13:31:03 +02:00
364bebed14
Replace KeyRingUtils usage with toCertificate() 2025-06-17 13:31:03 +02:00
0fbf7fac04
KeyRingInfo: Apply latest method name change from BC 2025-06-17 13:31:03 +02:00
8c58ca620d
Rename new CertifyCertificate API methods and add revocation methods 2025-06-17 13:31:03 +02:00
a8cbd36a52
Test v6 third party certification generation 2025-06-17 13:31:02 +02:00
4a7e690806
CertifyCertificate: Change visibility of internal members to private 2025-06-17 13:31:02 +02:00
312a00e5d4
Remove Tuple class 2025-06-17 13:31:02 +02:00
57b6795513
Remove unused KeyRingSelectionStrategy implementations 2025-06-17 13:31:02 +02:00
acbb93066e
Rework some more tests 2025-06-17 13:31:02 +02:00
9a7aeae9fa
Port SigningTest 2025-06-17 13:31:02 +02:00
bab448eb6d
Introduce PGPainless.toKeyOrCertificate(PGPKeyRing) and constrain argument type of PGPainless.toCertificate(PGPPublicKeyRing) 2025-06-17 13:31:01 +02:00
221d329254
Remove SignerUserId check, Policy setting only via constructor parameter 2025-06-17 13:31:01 +02:00
4c180bbd59
Port signature validation to BC 2025-06-17 13:31:01 +02:00
63d1f855de
Rework ModifiedPublicKeysInvestigation 2025-06-17 13:31:01 +02:00
e61c3007c0
Avoid usage of PGPainless.getPolicy() 2025-06-17 13:31:01 +02:00
c8880619f9
KeySpecBuilder: Do not use PGPainless.getPolicy() method 2025-06-17 13:31:00 +02:00
2d42457ce4
Policy is no longer a Singleton 2025-06-17 13:31:00 +02:00
b24d0ef99c
Determine, whether to use AEAD by cosulting KeyRingProtectionSettings 2025-06-17 13:31:00 +02:00
2ae9c94841
Port SelectUserId.validUserIds() 2025-06-17 13:31:00 +02:00
a00a90c175
Change argument type for toCertificate() method to more general PGPKeyRing 2025-06-17 13:31:00 +02:00
3a28b33355
Delete SignaturePicker class 2025-06-17 13:31:00 +02:00
eefc622f63
Fix test name 2025-06-17 13:30:59 +02:00
665db5ceb6
Port more extension functions 2025-06-17 13:30:59 +02:00
b828e5477c
Migrate some extension functions 2025-06-17 13:30:59 +02:00
053f6cf362
PGPSignatureExtensions: Port wasIssuedBy() to KeyIdentifier 2025-06-17 13:30:59 +02:00
8a48cc40f7
Update some examples in the README file 2025-06-17 13:30:59 +02:00
2200cb7372
SOP: Inject API instance 2025-06-17 13:30:58 +02:00
57540d8028
Port SecretKeyRingEditor, replace Singleton usage with API instance calls 2025-06-17 13:30:58 +02:00
2a71a98bba
Add more deprecation annotations, workaround for BC armor bug 2025-06-17 13:30:58 +02:00
74c821c1e8
GnuPGDummyKeyUtil: Migrate to KeyIdentifier 2025-06-17 13:30:58 +02:00
bca4ddcb6f
Remove ProviderFactory classes
It is no longer possible to inject custom SecurityProviders.
Instead, you can create and inject your own implementation of BCs OpenPGPImplementation
2025-06-17 13:30:58 +02:00
04160fbe27
Fix javadoc parameter names 2025-06-17 13:30:57 +02:00
429186c5e1
UserId: Remove deprecated method usage 2025-06-17 13:30:57 +02:00
b181efee00
KeyRingUtils: Replace deprecated method usage 2025-06-17 13:30:57 +02:00
7a5ece0907
Replace deprecated method usage and make policy injectable in UnlockSecretKey utility class 2025-06-17 13:30:57 +02:00
77890cc933
Remove deprecated KeyInfo class
If you relied on it, replace its usage with the Kotlin extension functions as documented.
If you are using Java, use static methods from PGPPublicKeyExtensionsKt and PGPSecretKeyExtensionsKt instead.
2025-06-17 13:30:57 +02:00
93ee037ef0
Move default parameters of Options classes to factory methods 2025-06-17 13:30:57 +02:00
12fd807f75
ConsumerOptions: Pass down API 2025-06-17 13:30:56 +02:00
7e345a0e33
More API down-handing 2025-06-17 13:30:56 +02:00
f74932c4d0
Cleanup PGPainless class 2025-06-17 13:30:56 +02:00
8a9b5aa567
Pass down API instance in more places 2025-06-17 13:30:56 +02:00
0e48e94a91
Pass down API instance 2025-06-17 13:30:56 +02:00
1967483984
More code cleanup 2025-06-17 13:30:55 +02:00
62f3a35c02
Add documentation 2025-06-17 13:30:55 +02:00
d6d52cd544
Code cleanup 2025-06-17 13:30:55 +02:00
1e7a357b68
Allow passing creation time into KeyRingTemplates, replace deprecated methods 2025-06-17 13:30:55 +02:00
0ff347b836
Fix GenerateV6KeyTest.generateAEADProtectedModernKey() test 2025-06-17 13:30:55 +02:00
e284fca0f8
Rework Policy to be immutable. Changes are now done by calling policy.copy().withXYZ().build() 2025-06-17 13:30:54 +02:00
33ee03ee35
PublicKeyAlgorithms: Update documentation 2025-06-17 13:30:54 +02:00
6cfa87201b
PublicKeyAlgorithm: Ask PublicKeyUtils for algorithm capabilities, add persistent symmetric key algorithm ids 2025-06-17 13:30:54 +02:00
a95ebce07b
Add OpenPGPImplementation.checksumCalculator() extension function 2025-06-17 13:30:54 +02:00
6c68285a95
Replace usage of .let() 2025-06-17 13:30:54 +02:00
97e6591f0a
Make secret key protection settings customizable via policy 2025-06-17 13:30:54 +02:00
16a2e77776
Copy deprecation annotation 2025-06-17 13:30:53 +02:00
aace92214a
Rename parameter 2025-06-17 13:30:53 +02:00
d92ae054d9
Use relaxed PBE parameters 2025-06-17 13:30:53 +02:00
18cdf6bbc7
WIP: Migrate SecretKeyRingEditor 2025-06-17 13:30:53 +02:00
3abc2a4e39
Transform SignatureSubpackets class into simple wrapper around PGPSignatureSubpacketGenerator 2025-06-17 13:30:53 +02:00
a25ba5943e
Avoid deprecated API and remove unnecessary code 2025-06-17 13:30:52 +02:00
34633cfeac
Tests: Avoid usage of now deprecated functionality 2025-06-17 13:30:52 +02:00
42c262a99f
Remove ImplementationFactory in favor of BCs OpenPGPImplementation 2025-06-17 13:30:52 +02:00
321053d66e
SigningOptions: Properly init PGPSignatureGenerator to support v6 keys 2025-06-17 13:30:52 +02:00
fc87d985b6
Policy: Change default compression algorithm to UNCOMPRESSED 2025-06-17 13:30:52 +02:00
f9c2ade2d0
Implement applying algorithm preferences as extension functions 2025-06-17 13:30:52 +02:00
8b5d9af522
buildKey(): Use BC KeyGenerator, but apply PGPainless algorithm preferences 2025-06-17 13:30:51 +02:00
d34cb2db61
Add missing method implementations 2025-06-17 13:30:51 +02:00
5de1e6a56d
Work on AlgorithmSuite 2025-06-17 13:30:51 +02:00
67af718db9
Fix: Do not set IssuerKeyId on v6 key-signatures 2025-06-17 13:30:51 +02:00
69fc590d26
Progress on the migration guide 2025-06-17 13:30:51 +02:00
44d90c600f
Start working on migration guide 2025-06-17 13:30:50 +02:00
9812d4d78c
Add some missing documentation to ConsumerOptions 2025-06-17 13:30:50 +02:00
996984cbb5
Rework OnePassSignatureCheck 2025-06-17 13:30:50 +02:00
63bdff58bf
Add documentation to PolicyAdapter 2025-06-17 13:30:50 +02:00
ac0c37925a
Add getKeyVersion() extension methods to certificate + subclasses and use it in KeyRingInfo.version 2025-06-17 13:30:50 +02:00
07d2311b0e
Fix more spotless formatting errors 2025-06-17 13:30:49 +02:00
0109624020
Fix spotless error 2025-06-17 13:30:49 +02:00
714b5bd9c9
Add comments to OpenPGPKeyVersion 2025-06-17 13:30:49 +02:00
f70792f92d
Add comments to HashAlgorithm 2025-06-17 13:30:49 +02:00
446b8eaaca
Add javadoc 2025-06-17 13:30:49 +02:00
22a1f54a9b
Clean up KeyAccessor class 2025-06-17 13:30:49 +02:00
e53e4f5f3c
Complete migration of KeyRingInfo to KeyIdentifier, javadoc 2025-06-17 13:30:48 +02:00
c00a9709de
Replace KeyRingInfo.publicKey with primaryKey 2025-06-17 13:30:48 +02:00
3030f2af2b
Improve KeyRingInfos getPreferences implementations 2025-06-17 13:30:48 +02:00
1379942c07
Migrate from MissingPublicKeyCallback to OpenPGPCertifcateProvider 2025-06-17 13:30:48 +02:00
0fc9ee716e
Fix addSubkey method 2025-06-17 13:30:48 +02:00
b61ba46d24
Fix some tests 2025-06-17 13:30:47 +02:00
88df92fd1f
Port SignatureBuilders over to new classes 2025-06-17 13:30:46 +02:00
975548fc76
Rename and document members of SubkeyIdentifier 2025-06-17 13:29:32 +02:00
2a2595a757
OpenPGPFingerprint(s): Use FingerprintUtil to calculate key-ids 2025-06-17 13:29:32 +02:00
58a96b5776
Remove unnecessary imports 2025-06-17 13:29:32 +02:00
0583a826d1
Add workaround for decryption with non-encryption subkey 2025-06-17 13:29:31 +02:00
fac87c371a
Fix version 2025-06-17 13:29:31 +02:00
23cb47365e
Port CanonicalizedDataEncryptionTest 2025-06-17 13:29:31 +02:00
0ea19d3b9a
Port Sign and UnlockSecretKeys examples 2025-06-17 13:29:31 +02:00
9e9ccc8624
Port ReadKeys example 2025-06-17 13:29:31 +02:00
df1d74962b
Progress porting the example tests 2025-06-17 13:29:31 +02:00
c0b6ea8f96
Improve KeyExceptions 2025-06-17 13:29:30 +02:00
3e8dd78e74
OpenPGPFingerprint: Add factory methods for new key / subkey classes 2025-06-17 13:29:30 +02:00
a54382a78e
Port test 2025-06-17 13:29:30 +02:00
0b4f1a0f01
Port EncryptionOptions over to OpenPGPCertificate 2025-06-17 13:29:30 +02:00
8c557ad945
Port ConsumerOptions, SigningOptions to new OpenPGPCertificate, OpenPGPKey classes 2025-06-17 13:29:30 +02:00
0c7055455b
Reenable disabled test and add workaround for broken one 2025-06-17 13:29:29 +02:00
0b165ee273
Even more migration and code compiles again 2025-06-17 13:29:29 +02:00
217a25bd62
WIP: Transform Options and OpenPgpMessageInputStream 2025-06-17 13:29:29 +02:00
53053cf3fc
Change return type of KeyRingBuilder.build() to OpenPGPKey 2025-06-17 13:29:29 +02:00
dd4a989606
WIP: Migrate away from static methods 2025-06-17 13:29:29 +02:00
66a2b7e0fc
Begin transition to instance-based PGPainless, adapt policy 2025-06-17 13:29:29 +02:00
ead93345e4
Tests: Remove unused throws declarations 2025-06-17 13:29:28 +02:00
7991af06d4
Fix tests 2025-06-17 13:29:28 +02:00
69f802d442
KeyRingInfo: Replace PGPainless signature evaluation with BCs 2025-06-17 13:29:28 +02:00
b488b70050
Disable ElGamal key tests 2025-06-17 13:29:28 +02:00
41a1d0d596
KeyRingInfo: Expose OpenPGPComponentKey in place of PGPPublicKey, OpenPGPSecretKey instead of PGPSecretKey 2025-06-17 13:29:28 +02:00
1738fb1d7d
Change type of KeyRingInfo.publicKey to OpenPGPPrimaryKey 2025-06-17 13:29:27 +02:00
5938ea9cff
Further integration of OpenPGPCertificate into KeyRingInfo 2025-06-17 13:29:27 +02:00
c9a7accec8
Add some debug checks to test 2025-06-17 13:29:27 +02:00
70cb9df8a9
Fix some tests 2025-06-17 13:29:27 +02:00
4ecc590d8f
Fix test stability 2025-06-17 13:29:27 +02:00
f9d217c0b1
Start porting KeyRingInfo over to OpenPGPCertificate 2025-06-17 13:29:26 +02:00
2b9c6e58ed
Integrate KeyIdentifier with SubkeyIdentifier 2025-06-17 13:29:26 +02:00
b571dd177e
Add missing license headers 2025-06-17 13:29:26 +02:00
0fceb4db2d
Basic v6 key generation test 2025-06-17 13:29:26 +02:00
da9c610d14
Add new key types to default policy 2025-06-17 13:29:26 +02:00
c6dbc029d7
Add new key types X25519, X448, Ed25519, Ed448 2025-06-17 13:29:26 +02:00
2a43d5704b
Pass version down in tests 2025-06-17 13:29:25 +02:00
31e6f2e73a
Allow passing version number to key generator 2025-06-17 13:29:25 +02:00
edea8121ce
Simplify code for setExpirationDate() 2025-06-17 13:29:25 +02:00
1acda0e970
Adapt PGPKeyPairGenerator and remove support for generating ElGamal keys 2025-06-17 13:29:25 +02:00
87f3d28567
PGPainless 2.0.0-SNAPSHOT 2025-06-17 13:29:23 +02:00
37042467f4
Bump bc to 1.80-SNAPSHOT, sop-java to 10.1.0-SNAPSHOT 2025-06-17 13:29:07 +02:00
4cf6c6b16a
Update CHANGELOG 2025-06-17 10:42:50 +02:00
0f54cc615c
Bump BC to 1.81, update native-image reflect-config, resource-config 2025-06-17 10:30:42 +02:00
a74db2d26d
Merge pull request #475 from felhag/fix/typo-readme
Fixed typo in sop readme
2025-06-04 16:37:00 +02:00
Felix Hagemans
5f30df6d16 Fixed typo in sop readme 2025-06-04 16:02:23 +02:00
7953ade136
Bump checkstyle to 10.25.0
Fixes https://github.com/pgpainless/pgpainless/security/dependabot/24
2025-06-03 12:37:04 +02:00
0649c041cd
gradle: migrate to new shadow plugin namespace 2025-04-21 19:12:10 +02:00
5a413f53a4
Specify license information for native-image metadata 2025-04-21 19:11:52 +02:00
3b92ccc59d
PGPainless 1.7.7-SNAPSHOT 2025-04-14 16:05:05 +02:00
83613250ef
PGPainless 1.7.6 2025-04-14 15:55:29 +02:00
05c84835e6
Bump SOP-Java to 10.1.1 2025-04-14 15:31:49 +02:00
d20a3b7556
Add config files for nativeimage
Those were generated by running the following commands in order:

gradle -Pagent test
gradle metadataCopy --task test --dir src/main/resources/META-INF/native-image

gradle nativeCompile

The resulting nativeimage can resolve method calls that use reflection. Yay
2025-04-14 15:31:49 +02:00
2d0608cf0f
Re-add shadow plugin 2025-04-13 19:45:12 +02:00
143c9777d6
Implement graal nativeimage compilation
Requires sop-java 10.1.1-SNAPSHOT for now, as that version includes picocli configurations files
2025-04-03 15:27:49 +02:00
9ac928fcf1
Update changelog 2025-03-26 15:04:22 +01:00
811f72ffef
Fix RevocationSignatureBuilder properly calculating 3rd-party delegation revocations 2025-03-26 15:02:52 +01:00
383 changed files with 12907 additions and 10137 deletions

22
BUILD.md Normal file
View file

@ -0,0 +1,22 @@
<!--
SPDX-FileCopyrightText: 2025 Paul Schaub <info@pgpainless.org>
SPDX-License-Identifier: Apache-2.0
-->
# 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.

View file

@ -5,6 +5,15 @@ SPDX-License-Identifier: CC0-1.0
# PGPainless Changelog # PGPainless Changelog
## 1.7.7-SNAPSHOT
- Bump `bcpg-jdk8on` to `1.81`
- Bump `bcprov-jdk18on` to `1.81`
## 1.7.6
- Fix `RevocationSignatureBuilder` properly calculating third-party signatures of type `KeyRevocation` (delegation revocations)
- Enable support for native images
- Re-enable shadow plugin and build fat-jar
## 1.7.5 ## 1.7.5
- Actually attempt to fix Kotlin desugaring. - Actually attempt to fix Kotlin desugaring.
- Bump javaSourceCompatibility and javaTargetCompatibility to 11 - Bump javaSourceCompatibility and javaTargetCompatibility to 11

View file

@ -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 It does so by introducing an intuitive Builder structure, which allows easy
setup of encryption/decryption operations, as well as straight forward key generation. 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. 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. 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. 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 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. > 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. > 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 ```java
String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"... String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"...
PGPSecretKeyRing secretKey = PGPainless.readKeyRing() OpenPGPKey secretKey = PGPainless.getInstance().readKey()
.secretKeyRing(key); .parseKey(key);
``` ```
Similarly, keys can quickly be exported:: Similarly, keys can quickly be exported::
```java ```java
PGPSecretKeyRing secretKey = ...; OpenPGPKey secretKey = ...;
String armored = PGPainless.asciiArmor(secretKey); String armored = secretKey.toAsciiArmoredString();
ByteArrayOutputStream binary = new ByteArrayOutputStream(); byte[] binary = secretKey.getEncoded();
secretKey.encode(binary);
``` ```
Extract a public key certificate from a secret key: Extract a public key certificate from a secret key:
```java ```java
PGPSecretKeyRing secretKey = ...; OpenPGPKey secretKey = ...;
PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); OpenPGPCertificate certificate = secretKey.toCertificate();
``` ```
### Easily Generate Keys ### 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. There are some predefined key archetypes, but it is possible to fully customize key generation to your needs.
```java ```java
PGPainless api = PGPainless.getInstance();
// RSA key without additional subkeys // RSA key without additional subkeys
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() OpenPGPKey secretKeys = api.generateKey()
.simpleRsaKeyRing("Juliet <juliet@montague.lit>", RsaLength._4096); .simpleRsaKeyRing("Juliet <juliet@montague.lit>", RsaLength._4096);
// EdDSA primary key with EdDSA signing- and XDH encryption subkeys // EdDSA primary key with EdDSA signing- and XDH encryption subkeys
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() OpenPGPKey secretKeys = api.generateKey()
.modernKeyRing("Romeo <romeo@montague.lit>", "I defy you, stars!"); .modernKeyRing("Romeo <romeo@montague.lit>", "I defy you, stars!");
// Customized key // Customized key
PGPSecretKeyRing keyRing = PGPainless.buildKeyRing() OpenPGPKey keyRing = api.buildKey()
.setPrimaryKey(KeySpec.getBuilder( .setPrimaryKey(KeySpec.getBuilder(
RSA.withLength(RsaLength._8192), RSA.withLength(RsaLength._8192),
KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) 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. Still it allows you to manually specify which algorithms to use of course.
```java ```java
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() PGPainless api = PGPainless.getInstance();
EncryptionStream encryptionStream = api.generateMessage()
.onOutputStream(outputStream) .onOutputStream(outputStream)
.withOptions( .withOptions(
ProducerOptions.signAndEncrypt( ProducerOptions.signAndEncrypt(
new EncryptionOptions() EncryptionOptions.get(api)
.addRecipient(aliceKey) .addRecipient(aliceKey)
.addRecipient(bobsKey) .addRecipient(bobsKey)
// optionally encrypt to a passphrase // optionally encrypt to a passphrase
.addMessagePassphrase(Passphrase.fromPassword("password123")) .addMessagePassphrase(Passphrase.fromPassword("password123"))
// optionally override symmetric encryption algorithm // optionally override symmetric encryption algorithm
.overrideEncryptionAlgorithm(SymmetricKeyAlgorithm.AES_192), .overrideEncryptionAlgorithm(SymmetricKeyAlgorithm.AES_192),
new SigningOptions() SigningOptions.get(api)
// Sign in-line (using one-pass-signature packet) // Sign in-line (using one-pass-signature packet)
.addInlineSignature(secretKeyDecryptor, aliceSecKey, signatureType) .addInlineSignature(secretKeyDecryptor, aliceSecKey, signatureType)
// Sign using a detached signature // Sign using a detached signature
.addDetachedSignature(secretKeyDecryptor, aliceSecKey, signatureType) .addDetachedSignature(secretKeyDecryptor, aliceSecKey, signatureType)
// optionally override hash algorithm // optionally override hash algorithm
.overrideHashAlgorithm(HashAlgorithm.SHA256) .overrideHashAlgorithm(HashAlgorithm.SHA256),
api
).setAsciiArmor(true) // Ascii armor or not ).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. This behaviour can be modified though using the `Policy` class.
```java ```java
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() DecryptionStream decryptionStream = PGPainless.getInstance().processMessage()
.onInputStream(encryptedInputStream) .onInputStream(encryptedInputStream)
.withOptions(new ConsumerOptions() .withOptions(ConsumerOptions.get(api)
.addDecryptionKey(bobSecKeys, secretKeyProtector) .addDecryptionKey(bobSecKeys, secretKeyProtector)
.addVerificationCert(alicePubKeys) .addVerificationCert(alicePubKeys)
); );
@ -191,7 +193,7 @@ repositories {
} }
dependencies { dependencies {
implementation 'org.pgpainless:pgpainless-core:1.7.5' implementation 'org.pgpainless:pgpainless-core:1.7.6'
} }
``` ```
@ -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. Please follow the [code of conduct](CODE_OF_CONDUCT.md) if you want to be part of the project.
## Acknowledgements ## 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) [![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 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/) [![NGI Assure Logo](https://blog.jabberhead.tk/wp-content/uploads/2022/05/NGIAssure_tag.svg)](https://nlnet.nl/assure/)

View file

@ -93,6 +93,12 @@ precedence = "aggregate"
SPDX-FileCopyrightText = "2022 Paul Schaub <info@pgpainless.org>, 2017 Steve Smith" SPDX-FileCopyrightText = "2022 Paul Schaub <info@pgpainless.org>, 2017 Steve Smith"
SPDX-License-Identifier = "CC-BY-SA-3.0" SPDX-License-Identifier = "CC-BY-SA-3.0"
[[annotations]]
path = "pgpainless-cli/src/main/resources/META-INF/native-image/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 Paul Schaub <info@pgpainless.org>"
SPDX-License-Identifier = "Apache-2.0"
[[annotations]] [[annotations]]
path = "pgpainless-cli/rewriteManPages.sh" path = "pgpainless-cli/rewriteManPages.sh"
precedence = "aggregate" precedence = "aggregate"

View file

@ -18,7 +18,7 @@ buildscript {
} }
plugins { 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 id 'com.diffplug.spotless' version '6.22.0' apply false
} }
@ -37,13 +37,12 @@ allprojects {
// without this we would generate an empty pgpainless.jar for the project root // without this we would generate an empty pgpainless.jar for the project root
// https://stackoverflow.com/a/25445035 // https://stackoverflow.com/a/25445035
jar { jar {
reproducibleFileOrder = true
onlyIf { !sourceSets.main.allSource.files.isEmpty() } onlyIf { !sourceSets.main.allSource.files.isEmpty() }
} }
// checkstyle // checkstyle
checkstyle { checkstyle {
toolVersion = '10.12.1' toolVersion = '10.25.0'
} }
spotless { spotless {

View file

@ -36,3 +36,4 @@ Contents
pgpainless-cli/usage.md pgpainless-cli/usage.md
sop.md sop.md
pgpainless-core/indepth.rst pgpainless-core/indepth.rst
pgpainless-core/migration_2.0.md

View file

@ -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`.

View file

@ -4,6 +4,12 @@
plugins { plugins {
id 'application' id 'application'
id 'org.graalvm.buildtools.native' version '0.10.6'
id 'com.gradleup.shadow' version '8.3.6'
}
graalvmNative {
toolchainDetection = true
} }
dependencies { dependencies {
@ -16,7 +22,8 @@ dependencies {
// implementation "ch.qos.logback:logback-core:1.2.6" // implementation "ch.qos.logback:logback-core:1.2.6"
// We want logback logging in tests and in the app // We want logback logging in tests and in the app
testImplementation "ch.qos.logback:logback-classic:$logbackVersion" testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "ch.qos.logback:logback-classic:$logbackVersion" // implementation "ch.qos.logback:logback-classic:$logbackVersion"
implementation "org.slf4j:slf4j-nop:$slf4jVersion"
implementation(project(":pgpainless-sop")) implementation(project(":pgpainless-sop"))
implementation "org.pgpainless:sop-java-picocli:$sopJavaVersion" implementation "org.pgpainless:sop-java-picocli:$sopJavaVersion"

View file

@ -0,0 +1,7 @@
[
{
"type":"agent-extracted",
"classes":[
]
}
]

View file

@ -0,0 +1,891 @@
[
{
"name":"[Ljava.lang.Object;"
},
{
"name":"ch.qos.logback.classic.encoder.PatternLayoutEncoder",
"queryAllPublicMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.joran.SerializedModelConfigurator",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.classic.util.DefaultJoranConfigurator",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"ch.qos.logback.core.ConsoleAppender",
"queryAllPublicMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"setTarget","parameterTypes":["java.lang.String"] }]
},
{
"name":"ch.qos.logback.core.OutputStreamAppender",
"methods":[{"name":"setEncoder","parameterTypes":["ch.qos.logback.core.encoder.Encoder"] }]
},
{
"name":"ch.qos.logback.core.encoder.Encoder",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"ch.qos.logback.core.encoder.LayoutWrappingEncoder",
"methods":[{"name":"setParent","parameterTypes":["ch.qos.logback.core.spi.ContextAware"] }]
},
{
"name":"ch.qos.logback.core.pattern.PatternLayoutEncoderBase",
"methods":[{"name":"setPattern","parameterTypes":["java.lang.String"] }]
},
{
"name":"ch.qos.logback.core.spi.ContextAware",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"groovy.lang.Closure"
},
{
"name":"java.io.FilePermission"
},
{
"name":"java.lang.Enum"
},
{
"name":"java.lang.Object",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"java.lang.RuntimePermission"
},
{
"name":"java.lang.System",
"methods":[{"name":"console","parameterTypes":[] }]
},
{
"name":"java.lang.invoke.MethodHandle"
},
{
"name":"java.net.NetPermission"
},
{
"name":"java.net.SocketPermission"
},
{
"name":"java.net.URLPermission",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }]
},
{
"name":"java.nio.channels.SelectionKey",
"fields":[{"name":"attachment"}]
},
{
"name":"java.nio.file.Path"
},
{
"name":"java.nio.file.Paths",
"methods":[{"name":"get","parameterTypes":["java.lang.String","java.lang.String[]"] }]
},
{
"name":"java.security.AllPermission"
},
{
"name":"java.security.MessageDigestSpi"
},
{
"name":"java.security.SecureRandomParameters"
},
{
"name":"java.security.SecurityPermission"
},
{
"name":"java.security.cert.PKIXRevocationChecker"
},
{
"name":"java.sql.Connection"
},
{
"name":"java.sql.Driver"
},
{
"name":"java.sql.DriverManager",
"methods":[{"name":"getConnection","parameterTypes":["java.lang.String"] }, {"name":"getDriver","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.sql.Time",
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"java.sql.Timestamp",
"methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.time.Duration",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.Instant",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.LocalDate",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.LocalDateTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.LocalTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.MonthDay",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.OffsetDateTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.OffsetTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.Period",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.Year",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.YearMonth",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.time.ZoneId",
"methods":[{"name":"of","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.time.ZoneOffset",
"methods":[{"name":"of","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.time.ZonedDateTime",
"methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }]
},
{
"name":"java.util.HashSet"
},
{
"name":"java.util.LinkedHashSet"
},
{
"name":"java.util.PropertyPermission"
},
{
"name":"java.util.concurrent.ArrayBlockingQueue"
},
{
"name":"java.util.concurrent.atomic.AtomicReference",
"fields":[{"name":"value"}]
},
{
"name":"java.util.concurrent.locks.AbstractOwnableSynchronizer"
},
{
"name":"java.util.concurrent.locks.AbstractQueuedSynchronizer"
},
{
"name":"java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject"
},
{
"name":"java.util.concurrent.locks.ReentrantLock"
},
{
"name":"java.util.concurrent.locks.ReentrantLock$NonfairSync"
},
{
"name":"java.util.concurrent.locks.ReentrantLock$Sync"
},
{
"name":"javax.smartcardio.CardPermission"
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.COMPOSITE$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.CONTEXT$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.CompositeSignatures$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.DH$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.DSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.DSTU4145$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.Dilithium$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.EC$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.ECGOST$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.EXTERNAL$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.EdEC$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.ElGamal$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.Falcon$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.GM$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.GOST$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.IES$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.LMS$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.MLDSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.MLKEM$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.NoSig$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.SLHDSA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.SPHINCSPlus$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.X509$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.edec.KeyPairGeneratorSpi$EdDSA",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.edec.KeyPairGeneratorSpi$XDH",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.asymmetric.rsa.KeyPairGeneratorSpi",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Blake2b$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Blake2s$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Blake3$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.DSTU7564$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.GOST3411$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Haraka$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Keccak$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.MD2$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.MD4$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.MD5$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.RIPEMD128$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.RIPEMD160$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.RIPEMD256$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.RIPEMD320$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.SHA1$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.SHA224$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.SHA256$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.SHA3$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.SHA384$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.SHA512$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.SM3$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Skein$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Tiger$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.digest.Whirlpool$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.drbg.DRBG$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.keystore.BC$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.keystore.BCFKS$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.keystore.PKCS12$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.AES$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.ARC4$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.ARIA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Blowfish$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.CAST5$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.CAST6$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Camellia$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.ChaCha$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.DES$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.DESede$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.DSTU7624$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.GOST28147$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.GOST3412_2015$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Grain128$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Grainv1$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.HC128$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.HC256$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.IDEA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Noekeon$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.OpenSSLPBKDF$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.PBEPBKDF1$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.PBEPBKDF2$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.PBEPKCS12$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Poly1305$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.RC2$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.RC5$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.RC6$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Rijndael$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.SCRYPT$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.SEED$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.SM4$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Salsa20$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Serpent$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Shacal2$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.SipHash$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.SipHash128$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Skipjack$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.TEA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.TLSKDF$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Threefish$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Twofish$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.VMPC$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.VMPCKSA3$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.XSalsa20$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.XTEA$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.bouncycastle.jcajce.provider.symmetric.Zuc$Mappings",
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.ExitCodeTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"successfulExecutionDoesNotTerminateJVM","parameterTypes":[] }, {"name":"testCommandWithUnknownOption_37","parameterTypes":[] }, {"name":"testUnknownCommand_69","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.TestUtils",
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true
},
{
"name":"org.pgpainless.cli.commands.ArmorCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"armorAlreadyArmoredDataIsIdempotent","parameterTypes":[] }, {"name":"armorMessage","parameterTypes":[] }, {"name":"armorPublicKey","parameterTypes":[] }, {"name":"armorSecretKey","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.CLITest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"methods":[{"name":"cleanup","parameterTypes":[] }, {"name":"setup","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.DearmorCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"dearmorBrokenArmoredKeyFails","parameterTypes":[] }, {"name":"dearmorCertificate","parameterTypes":[] }, {"name":"dearmorGarbageEmitsEmpty","parameterTypes":[] }, {"name":"dearmorMessage","parameterTypes":[] }, {"name":"dearmorSecretKey","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.ExtractCertCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"extractCertFromGarbageFails","parameterTypes":[] }, {"name":"testExtractCert","parameterTypes":[] }, {"name":"testExtractCertFromCertFails","parameterTypes":[] }, {"name":"testExtractCertUnarmored","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.GenerateKeyCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"testGenerateBinaryKey","parameterTypes":[] }, {"name":"testGenerateKey","parameterTypes":[] }, {"name":"testGenerateKeyWithMultipleUserIds","parameterTypes":[] }, {"name":"testGeneratePasswordProtectedKey_missingPasswordFile","parameterTypes":[] }, {"name":"testPasswordProtectedKey","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.InlineDetachCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"detachInbandSignatureAndMessage","parameterTypes":[] }, {"name":"detachInbandSignatureAndMessageNoArmor","parameterTypes":[] }, {"name":"detachMissingSignaturesFromCleartextSignedMessageFails","parameterTypes":[] }, {"name":"detachNonOpenPgpDataFails","parameterTypes":[] }, {"name":"existingSignatureOutCausesException","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.ListProfilesCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"listProfileOfGenerateKey","parameterTypes":[] }, {"name":"listProfilesOfEncrypt","parameterTypes":[] }, {"name":"listProfilesWithoutCommand","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.RoundTripEncryptDecryptCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"decryptGarbageFails","parameterTypes":[] }, {"name":"decryptMalformedMessageYieldsBadData","parameterTypes":[] }, {"name":"decryptMessageWithSessionKey","parameterTypes":[] }, {"name":"decryptMessageWithWrongKeyFails","parameterTypes":[] }, {"name":"decryptWithPasswordWithPendingWhitespaceWorks","parameterTypes":[] }, {"name":"decryptWithWhitespacePasswordWorks","parameterTypes":[] }, {"name":"decrypt_verifyWithGarbageCertFails","parameterTypes":[] }, {"name":"decrypt_withGarbageKeyFails","parameterTypes":[] }, {"name":"encryptAndDecryptAMessage","parameterTypes":[] }, {"name":"encryptAndDecryptMessageWithPassphrase","parameterTypes":[] }, {"name":"encryptWithGarbageCertFails","parameterTypes":[] }, {"name":"encryptWithPasswordADecryptWithPasswordBFails","parameterTypes":[] }, {"name":"encryptWithProtectedKey_wrongPassphraseFails","parameterTypes":[] }, {"name":"encryptWithTrailingWhitespaceDecryptWithoutWorks","parameterTypes":[] }, {"name":"encrypt_signWithGarbageKeyFails","parameterTypes":[] }, {"name":"testDecryptVerifyOut_withoutVerifyWithFails","parameterTypes":[] }, {"name":"testDecryptWithSessionKeyVerifyWithYieldsExpectedVerifications","parameterTypes":[] }, {"name":"testDecryptWithoutDecryptionOptionFails","parameterTypes":[] }, {"name":"testEncryptDecryptRoundTripWithPasswordProtectedKey","parameterTypes":[] }, {"name":"testEncryptDecryptWithFreshRSAKey","parameterTypes":[] }, {"name":"testEncryptWithIncapableCert","parameterTypes":[] }, {"name":"testEncrypt_SignWithCertFails","parameterTypes":[] }, {"name":"testMissingArgumentsIfNoArgsSupplied","parameterTypes":[] }, {"name":"testSessionKeyOutWritesSessionKeyOut","parameterTypes":[] }, {"name":"testSignWithIncapableKey","parameterTypes":[] }, {"name":"testVerificationsOutAlreadyExistFails","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.RoundTripInlineSignInlineVerifyCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"cannotVerifyEncryptedMessage","parameterTypes":[] }, {"name":"cannotVerifyMalformedMessage","parameterTypes":[] }, {"name":"createAndVerifyCleartextSignedMessage","parameterTypes":[] }, {"name":"createAndVerifyMultiKeyBinarySignedMessage","parameterTypes":[] }, {"name":"createAndVerifyTextSignedMessage","parameterTypes":[] }, {"name":"createCleartextSignedMessage","parameterTypes":[] }, {"name":"createMalformedMessage","parameterTypes":[] }, {"name":"createSignedMessageWithKeyAAndVerifyWithKeyBFails","parameterTypes":[] }, {"name":"createTextSignedMessageInlineDetachAndDetachedVerify","parameterTypes":[] }, {"name":"signWithProtectedKeyWithWrongPassphraseFails","parameterTypes":[] }, {"name":"testInlineSignWithMissingSecretKeysFails","parameterTypes":[] }, {"name":"testUnlockKeyWithOneOfMultiplePasswords","parameterTypes":[] }, {"name":"verifyPrependedSignedMessage","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.RoundTripInlineSignVerifyCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"encryptAndDecryptAMessage","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.RoundTripSignVerifyCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"createArmoredSignature","parameterTypes":[] }, {"name":"createUnarmoredSignature","parameterTypes":[] }, {"name":"signWithProtectedKey","parameterTypes":[] }, {"name":"signWithProtectedKey_missingPassphraseFails","parameterTypes":[] }, {"name":"signWithProtectedKey_wrongPassphraseFails","parameterTypes":[] }, {"name":"testNotAfter","parameterTypes":[] }, {"name":"testNotBefore","parameterTypes":[] }, {"name":"testSignWithIncapableKey","parameterTypes":[] }, {"name":"testSignatureCreationAndVerification","parameterTypes":[] }, {"name":"unarmorArmoredSigAndVerify","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.commands.VersionCmdTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"testExtendedVersion","parameterTypes":[] }, {"name":"testGetBackendVersion","parameterTypes":[] }, {"name":"testSopSpecVersion","parameterTypes":[] }, {"name":"testVersion","parameterTypes":[] }]
},
{
"name":"org.pgpainless.cli.misc.SignUsingPublicKeyBehaviorTest",
"allDeclaredFields":true,
"allDeclaredClasses":true,
"queryAllDeclaredMethods":true,
"queryAllPublicMethods":true,
"queryAllDeclaredConstructors":true,
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"testSignatureCreationAndVerification","parameterTypes":[] }]
},
{
"name":"picocli.AutoComplete$GenerateCompletion",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"picocli.CommandLine$AutoHelpMixin",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"picocli.CommandLine$HelpCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"sop.cli.picocli.SopCLI",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.SopCLI$InitLocale",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"sop.cli.picocli.commands.AbstractSopCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"sop.cli.picocli.commands.ArmorCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.ChangeKeyPasswordCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"sop.cli.picocli.commands.DearmorCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.DecryptCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.EncryptCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.ExtractCertCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.GenerateKeyCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.InlineDetachCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.InlineSignCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.InlineVerifyCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.ListProfilesCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.RevokeKeyCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"sop.cli.picocli.commands.SignCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.VerifyCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.VersionCmd",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sop.cli.picocli.commands.VersionCmd$Exclusive",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true,
"methods":[{"name":"<init>","parameterTypes":[] }]
},
{
"name":"sun.security.provider.NativePRNG",
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"<init>","parameterTypes":["java.security.SecureRandomParameters"] }]
},
{
"name":"sun.security.provider.SHA",
"methods":[{"name":"<init>","parameterTypes":[] }]
}
]

View file

@ -0,0 +1,93 @@
{
"resources":{
"includes":[{
"pattern":"\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E"
}, {
"pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E"
}, {
"pattern":"\\QMETA-INF/services/java.nio.channels.spi.SelectorProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/java.util.spi.ResourceBundleControlProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/javax.xml.parsers.SAXParserFactory\\E"
}, {
"pattern":"\\QMETA-INF/services/org.junit.platform.engine.TestEngine\\E"
}, {
"pattern":"\\QMETA-INF/services/org.junit.platform.launcher.LauncherDiscoveryListener\\E"
}, {
"pattern":"\\QMETA-INF/services/org.junit.platform.launcher.LauncherSessionListener\\E"
}, {
"pattern":"\\QMETA-INF/services/org.junit.platform.launcher.PostDiscoveryFilter\\E"
}, {
"pattern":"\\QMETA-INF/services/org.junit.platform.launcher.TestExecutionListener\\E"
}, {
"pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E"
}, {
"pattern":"\\Qjunit-platform.properties\\E"
}, {
"pattern":"\\Qlogback-test.scmo\\E"
}, {
"pattern":"\\Qlogback-test.xml\\E"
}, {
"pattern":"\\Qlogback.scmo\\E"
}, {
"pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E"
}, {
"pattern":"\\Qpgpainless-sop.properties\\E"
}, {
"pattern":"\\Qsop-java-version.properties\\E"
}, {
"pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E"
}]},
"bundles":[{
"name":"msg_armor",
"locales":["de", "und"]
}, {
"name":"msg_change-key-password",
"locales":["de", "und"]
}, {
"name":"msg_dearmor",
"locales":["de", "und"]
}, {
"name":"msg_decrypt",
"locales":["de", "und"]
}, {
"name":"msg_detached-sign",
"locales":["de", "und"]
}, {
"name":"msg_detached-verify",
"locales":["de", "und"]
}, {
"name":"msg_encrypt",
"locales":["de", "und"]
}, {
"name":"msg_extract-cert",
"locales":["de", "und"]
}, {
"name":"msg_generate-key",
"locales":["de", "und"]
}, {
"name":"msg_inline-detach",
"locales":["de", "und"]
}, {
"name":"msg_inline-sign",
"locales":["de", "und"]
}, {
"name":"msg_inline-verify",
"locales":["de", "und"]
}, {
"name":"msg_list-profiles",
"locales":["de", "und"]
}, {
"name":"msg_revoke-key",
"locales":["de", "und"]
}, {
"name":"msg_sop",
"locales":["de", "und"]
}, {
"name":"msg_version",
"locales":["de", "und"]
}]
}

View file

@ -0,0 +1,41 @@
{
"types":[
{
"name":"java.lang.Enum"
},
{
"name":"java.lang.Object[]"
},
{
"name":"java.util.HashSet"
},
{
"name":"java.util.LinkedHashSet"
},
{
"name":"java.util.concurrent.ArrayBlockingQueue"
},
{
"name":"java.util.concurrent.locks.AbstractOwnableSynchronizer"
},
{
"name":"java.util.concurrent.locks.AbstractQueuedSynchronizer"
},
{
"name":"java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject"
},
{
"name":"java.util.concurrent.locks.ReentrantLock"
},
{
"name":"java.util.concurrent.locks.ReentrantLock$NonfairSync"
},
{
"name":"java.util.concurrent.locks.ReentrantLock$Sync"
}
],
"lambdaCapturingTypes":[
],
"proxies":[
]
}

View file

@ -11,12 +11,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.api.OpenPGPCertificate;
import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.api.OpenPGPKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless; import org.pgpainless.PGPainless;
import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.info.KeyRingInfo;
@ -25,24 +22,26 @@ import sop.exception.SOPGPException;
public class ExtractCertCmdTest extends CLITest { public class ExtractCertCmdTest extends CLITest {
private final PGPainless api = PGPainless.getInstance();
public ExtractCertCmdTest() { public ExtractCertCmdTest() {
super(LoggerFactory.getLogger(ExtractCertCmdTest.class)); super(LoggerFactory.getLogger(ExtractCertCmdTest.class));
} }
@Test @Test
public void testExtractCert() public void testExtractCert()
throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { throws IOException {
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() OpenPGPKey key = api.generateKey()
.simpleEcKeyRing("Juliet Capulet <juliet@capulet.lit>"); .simpleEcKeyRing("Juliet Capulet <juliet@capulet.lit>");
pipeBytesToStdin(secretKeys.getEncoded()); pipeBytesToStdin(key.getEncoded());
ByteArrayOutputStream out = pipeStdoutToStream(); ByteArrayOutputStream out = pipeStdoutToStream();
assertSuccess(executeCommand("extract-cert", "--armor")); assertSuccess(executeCommand("extract-cert", "--armor"));
assertTrue(out.toString().startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n")); assertTrue(out.toString().startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"));
PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(out.toByteArray()); OpenPGPCertificate certificate = api.readKey().parseCertificate(out.toByteArray());
KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys); KeyRingInfo info = api.inspect(certificate);
assertFalse(info.isSecretKey()); assertFalse(info.isSecretKey());
assertTrue(info.isUserIdValid("Juliet Capulet <juliet@capulet.lit>")); assertTrue(info.isUserIdValid("Juliet Capulet <juliet@capulet.lit>"));
} }

View file

@ -11,13 +11,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.api.OpenPGPCertificate;
import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.api.OpenPGPKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless; import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.KeyFlag; 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.KeyType;
import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve;
import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec;
import org.pgpainless.sop.EncryptImpl;
import org.pgpainless.sop.GenerateKeyImpl;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
@ -138,7 +136,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
} }
@Test @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 { public void testEncryptingForKeyFails() throws IOException {
File notACert = writeFile("key.asc", KEY); File notACert = writeFile("key.asc", KEY);
@ -298,14 +296,14 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
} }
@Test @Test
public void testEncryptWithIncapableCert() throws PGPException, public void testEncryptWithIncapableCert() throws IOException {
InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPainless api = PGPainless.getInstance();
PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() OpenPGPKey key = api.buildKey()
.addUserId("No Crypt <no@crypt.key>") .addUserId("No Crypt <no@crypt.key>")
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519),
KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA))
.build(); .build();
PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); OpenPGPCertificate cert = key.toCertificate();
File certFile = writeFile("cert.pgp", cert.getEncoded()); File certFile = writeFile("cert.pgp", cert.getEncoded());
pipeStringToStdin("Hello, World!\n"); pipeStringToStdin("Hello, World!\n");
@ -318,15 +316,16 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
@Test @Test
public void testSignWithIncapableKey() public void testSignWithIncapableKey()
throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { throws IOException {
PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() PGPainless api = PGPainless.getInstance();
OpenPGPKey key = api.buildKey()
.addUserId("Cannot Sign <cannot@sign.key>") .addUserId("Cannot Sign <cannot@sign.key>")
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER))
.addSubkey(KeySpec.getBuilder( .addSubkey(KeySpec.getBuilder(
KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE))
.build(); .build();
File keyFile = writeFile("key.pgp", secretKeys.getEncoded()); File keyFile = writeFile("key.pgp", key.getEncoded());
File certFile = writeFile("cert.pgp", PGPainless.extractCertificate(secretKeys).getEncoded()); File certFile = writeFile("cert.pgp", key.toCertificate().getEncoded());
pipeStringToStdin("Hello, World!\n"); pipeStringToStdin("Hello, World!\n");
ByteArrayOutputStream out = pipeStdoutToStream(); ByteArrayOutputStream out = pipeStdoutToStream();
@ -650,7 +649,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
// Generate key // Generate key
File passwordFile = writeFile("password", "sw0rdf1sh"); File passwordFile = writeFile("password", "sw0rdf1sh");
File keyFile = pipeStdoutToFile("key.asc"); File keyFile = pipeStdoutToFile("key.asc");
assertSuccess(executeCommand("generate-key", "--profile=rfc4880", "--with-key-password", passwordFile.getAbsolutePath(), "Alice <alice@example.org>")); assertSuccess(executeCommand("generate-key", "--profile=" + GenerateKeyImpl.RFC4880_RSA4096_PROFILE.getName(), "--with-key-password", passwordFile.getAbsolutePath(), "Alice <alice@example.org>"));
File certFile = pipeStdoutToFile("cert.asc"); File certFile = pipeStdoutToFile("cert.asc");
pipeFileToStdin(keyFile); pipeFileToStdin(keyFile);
@ -662,7 +661,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
// Encrypt // Encrypt
File ciphertextFile = pipeStdoutToFile("msg.asc"); File ciphertextFile = pipeStdoutToFile("msg.asc");
pipeFileToStdin(plaintextFile); pipeFileToStdin(plaintextFile);
assertSuccess(executeCommand("encrypt", "--profile=rfc4880", certFile.getAbsolutePath())); assertSuccess(executeCommand("encrypt", "--profile=" + EncryptImpl.RFC4880_PROFILE.getName(), certFile.getAbsolutePath()));
ByteArrayOutputStream decrypted = pipeStdoutToStream(); ByteArrayOutputStream decrypted = pipeStdoutToStream();
pipeFileToStdin(ciphertextFile); pipeFileToStdin(ciphertextFile);

View file

@ -15,8 +15,8 @@ import java.nio.charset.StandardCharsets;
import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.api.OpenPGPKey;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless; import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.CompressionAlgorithm;
@ -350,12 +350,13 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest {
@Test @Test
public void createMalformedMessage() throws IOException, PGPException { public void createMalformedMessage() throws IOException, PGPException {
PGPainless api = PGPainless.getInstance();
String msg = "Hello, World!\n"; String msg = "Hello, World!\n";
PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY_2); OpenPGPKey key = api.readKey().parseKey(KEY_2);
ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); ByteArrayOutputStream ciphertext = new ByteArrayOutputStream();
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() EncryptionStream encryptionStream = api.generateMessage()
.onOutputStream(ciphertext) .onOutputStream(ciphertext)
.withOptions(ProducerOptions.sign(SigningOptions.get() .withOptions(ProducerOptions.sign(SigningOptions.get(api)
.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key) .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key)
).overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) ).overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)
.setAsciiArmor(false)); .setAsciiArmor(false));

View file

@ -11,12 +11,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException; import java.text.ParseException;
import java.util.Date; import java.util.Date;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -199,12 +196,13 @@ public class RoundTripSignVerifyCmdTest extends CLITest {
@Test @Test
public void testSignWithIncapableKey() public void testSignWithIncapableKey()
throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { throws IOException {
PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing()
.addUserId("Cannot Sign <cannot@sign.key>") .addUserId("Cannot Sign <cannot@sign.key>")
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .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)) .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE))
.build(); .build()
.getPGPSecretKeyRing();
File keyFile = writeFile("key.pgp", secretKeys.getEncoded()); File keyFile = writeFile("key.pgp", secretKeys.getEncoded());
pipeStringToStdin("Hello, World!\n"); pipeStringToStdin("Hello, World!\n");
@ -252,7 +250,7 @@ public class RoundTripSignVerifyCmdTest extends CLITest {
String verification = verificationsOut.toString(); String verification = verificationsOut.toString();
String[] split = verification.split(" "); String[] split = verification.split(" ");
OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(cert); 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(signingKeyFingerprint.toString(), split[1].trim(), verification);
assertEquals(primaryKeyFingerprint.toString(), split[2].trim()); assertEquals(primaryKeyFingerprint.toString(), split[2].trim());

View file

@ -1,31 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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;
}
}

View file

@ -1,210 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <a href="https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;hb=HEAD#l1489">
* GnuPGs doc/DETAILS - GNU extensions to the S2K algorithm</a>
*/
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<SubkeyIdentifier> getIdsOfKeysWithGnuPGS2KDivertedToCard(@Nonnull PGPSecretKeyRing secretKeys) {
Set<SubkeyIdentifier> 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<PGPSecretKey> 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<Long> ids) {
// noinspection Convert2MethodRef
return keyId -> ids.contains(keyId);
}
}
}

View file

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Utility classes related to creating keys with GNU DUMMY S2K values.
*/
package org.gnupg;

View file

@ -1,417 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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();
}
}

View file

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Classes used to decryption and verification of OpenPGP encrypted / signed data.
*/
package org.pgpainless.decryption_verification;

View file

@ -1,132 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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.";
}
}
}

View file

@ -1,16 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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");
}
}

View file

@ -1,30 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <a href="https://www.rfc-editor.org/rfc/rfc4880#section-11.3">RFC4880 §11.3. OpenPGP Messages</a>
*/
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);
}
}

View file

@ -1,15 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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.");
}
}

View file

@ -1,19 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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);
}
}

View file

@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<SubkeyIdentifier> keyIds;
public MissingPassphraseException(Set<SubkeyIdentifier> keyIds) {
super("Missing passphrase encountered for keys " + Arrays.toString(keyIds.toArray()));
this.keyIds = Collections.unmodifiableSet(keyIds);
}
public Set<SubkeyIdentifier> getKeyIds() {
return keyIds;
}
}

View file

@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 {
}

View file

@ -1,44 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<PGPSignature, Exception> rejections) {
super(message + ": " + exceptionMapToString(rejections));
}
private static String exceptionMapToString(Map<PGPSignature, Exception> 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();
}
}

View file

@ -1,17 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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);
}
}

View file

@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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);
}
}

View file

@ -1,22 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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);
}
}

View file

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Exceptions.
*/
package org.pgpainless.exception;

View file

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Classes related to OpenPGP keys.
*/
package org.pgpainless.key;

View file

@ -1,113 +0,0 @@
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<HashAlgorithm> getPreferredHashAlgorithms(PGPPublicKey publicKey) {
List<HashAlgorithm> 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<HashAlgorithm> 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<HashAlgorithm> getOrGuessPreferredHashAlgorithms(PGPPublicKey publicKey) {
List<HashAlgorithm> preferredHashAlgorithms = OpenPgpKeyAttributeUtil.getPreferredHashAlgorithms(publicKey);
if (preferredHashAlgorithms.isEmpty()) {
preferredHashAlgorithms = OpenPgpKeyAttributeUtil.guessPreferredHashAlgorithms(publicKey);
}
return new LinkedHashSet<>(preferredHashAlgorithms);
}
}

View file

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Utility functions to deal with OpenPGP keys.
*/
package org.pgpainless.key.util;

View file

@ -1,10 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* PGPainless - Use OpenPGP Painlessly!
*
* @see <a href="http://pgpainless.org">org.pgpainless.core.org</a>
*/
package org.pgpainless;

View file

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Utility classes.
*/
package org.pgpainless.util;

View file

@ -1,49 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <R> Type of {@link org.bouncycastle.openpgp.PGPKeyRing} ({@link org.bouncycastle.openpgp.PGPSecretKeyRing}
* or {@link org.bouncycastle.openpgp.PGPPublicKeyRing}).
* @param <C> Type of key ring collection (e.g. {@link org.bouncycastle.openpgp.PGPSecretKeyRingCollection}
* or {@link org.bouncycastle.openpgp.PGPPublicKeyRingCollection}).
* @param <O> Type of key identifier
*/
public interface KeyRingSelectionStrategy<R, C, O> {
/**
* Return true, if the filter accepts the given <pre>keyRing</pre> based on the given <pre>identifier</pre>.
*
* @param identifier identifier
* @param keyRing key ring
* @return acceptance
*/
boolean accept(O identifier, R keyRing);
/**
* Iterate of the given <pre>keyRingCollection</pre> and return a {@link Set} of all acceptable
* keyRings in the collection, based on the given <pre>identifier</pre>.
*
* @param identifier identifier
* @param keyRingCollection collection
* @return set of acceptable key rings
*/
Set<R> 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<O, R> selectKeyRingsFromCollections(MultiMap<O, C> keyRingCollections);
}

View file

@ -1,44 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <O> Type of identifier
*/
public abstract class PublicKeyRingSelectionStrategy<O> implements KeyRingSelectionStrategy<PGPPublicKeyRing, PGPPublicKeyRingCollection, O> {
@Override
public Set<PGPPublicKeyRing> selectKeyRingsFromCollection(@Nonnull O identifier, @Nonnull PGPPublicKeyRingCollection keyRingCollection) {
Set<PGPPublicKeyRing> accepted = new HashSet<>();
for (Iterator<PGPPublicKeyRing> i = keyRingCollection.getKeyRings(); i.hasNext(); ) {
PGPPublicKeyRing ring = i.next();
if (accept(identifier, ring)) accepted.add(ring);
}
return accepted;
}
@Override
public MultiMap<O, PGPPublicKeyRing> selectKeyRingsFromCollections(@Nonnull MultiMap<O, PGPPublicKeyRingCollection> keyRingCollections) {
MultiMap<O, PGPPublicKeyRing> keyRings = new MultiMap<>();
for (Map.Entry<O, Set<PGPPublicKeyRingCollection>> entry : keyRingCollections.entrySet()) {
for (PGPPublicKeyRingCollection collection : entry.getValue()) {
keyRings.plus(entry.getKey(), selectKeyRingsFromCollection(entry.getKey(), collection));
}
}
return keyRings;
}
}

View file

@ -1,43 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <O> Type of identifier
*/
public abstract class SecretKeyRingSelectionStrategy<O> implements KeyRingSelectionStrategy<PGPSecretKeyRing, PGPSecretKeyRingCollection, O> {
@Override
public Set<PGPSecretKeyRing> selectKeyRingsFromCollection(O identifier, @Nonnull PGPSecretKeyRingCollection keyRingCollection) {
Set<PGPSecretKeyRing> accepted = new HashSet<>();
for (Iterator<PGPSecretKeyRing> i = keyRingCollection.getKeyRings(); i.hasNext(); ) {
PGPSecretKeyRing ring = i.next();
if (accept(identifier, ring)) accepted.add(ring);
}
return accepted;
}
@Override
public MultiMap<O, PGPSecretKeyRing> selectKeyRingsFromCollections(@Nonnull MultiMap<O, PGPSecretKeyRingCollection> keyRingCollections) {
MultiMap<O, PGPSecretKeyRing> keyRings = new MultiMap<>();
for (Map.Entry<O, Set<PGPSecretKeyRingCollection>> entry : keyRingCollections.entrySet()) {
for (PGPSecretKeyRingCollection collection : entry.getValue()) {
keyRings.plus(entry.getKey(), selectKeyRingsFromCollection(entry.getKey(), collection));
}
}
return keyRings;
}
}

View file

@ -1,56 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>identifier</pre>.
*/
public static class PubRingSelectionStrategy extends PublicKeyRingSelectionStrategy<String> {
@Override
public boolean accept(String identifier, PGPPublicKeyRing keyRing) {
List<String> 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 <pre>identifier</pre>.
*/
public static class SecRingSelectionStrategy extends SecretKeyRingSelectionStrategy<String> {
@Override
public boolean accept(String identifier, PGPSecretKeyRing keyRing) {
List<String> userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keyRing.getPublicKey());
for (String userId : userIds) {
if (userId.equals(identifier)) return true;
}
return false;
}
}
}

View file

@ -1,92 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>whitelist</pre> 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 <O> Type of identifier for {@link org.bouncycastle.openpgp.PGPPublicKeyRingCollection PGPPublicKeyRingCollections}.
*/
public static class PubRingSelectionStrategy<O> extends PublicKeyRingSelectionStrategy<O> {
private final MultiMap<O, Long> whitelist;
public PubRingSelectionStrategy(MultiMap<O, Long> whitelist) {
this.whitelist = whitelist;
}
public PubRingSelectionStrategy(Map<O, Set<Long>> whitelist) {
this(new MultiMap<>(whitelist));
}
@Override
public boolean accept(O identifier, PGPPublicKeyRing keyRing) {
Set<Long> 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 <pre>whitelist</pre> 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 <O> Type of identifier for {@link org.bouncycastle.openpgp.PGPSecretKeyRingCollection PGPSecretKeyRingCollections}.
*/
public static class SecRingSelectionStrategy<O> extends SecretKeyRingSelectionStrategy<O> {
private final MultiMap<O, Long> whitelist;
public SecRingSelectionStrategy(MultiMap<O, Long> whitelist) {
this.whitelist = whitelist;
}
public SecRingSelectionStrategy(Map<O, Set<Long>> whitelist) {
this(new MultiMap<>(whitelist));
}
@Override
public boolean accept(O identifier, PGPSecretKeyRing keyRing) {
Set<Long> whitelistedKeyIds = whitelist.get(identifier);
if (whitelistedKeyIds == null) {
return false;
}
return whitelistedKeyIds.contains(keyRing.getPublicKey().getKeyID());
}
}
}

View file

@ -1,36 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<O> extends PublicKeyRingSelectionStrategy<O> {
@Override
public boolean accept(O identifier, PGPPublicKeyRing keyRing) {
return true;
}
}
public static class SecRingSelectionStrategy<O> extends SecretKeyRingSelectionStrategy<O> {
@Override
public boolean accept(O identifier, PGPSecretKeyRing keyRing) {
return true;
}
}
}

View file

@ -1,53 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <pre>jid</pre>.
*
* The argument <pre>jid</pre> 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 <pre>jid</pre>.
*
* The argument <pre>jid</pre> 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);
}
}
}

View file

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Implementations of Key Ring Selection Strategies.
*/
package org.pgpainless.util.selection.keyring.impl;

View file

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Different Key Ring Selection Strategies.
*/
package org.pgpainless.util.selection.keyring;

View file

@ -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 * 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. * expiration dates), this method will return 'null' if seconds is 0.
* *
* @param date date
* @param seconds number of seconds to be added * @param seconds number of seconds to be added
* @return date plus seconds or null if seconds is '0' * @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) return if (seconds == 0L) null else Date(this.time + 1000 * seconds)
} }
/** This date in seconds since epoch. */
val Date.asSeconds: Long val Date.asSeconds: Long
get() = time / 1000 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 { fun Date.secondsTill(later: Date): Long {
require(this <= later) { "Timestamp MUST be before the later timestamp." } require(this <= later) { "Timestamp MUST be before the later timestamp." }
return later.asSeconds - this.asSeconds return later.asSeconds - this.asSeconds

View file

@ -9,7 +9,7 @@ fun Long.openPgpKeyId(): String {
return String.format("%016X", this).uppercase() 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 { fun Long.Companion.fromOpenPgpKeyId(hexKeyId: String): Long {
require("^[0-9A-Fa-f]{16}$".toRegex().matches(hexKeyId)) { require("^[0-9A-Fa-f]{16}$".toRegex().matches(hexKeyId)) {
"Provided long key-id does not match expected format. " + "Provided long key-id does not match expected format. " +

View file

@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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)
}

View file

@ -0,0 +1,206 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<SubkeyIdentifier> =
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<KeyIdentifier>) = KeyFilter { ids.contains(it) }
}
}
}

View file

@ -4,12 +4,27 @@
package org.pgpainless package org.pgpainless
import java.io.ByteArrayOutputStream
import java.io.OutputStream import java.io.OutputStream
import java.util.* 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.PGPKeyRing
import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPPublicKeyRing
import org.bouncycastle.openpgp.PGPSecretKeyRing import org.bouncycastle.openpgp.PGPSecretKeyRing
import org.bouncycastle.openpgp.PGPSignature 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.decryption_verification.DecryptionBuilder
import org.pgpainless.encryption_signing.EncryptionBuilder import org.pgpainless.encryption_signing.EncryptionBuilder
import org.pgpainless.key.certification.CertifyCertificate 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.info.KeyRingInfo
import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor
import org.pgpainless.key.parsing.KeyRingReader import org.pgpainless.key.parsing.KeyRingReader
import org.pgpainless.key.util.KeyRingUtils
import org.pgpainless.policy.Policy import org.pgpainless.policy.Policy
import org.pgpainless.util.ArmorUtils 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 { 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 * @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. * Build a custom OpenPGP key ring.
* *
* @return builder * @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. * Read an existing OpenPGP key ring.
* *
* @return builder * @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. * Extract a public key certificate from a secret key.
@ -54,8 +305,9 @@ class PGPainless private constructor() {
* @return public key certificate * @return public key certificate
*/ */
@JvmStatic @JvmStatic
fun extractCertificate(secretKey: PGPSecretKeyRing) = @Deprecated("Use .toKey() and then .toCertificate() instead.")
KeyRingUtils.publicKeyRingFrom(secretKey) 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 * 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 * @throws PGPException in case of an error
*/ */
@JvmStatic @JvmStatic
fun mergeCertificate(originalCopy: PGPPublicKeyRing, updatedCopy: PGPPublicKeyRing) = @Deprecated(
PGPPublicKeyRing.join(originalCopy, updatedCopy) "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. * 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 * @throws IOException in case of an error during the armoring process
*/ */
@JvmStatic @JvmStatic
fun asciiArmor(key: PGPKeyRing) = fun asciiArmor(key: PGPKeyRing): String =
if (key is PGPSecretKeyRing) ArmorUtils.toAsciiArmoredString(key) getInstance().toAsciiArmor(getInstance().toKeyOrCertificate(key))
else ArmorUtils.toAsciiArmoredString(key as PGPPublicKeyRing)
@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 * 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 * @throws IOException in case of an error during the armoring process
*/ */
@JvmStatic @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 * Create an [EncryptionBuilder], which can be used to encrypt and/or sign data using
@ -113,7 +377,11 @@ class PGPainless private constructor() {
* *
* @return builder * @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 * Create a [DecryptionBuilder], which can be used to decrypt and/or verify data using
@ -121,7 +389,11 @@ class PGPainless private constructor() {
* *
* @return builder * @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 * 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 @JvmStatic
@JvmOverloads @JvmOverloads
fun modifyKeyRing(secretKey: PGPSecretKeyRing, referenceTime: Date = Date()) = fun modifyKeyRing(
SecretKeyRingEditor(secretKey, referenceTime) secretKey: PGPSecretKeyRing,
referenceTime: Date = Date()
): SecretKeyRingEditor = getInstance().modify(getInstance().toKey(secretKey), referenceTime)
/** /**
* Quickly access information about a [org.bouncycastle.openpgp.PGPPublicKeyRing] / * Quickly access information about a [org.bouncycastle.openpgp.PGPPublicKeyRing] /
* [PGPSecretKeyRing]. This method can be used to determine expiration dates, key flags and * [PGPSecretKeyRing]. This method can be used to determine expiration dates, key flags and
* other information about a key at a specific time. * other information about a key at a specific time.
* *
* @param keyRing key ring * @param key key ring
* @param referenceTime date of inspection * @param referenceTime date of inspection
* @return access object * @return access object
*/ */
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
fun inspectKeyRing(key: PGPKeyRing, referenceTime: Date = Date()) = @Deprecated(
KeyRingInfo(key, referenceTime) "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. * Access, and make changes to PGPainless policy on acceptable/default algorithms etc.
* *
* @return policy * @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. * Create different kinds of signatures on other keys.
* *
* @return builder * @return builder
*/ */
@JvmStatic fun certify() = CertifyCertificate() @Deprecated(
"Call .generateCertification() on an instance of PGPainless instead.",
replaceWith = ReplaceWith("generateCertification()"))
@JvmStatic
fun certify(): CertifyCertificate = getInstance().generateCertification()
} }
} }

View file

@ -4,35 +4,75 @@
package org.pgpainless.algorithm 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) { enum class AEADAlgorithm(val algorithmId: Int, val ivLength: Int, val tagLength: Int) {
/** /**
* Encrypt-then-Authenticate-then-Translate mode. * 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), EAX(1, 16, 16),
/** /**
* Offset-Codebook mode. OCB is mandatory to implement in crypto-refresh. Favored by GnuPG. Is * 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. * 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), OCB(2, 15, 16),
/** /**
* Galois/Counter-Mode. GCM is controversial. Some say it is hard to get right. Some * 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. * 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), 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 { 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 @JvmStatic
fun fromId(id: Int): AEADAlgorithm? { fun fromId(id: Int): AEADAlgorithm? {
return values().firstOrNull { algorithm -> algorithm.algorithmId == id } 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 @JvmStatic
fun requireFromId(id: Int): AEADAlgorithm { fun requireFromId(id: Int): AEADAlgorithm {
return fromId(id) ?: throw NoSuchElementException("No AEADAlgorithm found for id $id") return fromId(id) ?: throw NoSuchElementException("No AEADAlgorithm found for id $id")

View file

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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))
}

View file

@ -4,18 +4,91 @@
package org.pgpainless.algorithm package org.pgpainless.algorithm
class AlgorithmSuite( class AlgorithmSuite
symmetricKeyAlgorithms: List<SymmetricKeyAlgorithm>, private constructor(
hashAlgorithms: List<HashAlgorithm>, val symmetricKeyAlgorithms: Set<SymmetricKeyAlgorithm>?,
compressionAlgorithms: List<CompressionAlgorithm> val hashAlgorithms: Set<HashAlgorithm>?,
val compressionAlgorithms: Set<CompressionAlgorithm>?,
val aeadAlgorithms: Set<AEADCipherMode>?,
val features: Set<Feature>?
) { ) {
val symmetricKeyAlgorithms: Set<SymmetricKeyAlgorithm> = symmetricKeyAlgorithms.toSet() constructor(
val hashAlgorithms: Set<HashAlgorithm> = hashAlgorithms.toSet() symmetricKeyAlgorithms: List<SymmetricKeyAlgorithm>?,
val compressionAlgorithms: Set<CompressionAlgorithm> = compressionAlgorithms.toSet() hashAlgorithms: List<HashAlgorithm>?,
compressionAlgorithms: List<CompressionAlgorithm>?,
aeadAlgorithms: List<AEADCipherMode>?,
features: List<Feature>?
) : 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<SymmetricKeyAlgorithm>? =
suite?.symmetricKeyAlgorithms
private var hashAlgorithms: Set<HashAlgorithm>? = suite?.hashAlgorithms
private var compressionAlgorithms: Set<CompressionAlgorithm>? = suite?.compressionAlgorithms
private var aeadAlgorithms: Set<AEADCipherMode>? = suite?.aeadAlgorithms
private var features: Set<Feature>? = suite?.features
fun overrideSymmetricKeyAlgorithms(
vararg symmetricKeyAlgorithms: SymmetricKeyAlgorithm
): Builder = overrideSymmetricKeyAlgorithms(symmetricKeyAlgorithms.toSet())
fun overrideSymmetricKeyAlgorithms(
symmetricKeyAlgorithms: Collection<SymmetricKeyAlgorithm>?
): Builder = apply { this.symmetricKeyAlgorithms = symmetricKeyAlgorithms?.toSet() }
fun overrideHashAlgorithms(vararg hashAlgorithms: HashAlgorithm): Builder =
overrideHashAlgorithms(hashAlgorithms.toSet())
fun overrideHashAlgorithms(hashAlgorithms: Collection<HashAlgorithm>?): Builder = apply {
this.hashAlgorithms = hashAlgorithms?.toSet()
}
fun overrideCompressionAlgorithms(
vararg compressionAlgorithms: CompressionAlgorithm
): Builder = overrideCompressionAlgorithms(compressionAlgorithms.toSet())
fun overrideCompressionAlgorithms(
compressionAlgorithms: Collection<CompressionAlgorithm>?
): Builder = apply { this.compressionAlgorithms = compressionAlgorithms?.toSet() }
fun overrideAeadAlgorithms(vararg aeadAlgorithms: AEADCipherMode): Builder =
overrideAeadAlgorithms(aeadAlgorithms.toSet())
fun overrideAeadAlgorithms(aeadAlgorithms: Collection<AEADCipherMode>?): Builder = apply {
this.aeadAlgorithms = aeadAlgorithms?.toSet()
}
fun overrideFeatures(vararg features: Feature): Builder = overrideFeatures(features.toSet())
fun overrideFeatures(features: Collection<Feature>?): Builder = apply {
this.features = features?.toSet()
}
fun build(): AlgorithmSuite {
return AlgorithmSuite(
symmetricKeyAlgorithms,
hashAlgorithms,
compressionAlgorithms,
aeadAlgorithms,
features)
}
}
companion object { companion object {
@JvmStatic
fun emptyBuilder(): Builder {
return Builder()
}
@JvmStatic @JvmStatic
val defaultSymmetricKeyAlgorithms = val defaultSymmetricKeyAlgorithms =
listOf( listOf(
@ -39,9 +112,26 @@ class AlgorithmSuite(
CompressionAlgorithm.ZIP, CompressionAlgorithm.ZIP,
CompressionAlgorithm.UNCOMPRESSED) 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 @JvmStatic
val defaultAlgorithmSuite = val defaultAlgorithmSuite =
AlgorithmSuite( AlgorithmSuite(
defaultSymmetricKeyAlgorithms, defaultHashAlgorithms, defaultCompressionAlgorithms) defaultSymmetricKeyAlgorithms,
defaultHashAlgorithms,
defaultCompressionAlgorithms,
defaultAEADAlgorithmSuites,
defaultFeatures)
} }
} }

View file

@ -4,11 +4,13 @@
package org.pgpainless.algorithm 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... */ /** 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... */ /** 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. */ /** The stream will use keys with either flags to encrypt the data. */
ANY ANY(KeyFlags.ENCRYPT_COMMS or KeyFlags.ENCRYPT_STORAGE)
} }

View file

@ -11,15 +11,19 @@ package org.pgpainless.algorithm
*/ */
enum class HashAlgorithm(val algorithmId: Int, val algorithmName: String) { enum class HashAlgorithm(val algorithmId: Int, val algorithmName: String) {
// 0 is reserved
@Deprecated("MD5 is deprecated") MD5(1, "MD5"), @Deprecated("MD5 is deprecated") MD5(1, "MD5"),
SHA1(2, "SHA1"), SHA1(2, "SHA1"),
RIPEMD160(3, "RIPEMD160"), RIPEMD160(3, "RIPEMD160"),
// 4 - 7 are reserved
SHA256(8, "SHA256"), SHA256(8, "SHA256"),
SHA384(9, "SHA384"), SHA384(9, "SHA384"),
SHA512(10, "SHA512"), SHA512(10, "SHA512"),
SHA224(11, "SHA224"), SHA224(11, "SHA224"),
SHA3_256(12, "SHA3-256"), SHA3_256(12, "SHA3-256"),
// 13 is reserved
SHA3_512(14, "SHA3-512"), SHA3_512(14, "SHA3-512"),
// 100 - 110 are private / experimental
; ;
companion object { companion object {
@ -57,12 +61,14 @@ enum class HashAlgorithm(val algorithmId: Int, val algorithmName: String) {
* for a list of algorithms and names. * for a list of algorithms and names.
* *
* @param name text name * @param name text name
* @return enum value * @return enum value or null
*/ */
@JvmStatic @JvmStatic
fun fromName(name: String): HashAlgorithm? { fun fromName(name: String): HashAlgorithm? {
return name.uppercase().let { algoName -> return name.uppercase().let { algoName ->
// find value where it.algorithmName == ALGO-NAME
values().firstOrNull { it.algorithmName == algoName } values().firstOrNull { it.algorithmName == algoName }
// else, find value where it.algorithmName == ALGONAME
?: values().firstOrNull { it.algorithmName == algoName.replace("-", "") } ?: values().firstOrNull { it.algorithmName == algoName.replace("-", "") }
} }
} }

View file

@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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")
}
}
}

View file

@ -4,76 +4,105 @@
package org.pgpainless.algorithm 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( enum class PublicKeyAlgorithm(val algorithmId: Int) {
val algorithmId: Int,
val signingCapable: Boolean, // RFC4880
val encryptionCapable: Boolean
) {
/** RSA capable of encryption and signatures. */ /** RSA capable of encryption and signatures. */
RSA_GENERAL(1, true, true), RSA_GENERAL(1),
/** /**
* RSA with usage encryption. * RSA with usage encryption.
* *
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.5">Deprecation * @deprecated see [Deprecation notice](https://tools.ietf.org/html/rfc4880#section-13.5)
* notice</a>
*/ */
@Deprecated("RSA_ENCRYPT is deprecated in favor of RSA_GENERAL", ReplaceWith("RSA_GENERAL")) @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. * RSA with usage of creating signatures.
* *
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.5">Deprecation * @deprecated see [Deprecation notice](https://tools.ietf.org/html/rfc4880#section-13.5)
* notice</a>
*/ */
@Deprecated("RSA_SIGN is deprecated in favor of RSA_GENERAL", ReplaceWith("RSA_GENERAL")) @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 with usage encryption. */
ELGAMAL_ENCRYPT(16, false, true), ELGAMAL_ENCRYPT(16),
/** Digital Signature Algorithm. */ /** Digital Signature Algorithm. */
DSA(17, true, false), DSA(17),
/** Elliptic Curve Diffie-Hellman. */ /** Elliptic Curve Diffie-Hellman. */
ECDH(18, false, true), ECDH(18),
/** Elliptic Curve Digital Signature Algorithm. */ /** Elliptic Curve Digital Signature Algorithm. */
ECDSA(19, true, false), ECDSA(19),
/** /**
* ElGamal General. * ElGamal General.
* *
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.8">Deprecation * @deprecated see [Deprecation notice](https://tools.ietf.org/html/rfc4880#section-13.8)
* notice</a>
*/ */
@Deprecated("ElGamal is deprecated") ELGAMAL_GENERAL(20, true, true), @Deprecated("ElGamal is deprecated") ELGAMAL_GENERAL(20),
/** Diffie-Hellman key exchange algorithm. */ /** Diffie-Hellman key exchange algorithm. */
DIFFIE_HELLMAN(21, false, true), DIFFIE_HELLMAN(21),
/** Digital Signature Algorithm based on twisted Edwards Curves. */ /** Digital Signature Algorithm based on twisted Edwards Curves. */
EDDSA_LEGACY(22, true, false), EDDSA_LEGACY(22),
// RFC9580
/** X25519 encryption algorithm. */ /** X25519 encryption algorithm. */
X25519(25, false, true), X25519(25),
/** X448 encryption algorithm. */ /** X448 encryption algorithm. */
X448(26, false, true), X448(26),
/** Ed25519 signature algorithm. */ /** Ed25519 signature algorithm. */
ED25519(27, true, false), ED25519(27),
/** Ed448 signature algorithm. */ /** 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 fun isSigningCapable(): Boolean = signingCapable

View file

@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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>?
): 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
}
}
}

View file

@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<Set<Feature>>,
aeadAlgorithmPreferences: List<Set<AEADCipherMode>>,
symmetricAlgorithmPreferences: List<Set<SymmetricKeyAlgorithm>>
): MessageEncryptionMechanism
companion object {
@JvmStatic
fun modificationDetectionOrBetter(
symmetricKeyAlgorithmNegotiator: SymmetricKeyAlgorithmNegotiator
): EncryptionMechanismNegotiator =
object : EncryptionMechanismNegotiator {
override fun negotiate(
policy: Policy,
override: MessageEncryptionMechanism?,
features: List<Set<Feature>>,
aeadAlgorithmPreferences: List<Set<AEADCipherMode>>,
symmetricAlgorithmPreferences: List<Set<SymmetricKeyAlgorithm>>
): 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<AEADCipherMode, Int>()
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
}
}
}
}
}

View file

@ -21,7 +21,7 @@ interface HashAlgorithmNegotiator {
* @param orderedPrefs hash algorithm preferences * @param orderedPrefs hash algorithm preferences
* @return picked algorithms * @return picked algorithms
*/ */
fun negotiateHashAlgorithm(orderedPrefs: Set<HashAlgorithm>): HashAlgorithm fun negotiateHashAlgorithm(orderedPrefs: Set<HashAlgorithm>?): HashAlgorithm
companion object { companion object {
@ -62,9 +62,9 @@ interface HashAlgorithmNegotiator {
): HashAlgorithmNegotiator { ): HashAlgorithmNegotiator {
return object : HashAlgorithmNegotiator { return object : HashAlgorithmNegotiator {
override fun negotiateHashAlgorithm( override fun negotiateHashAlgorithm(
orderedPrefs: Set<HashAlgorithm> orderedPrefs: Set<HashAlgorithm>?
): HashAlgorithm { ): HashAlgorithm {
return orderedPrefs.firstOrNull { hashAlgorithmPolicy.isAcceptable(it) } return orderedPrefs?.firstOrNull { hashAlgorithmPolicy.isAcceptable(it) }
?: hashAlgorithmPolicy.defaultHashAlgorithm() ?: hashAlgorithmPolicy.defaultHashAlgorithm()
} }
} }

View file

@ -4,7 +4,6 @@
package org.pgpainless.algorithm.negotiation package org.pgpainless.algorithm.negotiation
import java.lang.IllegalArgumentException
import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.algorithm.SymmetricKeyAlgorithm
import org.pgpainless.policy.Policy import org.pgpainless.policy.Policy
@ -36,9 +35,8 @@ interface SymmetricKeyAlgorithmNegotiator {
override: SymmetricKeyAlgorithm?, override: SymmetricKeyAlgorithm?,
keyPreferences: List<Set<SymmetricKeyAlgorithm>> keyPreferences: List<Set<SymmetricKeyAlgorithm>>
): SymmetricKeyAlgorithm { ): SymmetricKeyAlgorithm {
if (override == SymmetricKeyAlgorithm.NULL) { require(override != SymmetricKeyAlgorithm.NULL) {
throw IllegalArgumentException( "Algorithm override cannot be NULL (plaintext)."
"Algorithm override cannot be NULL (plaintext).")
} }
if (override != null) { if (override != null) {

View file

@ -5,14 +5,46 @@
package org.pgpainless.authentication package org.pgpainless.authentication
import org.bouncycastle.openpgp.PGPPublicKeyRing 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( class CertificateAuthenticity(
val userId: String, val userId: CharSequence,
val certificate: PGPPublicKeyRing, val certificate: OpenPGPCertificate,
val certificationChains: Map<CertificationChain, Int>, val certificationChains: Map<CertificationChain, Int>,
val targetAmount: Int val targetAmount: Int
) { ) {
/** Legacy constructor accepting a [PGPPublicKeyRing]. */
@Deprecated("Pass in an OpenPGPCertificate instead of a PGPPublicKeyRing.")
constructor(
userId: String,
certificate: PGPPublicKeyRing,
certificationChains: Map<CertificationChain, Int>,
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 val totalTrustAmount: Int
get() = certificationChains.values.sum() get() = certificationChains.values.sum()
@ -44,5 +76,22 @@ class CertificateAuthenticity(
*/ */
class CertificationChain(val trustAmount: Int, val chainLinks: List<ChainLink>) {} class CertificationChain(val trustAmount: Int, val chainLinks: List<ChainLink>) {}
/** 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
}

View file

@ -5,14 +5,15 @@
package org.pgpainless.authentication package org.pgpainless.authentication
import java.util.* import java.util.*
import org.bouncycastle.bcpg.KeyIdentifier
import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.OpenPgpFingerprint
/** /**
* Interface for a CA that can authenticate trust-worthy certificates. Such a CA might be a fixed * 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. * list of trustworthy certificates, or a dynamic implementation like the Web-of-Trust.
* *
* @see <a href="https://github.com/pgpainless/pgpainless-wot">PGPainless-WOT</a> * @see [PGPainless-WOT](https://github.com/pgpainless/pgpainless-wot)
* @see <a href="https://sequoia-pgp.gitlab.io/sequoia-wot/">OpenPGP Web of Trust</a> * @see [OpenPGP Web of Trust](https://sequoia-pgp.gitlab.io/sequoia-wot/)
*/ */
interface CertificateAuthority { interface CertificateAuthority {
@ -36,7 +37,30 @@ interface CertificateAuthority {
email: Boolean, email: Boolean,
referenceTime: Date, referenceTime: Date,
targetAmount: Int 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. * Lookup certificates, which carry a trustworthy binding to the given userId.
@ -50,7 +74,7 @@ interface CertificateAuthority {
* @return list of identified bindings * @return list of identified bindings
*/ */
fun lookupByUserId( fun lookupByUserId(
userId: String, userId: CharSequence,
email: Boolean, email: Boolean,
referenceTime: Date, referenceTime: Date,
targetAmount: Int targetAmount: Int
@ -70,5 +94,22 @@ interface CertificateAuthority {
fingerprint: OpenPgpFingerprint, fingerprint: OpenPgpFingerprint,
referenceTime: Date, referenceTime: Date,
targetAmount: Int targetAmount: Int
): List<CertificateAuthenticity> =
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<CertificateAuthenticity> ): List<CertificateAuthenticity>
} }

View file

@ -0,0 +1,146 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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) }
}
}
}
}

View file

@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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))
}

View file

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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()
}
}
}
}

View file

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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())

View file

@ -0,0 +1,126 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<Feature>?
): 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<SymmetricKeyAlgorithm>?
): 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<HashAlgorithm>?
): 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<CompressionAlgorithm>?
): 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<AEADCipherMode>?
): 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) }
}
}
}

View file

@ -4,19 +4,45 @@
package org.pgpainless.bouncycastle.extensions package org.pgpainless.bouncycastle.extensions
import openpgp.openPgpKeyId import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPKeyRing
import org.bouncycastle.openpgp.PGPOnePassSignature import org.bouncycastle.openpgp.PGPOnePassSignature
import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.PGPSignature 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.PGPainless
import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.OpenPgpFingerprint
import org.pgpainless.key.SubkeyIdentifier 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 = fun PGPKeyRing.matches(subkeyIdentifier: SubkeyIdentifier): Boolean =
this.publicKey.keyID == subkeyIdentifier.primaryKeyId && this.publicKey.keyIdentifier.matchesExplicit(subkeyIdentifier.certificateIdentifier) &&
this.getPublicKey(subkeyIdentifier.subkeyId) != null 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. * 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 * @param keyId keyId
* @return true if key with the given key-ID is present, false otherwise * @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. * 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 * @return true if key with the given fingerprint is present, false otherwise
*/ */
fun PGPKeyRing.hasPublicKey(fingerprint: OpenPgpFingerprint): Boolean = 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. * 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 * @return public key
*/ */
fun PGPKeyRing.getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = fun PGPKeyRing.getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? =
this.getPublicKey(fingerprint.bytes) this.getPublicKey(fingerprint.keyIdentifier)
fun PGPKeyRing.requirePublicKey(keyId: Long): PGPPublicKey = /**
getPublicKey(keyId) * Return the [PGPPublicKey] with the given [keyIdentifier], or throw a [NoSuchElementException] if
?: throw NoSuchElementException( * no matching public key was found.
"OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.") *
* @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 = fun PGPKeyRing.requirePublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey =
getPublicKey(fingerprint) requirePublicKey(fingerprint.keyIdentifier)
?: throw NoSuchElementException(
"OpenPGP key does not contain key with fingerprint $fingerprint.")
/** /**
* Return the [PGPPublicKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. If * 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. * subpacket to identify the [PGPPublicKey] via its key-ID.
*/ */
fun PGPKeyRing.getPublicKeyFor(signature: PGPSignature): PGPPublicKey? = 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. */ /** Return the [PGPPublicKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */
fun PGPKeyRing.getPublicKeyFor(onePassSignature: PGPOnePassSignature): PGPPublicKey? = fun PGPKeyRing.getPublicKeyFor(onePassSignature: PGPOnePassSignature): PGPPublicKey? =
this.getPublicKey(onePassSignature.keyID) this.getPublicKey(onePassSignature.keyIdentifier)
/** Return the [OpenPgpFingerprint] of this OpenPGP key. */ /** Return the [OpenPgpFingerprint] of this OpenPGP key. */
val PGPKeyRing.openPgpFingerprint: OpenPgpFingerprint val PGPKeyRing.openPgpFingerprint: OpenPgpFingerprint
@ -72,3 +124,22 @@ val PGPKeyRing.openPgpFingerprint: OpenPgpFingerprint
/** Return this OpenPGP key as an ASCII armored String. */ /** Return this OpenPGP key as an ASCII armored String. */
fun PGPKeyRing.toAsciiArmor(): String = PGPainless.asciiArmor(this) 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)

View file

@ -4,10 +4,10 @@
package org.pgpainless.bouncycastle.extensions package org.pgpainless.bouncycastle.extensions
import org.bouncycastle.asn1.gnu.GNUObjectIdentifiers
import org.bouncycastle.bcpg.ECDHPublicBCPGKey import org.bouncycastle.bcpg.ECDHPublicBCPGKey
import org.bouncycastle.bcpg.ECDSAPublicBCPGKey import org.bouncycastle.bcpg.ECDSAPublicBCPGKey
import org.bouncycastle.bcpg.EdDSAPublicBCPGKey import org.bouncycastle.bcpg.EdDSAPublicBCPGKey
import org.bouncycastle.internal.asn1.gnu.GNUObjectIdentifiers
import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil
import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPPublicKey
import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.algorithm.PublicKeyAlgorithm

View file

@ -4,8 +4,16 @@
package org.pgpainless.bouncycastle.extensions package org.pgpainless.bouncycastle.extensions
import openpgp.openPgpKeyId import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.openpgp.* 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 import org.pgpainless.key.OpenPgpFingerprint
/** OpenPGP certificate containing the public keys of this OpenPGP key. */ /** 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 * @param keyId keyId of the secret key
* @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise * @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. * 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 * @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise
*/ */
fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean = fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean =
this.getSecretKey(fingerprint) != null hasSecretKey(fingerprint.keyIdentifier)
/** /**
* Return the [PGPSecretKey] with the given [OpenPgpFingerprint]. * Return the [PGPSecretKey] with the given [OpenPgpFingerprint].
@ -36,7 +54,7 @@ fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean =
* @return the secret key or null * @return the secret key or null
*/ */
fun PGPSecretKeyRing.getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? = fun PGPSecretKeyRing.getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? =
this.getSecretKey(fingerprint.bytes) this.getSecretKey(fingerprint.keyIdentifier)
/** /**
* Return the [PGPSecretKey] with the given key-ID. * 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 * @throws NoSuchElementException if the OpenPGP key doesn't contain a secret key with the given
* key-ID * key-ID
*/ */
@Deprecated("Pass in a KeyIdentifier instead.")
fun PGPSecretKeyRing.requireSecretKey(keyId: Long): PGPSecretKey = 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( ?: 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. * Return the [PGPSecretKey] with the given fingerprint.
@ -56,9 +84,7 @@ fun PGPSecretKeyRing.requireSecretKey(keyId: Long): PGPSecretKey =
* fingerprint * fingerprint
*/ */
fun PGPSecretKeyRing.requireSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey = fun PGPSecretKeyRing.requireSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey =
getSecretKey(fingerprint) requireSecretKey(fingerprint.keyIdentifier)
?: throw NoSuchElementException(
"OpenPGP key does not contain key with fingerprint $fingerprint.")
/** /**
* Return the [PGPSecretKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. If * 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. */ /** Return the [PGPSecretKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */
fun PGPSecretKeyRing.getSecretKeyFor(onePassSignature: PGPOnePassSignature): PGPSecretKey? = 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? = fun PGPSecretKeyRing.getSecretKeyFor(pkesk: PGPPublicKeyEncryptedData): PGPSecretKey? =
when (pkesk.version) { this.getSecretKey(pkesk.keyIdentifier)
3 -> this.getSecretKey(pkesk.keyID)
else -> throw NotImplementedError("Version 6 PKESKs are not yet supported.") /**
} * 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)

View file

@ -5,13 +5,16 @@
package org.pgpainless.bouncycastle.extensions package org.pgpainless.bouncycastle.extensions
import java.util.* import java.util.*
import openpgp.formatUTC
import openpgp.plusSeconds import openpgp.plusSeconds
import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.PGPSignature import org.bouncycastle.openpgp.PGPSignature
import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.HashAlgorithm
import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.algorithm.PublicKeyAlgorithm
import org.pgpainless.algorithm.RevocationState import org.pgpainless.algorithm.RevocationState
import org.pgpainless.algorithm.SignatureType import org.pgpainless.algorithm.SignatureType
import org.pgpainless.exception.SignatureValidationException
import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.OpenPgpFingerprint
import org.pgpainless.key.util.RevocationAttributes.Reason import org.pgpainless.key.util.RevocationAttributes.Reason
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil 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. * 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: OpenPgpFingerprint): Boolean =
fun PGPSignature.wasIssuedBy(fingerprint: ByteArray): Boolean = wasIssuedBy(fingerprint.keyIdentifier)
try {
wasIssuedBy(OpenPgpFingerprint.parseFromBinary(fingerprint))
} catch (e: IllegalArgumentException) {
// Unknown fingerprint length / format
false
}
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. */ /** Return true, if this signature is a hard revocation. */
val PGPSignature.isHardRevocation val PGPSignature.isHardRevocation
@ -90,19 +99,57 @@ val PGPSignature.isHardRevocation
else -> false // Not a revocation 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() = fun PGPSignature?.toRevocationState() =
if (this == null) RevocationState.notRevoked() if (this == null) RevocationState.notRevoked()
else if (isHardRevocation) RevocationState.hardRevoked() else if (isHardRevocation) RevocationState.hardRevoked()
else RevocationState.softRevoked(creationTime) else RevocationState.softRevoked(creationTime)
/** The signatures issuer fingerprint as [OpenPgpFingerprint]. */
val PGPSignature.fingerprint: OpenPgpFingerprint? val PGPSignature.fingerprint: OpenPgpFingerprint?
get() = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this) get() = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this)
/** The signatures [PublicKeyAlgorithm]. */
val PGPSignature.publicKeyAlgorithm: PublicKeyAlgorithm val PGPSignature.publicKeyAlgorithm: PublicKeyAlgorithm
get() = PublicKeyAlgorithm.requireFromId(keyAlgorithm) get() = PublicKeyAlgorithm.requireFromId(keyAlgorithm)
/** The signatures [HashAlgorithm]. */
val PGPSignature.signatureHashAlgorithm: HashAlgorithm val PGPSignature.signatureHashAlgorithm: HashAlgorithm
get() = HashAlgorithm.requireFromId(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 = fun PGPSignature.isOfType(type: SignatureType): Boolean =
SignatureType.fromCode(signatureType) == type SignatureType.fromCode(signatureType) == type

View file

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<AEADCipherMode> {
return this?.algorithms?.asSequence()?.map { AEADCipherMode(it) }?.toSet() ?: setOf()
}

View file

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<HashAlgorithm> {
return this?.preferences?.asSequence()?.map { HashAlgorithm.requireFromId(it) }?.toSet()
?: setOf()
}
/** Convert the [PreferredAlgorithms] packet into a [Set] of [SymmetricKeyAlgorithm] preferences. */
fun PreferredAlgorithms?.toSymmetricKeyAlgorithms(): Set<SymmetricKeyAlgorithm> {
return this?.preferences?.asSequence()?.map { SymmetricKeyAlgorithm.requireFromId(it) }?.toSet()
?: setOf()
}
/** Convert the [PreferredAlgorithms] packet into a [Set] of [CompressionAlgorithm] preferences. */
fun PreferredAlgorithms?.toCompressionAlgorithms(): Set<CompressionAlgorithm> {
return this?.preferences?.asSequence()?.map { CompressionAlgorithm.requireFromId(it) }?.toSet()
?: setOf()
}

View file

@ -7,9 +7,14 @@ package org.pgpainless.decryption_verification
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.* import java.util.*
import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.openpgp.* 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.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.InMemoryMultiPassStrategy
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy
import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.SubkeyIdentifier
@ -19,7 +24,7 @@ import org.pgpainless.util.Passphrase
import org.pgpainless.util.SessionKey import org.pgpainless.util.SessionKey
/** Options for decryption and signature verification. */ /** Options for decryption and signature verification. */
class ConsumerOptions { class ConsumerOptions(private val api: PGPainless) {
private var ignoreMDCErrors = false private var ignoreMDCErrors = false
var isDisableAsciiArmorCRC = false var isDisableAsciiArmorCRC = false
@ -27,17 +32,18 @@ class ConsumerOptions {
private var verifyNotBefore: Date? = null private var verifyNotBefore: Date? = null
private var verifyNotAfter: Date? = Date() private var verifyNotAfter: Date? = Date()
private val certificates = CertificateSource() private val certificates = CertificateSource(api)
private val detachedSignatures = mutableSetOf<PGPSignature>() private val detachedSignatures = mutableSetOf<PGPSignature>()
private var missingCertificateCallback: MissingPublicKeyCallback? = null private var missingCertificateCallback: OpenPGPCertificateProvider? = null
private var sessionKey: SessionKey? = null private var sessionKey: SessionKey? = null
private val customDecryptorFactories = private val customDecryptorFactories =
mutableMapOf<SubkeyIdentifier, PublicKeyDataDecryptorFactory>() mutableMapOf<SubkeyIdentifier, PublicKeyDataDecryptorFactory>()
private val decryptionKeys = mutableMapOf<PGPSecretKeyRing, SecretKeyRingProtector>() private val decryptionKeys = mutableMapOf<OpenPGPKey, SecretKeyRingProtector>()
private val decryptionPassphrases = mutableSetOf<Passphrase>() private val decryptionPassphrases = mutableSetOf<Passphrase>()
private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE
private var multiPassStrategy: MultiPassStrategy = InMemoryMultiPassStrategy() private var multiPassStrategy: MultiPassStrategy = InMemoryMultiPassStrategy()
private var allowDecryptionWithNonEncryptionKey: Boolean = false
/** /**
* Consider signatures on the message made before the given timestamp invalid. Null means no * Consider signatures on the message made before the given timestamp invalid. Null means no
@ -65,14 +71,26 @@ class ConsumerOptions {
fun getVerifyNotAfter() = verifyNotAfter fun getVerifyNotAfter() = verifyNotAfter
fun addVerificationCert(verificationCert: OpenPGPCertificate): ConsumerOptions = apply {
this.certificates.addCertificate(verificationCert)
}
fun addVerificationCerts(verificationCerts: Collection<OpenPGPCertificate>): ConsumerOptions =
apply {
for (cert in verificationCerts) {
addVerificationCert(cert)
}
}
/** /**
* Add a certificate (public key ring) for signature verification. * Add a certificate (public key ring) for signature verification.
* *
* @param verificationCert certificate for signature verification * @param verificationCert certificate for signature verification
* @return options * @return options
*/ */
@Deprecated("Pass OpenPGPCertificate instead.")
fun addVerificationCert(verificationCert: PGPPublicKeyRing): ConsumerOptions = apply { 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 * @param verificationCerts certificates for signature verification
* @return options * @return options
*/ */
@Deprecated("Use of methods taking PGPPublicKeyRingCollections is discouraged.")
fun addVerificationCerts(verificationCerts: PGPPublicKeyRingCollection): ConsumerOptions = fun addVerificationCerts(verificationCerts: PGPPublicKeyRingCollection): ConsumerOptions =
apply { apply {
for (cert in verificationCerts) { 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. * Add a detached signature for the signature verification process.
* *
@ -137,7 +164,8 @@ class ConsumerOptions {
* @param callback callback * @param callback callback
* @return options * @return options
*/ */
fun setMissingCertificateCallback(callback: MissingPublicKeyCallback): ConsumerOptions = apply { fun setMissingCertificateCallback(callback: OpenPGPCertificateProvider): ConsumerOptions =
apply {
this.missingCertificateCallback = callback this.missingCertificateCallback = callback
} }
@ -155,52 +183,45 @@ class ConsumerOptions {
fun getSessionKey() = sessionKey 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 * Add a key for message decryption. If the key is encrypted, the [SecretKeyRingProtector] is
* used to decrypt it when needed. * used to decrypt it when needed.
* *
* @param key key * @param key key
* @param keyRingProtector protector for the secret key * @param protector protector for the secret key
* @return options * @return options
*/ */
@JvmOverloads @JvmOverloads
@Deprecated("Pass OpenPGPKey instead.")
fun addDecryptionKey( fun addDecryptionKey(
key: PGPSecretKeyRing, key: PGPSecretKeyRing,
protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys() protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()
) = apply { decryptionKeys[key] = protector } ) = addDecryptionKey(api.toKey(key), protector)
/** /**
* Add the keys in the provided key collection for message decryption. * Add the keys in the provided key collection for message decryption.
* *
* @param keys key collection * @param keys key collection
* @param keyRingProtector protector for encrypted secret keys * @param protector protector for encrypted secret keys
* @return options * @return options
*/ */
@JvmOverloads @JvmOverloads
@Deprecated("Pass OpenPGPKey instances instead.")
fun addDecryptionKeys( fun addDecryptionKeys(
keys: PGPSecretKeyRingCollection, keys: PGPSecretKeyRingCollection,
protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys() protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()
) = apply { ) = apply {
for (key in keys) { 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 * Add a passphrase for message decryption. This passphrase will be used to try to decrypt
* messages which were symmetrically encrypted for a passphrase. * messages which were symmetrically encrypted for a passphrase.
@ -240,21 +261,21 @@ class ConsumerOptions {
* *
* @return decryption keys * @return decryption keys
*/ */
fun getDecryptionKeys() = decryptionKeys.keys.toSet() fun getDecryptionKeys(): Set<OpenPGPKey> = decryptionKeys.keys.toSet()
/** /**
* Return the set of available message decryption passphrases. * Return the set of available message decryption passphrases.
* *
* @return decryption passphrases * @return decryption passphrases
*/ */
fun getDecryptionPassphrases() = decryptionPassphrases.toSet() fun getDecryptionPassphrases(): Set<Passphrase> = decryptionPassphrases.toSet()
/** /**
* Return an object holding available certificates for signature verification. * Return an object holding available certificates for signature verification.
* *
* @return certificate source * @return certificate source
*/ */
fun getCertificateSource() = certificates fun getCertificateSource(): CertificateSource = certificates
/** /**
* Return the callback that gets called when a certificate for signature verification is * Return the callback that gets called when a certificate for signature verification is
@ -262,7 +283,7 @@ class ConsumerOptions {
* *
* @return missing public key callback * @return missing public key callback
*/ */
fun getMissingCertificateCallback() = missingCertificateCallback fun getMissingCertificateCallback(): OpenPGPCertificateProvider? = missingCertificateCallback
/** /**
* Return the [SecretKeyRingProtector] for the given [PGPSecretKeyRing]. * Return the [SecretKeyRingProtector] for the given [PGPSecretKeyRing].
@ -270,7 +291,7 @@ class ConsumerOptions {
* @param decryptionKeyRing secret key * @param decryptionKeyRing secret key
* @return protector for that particular secret key * @return protector for that particular secret key
*/ */
fun getSecretKeyProtector(decryptionKeyRing: PGPSecretKeyRing): SecretKeyRingProtector? { fun getSecretKeyProtector(decryptionKeyRing: OpenPGPKey): SecretKeyRingProtector? {
return decryptionKeys[decryptionKeyRing] return decryptionKeys[decryptionKeyRing]
} }
@ -306,7 +327,15 @@ class ConsumerOptions {
this.ignoreMDCErrors = ignoreMDCErrors 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 * 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 * @return true if non-OpenPGP data is forced
*/ */
fun isForceNonOpenPgpData() = forceNonOpenPgpData fun isForceNonOpenPgpData(): Boolean = forceNonOpenPgpData
/** /**
* Specify the [MissingKeyPassphraseStrategy]. This strategy defines, how missing passphrases * 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 * Source for OpenPGP certificates. When verifying signatures on a message, this object holds
* available signer certificates. * available signer certificates.
*/ */
class CertificateSource { class CertificateSource(private val api: PGPainless) {
private val explicitCertificates: MutableSet<PGPPublicKeyRing> = mutableSetOf() private val explicitCertificates: MutableSet<OpenPGPCertificate> = mutableSetOf()
/** /**
* Add a certificate as verification cert explicitly. * Add a certificate as verification cert explicitly.
* *
* @param certificate certificate * @param certificate certificate
*/ */
@Deprecated("Pass in an OpenPGPCertificate instead.")
fun addCertificate(certificate: PGPPublicKeyRing) { 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) explicitCertificates.add(certificate)
} }
@ -394,7 +433,7 @@ class ConsumerOptions {
* *
* @return explicitly set verification certs * @return explicitly set verification certs
*/ */
fun getExplicitCertificates(): Set<PGPPublicKeyRing> { fun getExplicitCertificates(): Set<OpenPGPCertificate> {
return explicitCertificates.toSet() return explicitCertificates.toSet()
} }
@ -406,15 +445,31 @@ class ConsumerOptions {
* @param keyId key id * @param keyId key id
* @return certificate * @return certificate
*/ */
fun getCertificate(keyId: Long): PGPPublicKeyRing? { @Deprecated("Pass in a KeyIdentifier instead.")
return explicitCertificates.firstOrNull { it.getPublicKey(keyId) != null } 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 { companion object {
@JvmStatic fun get() = ConsumerOptions() @JvmOverloads
@JvmStatic
fun get(api: PGPainless = PGPainless.getInstance()) = ConsumerOptions(api)
} }
} }

View file

@ -5,22 +5,24 @@
package org.pgpainless.decryption_verification package org.pgpainless.decryption_verification
import java.io.InputStream import java.io.InputStream
import org.pgpainless.PGPainless
/** /**
* Builder class that takes an [InputStream] of ciphertext (or plaintext signed data) and combines * 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 * it with a configured [ConsumerOptions] object to form a [DecryptionStream] which can be used to
* decrypt an OpenPGP message or verify signatures. * decrypt an OpenPGP message or verify signatures.
*/ */
class DecryptionBuilder : DecryptionBuilderInterface { class DecryptionBuilder(private val api: PGPainless) : DecryptionBuilderInterface {
override fun onInputStream(inputStream: InputStream): DecryptionBuilderInterface.DecryptWith { 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 { override fun withOptions(consumerOptions: ConsumerOptions): DecryptionStream {
return OpenPgpMessageInputStream.create(inputStream, consumerOptions) return OpenPgpMessageInputStream.create(inputStream, consumerOptions, api)
} }
} }
} }

View file

@ -6,6 +6,7 @@ package org.pgpainless.decryption_verification
import kotlin.jvm.Throws import kotlin.jvm.Throws
import org.bouncycastle.bcpg.AEADEncDataPacket import org.bouncycastle.bcpg.AEADEncDataPacket
import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket
import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPException
import org.bouncycastle.openpgp.PGPSessionKey import org.bouncycastle.openpgp.PGPSessionKey
@ -33,12 +34,36 @@ class HardwareSecurity {
* @return decrypted session key * @return decrypted session key
* @throws HardwareSecurityException exception * @throws HardwareSecurityException exception
*/ */
@Deprecated("Pass in a KeyIdentifier instead of a Long keyId.")
@Throws(HardwareSecurityException::class) @Throws(HardwareSecurityException::class)
fun decryptSessionKey( fun decryptSessionKey(
keyId: Long, keyId: Long,
keyAlgorithm: Int, keyAlgorithm: Int,
sessionKeyData: ByteArray, sessionKeyData: ByteArray,
pkeskVersion: Int 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 ): ByteArray
} }
@ -84,7 +109,7 @@ class HardwareSecurity {
): ByteArray { ): ByteArray {
return try { return try {
callback.decryptSessionKey( callback.decryptSessionKey(
subkeyIdentifier.subkeyId, keyAlgorithm, secKeyData[0], pkeskVersion) subkeyIdentifier.keyIdentifier, keyAlgorithm, secKeyData[0], pkeskVersion)
} catch (e: HardwareSecurityException) { } catch (e: HardwareSecurityException) {
throw PGPException("Hardware-backed decryption failed.", e) throw PGPException("Hardware-backed decryption failed.", e)
} }

View file

@ -9,8 +9,6 @@ import java.io.InputStream
import org.bouncycastle.openpgp.PGPEncryptedData import org.bouncycastle.openpgp.PGPEncryptedData
import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPException
import org.pgpainless.exception.ModificationDetectionException import org.pgpainless.exception.ModificationDetectionException
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class IntegrityProtectedInputStream( class IntegrityProtectedInputStream(
private val inputStream: InputStream, private val inputStream: InputStream,
@ -30,15 +28,9 @@ class IntegrityProtectedInputStream(
if (encryptedData.isIntegrityProtected && !options.isIgnoreMDCErrors()) { if (encryptedData.isIntegrityProtected && !options.isIgnoreMDCErrors()) {
try { try {
if (!encryptedData.verify()) throw ModificationDetectionException() if (!encryptedData.verify()) throw ModificationDetectionException()
LOGGER.debug("Integrity Protection check passed.")
} catch (e: PGPException) { } catch (e: PGPException) {
throw IOException("Data appears to not be integrity protected.", e) throw IOException("Data appears to not be integrity protected.", e)
} }
} }
} }
companion object {
@JvmStatic
val LOGGER: Logger = LoggerFactory.getLogger(IntegrityProtectedInputStream::class.java)
}
} }

View file

@ -6,69 +6,69 @@ package org.pgpainless.decryption_verification
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.*
import org.pgpainless.implementation.ImplementationFactory import org.bouncycastle.openpgp.api.OpenPGPImplementation
import org.pgpainless.PGPainless
import org.pgpainless.util.ArmorUtils import org.pgpainless.util.ArmorUtils
/** /**
* Inspect an OpenPGP message to determine IDs of its encryption keys or whether it is passphrase * Inspect an OpenPGP message to determine IDs of its encryption keys or whether it is passphrase
* protected. * protected.
*/ */
class MessageInspector { class MessageInspector(val api: PGPainless = PGPainless.getInstance()) {
/** /**
* Info about an OpenPGP message. * 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 isPassphraseEncrypted true, if the message is encrypted for a passphrase
* @param isSignedOnly true, if the message is not encrypted, but signed using OnePassSignatures * @param isSignedOnly true, if the message is not encrypted, but signed using OnePassSignatures
*/ */
data class EncryptionInfo( data class EncryptionInfo(
val keyIds: List<Long>, val keyIdentifiers: List<KeyIdentifier>,
val isPassphraseEncrypted: Boolean, val isPassphraseEncrypted: Boolean,
val isSignedOnly: Boolean val isSignedOnly: Boolean
) { ) {
val isEncrypted: Boolean val isEncrypted: Boolean
get() = isPassphraseEncrypted || keyIds.isNotEmpty() get() = isPassphraseEncrypted || keyIds.isNotEmpty()
val keyIds: List<Long> = keyIdentifiers.map { it.keyId }
} }
companion object {
/** /**
* Parses parts of the provided OpenPGP message in order to determine which keys were used * Parses parts of the provided OpenPGP message in order to determine which keys were used to
* to encrypt it. * encrypt it.
* *
* @param message OpenPGP message * @param message OpenPGP message
* @return encryption info * @return encryption info
* @throws PGPException in case the message is broken * @throws PGPException in case the message is broken
* @throws IOException in case of an IO error * @throws IOException in case of an IO error
*/ */
@JvmStatic
@Throws(PGPException::class, IOException::class) @Throws(PGPException::class, IOException::class)
fun determineEncryptionInfoForMessage(message: String): EncryptionInfo = fun determineEncryptionInfoForMessage(message: String): EncryptionInfo =
determineEncryptionInfoForMessage(message.byteInputStream()) determineEncryptionInfoForMessage(message.byteInputStream())
/** /**
* Parses parts of the provided OpenPGP message in order to determine which keys were used * Parses parts of the provided OpenPGP message in order to determine which keys were used to
* to encrypt it. Note: This method does not rewind the passed in Stream, so you might need * encrypt it. Note: This method does not rewind the passed in Stream, so you might need to take
* to take care of that yourselves. * care of that yourselves.
* *
* @param inputStream openpgp message * @param inputStream openpgp message
* @return encryption information * @return encryption information
* @throws IOException in case of an IO error * @throws IOException in case of an IO error
* @throws PGPException if the message is broken * @throws PGPException if the message is broken
*/ */
@JvmStatic
@Throws(PGPException::class, IOException::class) @Throws(PGPException::class, IOException::class)
fun determineEncryptionInfoForMessage(inputStream: InputStream): EncryptionInfo { fun determineEncryptionInfoForMessage(inputStream: InputStream): EncryptionInfo {
return processMessage(ArmorUtils.getDecoderStream(inputStream)) return processMessage(ArmorUtils.getDecoderStream(inputStream))
} }
@JvmStatic
@Throws(PGPException::class, IOException::class) @Throws(PGPException::class, IOException::class)
private fun processMessage(inputStream: InputStream): EncryptionInfo { private fun processMessage(inputStream: InputStream): EncryptionInfo {
var objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(inputStream) var objectFactory = api.implementation.pgpObjectFactory(inputStream)
var n: Any? var n: Any?
while (objectFactory.nextObject().also { n = it } != null) { while (objectFactory.nextObject().also { n = it } != null) {
@ -81,21 +81,21 @@ class MessageInspector {
} }
is PGPEncryptedDataList -> { is PGPEncryptedDataList -> {
var isPassphraseEncrypted = false var isPassphraseEncrypted = false
val keyIds = mutableListOf<Long>() val keyIdentifiers = mutableListOf<KeyIdentifier>()
for (encryptedData in next) { for (encryptedData in next) {
if (encryptedData is PGPPublicKeyEncryptedData) { if (encryptedData is PGPPublicKeyEncryptedData) {
keyIds.add(encryptedData.keyID) keyIdentifiers.add(encryptedData.keyIdentifier)
} else if (encryptedData is PGPPBEEncryptedData) { } else if (encryptedData is PGPPBEEncryptedData) {
isPassphraseEncrypted = true isPassphraseEncrypted = true
} }
} }
// Data is encrypted, we cannot go deeper // Data is encrypted, we cannot go deeper
return EncryptionInfo(keyIds, isPassphraseEncrypted, false) return EncryptionInfo(keyIdentifiers, isPassphraseEncrypted, false)
} }
is PGPCompressedData -> { is PGPCompressedData -> {
objectFactory = objectFactory =
ImplementationFactory.getInstance() OpenPGPImplementation.getInstance()
.getPGPObjectFactory(PGPUtil.getDecoderStream(next.dataStream)) .pgpObjectFactory(PGPUtil.getDecoderStream(next.dataStream))
continue continue
} }
is PGPLiteralData -> { is PGPLiteralData -> {
@ -106,4 +106,3 @@ class MessageInspector {
return EncryptionInfo(listOf(), isPassphraseEncrypted = false, isSignedOnly = false) return EncryptionInfo(listOf(), isPassphraseEncrypted = false, isSignedOnly = false)
} }
} }
}

View file

@ -6,8 +6,11 @@ package org.pgpainless.decryption_verification
import java.util.* import java.util.*
import javax.annotation.Nonnull import javax.annotation.Nonnull
import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPKeyRing
import org.bouncycastle.openpgp.PGPLiteralData 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.CompressionAlgorithm
import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.algorithm.StreamEncoding
import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.algorithm.SymmetricKeyAlgorithm
@ -21,35 +24,67 @@ import org.pgpainless.util.SessionKey
/** View for extracting metadata about a [Message]. */ /** View for extracting metadata about a [Message]. */
class MessageMetadata(val message: Message) { class MessageMetadata(val message: Message) {
// ################################################################################################################ // ##########################################################################################################
// ### Encryption // Encryption
// ### // ##########################################################################################################
// ################################################################################################################
/** /**
* The [SymmetricKeyAlgorithm] of the outermost encrypted data packet, or null if message is * The [SymmetricKeyAlgorithm] of the outermost encrypted data packet, or null if message is
* unencrypted. * unencrypted.
*/ */
@Deprecated(
"Deprecated in favor of encryptionMechanism",
replaceWith = ReplaceWith("encryptionMechanism"))
val encryptionAlgorithm: SymmetricKeyAlgorithm? val encryptionAlgorithm: SymmetricKeyAlgorithm?
get() = encryptionAlgorithms.let { if (it.hasNext()) it.next() else null } 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 * [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 * 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 * item that of the next nested encrypted data packet and so on. The iterator might also be
* empty, in case of an unencrypted message. * empty, in case of an unencrypted message.
*/ */
@Deprecated(
"Deprecated in favor of encryptionMechanisms",
replaceWith = ReplaceWith("encryptionMechanisms"))
val encryptionAlgorithms: Iterator<SymmetricKeyAlgorithm> val encryptionAlgorithms: Iterator<SymmetricKeyAlgorithm>
get() = encryptionLayers.asSequence().map { it.algorithm }.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<MessageEncryptionMechanism>
get() = encryptionLayers.asSequence().map { it.mechanism }.iterator()
/** Return true, if the message is encrypted, false otherwise. */
val isEncrypted: Boolean val isEncrypted: Boolean
get() = get() =
if (encryptionAlgorithm == null) false if (encryptionMechanism == null) false
else encryptionAlgorithm != SymmetricKeyAlgorithm.NULL 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 { 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() get() = encryptionLayers.asSequence().mapNotNull { it.decryptionKey }.firstOrNull()
/** List containing all recipient keyIDs. */ /** List containing all recipient keyIDs. */
@Deprecated(
"Use of key-ids is discouraged in favor of KeyIdentifiers",
replaceWith = ReplaceWith("recipientKeyIdentifiers"))
val recipientKeyIds: List<Long> val recipientKeyIds: List<Long>
get() = recipientKeyIdentifiers.map { it.keyId }.toList()
/** List containing all recipient [KeyIdentifiers][KeyIdentifier]. */
val recipientKeyIdentifiers: List<KeyIdentifier>
get() = get() =
encryptionLayers encryptionLayers
.asSequence() .asSequence()
.map { it.recipients.toMutableList() } .map { it.recipients.toMutableList() }
.reduce { all, keyIds -> .reduce { all, keyIdentifiers ->
all.addAll(keyIds) all.addAll(keyIdentifiers)
all all
} }
.toList() .toList()
/** [Iterator] of all [EncryptedData] layers of the message. */
val encryptionLayers: Iterator<EncryptedData> val encryptionLayers: Iterator<EncryptedData>
get() = get() =
object : LayerIterator<EncryptedData>(message) { object : LayerIterator<EncryptedData>(message) {
@ -97,10 +140,9 @@ class MessageMetadata(val message: Message) {
override fun getProperty(last: Layer) = last as EncryptedData override fun getProperty(last: Layer) = last as EncryptedData
} }
// ################################################################################################################ // ##########################################################################################################
// ### Compression // Compression
// ### // ##########################################################################################################
// ################################################################################################################
/** /**
* [CompressionAlgorithm] of the outermost compressed data packet, or null, if the message does * [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<CompressionAlgorithm> val compressionAlgorithms: Iterator<CompressionAlgorithm>
get() = compressionLayers.asSequence().map { it.algorithm }.iterator() get() = compressionLayers.asSequence().map { it.algorithm }.iterator()
/** [Iterator] of all [CompressedData] layers of the message. */
val compressionLayers: Iterator<CompressedData> val compressionLayers: Iterator<CompressedData>
get() = get() =
object : LayerIterator<CompressedData>(message) { object : LayerIterator<CompressedData>(message) {
@ -126,10 +169,9 @@ class MessageMetadata(val message: Message) {
override fun getProperty(last: Layer) = last as CompressedData override fun getProperty(last: Layer) = last as CompressedData
} }
// ################################################################################################################ // ##########################################################################################################
// ### Signatures // Signatures
// ### // ##########################################################################################################
// ################################################################################################################
val isUsingCleartextSignatureFramework: Boolean val isUsingCleartextSignatureFramework: Boolean
get() = message.cleartextSigned get() = message.cleartextSigned
@ -253,7 +295,8 @@ class MessageMetadata(val message: Message) {
email, email,
it.signature.creationTime, it.signature.creationTime,
targetAmount) targetAmount)
.authenticated ?.authenticated
?: false
} }
} }
@ -270,6 +313,9 @@ class MessageMetadata(val message: Message) {
fun isVerifiedSignedBy(keys: PGPKeyRing) = fun isVerifiedSignedBy(keys: PGPKeyRing) =
verifiedSignatures.any { keys.matches(it.signingKey) } verifiedSignatures.any { keys.matches(it.signingKey) }
fun isVerifiedSignedBy(cert: OpenPGPCertificate) =
verifiedSignatures.any { cert.pgpKeyRing.matches(it.signingKey) }
fun isVerifiedDetachedSignedBy(fingerprint: OpenPgpFingerprint) = fun isVerifiedDetachedSignedBy(fingerprint: OpenPgpFingerprint) =
verifiedDetachedSignatures.any { it.signingKey.matches(fingerprint) } verifiedDetachedSignatures.any { it.signingKey.matches(fingerprint) }
@ -282,18 +328,16 @@ class MessageMetadata(val message: Message) {
fun isVerifiedInlineSignedBy(keys: PGPKeyRing) = fun isVerifiedInlineSignedBy(keys: PGPKeyRing) =
verifiedInlineSignatures.any { keys.matches(it.signingKey) } 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 * 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 * 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. * the signed data of a message, usage of this field is discouraged.
* *
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-5.9">RFC4880 §5.9. Literal Data * @see [RFC4880 §5.9. Literal Data Packet](https://www.rfc-editor.org/rfc/rfc4880#section-5.9)
* Packet</a>
*/ */
val filename: String? = findLiteralData()?.fileName 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 modification date of a decrypted file, but since this field is not necessarily part of
* the signed data, its use is discouraged. * the signed data, its use is discouraged.
* *
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-5.9">RFC4880 §5.9. Literal Data * @see [RFC4880 §5.9. Literal Data Packet](https://www.rfc-editor.org/rfc/rfc4880#section-5.9)
* Packet</a>
*/ */
val modificationDate: Date? = findLiteralData()?.modificationDate 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 * binary data, ...) the data has. Since this field is not necessarily part of the signed data
* of a message, its usage is discouraged. * of a message, its usage is discouraged.
* *
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-5.9">RFC4880 §5.9. Literal Data * @see [RFC4880 §5.9. Literal Data Packet](https://www.rfc-editor.org/rfc/rfc4880#section-5.9)
* Packet</a>
*/ */
val literalDataEncoding: StreamEncoding? = findLiteralData()?.format val literalDataEncoding: StreamEncoding? = findLiteralData()?.format
@ -349,10 +391,9 @@ class MessageMetadata(val message: Message) {
return nested as LiteralData return nested as LiteralData
} }
// ################################################################################################################ // ##########################################################################################################
// ### Message Structure // Message Structure
// ### // ##########################################################################################################
// ################################################################################################################
interface Packet interface Packet
@ -415,8 +456,8 @@ class MessageMetadata(val message: Message) {
* Outermost OpenPGP Message structure. * Outermost OpenPGP Message structure.
* *
* @param cleartextSigned whether the message is using the Cleartext Signature Framework * @param cleartextSigned whether the message is using the Cleartext Signature Framework
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-7">RFC4880 §7. Cleartext * @see
* Signature Framework</a> * [RFC4880 §7. Cleartext Signature Framework](https://www.rfc-editor.org/rfc/rfc4880#section-7)
*/ */
class Message(var cleartextSigned: Boolean = false) : Layer(0) { class Message(var cleartextSigned: Boolean = false) : Layer(0) {
fun setCleartextSigned() = apply { cleartextSigned = true } fun setCleartextSigned() = apply { cleartextSigned = true }
@ -455,18 +496,24 @@ class MessageMetadata(val message: Message) {
/** /**
* Encrypted Data. * 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. * @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. */ /** [SessionKey] used to decrypt the packet. */
var sessionKey: SessionKey? = null var sessionKey: SessionKey? = null
/** List of all recipient key ids to which the packet was encrypted for. */ /** List of all recipient key ids to which the packet was encrypted for. */
val recipients: List<Long> = mutableListOf() val recipients: List<KeyIdentifier> = mutableListOf()
fun addRecipients(keyIds: List<Long>) = apply { (recipients as MutableList).addAll(keyIds) } val algorithm: SymmetricKeyAlgorithm =
SymmetricKeyAlgorithm.requireFromId(mechanism.symmetricKeyAlgorithm)
fun addRecipients(keyIds: List<KeyIdentifier>) = apply {
(recipients as MutableList).addAll(keyIds)
}
/** /**
* Identifier of the subkey that was used to decrypt the packet (in case of a public key * Identifier of the subkey that was used to decrypt the packet (in case of a public key

View file

@ -1,27 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.4">RFC</a>
*/
fun onMissingPublicKeyEncountered(keyId: Long): PGPPublicKeyRing?
}

View file

@ -0,0 +1,321 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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.
*
* <p>
* 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.
*
* <p>
* 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
}
}

View file

@ -11,22 +11,32 @@ import java.io.OutputStream
import java.util.zip.Inflater import java.util.zip.Inflater
import java.util.zip.InflaterInputStream import java.util.zip.InflaterInputStream
import openpgp.openPgpKeyId import openpgp.openPgpKeyId
import org.bouncycastle.bcpg.AEADEncDataPacket
import org.bouncycastle.bcpg.BCPGInputStream import org.bouncycastle.bcpg.BCPGInputStream
import org.bouncycastle.bcpg.CompressionAlgorithmTags import org.bouncycastle.bcpg.CompressionAlgorithmTags
import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket
import org.bouncycastle.bcpg.UnsupportedPacketVersionException import org.bouncycastle.bcpg.UnsupportedPacketVersionException
import org.bouncycastle.openpgp.PGPCompressedData import org.bouncycastle.openpgp.PGPCompressedData
import org.bouncycastle.openpgp.PGPEncryptedData import org.bouncycastle.openpgp.PGPEncryptedData
import org.bouncycastle.openpgp.PGPEncryptedDataList import org.bouncycastle.openpgp.PGPEncryptedDataList
import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPException
import org.bouncycastle.openpgp.PGPKeyPair
import org.bouncycastle.openpgp.PGPOnePassSignature import org.bouncycastle.openpgp.PGPOnePassSignature
import org.bouncycastle.openpgp.PGPPBEEncryptedData import org.bouncycastle.openpgp.PGPPBEEncryptedData
import org.bouncycastle.openpgp.PGPPrivateKey
import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData
import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPSessionKey
import org.bouncycastle.openpgp.PGPSecretKey
import org.bouncycastle.openpgp.PGPSecretKeyRing
import org.bouncycastle.openpgp.PGPSignature 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.PBEDataDecryptorFactory
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory
import org.bouncycastle.util.io.TeeInputStream import org.bouncycastle.util.io.TeeInputStream
@ -35,10 +45,10 @@ import org.pgpainless.algorithm.CompressionAlgorithm
import org.pgpainless.algorithm.OpenPgpPacket import org.pgpainless.algorithm.OpenPgpPacket
import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.algorithm.StreamEncoding
import org.pgpainless.algorithm.SymmetricKeyAlgorithm 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.getSecretKeyFor
import org.pgpainless.bouncycastle.extensions.getSigningKeyFor
import org.pgpainless.bouncycastle.extensions.issuerKeyId 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.CompressedData
import org.pgpainless.decryption_verification.MessageMetadata.EncryptedData import org.pgpainless.decryption_verification.MessageMetadata.EncryptedData
import org.pgpainless.decryption_verification.MessageMetadata.Layer 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.MissingPassphraseException
import org.pgpainless.exception.SignatureValidationException import org.pgpainless.exception.SignatureValidationException
import org.pgpainless.exception.UnacceptableAlgorithmException import org.pgpainless.exception.UnacceptableAlgorithmException
import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.exception.WrongPassphraseException
import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.SubkeyIdentifier
import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey
import org.pgpainless.policy.Policy
import org.pgpainless.signature.consumer.CertificateValidator
import org.pgpainless.signature.consumer.OnePassSignatureCheck 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.ArmoredInputStreamFactory
import org.pgpainless.util.SessionKey import org.pgpainless.util.SessionKey
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -72,10 +78,10 @@ class OpenPgpMessageInputStream(
inputStream: InputStream, inputStream: InputStream,
private val options: ConsumerOptions, private val options: ConsumerOptions,
private val layerMetadata: Layer, private val layerMetadata: Layer,
private val policy: Policy private val api: PGPainless
) : DecryptionStream() { ) : DecryptionStream() {
private val signatures: Signatures = Signatures(options) private val signatures: Signatures = Signatures(options, api)
private var packetInputStream: TeeBCPGInputStream? = null private var packetInputStream: TeeBCPGInputStream? = null
private var nestedInputStream: InputStream? = null private var nestedInputStream: InputStream? = null
private val syntaxVerifier = PDA() private val syntaxVerifier = PDA()
@ -129,8 +135,8 @@ class OpenPgpMessageInputStream(
inputStream: InputStream, inputStream: InputStream,
options: ConsumerOptions, options: ConsumerOptions,
metadata: Layer, metadata: Layer,
policy: Policy api: PGPainless
) : this(Type.standard, inputStream, options, metadata, policy) ) : this(Type.standard, inputStream, options, metadata, api)
private fun consumePackets() { private fun consumePackets() {
val pIn = packetInputStream ?: return val pIn = packetInputStream ?: return
@ -176,7 +182,7 @@ class OpenPgpMessageInputStream(
} }
OpenPgpPacket.PADDING -> { OpenPgpPacket.PADDING -> {
LOGGER.debug("Skipping Padding Packet") LOGGER.debug("Skipping Padding Packet")
pIn.readPacket() pIn.readPadding()
} }
OpenPgpPacket.SK, OpenPgpPacket.SK,
OpenPgpPacket.PK, OpenPgpPacket.PK,
@ -186,10 +192,6 @@ class OpenPgpMessageInputStream(
OpenPgpPacket.UID, OpenPgpPacket.UID,
OpenPgpPacket.UATTR -> OpenPgpPacket.UATTR ->
throw MalformedOpenPgpMessageException("Illegal Packet in Stream: $packet") throw MalformedOpenPgpMessageException("Illegal Packet in Stream: $packet")
OpenPgpPacket.PADDING -> {
LOGGER.debug("Padding packet")
pIn.readPadding()
}
OpenPgpPacket.EXP_1, OpenPgpPacket.EXP_1,
OpenPgpPacket.EXP_2, OpenPgpPacket.EXP_2,
OpenPgpPacket.EXP_3, OpenPgpPacket.EXP_3,
@ -230,7 +232,7 @@ class OpenPgpMessageInputStream(
LOGGER.debug( LOGGER.debug(
"Compressed Data Packet (${compressionLayer.algorithm}) at depth ${layerMetadata.depth} encountered.") "Compressed Data Packet (${compressionLayer.algorithm}) at depth ${layerMetadata.depth} encountered.")
nestedInputStream = nestedInputStream =
OpenPgpMessageInputStream(decompress(compressedData), options, compressionLayer, policy) OpenPgpMessageInputStream(decompress(compressedData), options, compressionLayer, api)
} }
private fun decompress(compressedData: PGPCompressedData): InputStream { private fun decompress(compressedData: PGPCompressedData): InputStream {
@ -311,7 +313,7 @@ class OpenPgpMessageInputStream(
signatures signatures
.leaveNesting() // TODO: Only leave nesting if all OPSs of the nesting layer are .leaveNesting() // TODO: Only leave nesting if all OPSs of the nesting layer are
// dealt with // dealt with
signatures.addCorrespondingOnePassSignature(signature, layerMetadata, policy) signatures.addCorrespondingOnePassSignature(signature, layerMetadata)
} else { } else {
LOGGER.debug( LOGGER.debug(
"Prepended Signature Packet by key ${keyId.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") "Prepended Signature Packet by key ${keyId.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.")
@ -320,20 +322,38 @@ class OpenPgpMessageInputStream(
} }
private fun processEncryptedData(): Boolean { private fun processEncryptedData(): Boolean {
LOGGER.debug( // TODO: Replace by dedicated encryption packet type input symbols
"Symmetrically Encrypted Data Packet at depth ${layerMetadata.depth} encountered.")
syntaxVerifier.next(InputSymbol.ENCRYPTED_DATA) syntaxVerifier.next(InputSymbol.ENCRYPTED_DATA)
val encDataList = packetInputStream!!.readEncryptedDataList() val encDataList = packetInputStream!!.readEncryptedDataList()
if (!encDataList.isIntegrityProtected && !encDataList.get(0).isAEAD) { 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.") LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected.")
if (!options.isIgnoreMDCErrors()) { if (!options.isIgnoreMDCErrors()) {
throw MessageNotIntegrityProtectedException() throw MessageNotIntegrityProtectedException()
} }
} }
}
val esks = SortedESKs(encDataList)
LOGGER.debug( 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)" + " ${esks.pkesks.size + esks.anonPkesks.size} PKESK(s) from which ${esks.anonPkesks.size} PKESK(s)" +
" have an anonymous recipient.") " have an anonymous recipient.")
@ -343,7 +363,7 @@ class OpenPgpMessageInputStream(
esks.pkesks esks.pkesks
.filter { .filter {
// find matching PKESK // find matching PKESK
it.keyID == key.subkeyId it.keyIdentifier == key.keyIdentifier
} }
.forEach { .forEach {
// attempt decryption // attempt decryption
@ -359,9 +379,9 @@ class OpenPgpMessageInputStream(
LOGGER.debug("Attempt decryption with provided session key.") LOGGER.debug("Attempt decryption with provided session key.")
throwIfUnacceptable(sk.algorithm) throwIfUnacceptable(sk.algorithm)
val decryptorFactory = val pgpSk = PGPSessionKey(sk.algorithm.algorithmId, sk.key)
ImplementationFactory.getInstance().getSessionKeyDataDecryptorFactory(sk) val decryptorFactory = api.implementation.sessionKeyDataDecryptorFactory(pgpSk)
val layer = EncryptedData(sk.algorithm, layerMetadata.depth + 1) val layer = esks.toEncryptedData(sk, layerMetadata.depth + 1)
val skEncData = encDataList.extractSessionKeyEncryptedData() val skEncData = encDataList.extractSessionKeyEncryptedData()
try { try {
val decrypted = skEncData.getDataStream(decryptorFactory) val decrypted = skEncData.getDataStream(decryptorFactory)
@ -369,7 +389,7 @@ class OpenPgpMessageInputStream(
val integrityProtected = val integrityProtected =
IntegrityProtectedInputStream(decrypted, skEncData, options) IntegrityProtectedInputStream(decrypted, skEncData, options)
nestedInputStream = nestedInputStream =
OpenPgpMessageInputStream(integrityProtected, options, layer, policy) OpenPgpMessageInputStream(integrityProtected, options, layer, api)
LOGGER.debug("Successfully decrypted data using provided session key") LOGGER.debug("Successfully decrypted data using provided session key")
return true return true
} catch (e: PGPException) { } catch (e: PGPException) {
@ -392,7 +412,7 @@ class OpenPgpMessageInputStream(
} }
val decryptorFactory = val decryptorFactory =
ImplementationFactory.getInstance().getPBEDataDecryptorFactory(passphrase) api.implementation.pbeDataDecryptorFactory(passphrase.getChars())
if (decryptSKESKAndStream(esks, skesk, decryptorFactory)) { if (decryptSKESKAndStream(esks, skesk, decryptorFactory)) {
return true return true
} }
@ -400,30 +420,45 @@ class OpenPgpMessageInputStream(
} }
val postponedDueToMissingPassphrase = val postponedDueToMissingPassphrase =
mutableListOf<Pair<PGPSecretKey, PGPPublicKeyEncryptedData>>() mutableListOf<Pair<OpenPGPSecretKey, PGPPublicKeyEncryptedData>>()
// try (known) secret keys // try (known) secret keys
esks.pkesks.forEach { pkesk -> 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) val decryptionKeyCandidates = getDecryptionKeys(pkesk)
for (decryptionKeys in decryptionKeyCandidates) { for (decryptionKeys in decryptionKeyCandidates) {
val secretKey = decryptionKeys.getSecretKeyFor(pkesk)!! val secretKey = decryptionKeys.getSecretKeyFor(pkesk)!!
val decryptionKeyId = SubkeyIdentifier(decryptionKeys, secretKey.keyID) if (!secretKey.isEncryptionKey &&
if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { !options.getAllowDecryptionWithNonEncryptionKey()) {
LOGGER.debug(
"Message is encrypted for ${secretKey.keyIdentifier}, but the key is not encryption capable.")
continue
}
if (hasUnsupportedS2KSpecifier(secretKey)) {
continue continue
} }
LOGGER.debug("Attempt decryption using secret key $decryptionKeyId") LOGGER.debug("Attempt decryption using secret key ${decryptionKeys.keyIdentifier}")
val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue
if (!protector.hasPassphraseFor(secretKey.keyID)) { if (!protector.hasPassphraseFor(secretKey.keyIdentifier)) {
LOGGER.debug( 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) postponedDueToMissingPassphrase.add(secretKey to pkesk)
continue continue
} }
val privateKey = secretKey.unlock(protector) val privateKey =
if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { 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 return true
} }
} }
@ -431,24 +466,24 @@ class OpenPgpMessageInputStream(
// try anonymous secret keys // try anonymous secret keys
for (pkesk in esks.anonPkesks) { for (pkesk in esks.anonPkesks) {
for ((decryptionKeys, secretKey) in findPotentialDecryptionKeys(pkesk)) { for (decryptionKey in findPotentialDecryptionKeys(pkesk)) {
val decryptionKeyId = SubkeyIdentifier(decryptionKeys, secretKey.keyID) if (hasUnsupportedS2KSpecifier(decryptionKey)) {
if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) {
continue continue
} }
LOGGER.debug("Attempt decryption of anonymous PKESK with key $decryptionKeyId.") LOGGER.debug("Attempt decryption of anonymous PKESK with key $decryptionKey.")
val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue val protector = options.getSecretKeyProtector(decryptionKey.openPGPKey) ?: continue
if (!protector.hasPassphraseFor(secretKey.keyID)) { if (!protector.hasPassphraseFor(decryptionKey.keyIdentifier)) {
LOGGER.debug( LOGGER.debug(
"Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") "Missing passphrase for key ${decryptionKey.keyIdentifier}. Postponing decryption until all other keys were tried.")
postponedDueToMissingPassphrase.add(secretKey to pkesk) postponedDueToMissingPassphrase.add(decryptionKey to pkesk)
continue continue
} }
val privateKey = secretKey.unlock(protector) val privateKey = decryptionKey.unlock(protector)
if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { if (decryptWithPrivateKey(
esks, privateKey.keyPair, SubkeyIdentifier(decryptionKey), pkesk)) {
return true return true
} }
} }
@ -458,23 +493,28 @@ class OpenPgpMessageInputStream(
MissingKeyPassphraseStrategy.THROW_EXCEPTION) { MissingKeyPassphraseStrategy.THROW_EXCEPTION) {
// Non-interactive mode: Throw an exception with all locked decryption keys // Non-interactive mode: Throw an exception with all locked decryption keys
postponedDueToMissingPassphrase postponedDueToMissingPassphrase
.map { SubkeyIdentifier(getDecryptionKey(it.first.keyID)!!, it.first.keyID) } .map { SubkeyIdentifier(it.first) }
.also { if (it.isNotEmpty()) throw MissingPassphraseException(it.toSet()) } .also { if (it.isNotEmpty()) throw MissingPassphraseException(it.toSet()) }
} else if (options.getMissingKeyPassphraseStrategy() == } else if (options.getMissingKeyPassphraseStrategy() ==
MissingKeyPassphraseStrategy.INTERACTIVE) { MissingKeyPassphraseStrategy.INTERACTIVE) {
for ((secretKey, pkesk) in postponedDueToMissingPassphrase) { for ((secretKey, pkesk) in postponedDueToMissingPassphrase) {
val keyId = secretKey.keyID val keyId = secretKey.keyIdentifier
val decryptionKeys = getDecryptionKey(pkesk)!! val decryptionKeys = getDecryptionKey(pkesk)!!
val decryptionKeyId = SubkeyIdentifier(decryptionKeys, keyId) val decryptionKeyId = SubkeyIdentifier(decryptionKeys.pgpSecretKeyRing, keyId)
if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { if (hasUnsupportedS2KSpecifier(secretKey)) {
continue continue
} }
LOGGER.debug( LOGGER.debug(
"Attempt decryption with key $decryptionKeyId while interactively requesting its passphrase.") "Attempt decryption with key $decryptionKeyId while interactively requesting its passphrase.")
val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue
val privateKey = secretKey.unlock(protector) val privateKey: OpenPGPPrivateKey =
if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { try {
unlockSecretKey(secretKey, protector)
} catch (e: PGPException) {
throw WrongPassphraseException(secretKey.keyIdentifier, e)
}
if (decryptWithPrivateKey(esks, privateKey.keyPair, decryptionKeyId, pkesk)) {
return true return true
} }
} }
@ -488,25 +528,22 @@ class OpenPgpMessageInputStream(
} }
private fun decryptWithPrivateKey( private fun decryptWithPrivateKey(
esks: SortedESKs, esks: ESKsAndData,
privateKey: PGPPrivateKey, privateKey: PGPKeyPair,
decryptionKeyId: SubkeyIdentifier, decryptionKeyId: SubkeyIdentifier,
pkesk: PGPPublicKeyEncryptedData pkesk: PGPPublicKeyEncryptedData
): Boolean { ): Boolean {
val decryptorFactory = val decryptorFactory =
ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey) api.implementation.publicKeyDataDecryptorFactory(privateKey.privateKey)
return decryptPKESKAndStream(esks, decryptionKeyId, decryptorFactory, pkesk) return decryptPKESKAndStream(esks, decryptionKeyId, decryptorFactory, pkesk)
} }
private fun hasUnsupportedS2KSpecifier( private fun hasUnsupportedS2KSpecifier(secretKey: OpenPGPSecretKey): Boolean {
secretKey: PGPSecretKey, val s2k = secretKey.pgpSecretKey.s2K
decryptionKeyId: SubkeyIdentifier
): Boolean {
val s2k = secretKey.s2K
if (s2k != null) { if (s2k != null) {
if (s2k.type in 100..110) { if (s2k.type in 100..110) {
LOGGER.debug( 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 return true
} }
} }
@ -514,7 +551,7 @@ class OpenPgpMessageInputStream(
} }
private fun decryptSKESKAndStream( private fun decryptSKESKAndStream(
esks: SortedESKs, esks: ESKsAndData,
skesk: PGPPBEEncryptedData, skesk: PGPPBEEncryptedData,
decryptorFactory: PBEDataDecryptorFactory decryptorFactory: PBEDataDecryptorFactory
): Boolean { ): Boolean {
@ -522,13 +559,13 @@ class OpenPgpMessageInputStream(
val decrypted = skesk.getDataStream(decryptorFactory) val decrypted = skesk.getDataStream(decryptorFactory)
val sessionKey = SessionKey(skesk.getSessionKey(decryptorFactory)) val sessionKey = SessionKey(skesk.getSessionKey(decryptorFactory))
throwIfUnacceptable(sessionKey.algorithm) throwIfUnacceptable(sessionKey.algorithm)
val encryptedData = EncryptedData(sessionKey.algorithm, layerMetadata.depth + 1) val encryptedData = esks.toEncryptedData(sessionKey, layerMetadata.depth + 1)
encryptedData.sessionKey = sessionKey encryptedData.sessionKey = sessionKey
encryptedData.addRecipients(esks.pkesks.map { it.keyID }) encryptedData.addRecipients(esks.pkesks.map { it.keyIdentifier })
LOGGER.debug("Successfully decrypted data with passphrase") LOGGER.debug("Successfully decrypted data with passphrase")
val integrityProtected = IntegrityProtectedInputStream(decrypted, skesk, options) val integrityProtected = IntegrityProtectedInputStream(decrypted, skesk, options)
nestedInputStream = nestedInputStream =
OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) OpenPgpMessageInputStream(integrityProtected, options, encryptedData, api)
return true return true
} catch (e: UnacceptableAlgorithmException) { } catch (e: UnacceptableAlgorithmException) {
throw e throw e
@ -540,7 +577,7 @@ class OpenPgpMessageInputStream(
} }
private fun decryptPKESKAndStream( private fun decryptPKESKAndStream(
esks: SortedESKs, esks: ESKsAndData,
decryptionKeyId: SubkeyIdentifier, decryptionKeyId: SubkeyIdentifier,
decryptorFactory: PublicKeyDataDecryptorFactory, decryptorFactory: PublicKeyDataDecryptorFactory,
pkesk: PGPPublicKeyEncryptedData pkesk: PGPPublicKeyEncryptedData
@ -550,18 +587,14 @@ class OpenPgpMessageInputStream(
val sessionKey = SessionKey(pkesk.getSessionKey(decryptorFactory)) val sessionKey = SessionKey(pkesk.getSessionKey(decryptorFactory))
throwIfUnacceptable(sessionKey.algorithm) throwIfUnacceptable(sessionKey.algorithm)
val encryptedData = val encryptedData = esks.toEncryptedData(sessionKey, layerMetadata.depth)
EncryptedData(
SymmetricKeyAlgorithm.requireFromId(
pkesk.getSymmetricAlgorithm(decryptorFactory)),
layerMetadata.depth + 1)
encryptedData.decryptionKey = decryptionKeyId encryptedData.decryptionKey = decryptionKeyId
encryptedData.sessionKey = sessionKey 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") LOGGER.debug("Successfully decrypted data with key $decryptionKeyId")
val integrityProtected = IntegrityProtectedInputStream(decrypted, pkesk, options) val integrityProtected = IntegrityProtectedInputStream(decrypted, pkesk, options)
nestedInputStream = nestedInputStream =
OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) OpenPgpMessageInputStream(integrityProtected, options, encryptedData, api)
return true return true
} catch (e: UnacceptableAlgorithmException) { } catch (e: UnacceptableAlgorithmException) {
throw e throw e
@ -599,7 +632,7 @@ class OpenPgpMessageInputStream(
throw RuntimeException(e) throw RuntimeException(e)
} }
} }
signatures.finish(layerMetadata, policy) signatures.finish(layerMetadata)
} }
return r return r
} }
@ -626,7 +659,7 @@ class OpenPgpMessageInputStream(
throw RuntimeException(e) throw RuntimeException(e)
} }
} }
signatures.finish(layerMetadata, policy) signatures.finish(layerMetadata)
} }
return r return r
} }
@ -672,44 +705,33 @@ class OpenPgpMessageInputStream(
return MessageMetadata((layerMetadata as Message)) return MessageMetadata((layerMetadata as Message))
} }
private fun getDecryptionKey(keyId: Long): PGPSecretKeyRing? = private fun getDecryptionKey(pkesk: PGPPublicKeyEncryptedData): OpenPGPKey? =
options.getDecryptionKeys().firstOrNull { options.getDecryptionKeys().firstOrNull {
it.any { k -> k.keyID == keyId } it.pgpSecretKeyRing.getSecretKeyFor(pkesk) != null &&
.and(PGPainless.inspectKeyRing(it).decryptionSubkeys.any { k -> k.keyID == keyId }) api.inspect(it).decryptionSubkeys.any { subkey ->
} pkesk.keyIdentifier.matchesExplicit(subkey.keyIdentifier)
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.")
}
} }
} }
private fun getDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List<PGPSecretKeyRing> = private fun getDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List<OpenPGPKey> =
options.getDecryptionKeys().filter { options.getDecryptionKeys().filter {
it.getSecretKeyFor(pkesk) != null && it.pgpSecretKeyRing.getSecretKeyFor(pkesk) != null &&
PGPainless.inspectKeyRing(it).decryptionSubkeys.any { subkey -> api.inspect(it).decryptionSubkeys.any { subkey ->
when (pkesk.version) { pkesk.keyIdentifier.matchesExplicit(subkey.keyIdentifier)
3 -> pkesk.keyID == subkey.keyID
else -> throw NotImplementedError("Version 6 PKESK not yet supported.")
}
} }
} }
private fun findPotentialDecryptionKeys( private fun findPotentialDecryptionKeys(
pkesk: PGPPublicKeyEncryptedData pkesk: PGPPublicKeyEncryptedData
): List<Pair<PGPSecretKeyRing, PGPSecretKey>> { ): List<OpenPGPSecretKey> {
val algorithm = pkesk.algorithm val algorithm = pkesk.algorithm
val candidates = mutableListOf<Pair<PGPSecretKeyRing, PGPSecretKey>>() val candidates = mutableListOf<OpenPGPSecretKey>()
options.getDecryptionKeys().forEach { options.getDecryptionKeys().forEach {
val info = PGPainless.inspectKeyRing(it) val info = api.inspect(it)
for (key in info.decryptionSubkeys) { for (key in info.decryptionSubkeys) {
if (key.algorithm == algorithm && info.isSecretKeyAvailable(key.keyID)) { if (key.pgpPublicKey.algorithm == algorithm &&
candidates.add(it to it.getSecretKey(key.keyID)) info.isSecretKeyAvailable(key.keyIdentifier)) {
candidates.add(it.getSecretKey(key.keyIdentifier))
} }
} }
} }
@ -717,7 +739,8 @@ class OpenPgpMessageInputStream(
} }
private fun isAcceptable(algorithm: SymmetricKeyAlgorithm): Boolean = private fun isAcceptable(algorithm: SymmetricKeyAlgorithm): Boolean =
policy.symmetricKeyDecryptionAlgorithmPolicy.isAcceptable(algorithm) api.algorithmPolicy.messageDecryptionAlgorithmPolicy.symmetricAlgorithmPolicy.isAcceptable(
algorithm)
private fun throwIfUnacceptable(algorithm: SymmetricKeyAlgorithm) { private fun throwIfUnacceptable(algorithm: SymmetricKeyAlgorithm) {
if (!isAcceptable(algorithm)) { 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<PGPPBEEncryptedData> val skesks: List<PGPPBEEncryptedData>
val pkesks: List<PGPPublicKeyEncryptedData> val pkesks: List<PGPPublicKeyEncryptedData>
val anonPkesks: List<PGPPublicKeyEncryptedData> val anonPkesks: List<PGPPublicKeyEncryptedData>
@ -735,14 +783,15 @@ class OpenPgpMessageInputStream(
skesks = mutableListOf() skesks = mutableListOf()
pkesks = mutableListOf() pkesks = mutableListOf()
anonPkesks = mutableListOf() anonPkesks = mutableListOf()
for (esk in esks) { for (esk in esks) {
if (esk is PGPPBEEncryptedData) { if (esk is PGPPBEEncryptedData) {
skesks.add(esk) skesks.add(esk)
} else if (esk is PGPPublicKeyEncryptedData) { } else if (esk is PGPPublicKeyEncryptedData) {
if (esk.keyID != 0L) { if (esk.keyIdentifier.isWildcard) {
pkesks.add(esk)
} else {
anonPkesks.add(esk) anonPkesks.add(esk)
} else {
pkesks.add(esk)
} }
} else { } else {
throw IllegalArgumentException("Unknown ESK class type ${esk.javaClass}") throw IllegalArgumentException("Unknown ESK class type ${esk.javaClass}")
@ -754,9 +803,9 @@ class OpenPgpMessageInputStream(
get() = skesks.plus(pkesks).plus(anonPkesks) get() = skesks.plus(pkesks).plus(anonPkesks)
} }
private class Signatures(val options: ConsumerOptions) : OutputStream() { private class Signatures(val options: ConsumerOptions, val api: PGPainless) : OutputStream() {
val detachedSignatures = mutableListOf<SignatureCheck>() val detachedSignatures = mutableListOf<OpenPGPDocumentSignature>()
val prependedSignatures = mutableListOf<SignatureCheck>() val prependedSignatures = mutableListOf<OpenPGPDocumentSignature>()
val onePassSignatures = mutableListOf<OnePassSignatureCheck>() val onePassSignatures = mutableListOf<OnePassSignatureCheck>()
val opsUpdateStack = ArrayDeque<MutableList<OnePassSignatureCheck>>() val opsUpdateStack = ArrayDeque<MutableList<OnePassSignatureCheck>>()
var literalOPS = mutableListOf<OnePassSignatureCheck>() var literalOPS = mutableListOf<OnePassSignatureCheck>()
@ -775,47 +824,49 @@ class OpenPgpMessageInputStream(
fun addDetachedSignature(signature: PGPSignature) { fun addDetachedSignature(signature: PGPSignature) {
val check = initializeSignature(signature) val check = initializeSignature(signature)
val keyId = signature.issuerKeyId val keyId = signature.issuerKeyId
if (check != null) { if (check.issuer != null) {
detachedSignatures.add(check) detachedSignatures.add(check)
} else { } else {
LOGGER.debug( LOGGER.debug(
"No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") "No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.")
detachedSignaturesWithMissingCert.add( detachedSignaturesWithMissingCert.add(
SignatureVerification.Failure( SignatureVerification.Failure(
signature, null, SignatureValidationException("Missing verification key."))) check, SignatureValidationException("Missing verification key.")))
} }
} }
fun addPrependedSignature(signature: PGPSignature) { fun addPrependedSignature(signature: PGPSignature) {
val check = initializeSignature(signature) val check = initializeSignature(signature)
val keyId = signature.issuerKeyId val keyId = signature.issuerKeyId
if (check != null) { if (check.issuer != null) {
prependedSignatures.add(check) prependedSignatures.add(check)
} else { } else {
LOGGER.debug( LOGGER.debug(
"No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") "No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.")
prependedSignaturesWithMissingCert.add( prependedSignaturesWithMissingCert.add(
SignatureVerification.Failure( SignatureVerification.Failure(
signature, null, SignatureValidationException("Missing verification key"))) check, SignatureValidationException("Missing verification key")))
} }
} }
fun initializeSignature(signature: PGPSignature): SignatureCheck? { fun initializeSignature(signature: PGPSignature): OpenPGPDocumentSignature {
val certificate = findCertificate(signature) ?: return null val certificate =
val publicKey = certificate.getPublicKeyFor(signature) ?: return null findCertificate(signature) ?: return OpenPGPDocumentSignature(signature, null)
val verifierKey = SubkeyIdentifier(certificate, publicKey.keyID) val publicKey =
initialize(signature, publicKey) certificate.getSigningKeyFor(signature)
return SignatureCheck(signature, certificate, verifierKey) ?: return OpenPGPDocumentSignature(signature, null)
initialize(signature, publicKey.pgpPublicKey)
return OpenPGPDocumentSignature(signature, publicKey)
} }
fun addOnePassSignature(signature: PGPOnePassSignature) { fun addOnePassSignature(signature: PGPOnePassSignature) {
val certificate = findCertificate(signature) val certificate = findCertificate(signature)
if (certificate != null) { if (certificate != null) {
val publicKey = certificate.getPublicKeyFor(signature) val publicKey = certificate.getSigningKeyFor(signature)
if (publicKey != null) { if (publicKey != null) {
val ops = OnePassSignatureCheck(signature, certificate) val ops = OnePassSignatureCheck(signature, certificate)
initialize(signature, publicKey) initialize(signature, publicKey.pgpPublicKey)
onePassSignatures.add(ops) onePassSignatures.add(ops)
literalOPS.add(ops) literalOPS.add(ops)
} }
@ -825,15 +876,11 @@ class OpenPgpMessageInputStream(
} }
} }
fun addCorrespondingOnePassSignature( fun addCorrespondingOnePassSignature(signature: PGPSignature, layer: Layer) {
signature: PGPSignature,
layer: Layer,
policy: Policy
) {
var found = false var found = false
val keyId = signature.issuerKeyId for (check in onePassSignatures.reversed()) {
for ((i, check) in onePassSignatures.withIndex().reversed()) { if (!KeyIdentifier.matches(
if (check.onePassSignature.keyID != keyId) { signature.keyIdentifiers, check.onePassSignature.keyIdentifier, true)) {
continue continue
} }
found = true found = true
@ -841,34 +888,42 @@ class OpenPgpMessageInputStream(
if (check.signature != null) { if (check.signature != null) {
continue continue
} }
check.signature = signature check.signature = signature
val verification =
SignatureVerification( val documentSignature =
signature, OpenPGPDocumentSignature(
SubkeyIdentifier(check.verificationKeys, check.onePassSignature.keyID)) signature, check.verificationKeys.getSigningKeyFor(signature))
val verification = SignatureVerification(documentSignature)
try { try {
SignatureValidator.signatureWasCreatedInBounds( signature.assertCreatedInBounds(
options.getVerifyNotBefore(), options.getVerifyNotAfter()) options.getVerifyNotBefore(), options.getVerifyNotAfter())
.verify(signature) if (documentSignature.verify(check.onePassSignature) &&
CertificateValidator.validateCertificateAndVerifyOnePassSignature(check, policy) documentSignature.isValid(api.implementation.policy())) {
LOGGER.debug("Acceptable signature by key ${verification.signingKey}")
layer.addVerifiedOnePassSignature(verification) layer.addVerifiedOnePassSignature(verification)
} else {
throw SignatureValidationException("Incorrect OnePassSignature.")
}
} catch (e: MalformedOpenPGPSignatureException) {
throw SignatureValidationException("Malformed OnePassSignature.", e)
} catch (e: SignatureValidationException) { } catch (e: SignatureValidationException) {
LOGGER.debug("Rejected signature by key ${verification.signingKey}", e)
layer.addRejectedOnePassSignature( layer.addRejectedOnePassSignature(
SignatureVerification.Failure(verification, e)) SignatureVerification.Failure(verification, e))
} catch (e: PGPSignatureException) {
layer.addRejectedOnePassSignature(
SignatureVerification.Failure(
verification, SignatureValidationException(e.message, e)))
} }
break break
} }
if (!found) { if (!found) {
LOGGER.debug( 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( inbandSignaturesWithMissingCert.add(
SignatureVerification.Failure( SignatureVerification.Failure(
signature, null, SignatureValidationException("Missing verification key."))) OpenPGPDocumentSignature(signature, null),
SignatureValidationException("Missing verification key.")))
} }
} }
@ -884,7 +939,7 @@ class OpenPgpMessageInputStream(
opsUpdateStack.removeFirst() opsUpdateStack.removeFirst()
} }
private fun findCertificate(signature: PGPSignature): PGPPublicKeyRing? { private fun findCertificate(signature: PGPSignature): OpenPGPCertificate? {
val cert = options.getCertificateSource().getCertificate(signature) val cert = options.getCertificateSource().getCertificate(signature)
if (cert != null) { if (cert != null) {
return cert return cert
@ -893,21 +948,19 @@ class OpenPgpMessageInputStream(
if (options.getMissingCertificateCallback() != null) { if (options.getMissingCertificateCallback() != null) {
return options return options
.getMissingCertificateCallback()!! .getMissingCertificateCallback()!!
.onMissingPublicKeyEncountered(signature.keyID) .provide(signature.keyIdentifiers.first())
} }
return null // TODO: Missing cert for sig return null // TODO: Missing cert for sig
} }
private fun findCertificate(signature: PGPOnePassSignature): PGPPublicKeyRing? { private fun findCertificate(signature: PGPOnePassSignature): OpenPGPCertificate? {
val cert = options.getCertificateSource().getCertificate(signature.keyID) val cert = options.getCertificateSource().getCertificate(signature.keyIdentifier)
if (cert != null) { if (cert != null) {
return cert return cert
} }
if (options.getMissingCertificateCallback() != null) { if (options.getMissingCertificateCallback() != null) {
return options return options.getMissingCertificateCallback()!!.provide(signature.keyIdentifier)
.getMissingCertificateCallback()!!
.onMissingPublicKeyEncountered(signature.keyID)
} }
return null // TODO: Missing cert for sig 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) { for (detached in detachedSignatures) {
val verification = val verification = SignatureVerification(detached)
SignatureVerification(detached.signature, detached.signingKeyIdentifier)
try { try {
SignatureValidator.signatureWasCreatedInBounds( detached.signature.assertCreatedInBounds(
options.getVerifyNotBefore(), options.getVerifyNotAfter()) options.getVerifyNotBefore(), options.getVerifyNotAfter())
.verify(detached.signature) if (!detached.verify()) {
CertificateValidator.validateCertificateAndVerifyInitializedSignature( throw SignatureValidationException("Incorrect detached signature.")
detached.signature, } else if (!detached.isValid(api.implementation.policy())) {
KeyRingUtils.publicKeys(detached.signingKeyRing), throw SignatureValidationException("Detached signature is not valid.")
policy) } else {
LOGGER.debug("Acceptable signature by key ${verification.signingKey}")
layer.addVerifiedDetachedSignature(verification) layer.addVerifiedDetachedSignature(verification)
}
} catch (e: MalformedOpenPGPSignatureException) {
throw SignatureValidationException("Malformed detached signature.", e)
} catch (e: SignatureValidationException) { } catch (e: SignatureValidationException) {
LOGGER.debug("Rejected signature by key ${verification.signingKey}", e)
layer.addRejectedDetachedSignature( layer.addRejectedDetachedSignature(
SignatureVerification.Failure(verification, e)) SignatureVerification.Failure(verification, e))
} }
} }
for (prepended in prependedSignatures) { for (prepended in prependedSignatures) {
val verification = val verification = SignatureVerification(prepended)
SignatureVerification(prepended.signature, prepended.signingKeyIdentifier)
try { try {
SignatureValidator.signatureWasCreatedInBounds( prepended.signature.assertCreatedInBounds(
options.getVerifyNotBefore(), options.getVerifyNotAfter()) options.getVerifyNotBefore(), options.getVerifyNotAfter())
.verify(prepended.signature) if (prepended.verify() && prepended.isValid(api.implementation.policy())) {
CertificateValidator.validateCertificateAndVerifyInitializedSignature(
prepended.signature,
KeyRingUtils.publicKeys(prepended.signingKeyRing),
policy)
LOGGER.debug("Acceptable signature by key ${verification.signingKey}")
layer.addVerifiedPrependedSignature(verification) layer.addVerifiedPrependedSignature(verification)
} else {
throw SignatureValidationException("Incorrect prepended signature.")
}
} catch (e: MalformedOpenPGPSignatureException) {
throw SignatureValidationException("Malformed prepended signature.", e)
} catch (e: SignatureValidationException) { } catch (e: SignatureValidationException) {
LOGGER.debug("Rejected signature by key ${verification.signingKey}", e) LOGGER.debug("Rejected signature by key ${verification.signingKey}", e)
layer.addRejectedPrependedSignature( layer.addRejectedPrependedSignature(
@ -1029,11 +1081,8 @@ class OpenPgpMessageInputStream(
} }
} }
companion object {
@JvmStatic
private fun initialize(signature: PGPSignature, publicKey: PGPPublicKey) { private fun initialize(signature: PGPSignature, publicKey: PGPPublicKey) {
val verifierProvider = val verifierProvider = api.implementation.pgpContentVerifierBuilderProvider()
ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider
try { try {
signature.init(verifierProvider, publicKey) signature.init(verifierProvider, publicKey)
} catch (e: PGPException) { } catch (e: PGPException) {
@ -1041,10 +1090,8 @@ class OpenPgpMessageInputStream(
} }
} }
@JvmStatic
private fun initialize(ops: PGPOnePassSignature, publicKey: PGPPublicKey) { private fun initialize(ops: PGPOnePassSignature, publicKey: PGPPublicKey) {
val verifierProvider = val verifierProvider = api.implementation.pgpContentVerifierBuilderProvider()
ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider
try { try {
ops.init(verifierProvider, publicKey) ops.init(verifierProvider, publicKey)
} catch (e: PGPException) { } catch (e: PGPException) {
@ -1052,39 +1099,33 @@ class OpenPgpMessageInputStream(
} }
} }
} }
}
companion object { companion object {
@JvmStatic @JvmStatic
private val LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream::class.java) private val LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream::class.java)
@JvmStatic @JvmStatic
fun create(inputStream: InputStream, options: ConsumerOptions) = fun create(inputStream: InputStream, options: ConsumerOptions, api: PGPainless) =
create(inputStream, options, PGPainless.getPolicy()) create(inputStream, options, Message(), api)
@JvmStatic
fun create(inputStream: InputStream, options: ConsumerOptions, policy: Policy) =
create(inputStream, options, Message(), policy)
@JvmStatic @JvmStatic
internal fun create( internal fun create(
inputStream: InputStream, inputStream: InputStream,
options: ConsumerOptions, options: ConsumerOptions,
metadata: Layer, metadata: Layer,
policy: Policy api: PGPainless
): OpenPgpMessageInputStream { ): OpenPgpMessageInputStream {
val openPgpIn = OpenPgpInputStream(inputStream) val openPgpIn = OpenPGPAnimalSnifferInputStream(inputStream)
openPgpIn.reset() openPgpIn.reset()
if (openPgpIn.isNonOpenPgp || options.isForceNonOpenPgpData()) { if (openPgpIn.isNonOpenPgp || options.isForceNonOpenPgpData()) {
return OpenPgpMessageInputStream( return OpenPgpMessageInputStream(
Type.non_openpgp, openPgpIn, options, metadata, policy) Type.non_openpgp, openPgpIn, options, metadata, api)
} }
if (openPgpIn.isBinaryOpenPgp) { if (openPgpIn.isBinaryOpenPgp) {
// Simply consume OpenPGP message // Simply consume OpenPGP message
return OpenPgpMessageInputStream( return OpenPgpMessageInputStream(Type.standard, openPgpIn, options, metadata, api)
Type.standard, openPgpIn, options, metadata, policy)
} }
return if (openPgpIn.isAsciiArmored) { return if (openPgpIn.isAsciiArmored) {
@ -1092,10 +1133,10 @@ class OpenPgpMessageInputStream(
if (armorIn.isClearText) { if (armorIn.isClearText) {
(metadata as Message).setCleartextSigned() (metadata as Message).setCleartextSigned()
OpenPgpMessageInputStream( OpenPgpMessageInputStream(
Type.cleartext_signed, armorIn, options, metadata, policy) Type.cleartext_signed, armorIn, options, metadata, api)
} else { } else {
// Simply consume dearmored OpenPGP message // Simply consume dearmored OpenPGP message
OpenPgpMessageInputStream(Type.standard, armorIn, options, metadata, policy) OpenPgpMessageInputStream(Type.standard, armorIn, options, metadata, api)
} }
} else { } else {
throw AssertionError("Cannot deduce type of data.") throw AssertionError("Cannot deduce type of data.")

View file

@ -5,22 +5,23 @@
package org.pgpainless.decryption_verification package org.pgpainless.decryption_verification
import org.bouncycastle.openpgp.PGPSignature 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.exception.SignatureValidationException
import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.SubkeyIdentifier
import org.pgpainless.signature.SignatureUtils import org.pgpainless.signature.SignatureUtils
/** /**
* Tuple of a signature and an identifier of its corresponding verification key. Semantic meaning of * An evaluated document signature.
* the signature verification (success, failure) is merely given by context. E.g.
* [MessageMetadata.getVerifiedInlineSignatures] contains verified verifications, while the class
* [Failure] contains failed verifications.
* *
* @param signature PGPSignature object * @param documentSignature OpenPGPDocumentSignature 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.
*/ */
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 { override fun toString(): String {
return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)};" + 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 * Tuple object of a [SignatureVerification] and the corresponding
* [SignatureValidationException] that caused the verification to fail. * [SignatureValidationException] that caused the verification to fail.
* *
* @param signatureVerification verification (tuple of [PGPSignature] and corresponding * @param documentSignature signature that could not be verified
* [SubkeyIdentifier])
* @param validationException exception that caused the verification to fail * @param validationException exception that caused the verification to fail
*/ */
data class Failure( data class Failure(
val signature: PGPSignature, val documentSignature: OpenPGPDocumentSignature,
val signingKey: SubkeyIdentifier?,
val validationException: SignatureValidationException 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( constructor(
verification: SignatureVerification, verification: SignatureVerification,
validationException: SignatureValidationException validationException: SignatureValidationException
) : this(verification.signature, verification.signingKey, validationException) ) : this(verification.documentSignature, validationException)
override fun toString(): String { override fun toString(): String {
return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)}; Key: ${signingKey?.toString() ?: "null"}; Failure: ${validationException.message}" return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)}; Key: ${signingKey?.toString() ?: "null"}; Failure: ${validationException.message}"

View file

@ -8,9 +8,9 @@ import java.io.*
import kotlin.jvm.Throws import kotlin.jvm.Throws
import org.bouncycastle.bcpg.ArmoredInputStream import org.bouncycastle.bcpg.ArmoredInputStream
import org.bouncycastle.openpgp.PGPSignatureList import org.bouncycastle.openpgp.PGPSignatureList
import org.bouncycastle.openpgp.api.OpenPGPImplementation
import org.bouncycastle.util.Strings import org.bouncycastle.util.Strings
import org.pgpainless.exception.WrongConsumingMethodException import org.pgpainless.exception.WrongConsumingMethodException
import org.pgpainless.implementation.ImplementationFactory
import org.pgpainless.util.ArmoredInputStreamFactory 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()) val next = objectFactory.nextObject() ?: PGPSignatureList(arrayOf())
return next as PGPSignatureList return next as PGPSignatureList
} }

View file

@ -6,14 +6,13 @@ package org.pgpainless.encryption_signing
import java.security.MessageDigest import java.security.MessageDigest
import org.bouncycastle.openpgp.PGPException 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.PGPSignatureGenerator
import org.bouncycastle.openpgp.api.OpenPGPKey
import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature
import org.pgpainless.PGPainless import org.pgpainless.PGPainless
import org.pgpainless.algorithm.SignatureType import org.pgpainless.algorithm.SignatureType
import org.pgpainless.bouncycastle.extensions.unlock
import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.key.protection.SecretKeyRingProtector
import org.pgpainless.key.protection.UnlockSecretKey
class BcHashContextSigner { class BcHashContextSigner {
@ -22,14 +21,17 @@ class BcHashContextSigner {
fun signHashContext( fun signHashContext(
hashContext: MessageDigest, hashContext: MessageDigest,
signatureType: SignatureType, signatureType: SignatureType,
secretKey: PGPSecretKeyRing, secretKey: OpenPGPKey,
protector: SecretKeyRingProtector protector: SecretKeyRingProtector
): PGPSignature { ): OpenPGPDocumentSignature {
val info = PGPainless.inspectKeyRing(secretKey) val info = PGPainless.getInstance().inspect(secretKey)
return info.signingSubkeys return info.signingSubkeys
.mapNotNull { info.getSecretKey(it.keyID) } .mapNotNull { info.getSecretKey(it.keyIdentifier) }
.firstOrNull() .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.") ?: throw PGPException("Key does not contain suitable signing subkey.")
} }
@ -45,11 +47,13 @@ class BcHashContextSigner {
internal fun signHashContext( internal fun signHashContext(
hashContext: MessageDigest, hashContext: MessageDigest,
signatureType: SignatureType, signatureType: SignatureType,
privateKey: PGPPrivateKey privateKey: OpenPGPKey.OpenPGPPrivateKey
): PGPSignature { ): OpenPGPDocumentSignature {
return PGPSignatureGenerator(BcPGPHashContextContentSignerBuilder(hashContext)) return PGPSignatureGenerator(
.apply { init(signatureType.code, privateKey) } BcPGPHashContextContentSignerBuilder(hashContext), privateKey.keyPair.publicKey)
.apply { init(signatureType.code, privateKey.keyPair.privateKey) }
.generate() .generate()
.let { OpenPGPDocumentSignature(it, privateKey.publicKey) }
} }
} }
} }

View file

@ -5,69 +5,25 @@
package org.pgpainless.encryption_signing package org.pgpainless.encryption_signing
import java.io.OutputStream import java.io.OutputStream
import org.pgpainless.PGPainless.Companion.getPolicy import org.pgpainless.PGPainless
import org.pgpainless.algorithm.CompressionAlgorithm
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity
import org.pgpainless.util.NullOutputStream 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( override fun onOutputStream(
outputStream: OutputStream outputStream: OutputStream
): EncryptionBuilderInterface.WithOptions { ): EncryptionBuilderInterface.WithOptions {
return WithOptionsImpl(outputStream) return WithOptionsImpl(outputStream, api)
} }
override fun discardOutput(): EncryptionBuilderInterface.WithOptions { override fun discardOutput(): EncryptionBuilderInterface.WithOptions {
return onOutputStream(NullOutputStream()) 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 { override fun withOptions(options: ProducerOptions): EncryptionStream {
return EncryptionStream(outputStream, options) return EncryptionStream(outputStream, options, api)
}
}
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
} }
} }
} }

View file

@ -5,55 +5,69 @@
package org.pgpainless.encryption_signing package org.pgpainless.encryption_signing
import java.util.* import java.util.*
import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.PGPPublicKeyRing 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.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator
import org.pgpainless.PGPainless
import org.pgpainless.algorithm.EncryptionPurpose import org.pgpainless.algorithm.EncryptionPurpose
import org.pgpainless.algorithm.SymmetricKeyAlgorithm 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.authentication.CertificateAuthority
import org.pgpainless.encryption_signing.EncryptionOptions.EncryptionKeySelector import org.pgpainless.encryption_signing.EncryptionOptions.EncryptionKeySelector
import org.pgpainless.exception.KeyException import org.pgpainless.exception.KeyException.ExpiredKeyException
import org.pgpainless.exception.KeyException.* import org.pgpainless.exception.KeyException.UnacceptableEncryptionKeyException
import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.exception.KeyException.UnacceptableSelfSignatureException
import org.pgpainless.key.OpenPgpFingerprint
import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.SubkeyIdentifier
import org.pgpainless.key.info.KeyAccessor import org.pgpainless.key.info.KeyAccessor
import org.pgpainless.key.info.KeyRingInfo import org.pgpainless.key.info.KeyRingInfo
import org.pgpainless.util.Passphrase 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<PGPKeyEncryptionMethodGenerator> = mutableSetOf() private val _encryptionMethods: MutableSet<PGPKeyEncryptionMethodGenerator> = mutableSetOf()
private val _encryptionKeyIdentifiers: MutableSet<SubkeyIdentifier> = mutableSetOf() private val keysAndAccessors: MutableMap<OpenPGPComponentKey, KeyAccessor> = mutableMapOf()
private val _keyRingInfo: MutableMap<SubkeyIdentifier, KeyRingInfo> = mutableMapOf() private val _keyRingInfo: MutableMap<SubkeyIdentifier, KeyRingInfo> = mutableMapOf()
private val _keyViews: MutableMap<SubkeyIdentifier, KeyAccessor> = mutableMapOf()
private val encryptionKeySelector: EncryptionKeySelector = encryptToAllCapableSubkeys() private val encryptionKeySelector: EncryptionKeySelector = encryptToAllCapableSubkeys()
private var allowEncryptionWithMissingKeyFlags = false private var allowEncryptionWithMissingKeyFlags = false
private var evaluationDate = Date() private var evaluationDate = Date()
private var _encryptionAlgorithmOverride: SymmetricKeyAlgorithm? = null private var _encryptionMechanismOverride: MessageEncryptionMechanism? = null
val encryptionMethods val encryptionMethods
get() = _encryptionMethods.toSet() get() = _encryptionMethods.toSet()
val encryptionKeyIdentifiers val encryptionKeyIdentifiers
get() = _encryptionKeyIdentifiers.toSet() get() = keysAndAccessors.keys.map { SubkeyIdentifier(it) }
val keyRingInfo val encryptionKeys
get() = _keyRingInfo.toMap() get() = keysAndAccessors.keys.toSet()
val keyViews
get() = _keyViews.toMap()
@Deprecated(
"Deprecated in favor of encryptionMechanismOverride",
replaceWith = ReplaceWith("encryptionMechanismOverride"))
// TODO: Remove in 2.1
val encryptionAlgorithmOverride 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 * Set the evaluation date for certificate evaluation.
* carry the flag [org.pgpainless.algorithm.KeyFlag.ENCRYPT_COMMS].
* *
* @return encryption options * @param evaluationDate reference time
* @return this
*/ */
fun setEvaluationDate(evaluationDate: Date) = apply { this.evaluationDate = evaluationDate } 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. * 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 * @param keys keys
* @return this * @return this
*/ */
@Deprecated("Repeatedly pass OpenPGPCertificate instances instead.")
fun addRecipients(keys: Iterable<PGPPublicKeyRing>) = apply { fun addRecipients(keys: Iterable<PGPPublicKeyRing>) = apply {
keys.toList().let { keys.toList().let {
require(it.isNotEmpty()) { "Set of recipient keys cannot be empty." } 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. * Add all key rings in the provided [Iterable] (e.g.
* [org.bouncycastle.openpgp.PGPPublicKeyRingCollection]) as recipients. Per key ring, the * [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 keys keys
* @param selector encryption key selector * @param selector encryption key selector
* @return this * @return this
*/ */
@Deprecated("Repeatedly pass OpenPGPCertificate instances instead.")
fun addRecipients(keys: Iterable<PGPPublicKeyRing>, selector: EncryptionKeySelector) = apply { fun addRecipients(keys: Iterable<PGPPublicKeyRing>, selector: EncryptionKeySelector) = apply {
keys.toList().let { keys.toList().let {
require(it.isNotEmpty()) { "Set of recipient keys cannot be empty." } 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. * Add a recipient by providing a key.
* *
* @param key key ring * @param key key ring
* @return this * @return this
*/ */
@Deprecated(
"Pass in OpenPGPCertificate instead.",
replaceWith =
ReplaceWith("addRecipient(key.toOpenPGPCertificate(), encryptionKeySelector)"))
fun addRecipient(key: PGPPublicKeyRing) = addRecipient(key, 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 * 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 key key ring
* @param userId user id * @param userId user id
* @return this * @return this
*/ */
@Deprecated(
"Pass in OpenPGPCertificate instead.",
replaceWith = ReplaceWith("addRecipient(key.toOpenPGPCertificate(), userId)"))
fun addRecipient(key: PGPPublicKeyRing, userId: CharSequence) = fun addRecipient(key: PGPPublicKeyRing, userId: CharSequence) =
addRecipient(key, userId, encryptionKeySelector) 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( fun addRecipient(
key: PGPPublicKeyRing, cert: OpenPGPCertificate,
userId: CharSequence, userId: CharSequence,
encryptionKeySelector: EncryptionKeySelector encryptionKeySelector: EncryptionKeySelector
) = apply { ) = apply {
val info = KeyRingInfo(key, evaluationDate) val info = api.inspect(cert, evaluationDate)
val subkeys = val subkeys =
encryptionKeySelector.selectEncryptionSubkeys( encryptionKeySelector.selectEncryptionSubkeys(
info.getEncryptionSubkeys(userId, purpose)) info.getEncryptionSubkeys(userId, purpose))
if (subkeys.isEmpty()) { if (subkeys.isEmpty()) {
throw KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)) throw UnacceptableEncryptionKeyException(cert)
} }
for (subkey in subkeys) { for (subkey in subkeys) {
val keyId = SubkeyIdentifier(key, subkey.keyID) val keyId = SubkeyIdentifier(subkey)
_keyRingInfo[keyId] = info _keyRingInfo[keyId] = info
_keyViews[keyId] = KeyAccessor.ViaUserId(info, keyId, userId.toString()) val accessor = KeyAccessor.ViaUserId(subkey, cert.getUserId(userId.toString()))
addRecipientKey(key, subkey, false) 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 @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( fun addHiddenRecipient(
key: PGPPublicKeyRing, key: PGPPublicKeyRing,
selector: EncryptionKeySelector = encryptionKeySelector selector: EncryptionKeySelector = encryptionKeySelector
) = apply { addAsRecipient(key, selector, true) } ) = addHiddenRecipient(api.toCertificate(key), selector)
private fun addAsRecipient( private fun addAsRecipient(
key: PGPPublicKeyRing, cert: OpenPGPCertificate,
selector: EncryptionKeySelector, selector: EncryptionKeySelector,
wildcardKeyId: Boolean wildcardKeyId: Boolean
) = apply { ) = apply {
val info = KeyRingInfo(key, evaluationDate) val info = api.inspect(cert, evaluationDate)
val primaryKeyExpiration = val primaryKeyExpiration =
try { try {
info.primaryKeyExpirationDate info.primaryKeyExpirationDate
} catch (e: NoSuchElementException) { } catch (e: NoSuchElementException) {
throw UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key)) throw UnacceptableSelfSignatureException(cert)
} }
if (primaryKeyExpiration != null && primaryKeyExpiration < evaluationDate) { if (primaryKeyExpiration != null && primaryKeyExpiration < evaluationDate) {
throw ExpiredKeyException(OpenPgpFingerprint.of(key), primaryKeyExpiration) throw ExpiredKeyException(cert, primaryKeyExpiration)
} }
var encryptionSubkeys = selector.selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose)) var encryptionSubkeys = selector.selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose))
@ -193,31 +317,31 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
if (encryptionSubkeys.isEmpty() && allowEncryptionWithMissingKeyFlags) { if (encryptionSubkeys.isEmpty() && allowEncryptionWithMissingKeyFlags) {
encryptionSubkeys = encryptionSubkeys =
info.validSubkeys info.validSubkeys
.filter { it.isEncryptionKey } .filter { it.pgpPublicKey.isEncryptionKey }
.filter { info.getKeyFlagsOf(it.keyID).isEmpty() } .filter { info.getKeyFlagsOf(it.keyIdentifier).isEmpty() }
} }
if (encryptionSubkeys.isEmpty()) { if (encryptionSubkeys.isEmpty()) {
throw UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)) throw UnacceptableEncryptionKeyException(cert)
} }
for (subkey in encryptionSubkeys) { for (subkey in encryptionSubkeys) {
val keyId = SubkeyIdentifier(key, subkey.keyID) val keyId = SubkeyIdentifier(subkey)
_keyRingInfo[keyId] = info _keyRingInfo[keyId] = info
_keyViews[keyId] = KeyAccessor.ViaKeyId(info, keyId) val accessor = KeyAccessor.ViaKeyIdentifier(subkey)
addRecipientKey(key, subkey, wildcardKeyId) addRecipientKey(subkey, accessor, wildcardKeyId)
} }
} }
private fun addRecipientKey( private fun addRecipientKey(
certificate: PGPPublicKeyRing, key: OpenPGPComponentKey,
key: PGPPublicKey, accessor: KeyAccessor,
wildcardKeyId: Boolean wildcardRecipient: Boolean
) { ) {
_encryptionKeyIdentifiers.add(SubkeyIdentifier(certificate, key.keyID)) keysAndAccessors[key] = accessor
addEncryptionMethod( addEncryptionMethod(
ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(key).also { api.implementation.publicKeyKeyEncryptionMethodGenerator(key.pgpPublicKey).also {
it.setUseWildcardKeyID(wildcardKeyId) it.setUseWildcardRecipient(wildcardRecipient)
}) })
} }
@ -241,7 +365,7 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
fun addMessagePassphrase(passphrase: Passphrase) = apply { fun addMessagePassphrase(passphrase: Passphrase) = apply {
require(!passphrase.isEmpty) { "Passphrase MUST NOT be empty." } require(!passphrase.isEmpty) { "Passphrase MUST NOT be empty." }
addEncryptionMethod( 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 * @param encryptionAlgorithm encryption algorithm override
* @return this * @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 { fun overrideEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply {
require(encryptionAlgorithm != SymmetricKeyAlgorithm.NULL) { require(encryptionAlgorithm != SymmetricKeyAlgorithm.NULL) {
"Encryption algorithm override cannot be 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 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 interface EncryptionKeySelector {
fun selectEncryptionSubkeys(encryptionCapableKeys: List<PGPPublicKey>): List<PGPPublicKey> fun selectEncryptionSubkeys(
encryptionCapableKeys: List<OpenPGPComponentKey>
): List<OpenPGPComponentKey>
} }
companion object { 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. * Only encrypt to the first valid encryption capable subkey we stumble upon.

View file

@ -8,23 +8,45 @@ import java.util.*
import org.bouncycastle.openpgp.PGPLiteralData import org.bouncycastle.openpgp.PGPLiteralData
import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPPublicKeyRing
import org.bouncycastle.openpgp.PGPSignature 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.CompressionAlgorithm
import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.algorithm.StreamEncoding
import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.algorithm.SymmetricKeyAlgorithm
import org.pgpainless.bouncycastle.extensions.matches import org.pgpainless.bouncycastle.extensions.matches
import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.SubkeyIdentifier
import org.pgpainless.util.MultiMap import org.pgpainless.util.MultiMap
import org.pgpainless.util.SessionKey
data class EncryptionResult( data class EncryptionResult(
val encryptionAlgorithm: SymmetricKeyAlgorithm, val encryptionMechanism: MessageEncryptionMechanism,
val sessionKey: SessionKey?,
val compressionAlgorithm: CompressionAlgorithm, val compressionAlgorithm: CompressionAlgorithm,
val detachedSignatures: MultiMap<SubkeyIdentifier, PGPSignature>, val detachedDocumentSignatures: OpenPGPSignatureSet<OpenPGPDocumentSignature>,
val recipients: Set<SubkeyIdentifier>, val recipients: Set<SubkeyIdentifier>,
val fileName: String, val fileName: String,
val modificationDate: Date, val modificationDate: Date,
val fileEncoding: StreamEncoding 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<SubkeyIdentifier, PGPSignature>
get() {
val map = MultiMap<SubkeyIdentifier, PGPSignature>()
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 * Return true, if the message is marked as for-your-eyes-only. This is typically done by
* setting the filename "_CONSOLE". * setting the filename "_CONSOLE".
@ -34,6 +56,9 @@ data class EncryptionResult(
val isForYourEyesOnly: Boolean val isForYourEyesOnly: Boolean
get() = PGPLiteralData.CONSOLE == fileName 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. * 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 { class Builder {
var _encryptionAlgorithm: SymmetricKeyAlgorithm? = null var _encryptionMechanism: MessageEncryptionMechanism =
MessageEncryptionMechanism.unencrypted()
var _compressionAlgorithm: CompressionAlgorithm? = null var _compressionAlgorithm: CompressionAlgorithm? = null
val detachedSignatures: MultiMap<SubkeyIdentifier, PGPSignature> = MultiMap() val detachedSignatures: MutableList<OpenPGPDocumentSignature> = mutableListOf()
val recipients: Set<SubkeyIdentifier> = mutableSetOf() val recipients: Set<SubkeyIdentifier> = mutableSetOf()
private var _fileName = "" private var _fileName = ""
private var _modificationDate = Date(0) private var _modificationDate = Date(0)
private var _encoding = StreamEncoding.BINARY private var _encoding = StreamEncoding.BINARY
private var _sessionKey: SessionKey? = null
fun setEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply { fun setEncryptionMechanism(mechanism: MessageEncryptionMechanism): Builder = apply {
_encryptionAlgorithm = encryptionAlgorithm _encryptionMechanism = mechanism
} }
fun setCompressionAlgorithm(compressionAlgorithm: CompressionAlgorithm) = apply { fun setCompressionAlgorithm(compressionAlgorithm: CompressionAlgorithm) = apply {
@ -81,19 +108,20 @@ data class EncryptionResult(
(recipients as MutableSet).add(recipient) (recipients as MutableSet).add(recipient)
} }
fun addDetachedSignature( fun setSessionKey(sessionKey: SessionKey) = apply { _sessionKey = sessionKey }
signingSubkeyIdentifier: SubkeyIdentifier,
detachedSignature: PGPSignature fun addDetachedSignature(signature: OpenPGPDocumentSignature): Builder = apply {
) = apply { detachedSignatures.put(signingSubkeyIdentifier, detachedSignature) } detachedSignatures.add(signature)
}
fun build(): EncryptionResult { fun build(): EncryptionResult {
checkNotNull(_encryptionAlgorithm) { "Encryption algorithm not set." }
checkNotNull(_compressionAlgorithm) { "Compression algorithm not set." } checkNotNull(_compressionAlgorithm) { "Compression algorithm not set." }
return EncryptionResult( return EncryptionResult(
_encryptionAlgorithm!!, _encryptionMechanism,
_sessionKey,
_compressionAlgorithm!!, _compressionAlgorithm!!,
detachedSignatures, OpenPGPSignatureSet(detachedSignatures),
recipients, recipients,
_fileName, _fileName,
_modificationDate, _modificationDate,

View file

@ -13,12 +13,14 @@ import org.bouncycastle.openpgp.PGPCompressedDataGenerator
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator import org.bouncycastle.openpgp.PGPEncryptedDataGenerator
import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPException
import org.bouncycastle.openpgp.PGPLiteralDataGenerator 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.CompressionAlgorithm
import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.algorithm.StreamEncoding
import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.bouncycastle.extensions.pgpDataEncryptorBuilder
import org.pgpainless.implementation.ImplementationFactory
import org.pgpainless.util.ArmoredOutputStreamFactory import org.pgpainless.util.ArmoredOutputStreamFactory
import org.slf4j.LoggerFactory import org.pgpainless.util.SessionKey
// 1 << 8 causes wrong partial body length encoding // 1 << 8 causes wrong partial body length encoding
// 1 << 9 fixes this. // 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, * OutputStream that produces an OpenPGP message. The message can be encrypted, signed, or both,
* depending on its configuration. * 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 <a * @see
* href="https://github.com/neuhalje/bouncy-gpg/blob/master/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStream.java">Source</a> * [PGPEncryptingStream](https://github.com/neuhalje/bouncy-gpg/blob/master/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStream.java)
*/ */
class EncryptionStream( class EncryptionStream(
private var outermostStream: OutputStream, private var outermostStream: OutputStream,
private val options: ProducerOptions, private val options: ProducerOptions,
private val api: PGPainless
) : OutputStream() { ) : OutputStream() {
private val resultBuilder: EncryptionResult.Builder = EncryptionResult.builder() private val resultBuilder: EncryptionResult.Builder = EncryptionResult.builder()
@ -62,12 +65,10 @@ class EncryptionStream(
private fun prepareArmor() { private fun prepareArmor() {
if (!options.isAsciiArmor) { if (!options.isAsciiArmor) {
LOGGER.debug("Output will be unarmored.")
return return
} }
outermostStream = BufferedOutputStream(outermostStream) outermostStream = BufferedOutputStream(outermostStream)
LOGGER.debug("Wrap encryption output in ASCII armor.")
armorOutputStream = armorOutputStream =
ArmoredOutputStreamFactory.get(outermostStream, options).also { outermostStream = it } ArmoredOutputStreamFactory.get(outermostStream, options).also { outermostStream = it }
} }
@ -75,45 +76,43 @@ class EncryptionStream(
@Throws(IOException::class, PGPException::class) @Throws(IOException::class, PGPException::class)
private fun prepareEncryption() { private fun prepareEncryption() {
if (options.encryptionOptions == null) { if (options.encryptionOptions == null) {
// No encryption options -> no encryption
resultBuilder.setEncryptionAlgorithm(SymmetricKeyAlgorithm.NULL)
return return
} }
require(options.encryptionOptions.encryptionMethods.isNotEmpty()) { require(options.encryptionOptions.encryptionMethods.isNotEmpty()) {
"If EncryptionOptions are provided, at least one encryption method MUST be provided as well." "If EncryptionOptions are provided, at least one encryption method MUST be provided as well."
} }
EncryptionBuilder.negotiateSymmetricEncryptionAlgorithm(options.encryptionOptions).let { val mechanism: MessageEncryptionMechanism =
resultBuilder.setEncryptionAlgorithm(it) options.encryptionOptions.negotiateEncryptionMechanism()
LOGGER.debug("Encrypt message using symmetric algorithm $it.") resultBuilder.setEncryptionMechanism(mechanism)
val encryptedDataGenerator = val encryptedDataGenerator =
PGPEncryptedDataGenerator( PGPEncryptedDataGenerator(api.implementation.pgpDataEncryptorBuilder(mechanism))
ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(it).apply {
setWithIntegrityPacket(true)
})
options.encryptionOptions.encryptionMethods.forEach { m -> options.encryptionOptions.encryptionMethods.forEach { m ->
encryptedDataGenerator.addMethod(m) encryptedDataGenerator.addMethod(m)
} }
options.encryptionOptions.encryptionKeyIdentifiers.forEach { r -> options.encryptionOptions.encryptionKeyIdentifiers.forEach { r ->
resultBuilder.addRecipient(r) resultBuilder.addRecipient(r)
} }
encryptedDataGenerator.setSessionKeyExtractionCallback { pgpSessionKey ->
if (pgpSessionKey != null) {
resultBuilder.setSessionKey(SessionKey(pgpSessionKey))
}
}
publicKeyEncryptedStream = publicKeyEncryptedStream =
encryptedDataGenerator.open(outermostStream, ByteArray(BUFFER_SIZE)).also { stream encryptedDataGenerator.open(outermostStream, ByteArray(BUFFER_SIZE)).also { stream ->
->
outermostStream = stream outermostStream = stream
} }
} }
}
@Throws(IOException::class) @Throws(IOException::class)
private fun prepareCompression() { private fun prepareCompression() {
EncryptionBuilder.negotiateCompressionAlgorithm(options).let { options.negotiateCompressionAlgorithm(api.algorithmPolicy).let {
resultBuilder.setCompressionAlgorithm(it) resultBuilder.setCompressionAlgorithm(it)
compressedDataGenerator = PGPCompressedDataGenerator(it.algorithmId) compressedDataGenerator = PGPCompressedDataGenerator(it.algorithmId)
if (it == CompressionAlgorithm.UNCOMPRESSED) return if (it == CompressionAlgorithm.UNCOMPRESSED) return
LOGGER.debug("Compress using $it.")
basicCompressionStream = basicCompressionStream =
BCPGOutputStream(compressedDataGenerator!!.open(outermostStream)).also { stream -> BCPGOutputStream(compressedDataGenerator!!.open(outermostStream)).also { stream ->
outermostStream = stream outermostStream = stream
@ -249,8 +248,9 @@ class EncryptionStream(
options.signingOptions.signingMethods.entries.reversed().forEach { (key, method) -> options.signingOptions.signingMethods.entries.reversed().forEach { (key, method) ->
method.signatureGenerator.generate().let { sig -> method.signatureGenerator.generate().let { sig ->
val documentSignature = OpenPGPDocumentSignature(sig, key.publicKey)
if (method.isDetached) { if (method.isDetached) {
resultBuilder.addDetachedSignature(key, sig) resultBuilder.addDetachedSignature(documentSignature)
} }
if (!method.isDetached || options.isCleartextSigned) { if (!method.isDetached || options.isCleartextSigned) {
sig.encode(signatureLayerStream) sig.encode(signatureLayerStream)
@ -266,8 +266,4 @@ class EncryptionStream(
val isClosed val isClosed
get() = closed get() = closed
companion object {
@JvmStatic private val LOGGER = LoggerFactory.getLogger(EncryptionStream::class.java)
}
} }

View file

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<S : OpenPGPSignature>(val signatures: List<S>) : Iterable<S> {
fun getSignaturesBy(cert: OpenPGPCertificate): List<S> =
signatures.filter { sig -> sig.signature.keyIdentifiers.any { cert.getKey(it) != null } }
fun getSignaturesBy(componentKey: OpenPGPCertificate.OpenPGPComponentKey): List<S> =
signatures.filter { sig ->
sig.signature.keyIdentifiers.any { componentKey.keyIdentifier.matchesExplicit(it) }
}
override fun iterator(): Iterator<S> {
return signatures.iterator()
}
}

View file

@ -6,16 +6,17 @@ package org.pgpainless.encryption_signing
import java.util.* import java.util.*
import org.bouncycastle.openpgp.PGPLiteralData import org.bouncycastle.openpgp.PGPLiteralData
import org.pgpainless.PGPainless
import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.CompressionAlgorithm
import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.algorithm.StreamEncoding
import org.pgpainless.algorithm.negotiation.CompressionAlgorithmNegotiator
import org.pgpainless.policy.Policy
class ProducerOptions class ProducerOptions(
private constructor(
val encryptionOptions: EncryptionOptions?, val encryptionOptions: EncryptionOptions?,
val signingOptions: SigningOptions? val signingOptions: SigningOptions?
) { ) {
var compressionAlgorithmNegotiator: CompressionAlgorithmNegotiator =
CompressionAlgorithmNegotiator.staticNegotiation()
private var _fileName: String = "" private var _fileName: String = ""
private var _modificationDate: Date = PGPLiteralData.NOW private var _modificationDate: Date = PGPLiteralData.NOW
private var encodingField: StreamEncoding = StreamEncoding.BINARY private var encodingField: StreamEncoding = StreamEncoding.BINARY
@ -24,8 +25,8 @@ private constructor(
private var _hideArmorHeaders = false private var _hideArmorHeaders = false
var isDisableAsciiArmorCRC = false var isDisableAsciiArmorCRC = false
private var _compressionAlgorithmOverride: CompressionAlgorithm = private var _compressionAlgorithmOverride: CompressionAlgorithm? = null
PGPainless.getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm
private var asciiArmor = true private var asciiArmor = true
private var _comment: String? = null private var _comment: String? = null
private var _version: String? = null private var _version: String? = null
@ -104,6 +105,13 @@ private constructor(
*/ */
fun hasVersion() = version != null 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 { fun setCleartextSigned() = apply {
require(signingOptions != null) { require(signingOptions != null) {
"Signing Options cannot be null if cleartext signing is enabled." "Signing Options cannot be null if cleartext signing is enabled."
@ -174,8 +182,8 @@ private constructor(
* *
* @param encoding encoding * @param encoding encoding
* @return this * @return this
* @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.9">RFC4880 §5.9. * @see
* Literal Data Packet</a> * [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 the default value of [StreamEncoding.BINARY] are discouraged.
*/ */
@Deprecated("Options other than BINARY are discouraged.") @Deprecated("Options other than BINARY are discouraged.")
@ -212,7 +220,7 @@ private constructor(
_compressionAlgorithmOverride = compressionAlgorithm _compressionAlgorithmOverride = compressionAlgorithm
} }
val compressionAlgorithmOverride: CompressionAlgorithm val compressionAlgorithmOverride: CompressionAlgorithm?
get() = _compressionAlgorithmOverride get() = _compressionAlgorithmOverride
val isHideArmorHeaders: Boolean val isHideArmorHeaders: Boolean
@ -230,6 +238,11 @@ private constructor(
_hideArmorHeaders = hideArmorHeaders _hideArmorHeaders = hideArmorHeaders
} }
internal fun negotiateCompressionAlgorithm(policy: Policy): CompressionAlgorithm {
return compressionAlgorithmNegotiator.negotiate(
policy, compressionAlgorithmOverride, setOf())
}
companion object { companion object {
/** /**
* Sign and encrypt some data. * Sign and encrypt some data.
@ -239,8 +252,10 @@ private constructor(
* @return builder * @return builder
*/ */
@JvmStatic @JvmStatic
fun signAndEncrypt(encryptionOptions: EncryptionOptions, signingOptions: SigningOptions) = fun signAndEncrypt(
ProducerOptions(encryptionOptions, signingOptions) encryptionOptions: EncryptionOptions,
signingOptions: SigningOptions
): ProducerOptions = ProducerOptions(encryptionOptions, signingOptions)
/** /**
* Sign some data without encryption. * Sign some data without encryption.
@ -248,7 +263,9 @@ private constructor(
* @param signingOptions signing options * @param signingOptions signing options
* @return builder * @return builder
*/ */
@JvmStatic fun sign(signingOptions: SigningOptions) = ProducerOptions(null, signingOptions) @JvmStatic
fun sign(signingOptions: SigningOptions): ProducerOptions =
ProducerOptions(null, signingOptions)
/** /**
* Encrypt some data without signing. * Encrypt some data without signing.
@ -257,13 +274,14 @@ private constructor(
* @return builder * @return builder
*/ */
@JvmStatic @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. * Only wrap the data in an OpenPGP packet. No encryption or signing will be applied.
* *
* @return builder * @return builder
*/ */
@JvmStatic fun noEncryptionNoSigning() = ProducerOptions(null, null) @JvmStatic fun noEncryptionNoSigning(): ProducerOptions = ProducerOptions(null, null)
} }
} }

View file

@ -5,28 +5,32 @@
package org.pgpainless.encryption_signing package org.pgpainless.encryption_signing
import java.util.* import java.util.*
import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.*
import org.pgpainless.PGPainless.Companion.getPolicy import org.bouncycastle.openpgp.api.OpenPGPImplementation
import org.pgpainless.PGPainless.Companion.inspectKeyRing 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.DocumentSignatureType
import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.HashAlgorithm
import org.pgpainless.algorithm.PublicKeyAlgorithm.Companion.requireFromId import org.pgpainless.algorithm.PublicKeyAlgorithm.Companion.requireFromId
import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator
import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator.Companion.negotiateSignatureHashAlgorithm 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.exception.KeyException.* import org.pgpainless.exception.KeyException.*
import org.pgpainless.implementation.ImplementationFactory
import org.pgpainless.key.OpenPgpFingerprint.Companion.of import org.pgpainless.key.OpenPgpFingerprint.Companion.of
import org.pgpainless.key.SubkeyIdentifier
import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.key.protection.SecretKeyRingProtector
import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey
import org.pgpainless.policy.Policy import org.pgpainless.policy.Policy
import org.pgpainless.signature.subpackets.BaseSignatureSubpackets.Callback import org.pgpainless.signature.subpackets.BaseSignatureSubpackets.Callback
import org.pgpainless.signature.subpackets.SignatureSubpackets import org.pgpainless.signature.subpackets.SignatureSubpackets
import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper
class SigningOptions { class SigningOptions(private val api: PGPainless) {
var hashAlgorithmNegotiator: HashAlgorithmNegotiator =
val signingMethods: Map<SubkeyIdentifier, SigningMethod> = mutableMapOf() negotiateSignatureHashAlgorithm(api.algorithmPolicy)
val signingMethods: Map<OpenPGPPrivateKey, SigningMethod> = mutableMapOf()
private var _hashAlgorithmOverride: HashAlgorithm? = null private var _hashAlgorithmOverride: HashAlgorithm? = null
private var _evaluationDate: Date = Date() private var _evaluationDate: Date = Date()
@ -46,6 +50,7 @@ class SigningOptions {
_hashAlgorithmOverride = hashAlgorithmOverride _hashAlgorithmOverride = hashAlgorithmOverride
} }
/** Evaluation date for signing keys. */
val evaluationDate: Date val evaluationDate: Date
get() = _evaluationDate get() = _evaluationDate
@ -61,18 +66,35 @@ class SigningOptions {
* Sign the message using an inline signature made by the provided signing key. * Sign the message using an inline signature made by the provided signing key.
* *
* @param signingKeyProtector protector to unlock the 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 * @return this
* @throws KeyException if something is wrong with the key * @throws KeyException if something is wrong with the key
* @throws PGPException if the key cannot be unlocked or a signing method cannot be created * @throws PGPException if the key cannot be unlocked or a signing method cannot be created
*/ */
@Throws(KeyException::class, PGPException::class) @Throws(KeyException::class, PGPException::class)
fun addSignature(signingKeyProtector: SecretKeyRingProtector, signingKey: PGPSecretKeyRing) = fun addSignature(
apply { signingKeyProtector: SecretKeyRingProtector,
signingKey: OpenPGPKey
): SigningOptions = apply {
addInlineSignature( addInlineSignature(
signingKeyProtector, signingKey, null, DocumentSignatureType.BINARY_DOCUMENT) 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) =
addSignature(signingKeyProtector, api.toKey(signingKey))
/** /**
* Add inline signatures with all secret key rings in the provided secret key ring collection. * Add inline signatures with all secret key rings in the provided secret key ring collection.
* *
@ -85,6 +107,8 @@ class SigningOptions {
* created * created
*/ */
@Throws(KeyException::class, PGPException::class) @Throws(KeyException::class, PGPException::class)
@Deprecated("Repeatedly call addInlineSignature(), passing an OpenPGPKey instead.")
// TODO: Remove in 2.1
fun addInlineSignatures( fun addInlineSignatures(
signingKeyProtector: SecretKeyRingProtector, signingKeyProtector: SecretKeyRingProtector,
signingKeys: Iterable<PGPSecretKeyRing>, signingKeys: Iterable<PGPSecretKeyRing>,
@ -93,6 +117,23 @@ class SigningOptions {
signingKeys.forEach { addInlineSignature(signingKeyProtector, it, null, signatureType) } 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 * 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. * 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 PGPException if the key cannot be unlocked or the signing method cannot be created
*/ */
@Throws(KeyException::class, PGPException::class) @Throws(KeyException::class, PGPException::class)
@Deprecated("Pass an OpenPGPKey instead.")
// TODO: Remove in 2.1
fun addInlineSignature( fun addInlineSignature(
signingKeyProtector: SecretKeyRingProtector, signingKeyProtector: SecretKeyRingProtector,
signingKey: PGPSecretKeyRing, signingKey: PGPSecretKeyRing,
signatureType: DocumentSignatureType 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 * 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 KeyException if the key is invalid
* @throws PGPException if the key cannot be unlocked or the signing method cannot be created * @throws PGPException if the key cannot be unlocked or the signing method cannot be created
*/ */
@Throws(KeyException::class, PGPException::class)
@JvmOverloads @JvmOverloads
fun addInlineSignature( fun addInlineSignature(
signingKeyProtector: SecretKeyRingProtector, signingKeyProtector: SecretKeyRingProtector,
signingKey: PGPSecretKeyRing, signingKey: OpenPGPKey,
userId: CharSequence? = null, userId: CharSequence? = null,
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
subpacketsCallback: Callback? = null subpacketsCallback: Callback? = null
) = apply { ) = apply {
val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) val keyRingInfo = api.inspect(signingKey, evaluationDate)
if (userId != null && !keyRingInfo.isUserIdValid(userId)) { if (userId != null && !keyRingInfo.isUserIdValid(userId)) {
throw UnboundUserIdException( throw UnboundUserIdException(
of(signingKey), of(signingKey),
@ -148,23 +190,89 @@ class SigningOptions {
val signingPubKeys = keyRingInfo.signingSubkeys val signingPubKeys = keyRingInfo.signingSubkeys
if (signingPubKeys.isEmpty()) { if (signingPubKeys.isEmpty()) {
throw UnacceptableSigningKeyException(of(signingKey)) throw UnacceptableSigningKeyException(signingKey)
} }
for (signingPubKey in signingPubKeys) { for (signingPubKey in signingPubKeys) {
val signingSecKey: PGPSecretKey = val signingSecKey: OpenPGPSecretKey =
signingKey.getSecretKey(signingPubKey.keyID) signingKey.getSecretKey(signingPubKey)
?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) ?: throw MissingSecretKeyException(signingPubKey)
val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector) val signingPrivKey: OpenPGPPrivateKey =
unlockSecretKey(signingSecKey, signingKeyProtector)
val hashAlgorithms = val hashAlgorithms =
if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId)
else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyIdentifier)
val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms)
addSigningMethod( 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.
*
* <p>
* 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. * Create an inline signature using the signing key with the given keyId.
* *
@ -183,35 +291,23 @@ class SigningOptions {
*/ */
@Throws(KeyException::class, PGPException::class) @Throws(KeyException::class, PGPException::class)
@JvmOverloads @JvmOverloads
@Deprecated("Pass in an OpenPGPSecretKey instead.")
// TODO: Remove in 2.1
fun addInlineSignature( fun addInlineSignature(
signingKeyProtector: SecretKeyRingProtector, signingKeyProtector: SecretKeyRingProtector,
signingKey: PGPSecretKeyRing, signingKey: PGPSecretKeyRing,
keyId: Long, keyId: Long,
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
subpacketsCallback: Callback? = null subpacketsCallback: Callback? = null
) = apply { ): SigningOptions {
val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) val key = api.toKey(signingKey)
val signingPubKeys = keyRingInfo.signingSubkeys val subkeyIdentifier = KeyIdentifier(keyId)
if (signingPubKeys.isEmpty()) { return addInlineSignature(
throw UnacceptableSigningKeyException(of(signingKey)) signingKeyProtector,
} key.getSecretKey(subkeyIdentifier)
?: throw MissingSecretKeyException(of(signingKey), subkeyIdentifier),
for (signingPubKey in signingPubKeys) { signatureType,
if (signingPubKey.keyID != keyId) { subpacketsCallback)
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)
} }
/** /**
@ -226,6 +322,8 @@ class SigningOptions {
* method cannot be created * method cannot be created
*/ */
@Throws(KeyException::class, PGPException::class) @Throws(KeyException::class, PGPException::class)
@Deprecated("Repeatedly call addDetachedSignature(), passing an OpenPGPKey instead.")
// TODO: Remove in 2.1
fun addDetachedSignatures( fun addDetachedSignatures(
signingKeyProtector: SecretKeyRingProtector, signingKeyProtector: SecretKeyRingProtector,
signingKeys: Iterable<PGPSecretKeyRing>, signingKeys: Iterable<PGPSecretKeyRing>,
@ -234,6 +332,12 @@ class SigningOptions {
signingKeys.forEach { addDetachedSignature(signingKeyProtector, it, null, signatureType) } 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 * 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 * 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 * @throws PGPException if the key cannot be validated or unlocked, or if no signature method
* can be created * can be created
*/ */
@Deprecated("Pass an OpenPGPKey instead.")
@Throws(KeyException::class, PGPException::class) @Throws(KeyException::class, PGPException::class)
// TODO: Remove in 2.1
fun addDetachedSignature( fun addDetachedSignature(
signingKeyProtector: SecretKeyRingProtector, signingKeyProtector: SecretKeyRingProtector,
signingKey: PGPSecretKeyRing, signingKey: PGPSecretKeyRing,
@ -273,15 +379,14 @@ class SigningOptions {
* can be created * can be created
*/ */
@JvmOverloads @JvmOverloads
@Throws(KeyException::class, PGPException::class)
fun addDetachedSignature( fun addDetachedSignature(
signingKeyProtector: SecretKeyRingProtector, signingKeyProtector: SecretKeyRingProtector,
signingKey: PGPSecretKeyRing, signingKey: OpenPGPKey,
userId: String? = null, userId: CharSequence? = null,
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
subpacketCallback: Callback? = null subpacketCallback: Callback? = null
) = apply { ): SigningOptions = apply {
val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) val keyRingInfo = api.inspect(signingKey, evaluationDate)
if (userId != null && !keyRingInfo.isUserIdValid(userId)) { if (userId != null && !keyRingInfo.isUserIdValid(userId)) {
throw UnboundUserIdException( throw UnboundUserIdException(
of(signingKey), of(signingKey),
@ -292,21 +397,78 @@ class SigningOptions {
val signingPubKeys = keyRingInfo.signingSubkeys val signingPubKeys = keyRingInfo.signingSubkeys
if (signingPubKeys.isEmpty()) { if (signingPubKeys.isEmpty()) {
throw UnacceptableSigningKeyException(of(signingKey)) throw UnacceptableSigningKeyException(signingKey)
} }
for (signingPubKey in signingPubKeys) { for (signingPubKey in signingPubKeys) {
val signingSecKey: PGPSecretKey = val signingSecKey: OpenPGPSecretKey =
signingKey.getSecretKey(signingPubKey.keyID) signingKey.getSecretKey(signingPubKey.keyIdentifier)
?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) ?: throw MissingSecretKeyException(signingPubKey)
val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector) 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).
*
* <p>
* 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 = val hashAlgorithms =
if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId)
else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) else keyRingInfo.getPreferredHashAlgorithms(signingKey.keyIdentifier)
val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms)
addSigningMethod( addSigningMethod(signingPrivKey, hashAlgorithm, signatureType, true, subpacketCallback)
signingKey, signingSubkey, hashAlgorithm, signatureType, true, subpacketCallback)
}
} }
/** /**
@ -327,63 +489,45 @@ class SigningOptions {
*/ */
@Throws(KeyException::class, PGPException::class) @Throws(KeyException::class, PGPException::class)
@JvmOverloads @JvmOverloads
@Deprecated("Pass an OpenPGPSecretKey instead.")
// TODO: Remove in 2.1
fun addDetachedSignature( fun addDetachedSignature(
signingKeyProtector: SecretKeyRingProtector, signingKeyProtector: SecretKeyRingProtector,
signingKey: PGPSecretKeyRing, signingKey: PGPSecretKeyRing,
keyId: Long, keyId: Long,
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
subpacketsCallback: Callback? = null subpacketsCallback: Callback? = null
) = apply { ): SigningOptions {
val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) val key = api.toKey(signingKey)
val signingKeyIdentifier = KeyIdentifier(keyId)
val signingPubKeys = keyRingInfo.signingSubkeys return addDetachedSignature(
if (signingPubKeys.isEmpty()) { signingKeyProtector,
throw UnacceptableSigningKeyException(of(signingKey)) key.getSecretKey(signingKeyIdentifier)
} ?: throw MissingSecretKeyException(of(key), signingKeyIdentifier),
null,
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, signatureType,
true,
subpacketsCallback) subpacketsCallback)
return this
}
}
throw MissingSecretKeyException(of(signingKey), keyId)
} }
private fun addSigningMethod( private fun addSigningMethod(
signingKey: PGPSecretKeyRing, signingKey: OpenPGPPrivateKey,
signingSubkey: PGPPrivateKey,
hashAlgorithm: HashAlgorithm, hashAlgorithm: HashAlgorithm,
signatureType: DocumentSignatureType, signatureType: DocumentSignatureType,
detached: Boolean, detached: Boolean,
subpacketCallback: Callback? = null subpacketCallback: Callback? = null
) { ) {
val signingKeyIdentifier = SubkeyIdentifier(signingKey, signingSubkey.keyID) val signingSecretKey: PGPSecretKey = signingKey.secretKey.pgpSecretKey
val signingSecretKey: PGPSecretKey = signingKey.getSecretKey(signingSubkey.keyID)
val publicKeyAlgorithm = requireFromId(signingSecretKey.publicKey.algorithm) val publicKeyAlgorithm = requireFromId(signingSecretKey.publicKey.algorithm)
val bitStrength = signingSecretKey.publicKey.bitStrength val bitStrength = signingSecretKey.publicKey.bitStrength
if (!getPolicy().publicKeyAlgorithmPolicy.isAcceptable(publicKeyAlgorithm, bitStrength)) { if (!api.algorithmPolicy.publicKeyAlgorithmPolicy.isAcceptable(
publicKeyAlgorithm, bitStrength)) {
throw UnacceptableSigningKeyException( throw UnacceptableSigningKeyException(
PublicKeyAlgorithmPolicyException( PublicKeyAlgorithmPolicyException(
of(signingKey), signingSecretKey.keyID, publicKeyAlgorithm, bitStrength)) signingKey.secretKey, publicKeyAlgorithm, bitStrength))
} }
val generator: PGPSignatureGenerator = val generator: PGPSignatureGenerator =
createSignatureGenerator(signingSubkey, hashAlgorithm, signatureType) createSignatureGenerator(signingKey.keyPair, hashAlgorithm, signatureType)
// Subpackets // Subpackets
val hashedSubpackets = val hashedSubpackets =
@ -399,7 +543,7 @@ class SigningOptions {
val signingMethod = val signingMethod =
if (detached) SigningMethod.detachedSignature(generator, hashAlgorithm) if (detached) SigningMethod.detachedSignature(generator, hashAlgorithm)
else SigningMethod.inlineSignature(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 * @param policy policy
* @return selected hash algorithm * @return selected hash algorithm
*/ */
private fun negotiateHashAlgorithm( private fun negotiateHashAlgorithm(preferences: Set<HashAlgorithm>?): HashAlgorithm {
preferences: Set<HashAlgorithm>, return _hashAlgorithmOverride ?: hashAlgorithmNegotiator.negotiateHashAlgorithm(preferences)
policy: Policy
): HashAlgorithm {
return _hashAlgorithmOverride
?: negotiateSignatureHashAlgorithm(policy).negotiateHashAlgorithm(preferences)
} }
@Throws(PGPException::class) @Throws(PGPException::class)
private fun createSignatureGenerator( private fun createSignatureGenerator(
privateKey: PGPPrivateKey, signingKey: PGPKeyPair,
hashAlgorithm: HashAlgorithm, hashAlgorithm: HashAlgorithm,
signatureType: DocumentSignatureType signatureType: DocumentSignatureType
): PGPSignatureGenerator { ): PGPSignatureGenerator {
return ImplementationFactory.getInstance() return OpenPGPImplementation.getInstance()
.getPGPContentSignerBuilder( .pgpContentSignerBuilder(signingKey.publicKey.algorithm, hashAlgorithm.algorithmId)
privateKey.publicKeyPacket.algorithm, hashAlgorithm.algorithmId)
.let { csb -> .let { csb ->
PGPSignatureGenerator(csb).also { PGPSignatureGenerator(csb, signingKey.publicKey).also {
it.init(signatureType.signatureType.code, privateKey) it.init(signatureType.signatureType.code, signingKey.privateKey)
} }
} }
} }
companion object { companion object {
@JvmStatic fun get() = SigningOptions() @JvmOverloads
@JvmStatic
fun get(api: PGPainless = PGPainless.getInstance()) = SigningOptions(api)
} }
/** A method of signing. */ /** A method of signing. */

View file

@ -0,0 +1,207 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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)
}
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."
}
}
}
}

View file

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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")

View file

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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.",
)
}

Some files were not shown because too many files have changed in this diff Show more