1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-09-09 02:09:38 +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
## 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
- Actually attempt to fix Kotlin desugaring.
- 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
setup of encryption/decryption operations, as well as straight forward key generation.
PGPainless is based around the Bouncy Castle java library and can be used on Android down to API level 10.
PGPainless is based around the Bouncy Castle java library and can be used on Android.
It can be configured to either use the Java Cryptographic Engine (JCE), or Bouncy Castles lightweight reimplementation.
While signature verification in Bouncy Castle is limited to signature correctness, PGPainless goes much further.
@ -32,7 +32,7 @@ It also checks if signing subkeys are properly bound to their primary key, if ke
if keys are allowed to create signatures in the first place.
These rigorous checks make PGPainless stand out from other Java-based OpenPGP libraries and are the reason why
PGPainless currently [*scores first place* on Sequoia-PGPs Interoperability Test-Suite](https://tests.sequoia-pgp.org).
PGPainless currently scores above average on Sequoia-PGPs [Interoperability Test-Suite](https://tests.sequoia-pgp.org).
> At FlowCrypt we are using PGPainless in our Kotlin code bases on Android and on server side.
> The ergonomics of legacy PGP tooling on Java is not very good, and PGPainless improves it greatly.
@ -65,24 +65,23 @@ Reading keys from ASCII armored strings or from binary files is easy:
```java
String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"...
PGPSecretKeyRing secretKey = PGPainless.readKeyRing()
.secretKeyRing(key);
OpenPGPKey secretKey = PGPainless.getInstance().readKey()
.parseKey(key);
```
Similarly, keys can quickly be exported::
```java
PGPSecretKeyRing secretKey = ...;
String armored = PGPainless.asciiArmor(secretKey);
ByteArrayOutputStream binary = new ByteArrayOutputStream();
secretKey.encode(binary);
OpenPGPKey secretKey = ...;
String armored = secretKey.toAsciiArmoredString();
byte[] binary = secretKey.getEncoded();
```
Extract a public key certificate from a secret key:
```java
PGPSecretKeyRing secretKey = ...;
PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey);
OpenPGPKey secretKey = ...;
OpenPGPCertificate certificate = secretKey.toCertificate();
```
### Easily Generate Keys
@ -90,16 +89,17 @@ PGPainless comes with a simple to use `KeyRingBuilder` class that helps you to q
There are some predefined key archetypes, but it is possible to fully customize key generation to your needs.
```java
PGPainless api = PGPainless.getInstance();
// RSA key without additional subkeys
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing()
OpenPGPKey secretKeys = api.generateKey()
.simpleRsaKeyRing("Juliet <juliet@montague.lit>", RsaLength._4096);
// 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!");
// Customized key
PGPSecretKeyRing keyRing = PGPainless.buildKeyRing()
OpenPGPKey keyRing = api.buildKey()
.setPrimaryKey(KeySpec.getBuilder(
RSA.withLength(RsaLength._8192),
KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER))
@ -124,24 +124,26 @@ algorithms accordingly.
Still it allows you to manually specify which algorithms to use of course.
```java
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
PGPainless api = PGPainless.getInstance();
EncryptionStream encryptionStream = api.generateMessage()
.onOutputStream(outputStream)
.withOptions(
ProducerOptions.signAndEncrypt(
new EncryptionOptions()
EncryptionOptions.get(api)
.addRecipient(aliceKey)
.addRecipient(bobsKey)
// optionally encrypt to a passphrase
.addMessagePassphrase(Passphrase.fromPassword("password123"))
// optionally override symmetric encryption algorithm
.overrideEncryptionAlgorithm(SymmetricKeyAlgorithm.AES_192),
new SigningOptions()
SigningOptions.get(api)
// Sign in-line (using one-pass-signature packet)
.addInlineSignature(secretKeyDecryptor, aliceSecKey, signatureType)
// Sign using a detached signature
.addDetachedSignature(secretKeyDecryptor, aliceSecKey, signatureType)
// optionally override hash algorithm
.overrideHashAlgorithm(HashAlgorithm.SHA256)
.overrideHashAlgorithm(HashAlgorithm.SHA256),
api
).setAsciiArmor(true) // Ascii armor or not
);
@ -161,9 +163,9 @@ Furthermore, PGPainless will reject signatures made using weak algorithms like S
This behaviour can be modified though using the `Policy` class.
```java
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
DecryptionStream decryptionStream = PGPainless.getInstance().processMessage()
.onInputStream(encryptedInputStream)
.withOptions(new ConsumerOptions()
.withOptions(ConsumerOptions.get(api)
.addDecryptionKey(bobSecKeys, secretKeyProtector)
.addVerificationCert(alicePubKeys)
);
@ -191,7 +193,7 @@ repositories {
}
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.
## Acknowledgements
Development on PGPainless is generously sponsored by [FlowCrypt.com](https://flowcrypt.com). Thank you very very very much!
In the past, development on PGPainless has been generously sponsored by [FlowCrypt.com](https://flowcrypt.com). Thank you very very very much!
[![FlowCrypt Logo](https://blog.jabberhead.tk/wp-content/uploads/2022/05/flowcrypt-logo.svg)](https://flowcrypt.com)
Parts of PGPainless development ([project page](https://nlnet.nl/project/PGPainless/)) will be funded by [NGI Assure](https://nlnet.nl/assure/) through [NLNet](https://nlnet.nl).
Parts of PGPainless development ([project page](https://nlnet.nl/project/PGPainless/)) has been funded by [NGI Assure](https://nlnet.nl/assure/) through [NLNet](https://nlnet.nl).
NGI Assure is made possible with financial support from the [European Commission](https://ec.europa.eu/)'s [Next Generation Internet](https://ngi.eu/) programme, under the aegis of [DG Communications Networks, Content and Technology](https://ec.europa.eu/info/departments/communications-networks-content-and-technology_en).
[![NGI Assure Logo](https://blog.jabberhead.tk/wp-content/uploads/2022/05/NGIAssure_tag.svg)](https://nlnet.nl/assure/)

View file

@ -93,6 +93,12 @@ precedence = "aggregate"
SPDX-FileCopyrightText = "2022 Paul Schaub <info@pgpainless.org>, 2017 Steve Smith"
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]]
path = "pgpainless-cli/rewriteManPages.sh"
precedence = "aggregate"

View file

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

View file

@ -35,4 +35,5 @@ Contents
quickstart.md
pgpainless-cli/usage.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 {
id 'application'
id 'org.graalvm.buildtools.native' version '0.10.6'
id 'com.gradleup.shadow' version '8.3.6'
}
graalvmNative {
toolchainDetection = true
}
dependencies {
@ -16,7 +22,8 @@ dependencies {
// implementation "ch.qos.logback:logback-core:1.2.6"
// We want logback logging in tests and in the app
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 "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.File;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.api.OpenPGPCertificate;
import org.bouncycastle.openpgp.api.OpenPGPKey;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.key.info.KeyRingInfo;
@ -25,24 +22,26 @@ import sop.exception.SOPGPException;
public class ExtractCertCmdTest extends CLITest {
private final PGPainless api = PGPainless.getInstance();
public ExtractCertCmdTest() {
super(LoggerFactory.getLogger(ExtractCertCmdTest.class));
}
@Test
public void testExtractCert()
throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException {
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing()
throws IOException {
OpenPGPKey key = api.generateKey()
.simpleEcKeyRing("Juliet Capulet <juliet@capulet.lit>");
pipeBytesToStdin(secretKeys.getEncoded());
pipeBytesToStdin(key.getEncoded());
ByteArrayOutputStream out = pipeStdoutToStream();
assertSuccess(executeCommand("extract-cert", "--armor"));
assertTrue(out.toString().startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"));
PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(out.toByteArray());
KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys);
OpenPGPCertificate certificate = api.readKey().parseCertificate(out.toByteArray());
KeyRingInfo info = api.inspect(certificate);
assertFalse(info.isSecretKey());
assertTrue(info.isUserIdValid("Juliet Capulet <juliet@capulet.lit>"));
}

View file

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

View file

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

View file

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

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
* expiration dates), this method will return 'null' if seconds is 0.
*
* @param date date
* @param seconds number of seconds to be added
* @return date plus seconds or null if seconds is '0'
*/
@ -25,9 +24,19 @@ fun Date.plusSeconds(seconds: Long): Date? {
return if (seconds == 0L) null else Date(this.time + 1000 * seconds)
}
/** This date in seconds since epoch. */
val Date.asSeconds: Long
get() = time / 1000
/**
* Return the number of seconds that would need to be added to this date in order to reach the later
* date. Note: This method requires the [later] date to be indeed greater or equal to this date,
* otherwise an [IllegalArgumentException] is thrown.
*
* @param later later date
* @return difference between this and [later] in seconds
* @throws IllegalArgumentException if later is not greater or equal to this date
*/
fun Date.secondsTill(later: Date): Long {
require(this <= later) { "Timestamp MUST be before the later timestamp." }
return later.asSeconds - this.asSeconds

View file

@ -9,7 +9,7 @@ fun Long.openPgpKeyId(): String {
return String.format("%016X", this).uppercase()
}
/** Parse a Long form a 16 digit hex encoded OpenPgp key-ID. */
/** Parse a Long from a 16 digit hex encoded OpenPgp key-ID. */
fun Long.Companion.fromOpenPgpKeyId(hexKeyId: String): Long {
require("^[0-9A-Fa-f]{16}$".toRegex().matches(hexKeyId)) {
"Provided long key-id does not match expected format. " +

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
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import java.util.*
import org.bouncycastle.bcpg.ArmoredOutputStream
import org.bouncycastle.bcpg.BCPGOutputStream
import org.bouncycastle.bcpg.PacketFormat
import org.bouncycastle.openpgp.PGPKeyRing
import org.bouncycastle.openpgp.PGPPublicKeyRing
import org.bouncycastle.openpgp.PGPSecretKeyRing
import org.bouncycastle.openpgp.PGPSignature
import org.bouncycastle.openpgp.api.OpenPGPApi
import org.bouncycastle.openpgp.api.OpenPGPCertificate
import org.bouncycastle.openpgp.api.OpenPGPImplementation
import org.bouncycastle.openpgp.api.OpenPGPKey
import org.bouncycastle.openpgp.api.OpenPGPKeyGenerator
import org.bouncycastle.openpgp.api.OpenPGPKeyReader
import org.bouncycastle.openpgp.api.OpenPGPSignature
import org.bouncycastle.openpgp.api.bc.BcOpenPGPApi
import org.pgpainless.algorithm.OpenPGPKeyVersion
import org.pgpainless.bouncycastle.PolicyAdapter
import org.pgpainless.bouncycastle.extensions.setAlgorithmSuite
import org.pgpainless.decryption_verification.DecryptionBuilder
import org.pgpainless.encryption_signing.EncryptionBuilder
import org.pgpainless.key.certification.CertifyCertificate
@ -18,34 +33,270 @@ import org.pgpainless.key.generation.KeyRingTemplates
import org.pgpainless.key.info.KeyRingInfo
import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor
import org.pgpainless.key.parsing.KeyRingReader
import org.pgpainless.key.util.KeyRingUtils
import org.pgpainless.policy.Policy
import org.pgpainless.util.ArmorUtils
class PGPainless private constructor() {
/**
* Main entry point to the PGPainless OpenPGP API. Historically, this class was used through static
* factory methods only, and configuration was done using the Singleton pattern. However, now it is
* recommended to instantiate the API and apply configuration on a per-instance manner. The benefit
* of this being that you can have multiple differently configured instances at the same time.
*
* @param implementation OpenPGP Implementation - either BCs lightweight
* [org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation] or JCAs
* [org.bouncycastle.openpgp.api.jcajce.JcaOpenPGPImplementation].
* @param algorithmPolicy policy, deciding acceptable algorithms
*/
class PGPainless(
val implementation: OpenPGPImplementation = OpenPGPImplementation.getInstance(),
val algorithmPolicy: Policy = Policy()
) {
constructor(
algorithmPolicy: Policy
) : this(OpenPGPImplementation.getInstance(), algorithmPolicy)
private val api: OpenPGPApi
init {
implementation.setPolicy(
PolicyAdapter(algorithmPolicy)) // adapt PGPainless' Policy to BCs OpenPGPPolicy
api = BcOpenPGPApi(implementation)
}
@JvmOverloads
fun toAsciiArmor(
certOrKey: OpenPGPCertificate,
packetFormat: PacketFormat = PacketFormat.ROUNDTRIP
): String {
val armorBuilder = ArmoredOutputStream.builder().clearHeaders()
ArmorUtils.keyToHeader(certOrKey.primaryKey.pgpPublicKey)
.getOrDefault(ArmorUtils.HEADER_COMMENT, setOf())
.forEach { armorBuilder.addComment(it) }
return certOrKey.toAsciiArmoredString(packetFormat, armorBuilder)
}
@JvmOverloads
fun toAsciiArmor(
signature: OpenPGPSignature,
packetFormat: PacketFormat = PacketFormat.ROUNDTRIP
): String {
val armorBuilder = ArmoredOutputStream.builder().clearHeaders()
armorBuilder.addComment(signature.keyIdentifier.toPrettyPrint())
return signature.toAsciiArmoredString(packetFormat, armorBuilder)
}
@JvmOverloads
fun toAsciiArmor(
signature: PGPSignature,
packetFormat: PacketFormat = PacketFormat.ROUNDTRIP
): String {
val armorBuilder = ArmoredOutputStream.builder().clearHeaders()
OpenPGPSignature.getMostExpressiveIdentifier(signature.keyIdentifiers)?.let {
armorBuilder.addComment(it.toPrettyPrint())
}
val bOut = ByteArrayOutputStream()
val aOut = armorBuilder.build(bOut)
val pOut = BCPGOutputStream(aOut, packetFormat)
signature.encode(pOut)
pOut.close()
aOut.close()
return bOut.toString()
}
/**
* Generate a new [OpenPGPKey] from predefined templates.
*
* @param version [OpenPGPKeyVersion], defaults to [OpenPGPKeyVersion.v4]
* @param creationTime of the key, defaults to now
* @return [KeyRingTemplates] api
*/
@JvmOverloads
fun generateKey(
version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4,
creationTime: Date = Date()
): KeyRingTemplates = KeyRingTemplates(version, creationTime, this)
/**
* Build a fresh, custom [OpenPGPKey] using PGPainless' API.
*
* @param version [OpenPGPKeyVersion], defaults to [OpenPGPKeyVersion.v4]
* @return [KeyRingBuilder] api
*/
@JvmOverloads
fun buildKey(version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4): KeyRingBuilder =
KeyRingBuilder(version, this)
/**
* Build a fresh, custom [OpenPGPKey] using BCs new API.
*
* @param version [OpenPGPKeyVersion], defaults to [OpenPGPKeyVersion.v4]
* @param creationTime creation time of the key, defaults to now
* @return [OpenPGPKeyGenerator] api
*/
@JvmOverloads
fun _buildKey(
version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4,
creationTime: Date = Date()
): OpenPGPKeyGenerator =
OpenPGPKeyGenerator(
implementation,
version.numeric,
algorithmPolicy.keyProtectionSettings.aead,
creationTime)
.setAlgorithmSuite(algorithmPolicy.keyGenerationAlgorithmSuite)
/**
* Inspect an [OpenPGPKey] or [OpenPGPCertificate], gaining convenient access to its properties.
*
* @param keyOrCertificate [OpenPGPKey] or [OpenPGPCertificate]
* @param referenceTime reference time for evaluation
* @return [KeyRingInfo] wrapper
*/
@JvmOverloads
fun inspect(keyOrCertificate: OpenPGPCertificate, referenceTime: Date = Date()): KeyRingInfo =
KeyRingInfo(keyOrCertificate, this, referenceTime)
/**
* Modify an [OpenPGPKey], adding new components and signatures. This API can be used to add new
* subkeys, user-ids or user-attributes to the key, extend or alter its expiration time, revoke
* individual components of the entire certificate, etc.
*
* @param key key to modify
* @param referenceTime timestamp for modifications
* @return [SecretKeyRingEditor] api
*/
@JvmOverloads
fun modify(key: OpenPGPKey, referenceTime: Date = Date()): SecretKeyRingEditor =
SecretKeyRingEditor(key, this, referenceTime)
/**
* Parse [OpenPGPKey]/[OpenPGPCertificate] material from binary or ASCII armored encoding.
*
* @return [OpenPGPKeyReader] api
*/
fun readKey(): OpenPGPKeyReader = api.readKeyOrCertificate()
/**
* Convert a [PGPSecretKeyRing] into an [OpenPGPKey].
*
* @param secretKeyRing mid-level API [PGPSecretKeyRing] object
* @return high-level API [OpenPGPKey] object
*/
fun toKey(secretKeyRing: PGPSecretKeyRing): OpenPGPKey =
OpenPGPKey(secretKeyRing, implementation)
/**
* Convert a [PGPPublicKeyRing] into an [OpenPGPCertificate].
*
* @param certificate mid-level API [PGPSecretKeyRing] object
* @return high-level API [OpenPGPCertificate] object
*/
fun toCertificate(certificate: PGPPublicKeyRing): OpenPGPCertificate =
OpenPGPCertificate(certificate, implementation)
/**
* Depending on the type, convert either a [PGPSecretKeyRing] into an [OpenPGPKey] or a
* [PGPPublicKeyRing] into an [OpenPGPCertificate].
*
* @param keyOrCertificate [PGPKeyRing], either [PGPSecretKeyRing] or [PGPPublicKeyRing]
* @return depending on the type of [keyOrCertificate], either an [OpenPGPKey] or
* [OpenPGPCertificate]
*/
fun toKeyOrCertificate(keyOrCertificate: PGPKeyRing): OpenPGPCertificate =
when (keyOrCertificate) {
is PGPSecretKeyRing -> toKey(keyOrCertificate)
is PGPPublicKeyRing -> toCertificate(keyOrCertificate)
else ->
throw IllegalArgumentException(
"Unexpected PGPKeyRing subclass: ${keyOrCertificate.javaClass.name}")
}
/**
* Merge two copies of an [OpenPGPCertificate] into a single copy. This method can be used to
* import new third-party signatures into a certificate.
*
* @param originalCopy local copy of the certificate
* @param updatedCopy copy of the same certificate, potentially carrying new signatures and
* components
* @return merged [OpenPGPCertificate]
*/
fun mergeCertificate(
originalCopy: OpenPGPCertificate,
updatedCopy: OpenPGPCertificate
): OpenPGPCertificate {
return OpenPGPCertificate.join(originalCopy, updatedCopy)
}
/**
* Generate an encrypted and/or signed OpenPGP message.
*
* @return [EncryptionBuilder] api
*/
fun generateMessage(): EncryptionBuilder = EncryptionBuilder(this)
/**
* Process an OpenPGP message. This method attempts decryption of encrypted messages and
* performs signature verification.
*
* @return [DecryptionBuilder] api
*/
fun processMessage(): DecryptionBuilder = DecryptionBuilder(this)
/**
* Create certification signatures on third-party [OpenPGPCertificates][OpenPGPCertificate].
*
* @return [CertifyCertificate] api
*/
fun generateCertification(): CertifyCertificate = CertifyCertificate(this)
companion object {
@Volatile private var instance: PGPainless? = null
@JvmStatic
fun getInstance(): PGPainless =
instance ?: synchronized(this) { instance ?: PGPainless().also { instance = it } }
@JvmStatic
fun setInstance(api: PGPainless) {
instance = api
}
/**
* Generate a fresh OpenPGP key ring from predefined templates.
* Generate a fresh [OpenPGPKey] from predefined templates.
*
* @return templates
*/
@JvmStatic fun generateKeyRing() = KeyRingTemplates()
@JvmStatic
@JvmOverloads
@Deprecated(
"Call .generateKey() on an instance of PGPainless instead.",
replaceWith = ReplaceWith("generateKey(version)"))
fun generateKeyRing(version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4): KeyRingTemplates =
getInstance().generateKey(version)
/**
* Build a custom OpenPGP key ring.
*
* @return builder
*/
@JvmStatic fun buildKeyRing() = KeyRingBuilder()
@JvmStatic
@JvmOverloads
@Deprecated(
"Call buildKey() on an instance of PGPainless instead.",
replaceWith = ReplaceWith("buildKey(version)"))
fun buildKeyRing(version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4): KeyRingBuilder =
getInstance().buildKey(version)
/**
* Read an existing OpenPGP key ring.
*
* @return builder
*/
@JvmStatic fun readKeyRing() = KeyRingReader()
@JvmStatic
@Deprecated("Use readKey() instead.", replaceWith = ReplaceWith("readKey()"))
fun readKeyRing(): KeyRingReader = KeyRingReader()
/**
* Extract a public key certificate from a secret key.
@ -54,8 +305,9 @@ class PGPainless private constructor() {
* @return public key certificate
*/
@JvmStatic
fun extractCertificate(secretKey: PGPSecretKeyRing) =
KeyRingUtils.publicKeyRingFrom(secretKey)
@Deprecated("Use .toKey() and then .toCertificate() instead.")
fun extractCertificate(secretKey: PGPSecretKeyRing): PGPPublicKeyRing =
secretKey.toCertificate()
/**
* Merge two copies of the same certificate (e.g. an old copy, and one retrieved from a key
@ -67,8 +319,12 @@ class PGPainless private constructor() {
* @throws PGPException in case of an error
*/
@JvmStatic
fun mergeCertificate(originalCopy: PGPPublicKeyRing, updatedCopy: PGPPublicKeyRing) =
PGPPublicKeyRing.join(originalCopy, updatedCopy)
@Deprecated(
"Use mergeCertificate() instead.", replaceWith = ReplaceWith("mergeCertificate()"))
fun mergeCertificate(
originalCopy: PGPPublicKeyRing,
updatedCopy: PGPPublicKeyRing
): PGPPublicKeyRing = PGPPublicKeyRing.join(originalCopy, updatedCopy)
/**
* Wrap a key or certificate in ASCII armor.
@ -78,9 +334,14 @@ class PGPainless private constructor() {
* @throws IOException in case of an error during the armoring process
*/
@JvmStatic
fun asciiArmor(key: PGPKeyRing) =
if (key is PGPSecretKeyRing) ArmorUtils.toAsciiArmoredString(key)
else ArmorUtils.toAsciiArmoredString(key as PGPPublicKeyRing)
fun asciiArmor(key: PGPKeyRing): String =
getInstance().toAsciiArmor(getInstance().toKeyOrCertificate(key))
@JvmStatic
@Deprecated(
"Call getInstance().toAsciiArmor(cert) instead.",
replaceWith = ReplaceWith("getInstance().toAsciiArmor(cert)"))
fun asciiArmor(cert: OpenPGPCertificate): String = getInstance().toAsciiArmor(cert)
/**
* Wrap a key of certificate in ASCII armor and write the result into the given
@ -105,7 +366,10 @@ class PGPainless private constructor() {
* @throws IOException in case of an error during the armoring process
*/
@JvmStatic
fun asciiArmor(signature: PGPSignature) = ArmorUtils.toAsciiArmoredString(signature)
@Deprecated(
"Call toAsciiArmor(signature) on an instance of PGPainless instead.",
replaceWith = ReplaceWith("getInstance().toAsciiArmor(signature)"))
fun asciiArmor(signature: PGPSignature): String = getInstance().toAsciiArmor(signature)
/**
* Create an [EncryptionBuilder], which can be used to encrypt and/or sign data using
@ -113,7 +377,11 @@ class PGPainless private constructor() {
*
* @return builder
*/
@JvmStatic fun encryptAndOrSign() = EncryptionBuilder()
@Deprecated(
"Call generateMessage() on an instance of PGPainless instead.",
replaceWith = ReplaceWith("generateMessage()"))
@JvmStatic
fun encryptAndOrSign(): EncryptionBuilder = getInstance().generateMessage()
/**
* Create a [DecryptionBuilder], which can be used to decrypt and/or verify data using
@ -121,7 +389,11 @@ class PGPainless private constructor() {
*
* @return builder
*/
@JvmStatic fun decryptAndOrVerify() = DecryptionBuilder()
@Deprecated(
"Call processMessage() on an instance of PGPainless instead.",
replaceWith = ReplaceWith("processMessage()"))
@JvmStatic
fun decryptAndOrVerify(): DecryptionBuilder = getInstance().processMessage()
/**
* Make changes to a secret key at the given reference time. This method can be used to
@ -137,35 +409,56 @@ class PGPainless private constructor() {
*/
@JvmStatic
@JvmOverloads
fun modifyKeyRing(secretKey: PGPSecretKeyRing, referenceTime: Date = Date()) =
SecretKeyRingEditor(secretKey, referenceTime)
fun modifyKeyRing(
secretKey: PGPSecretKeyRing,
referenceTime: Date = Date()
): SecretKeyRingEditor = getInstance().modify(getInstance().toKey(secretKey), referenceTime)
/**
* Quickly access information about a [org.bouncycastle.openpgp.PGPPublicKeyRing] /
* [PGPSecretKeyRing]. This method can be used to determine expiration dates, key flags and
* other information about a key at a specific time.
*
* @param keyRing key ring
* @param key key ring
* @param referenceTime date of inspection
* @return access object
*/
@JvmStatic
@JvmOverloads
fun inspectKeyRing(key: PGPKeyRing, referenceTime: Date = Date()) =
KeyRingInfo(key, referenceTime)
@Deprecated(
"Use inspect(key) on an instance of PGPainless instead.",
replaceWith = ReplaceWith("inspect(key)"))
fun inspectKeyRing(key: PGPKeyRing, referenceTime: Date = Date()): KeyRingInfo =
getInstance().inspect(getInstance().toKeyOrCertificate(key), referenceTime)
@JvmStatic
@JvmOverloads
@Deprecated(
"Use inspect(key) on an instance of PGPainless instead.",
replaceWith = ReplaceWith("inspect(key)"))
fun inspectKeyRing(key: OpenPGPCertificate, referenceTime: Date = Date()): KeyRingInfo =
getInstance().inspect(key, referenceTime)
/**
* Access, and make changes to PGPainless policy on acceptable/default algorithms etc.
*
* @return policy
*/
@JvmStatic fun getPolicy() = Policy.getInstance()
@Deprecated(
"Use PGPainless.getInstance().getAlgorithmPolicy() instead.",
replaceWith = ReplaceWith("getInstance().algorithmPolicy"))
@JvmStatic
fun getPolicy(): Policy = getInstance().algorithmPolicy
/**
* Create different kinds of signatures on other keys.
*
* @return builder
*/
@JvmStatic fun certify() = CertifyCertificate()
@Deprecated(
"Call .generateCertification() on an instance of PGPainless instead.",
replaceWith = ReplaceWith("generateCertification()"))
@JvmStatic
fun certify(): CertifyCertificate = getInstance().generateCertification()
}
}

View file

@ -4,35 +4,75 @@
package org.pgpainless.algorithm
import org.bouncycastle.openpgp.api.MessageEncryptionMechanism
/**
* AEAD Algorithm.
*
* @param algorithmId numeric algorithm id
* @param ivLength length of the initialization vector
* @param tagLength length of the tag
* @see
* [RFC9580 - AEAD Algorithms](https://www.rfc-editor.org/rfc/rfc9580.html#name-aead-algorithms)
*/
enum class AEADAlgorithm(val algorithmId: Int, val ivLength: Int, val tagLength: Int) {
/**
* Encrypt-then-Authenticate-then-Translate mode.
* https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-eax-mode
*
* @see [RFC9580 - EAX Mode](https://www.rfc-editor.org/rfc/rfc9580.html#name-eax-mode)
*/
EAX(1, 16, 16),
/**
* Offset-Codebook mode. OCB is mandatory to implement in crypto-refresh. Favored by GnuPG. Is
* not yet FIPS compliant, but supported by most implementations and therefore favorable.
* https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-ocb-mode
*
* @see [RFC9580 - OCB Mode](https://www.rfc-editor.org/rfc/rfc9580.html#name-ocb-mode)
*/
OCB(2, 15, 16),
/**
* Galois/Counter-Mode. GCM is controversial. Some say it is hard to get right. Some
* implementations like GnuPG therefore avoid it. May be necessary to achieve FIPS compliance.
* https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-gcm-mode
*
* @see [RFC9580 - GCM Mode](https://www.rfc-editor.org/rfc/rfc9580.html#name-gcm-mode)
*/
GCM(3, 12, 16),
;
/**
* Return a [MessageEncryptionMechanism] instance representing AEAD using this algorithm and the
* given [SymmetricKeyAlgorithm].
*
* @param ciphermode symmetric key algorithm
* @return MessageEncryptionMechanism representing aead(this, ciphermode)
*/
fun toMechanism(ciphermode: SymmetricKeyAlgorithm): MessageEncryptionMechanism =
MessageEncryptionMechanism.aead(ciphermode.algorithmId, this.algorithmId)
companion object {
/**
* Parse an [AEADAlgorithm] from an algorithm id. If no matching [AEADAlgorithm] is known,
* return `null`.
*
* @param id algorithm id
* @return aeadAlgorithm or null
*/
@JvmStatic
fun fromId(id: Int): AEADAlgorithm? {
return values().firstOrNull { algorithm -> algorithm.algorithmId == id }
}
/**
* Parse an [AEADAlgorithm] from an algorithm id. If no matching [AEADAlgorithm] is known,
* throw a [NoSuchElementException].
*
* @param id algorithm id
* @return aeadAlgorithm
* @throws NoSuchElementException for unknown algorithm ids
*/
@JvmStatic
fun requireFromId(id: Int): AEADAlgorithm {
return fromId(id) ?: throw NoSuchElementException("No AEADAlgorithm found for id $id")

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
class AlgorithmSuite(
symmetricKeyAlgorithms: List<SymmetricKeyAlgorithm>,
hashAlgorithms: List<HashAlgorithm>,
compressionAlgorithms: List<CompressionAlgorithm>
class AlgorithmSuite
private constructor(
val symmetricKeyAlgorithms: Set<SymmetricKeyAlgorithm>?,
val hashAlgorithms: Set<HashAlgorithm>?,
val compressionAlgorithms: Set<CompressionAlgorithm>?,
val aeadAlgorithms: Set<AEADCipherMode>?,
val features: Set<Feature>?
) {
val symmetricKeyAlgorithms: Set<SymmetricKeyAlgorithm> = symmetricKeyAlgorithms.toSet()
val hashAlgorithms: Set<HashAlgorithm> = hashAlgorithms.toSet()
val compressionAlgorithms: Set<CompressionAlgorithm> = compressionAlgorithms.toSet()
constructor(
symmetricKeyAlgorithms: List<SymmetricKeyAlgorithm>?,
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 {
@JvmStatic
fun emptyBuilder(): Builder {
return Builder()
}
@JvmStatic
val defaultSymmetricKeyAlgorithms =
listOf(
@ -39,9 +112,26 @@ class AlgorithmSuite(
CompressionAlgorithm.ZIP,
CompressionAlgorithm.UNCOMPRESSED)
@JvmStatic
val defaultAEADAlgorithmSuites =
listOf(
AEADCipherMode(AEADAlgorithm.EAX, SymmetricKeyAlgorithm.AES_256),
AEADCipherMode(AEADAlgorithm.OCB, SymmetricKeyAlgorithm.AES_256),
AEADCipherMode(AEADAlgorithm.GCM, SymmetricKeyAlgorithm.AES_256),
AEADCipherMode(AEADAlgorithm.EAX, SymmetricKeyAlgorithm.AES_192),
AEADCipherMode(AEADAlgorithm.EAX, SymmetricKeyAlgorithm.AES_192))
@JvmStatic
val defaultFeatures =
listOf(Feature.MODIFICATION_DETECTION, Feature.MODIFICATION_DETECTION_2)
@JvmStatic
val defaultAlgorithmSuite =
AlgorithmSuite(
defaultSymmetricKeyAlgorithms, defaultHashAlgorithms, defaultCompressionAlgorithms)
defaultSymmetricKeyAlgorithms,
defaultHashAlgorithms,
defaultCompressionAlgorithms,
defaultAEADAlgorithmSuites,
defaultFeatures)
}
}

View file

@ -4,11 +4,13 @@
package org.pgpainless.algorithm
enum class EncryptionPurpose {
import org.bouncycastle.bcpg.sig.KeyFlags
enum class EncryptionPurpose(val code: Int) {
/** The stream will encrypt communication that goes over the wire. E.g. EMail, Chat... */
COMMUNICATIONS,
COMMUNICATIONS(KeyFlags.ENCRYPT_COMMS),
/** The stream will encrypt data at rest. E.g. Encrypted backup... */
STORAGE,
STORAGE(KeyFlags.ENCRYPT_STORAGE),
/** The stream will use keys with either flags to encrypt the data. */
ANY
ANY(KeyFlags.ENCRYPT_COMMS or KeyFlags.ENCRYPT_STORAGE)
}

View file

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

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
import org.bouncycastle.bcpg.PublicKeyUtils
/**
* Enumeration of public key algorithms as defined in RFC4880.
* Enumeration of public key algorithms as defined in RFC4880, RFC9580, Persistent Symmetric Keys.
*
* See [RFC4880: Public-Key Algorithms](https://tools.ietf.org/html/rfc4880#section-9.1)
* @see [RFC4880: Public-Key Algorithms](https://tools.ietf.org/html/rfc4880#section-9.1)
* @see
* [RFC9580: Public-Key Algorithms](https://www.rfc-editor.org/rfc/rfc9580.html#name-public-key-algorithms)
* @see
* [Persistent Symmetric Keys in OpenPGP](https://www.ietf.org/archive/id/draft-ietf-openpgp-persistent-symmetric-keys-01.html#name-persistent-symmetric-key-al)
*/
enum class PublicKeyAlgorithm(
val algorithmId: Int,
val signingCapable: Boolean,
val encryptionCapable: Boolean
) {
enum class PublicKeyAlgorithm(val algorithmId: Int) {
// RFC4880
/** RSA capable of encryption and signatures. */
RSA_GENERAL(1, true, true),
RSA_GENERAL(1),
/**
* RSA with usage encryption.
*
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.5">Deprecation
* notice</a>
* @deprecated see [Deprecation notice](https://tools.ietf.org/html/rfc4880#section-13.5)
*/
@Deprecated("RSA_ENCRYPT is deprecated in favor of RSA_GENERAL", ReplaceWith("RSA_GENERAL"))
RSA_ENCRYPT(2, false, true),
RSA_ENCRYPT(2),
/**
* RSA with usage of creating signatures.
*
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.5">Deprecation
* notice</a>
* @deprecated see [Deprecation notice](https://tools.ietf.org/html/rfc4880#section-13.5)
*/
@Deprecated("RSA_SIGN is deprecated in favor of RSA_GENERAL", ReplaceWith("RSA_GENERAL"))
RSA_SIGN(3, true, false),
RSA_SIGN(3),
/** ElGamal with usage encryption. */
ELGAMAL_ENCRYPT(16, false, true),
ELGAMAL_ENCRYPT(16),
/** Digital Signature Algorithm. */
DSA(17, true, false),
DSA(17),
/** Elliptic Curve Diffie-Hellman. */
ECDH(18, false, true),
ECDH(18),
/** Elliptic Curve Digital Signature Algorithm. */
ECDSA(19, true, false),
ECDSA(19),
/**
* ElGamal General.
*
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.8">Deprecation
* notice</a>
* @deprecated see [Deprecation notice](https://tools.ietf.org/html/rfc4880#section-13.8)
*/
@Deprecated("ElGamal is deprecated") ELGAMAL_GENERAL(20, true, true),
@Deprecated("ElGamal is deprecated") ELGAMAL_GENERAL(20),
/** Diffie-Hellman key exchange algorithm. */
DIFFIE_HELLMAN(21, false, true),
DIFFIE_HELLMAN(21),
/** Digital Signature Algorithm based on twisted Edwards Curves. */
EDDSA_LEGACY(22, true, false),
EDDSA_LEGACY(22),
// RFC9580
/** X25519 encryption algorithm. */
X25519(25, false, true),
X25519(25),
/** X448 encryption algorithm. */
X448(26, false, true),
X448(26),
/** Ed25519 signature algorithm. */
ED25519(27, true, false),
ED25519(27),
/** Ed448 signature algorithm. */
ED448(28, true, false),
;
ED448(28),
// Persistent Symmetric Keys in OpenPGP
/**
* AEAD can be used as a persistent key symmetric encryption algorithm for message encryption.
*
* @see
* [Persistent Symmetric Keys in OpenPGP](https://datatracker.ietf.org/doc/draft-ietf-openpgp-persistent-symmetric-keys/)
*/
AEAD(128) {
override val signingCapable = false
override val encryptionCapable = true
},
/**
* HMAC can be used as a persistent key symmetric signing algorithm for message signing.
*
* @see
* [Persistent Symmetric Keys in OpenPGP](https://datatracker.ietf.org/doc/draft-ietf-openpgp-persistent-symmetric-keys/)
*/
HMAC(129) {
override val signingCapable = true
override val encryptionCapable = false
};
open val signingCapable: Boolean = PublicKeyUtils.isSigningAlgorithm(algorithmId)
open val encryptionCapable: Boolean = PublicKeyUtils.isEncryptionAlgorithm(algorithmId)
fun isSigningCapable(): Boolean = signingCapable

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

View file

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

View file

@ -5,14 +5,46 @@
package org.pgpainless.authentication
import org.bouncycastle.openpgp.PGPPublicKeyRing
import org.bouncycastle.openpgp.api.OpenPGPCertificate
import org.pgpainless.PGPainless
/**
* A certificate authenticity record, indicating, to what degree the certificate is authenticated.
*
* @param userId identity, was changed to [CharSequence] instead of [String] starting with
* PGPainless 2.0.
* @param certificate certificate, was changed to [OpenPGPCertificate] instead of
* [PGPPublicKeyRing]. Use [pgpPublicKeyRing] if you need to access a [PGPPublicKeyRing].
* @param certificationChains map of chains and their trust degrees
* @param targetAmount targeted trust amount
*/
class CertificateAuthenticity(
val userId: String,
val certificate: PGPPublicKeyRing,
val userId: CharSequence,
val certificate: OpenPGPCertificate,
val certificationChains: Map<CertificationChain, 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
get() = certificationChains.values.sum()
@ -44,5 +76,22 @@ class CertificateAuthenticity(
*/
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
import java.util.*
import org.bouncycastle.bcpg.KeyIdentifier
import org.pgpainless.key.OpenPgpFingerprint
/**
* Interface for a CA that can authenticate trust-worthy certificates. Such a CA might be a fixed
* list of trustworthy certificates, or a dynamic implementation like the Web-of-Trust.
*
* @see <a href="https://github.com/pgpainless/pgpainless-wot">PGPainless-WOT</a>
* @see <a href="https://sequoia-pgp.gitlab.io/sequoia-wot/">OpenPGP Web of Trust</a>
* @see [PGPainless-WOT](https://github.com/pgpainless/pgpainless-wot)
* @see [OpenPGP Web of Trust](https://sequoia-pgp.gitlab.io/sequoia-wot/)
*/
interface CertificateAuthority {
@ -36,7 +37,30 @@ interface CertificateAuthority {
email: Boolean,
referenceTime: Date,
targetAmount: Int
): CertificateAuthenticity
): CertificateAuthenticity? =
authenticateBinding(fingerprint.keyIdentifier, userId, email, referenceTime, targetAmount)
/**
* Determine the authenticity of the binding between the given cert identifier and the userId.
* In other words, determine, how much evidence can be gathered, that the certificate with the
* given fingerprint really belongs to the user with the given userId.
*
* @param certIdentifier identifier of the certificate
* @param userId userId
* @param email if true, the userId will be treated as an email address and all user-IDs
* containing the email address will be matched.
* @param referenceTime reference time at which the binding shall be evaluated
* @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly
* authenticated, 60 = partially authenticated...)
* @return information about the authenticity of the binding
*/
fun authenticateBinding(
certIdentifier: KeyIdentifier,
userId: CharSequence,
email: Boolean,
referenceTime: Date,
targetAmount: Int
): CertificateAuthenticity?
/**
* Lookup certificates, which carry a trustworthy binding to the given userId.
@ -50,7 +74,7 @@ interface CertificateAuthority {
* @return list of identified bindings
*/
fun lookupByUserId(
userId: String,
userId: CharSequence,
email: Boolean,
referenceTime: Date,
targetAmount: Int
@ -70,5 +94,22 @@ interface CertificateAuthority {
fingerprint: OpenPgpFingerprint,
referenceTime: Date,
targetAmount: Int
): List<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>
}

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
import openpgp.openPgpKeyId
import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.openpgp.PGPKeyRing
import org.bouncycastle.openpgp.PGPOnePassSignature
import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.PGPSignature
import org.bouncycastle.openpgp.api.OpenPGPCertificate
import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey
import org.bouncycastle.openpgp.api.OpenPGPImplementation
import org.pgpainless.PGPainless
import org.pgpainless.key.OpenPgpFingerprint
import org.pgpainless.key.SubkeyIdentifier
/** Return true, if this [PGPKeyRing] contains the subkey identified by the [SubkeyIdentifier]. */
/**
* Return true, if this [PGPKeyRing] contains the subkey identified by the [SubkeyIdentifier].
*
* @param subkeyIdentifier subkey identifier
* @return true if the [PGPKeyRing] contains the [SubkeyIdentifier]
*/
fun PGPKeyRing.matches(subkeyIdentifier: SubkeyIdentifier): Boolean =
this.publicKey.keyID == subkeyIdentifier.primaryKeyId &&
this.getPublicKey(subkeyIdentifier.subkeyId) != null
this.publicKey.keyIdentifier.matchesExplicit(subkeyIdentifier.certificateIdentifier) &&
this.getPublicKey(subkeyIdentifier.componentKeyIdentifier) != null
/**
* Return true, if this [PGPKeyRing] contains the given [componentKey].
*
* @param componentKey component key
* @return true if the [PGPKeyRing] contains the [componentKey]
*/
fun PGPKeyRing.matches(componentKey: OpenPGPComponentKey): Boolean =
this.matches(SubkeyIdentifier(componentKey))
/**
* Return true, if the [PGPKeyRing] contains a public key with the given [keyIdentifier].
*
* @param keyIdentifier KeyIdentifier
* @return true if key with the given key-ID is present, false otherwise
*/
fun PGPKeyRing.hasPublicKey(keyIdentifier: KeyIdentifier): Boolean =
this.getPublicKey(keyIdentifier) != null
/**
* Return true, if the [PGPKeyRing] contains a public key with the given key-ID.
@ -24,7 +50,8 @@ fun PGPKeyRing.matches(subkeyIdentifier: SubkeyIdentifier): Boolean =
* @param keyId keyId
* @return true if key with the given key-ID is present, false otherwise
*/
fun PGPKeyRing.hasPublicKey(keyId: Long): Boolean = this.getPublicKey(keyId) != null
@Deprecated("Pass in a KeyIdentifier instead.")
fun PGPKeyRing.hasPublicKey(keyId: Long): Boolean = hasPublicKey(KeyIdentifier(keyId))
/**
* Return true, if the [PGPKeyRing] contains a public key with the given fingerprint.
@ -33,7 +60,7 @@ fun PGPKeyRing.hasPublicKey(keyId: Long): Boolean = this.getPublicKey(keyId) !=
* @return true if key with the given fingerprint is present, false otherwise
*/
fun PGPKeyRing.hasPublicKey(fingerprint: OpenPgpFingerprint): Boolean =
this.getPublicKey(fingerprint) != null
hasPublicKey(fingerprint.keyIdentifier)
/**
* Return the [PGPPublicKey] with the given [OpenPgpFingerprint] or null, if no such key is present.
@ -42,17 +69,41 @@ fun PGPKeyRing.hasPublicKey(fingerprint: OpenPgpFingerprint): Boolean =
* @return public key
*/
fun PGPKeyRing.getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? =
this.getPublicKey(fingerprint.bytes)
this.getPublicKey(fingerprint.keyIdentifier)
fun PGPKeyRing.requirePublicKey(keyId: Long): PGPPublicKey =
getPublicKey(keyId)
?: throw NoSuchElementException(
"OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.")
/**
* Return the [PGPPublicKey] with the given [keyIdentifier], or throw a [NoSuchElementException] if
* no matching public key was found.
*
* @param keyIdentifier key identifier
* @return public key
* @throws NoSuchElementException if no matching public key was found
*/
fun PGPKeyRing.requirePublicKey(keyIdentifier: KeyIdentifier): PGPPublicKey =
getPublicKey(keyIdentifier)
?: throw NoSuchElementException("OpenPGP key does not contain key with id $keyIdentifier.")
/**
* Return the [PGPPublicKey] with the given key-id, or throw a [NoSuchElementException] if no
* matching public key was found.
*
* @param keyId key id
* @return public key
* @throws NoSuchElementException if no matching public key was found
*/
@Deprecated("Pass in a KeyIdentifier instead.")
fun PGPKeyRing.requirePublicKey(keyId: Long): PGPPublicKey = requirePublicKey(KeyIdentifier(keyId))
/**
* Return the [PGPPublicKey] with the given [fingerprint], or throw a [NoSuchElementException] if no
* matching public key was found.
*
* @param fingerprint key fingerprint
* @return public key
* @throws NoSuchElementException if no matching public key was found
*/
fun PGPKeyRing.requirePublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey =
getPublicKey(fingerprint)
?: throw NoSuchElementException(
"OpenPGP key does not contain key with fingerprint $fingerprint.")
requirePublicKey(fingerprint.keyIdentifier)
/**
* Return the [PGPPublicKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. If
@ -60,11 +111,12 @@ fun PGPKeyRing.requirePublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey =
* subpacket to identify the [PGPPublicKey] via its key-ID.
*/
fun PGPKeyRing.getPublicKeyFor(signature: PGPSignature): PGPPublicKey? =
signature.fingerprint?.let { this.getPublicKey(it) } ?: this.getPublicKey(signature.keyID)
signature.fingerprint?.let { this.getPublicKey(it.keyIdentifier) }
?: this.getPublicKey(signature.keyID)
/** Return the [PGPPublicKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */
fun PGPKeyRing.getPublicKeyFor(onePassSignature: PGPOnePassSignature): PGPPublicKey? =
this.getPublicKey(onePassSignature.keyID)
this.getPublicKey(onePassSignature.keyIdentifier)
/** Return the [OpenPgpFingerprint] of this OpenPGP key. */
val PGPKeyRing.openPgpFingerprint: OpenPgpFingerprint
@ -72,3 +124,22 @@ val PGPKeyRing.openPgpFingerprint: OpenPgpFingerprint
/** Return this OpenPGP key as an ASCII armored String. */
fun PGPKeyRing.toAsciiArmor(): String = PGPainless.asciiArmor(this)
/**
* Convert the given [PGPKeyRing] into an [OpenPGPCertificate].
*
* @return certificate
*/
@Deprecated("Use toOpenPGPCertificate(implementation) instead.")
fun PGPKeyRing.toOpenPGPCertificate(): OpenPGPCertificate =
toOpenPGPCertificate(PGPainless.getInstance().implementation)
/**
* Convert the given [PGPKeyRing] into an [OpenPGPCertificate] using the given
* [OpenPGPImplementation].
*
* @param implementation OpenPGP implementation
* @return certificate
*/
fun PGPKeyRing.toOpenPGPCertificate(implementation: OpenPGPImplementation): OpenPGPCertificate =
OpenPGPCertificate(this, implementation)

View file

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

View file

@ -4,8 +4,16 @@
package org.pgpainless.bouncycastle.extensions
import openpgp.openPgpKeyId
import org.bouncycastle.openpgp.*
import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.openpgp.PGPOnePassSignature
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData
import org.bouncycastle.openpgp.PGPPublicKeyRing
import org.bouncycastle.openpgp.PGPSecretKey
import org.bouncycastle.openpgp.PGPSecretKeyRing
import org.bouncycastle.openpgp.PGPSignature
import org.bouncycastle.openpgp.api.OpenPGPImplementation
import org.bouncycastle.openpgp.api.OpenPGPKey
import org.pgpainless.PGPainless
import org.pgpainless.key.OpenPgpFingerprint
/** OpenPGP certificate containing the public keys of this OpenPGP key. */
@ -18,7 +26,17 @@ val PGPSecretKeyRing.certificate: PGPPublicKeyRing
* @param keyId keyId of the secret key
* @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise
*/
fun PGPSecretKeyRing.hasSecretKey(keyId: Long): Boolean = this.getSecretKey(keyId) != null
@Deprecated("Pass in a KeyIdentifier instead.")
fun PGPSecretKeyRing.hasSecretKey(keyId: Long): Boolean = hasSecretKey(KeyIdentifier(keyId))
/**
* Return true, if the [PGPSecretKeyRing] contains a [PGPSecretKey] with the given [keyIdentifier].
*
* @param keyIdentifier identifier of the secret key
* @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise
*/
fun PGPSecretKeyRing.hasSecretKey(keyIdentifier: KeyIdentifier): Boolean =
this.getSecretKey(keyIdentifier) != null
/**
* Return true, if the [PGPSecretKeyRing] contains a [PGPSecretKey] with the given fingerprint.
@ -27,7 +45,7 @@ fun PGPSecretKeyRing.hasSecretKey(keyId: Long): Boolean = this.getSecretKey(keyI
* @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise
*/
fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean =
this.getSecretKey(fingerprint) != null
hasSecretKey(fingerprint.keyIdentifier)
/**
* Return the [PGPSecretKey] with the given [OpenPgpFingerprint].
@ -36,7 +54,7 @@ fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean =
* @return the secret key or null
*/
fun PGPSecretKeyRing.getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? =
this.getSecretKey(fingerprint.bytes)
this.getSecretKey(fingerprint.keyIdentifier)
/**
* Return the [PGPSecretKey] with the given key-ID.
@ -44,10 +62,20 @@ fun PGPSecretKeyRing.getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey
* @throws NoSuchElementException if the OpenPGP key doesn't contain a secret key with the given
* key-ID
*/
@Deprecated("Pass in a KeyIdentifier instead.")
fun PGPSecretKeyRing.requireSecretKey(keyId: Long): PGPSecretKey =
getSecretKey(keyId)
requireSecretKey(KeyIdentifier(keyId))
/**
* Return the [PGPSecretKey] with the given [keyIdentifier].
*
* @throws NoSuchElementException if the OpenPGP key doesn't contain a secret key with the given
* keyIdentifier
*/
fun PGPSecretKeyRing.requireSecretKey(keyIdentifier: KeyIdentifier): PGPSecretKey =
getSecretKey(keyIdentifier)
?: throw NoSuchElementException(
"OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.")
"OpenPGP key does not contain key with id ${keyIdentifier}.")
/**
* Return the [PGPSecretKey] with the given fingerprint.
@ -56,9 +84,7 @@ fun PGPSecretKeyRing.requireSecretKey(keyId: Long): PGPSecretKey =
* fingerprint
*/
fun PGPSecretKeyRing.requireSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey =
getSecretKey(fingerprint)
?: throw NoSuchElementException(
"OpenPGP key does not contain key with fingerprint $fingerprint.")
requireSecretKey(fingerprint.keyIdentifier)
/**
* Return the [PGPSecretKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. If
@ -70,10 +96,32 @@ fun PGPSecretKeyRing.getSecretKeyFor(signature: PGPSignature): PGPSecretKey? =
/** Return the [PGPSecretKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */
fun PGPSecretKeyRing.getSecretKeyFor(onePassSignature: PGPOnePassSignature): PGPSecretKey? =
this.getSecretKey(onePassSignature.keyID)
this.getSecretKey(onePassSignature.keyIdentifier)
/**
* Return the [PGPSecretKey] that can be used to decrypt the given [PGPPublicKeyEncryptedData]
* packet.
*
* @param pkesk public-key encrypted session-key packet
* @return secret-key or null if no matching secret key was found
*/
fun PGPSecretKeyRing.getSecretKeyFor(pkesk: PGPPublicKeyEncryptedData): PGPSecretKey? =
when (pkesk.version) {
3 -> this.getSecretKey(pkesk.keyID)
else -> throw NotImplementedError("Version 6 PKESKs are not yet supported.")
}
this.getSecretKey(pkesk.keyIdentifier)
/**
* Convert the [PGPSecretKeyRing] into an [OpenPGPKey].
*
* @return key
*/
@Deprecated("Use toOpenPGPKey(implementation) instead.")
fun PGPSecretKeyRing.toOpenPGPKey(): OpenPGPKey =
toOpenPGPKey(PGPainless.getInstance().implementation)
/**
* Convert the [PGPSecretKeyRing] into an [OpenPGPKey] using the given [OpenPGPImplementation].
*
* @param implementation openpgp implementation
* @return key
*/
fun PGPSecretKeyRing.toOpenPGPKey(implementation: OpenPGPImplementation): OpenPGPKey =
OpenPGPKey(this, implementation)

View file

@ -5,13 +5,16 @@
package org.pgpainless.bouncycastle.extensions
import java.util.*
import openpgp.formatUTC
import openpgp.plusSeconds
import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.PGPSignature
import org.pgpainless.algorithm.HashAlgorithm
import org.pgpainless.algorithm.PublicKeyAlgorithm
import org.pgpainless.algorithm.RevocationState
import org.pgpainless.algorithm.SignatureType
import org.pgpainless.exception.SignatureValidationException
import org.pgpainless.key.OpenPgpFingerprint
import org.pgpainless.key.util.RevocationAttributes.Reason
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil
@ -54,25 +57,31 @@ val PGPSignature.issuerKeyId: Long
}
}
/** Return true, if the signature was likely issued by a key with the given fingerprint. */
fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint): Boolean =
this.fingerprint?.let { it.keyId == fingerprint.keyId } ?: (keyID == fingerprint.keyId)
/**
* Return true, if the signature was likely issued by a key with the given fingerprint.
*
* @param fingerprint fingerprint bytes
* @param fingerprint fingerprint of the key
* @return true if signature was likely issued by the key
*/
@Deprecated("Discouraged in favor of method taking an OpenPgpFingerprint.")
fun PGPSignature.wasIssuedBy(fingerprint: ByteArray): Boolean =
try {
wasIssuedBy(OpenPgpFingerprint.parseFromBinary(fingerprint))
} catch (e: IllegalArgumentException) {
// Unknown fingerprint length / format
false
}
fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint): Boolean =
wasIssuedBy(fingerprint.keyIdentifier)
fun PGPSignature.wasIssuedBy(key: PGPPublicKey): Boolean = wasIssuedBy(OpenPgpFingerprint.of(key))
/**
* Return true, if the signature was likely issued by the given key.
*
* @param key key
* @return true if signature was likely issued by the key
*/
fun PGPSignature.wasIssuedBy(key: PGPPublicKey): Boolean = wasIssuedBy(key.keyIdentifier)
/**
* Return true, if the signature was likely issued by a key with the given identifier.
*
* @param keyIdentifier key identifier
* @return true if signature was likely issued by the key
*/
fun PGPSignature.wasIssuedBy(keyIdentifier: KeyIdentifier): Boolean =
KeyIdentifier.matches(this.keyIdentifiers, keyIdentifier, true)
/** Return true, if this signature is a hard revocation. */
val PGPSignature.isHardRevocation
@ -90,19 +99,57 @@ val PGPSignature.isHardRevocation
else -> false // Not a revocation
}
/**
* Assert that the signatures creation time falls into the period between [notBefore] and
* [notAfter].
*
* @param notBefore lower bound. If null, do not check the lower bound
* @param notAfter upper bound. If null, do not check the upper bound
*/
fun PGPSignature.assertCreatedInBounds(notBefore: Date?, notAfter: Date?) {
if (notBefore != null && creationTime < notBefore) {
throw SignatureValidationException(
"Signature was made before the earliest allowed signature creation time." +
" Created: ${creationTime.formatUTC()}," +
" earliest allowed: ${notBefore.formatUTC()}")
}
if (notAfter != null && creationTime > notAfter) {
throw SignatureValidationException(
"Signature was made after the latest allowed signature creation time." +
" Created: ${creationTime.formatUTC()}," +
" latest allowed: ${notAfter.formatUTC()}")
}
}
/**
* Deduce a [RevocationState] from the signature. Non-revocation signatures result in
* [RevocationState.notRevoked]. Hard revocations result in [RevocationState.hardRevoked], while
* soft revocations return [RevocationState.softRevoked]
*
* @return revocation state
*/
fun PGPSignature?.toRevocationState() =
if (this == null) RevocationState.notRevoked()
else if (isHardRevocation) RevocationState.hardRevoked()
else RevocationState.softRevoked(creationTime)
/** The signatures issuer fingerprint as [OpenPgpFingerprint]. */
val PGPSignature.fingerprint: OpenPgpFingerprint?
get() = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this)
/** The signatures [PublicKeyAlgorithm]. */
val PGPSignature.publicKeyAlgorithm: PublicKeyAlgorithm
get() = PublicKeyAlgorithm.requireFromId(keyAlgorithm)
/** The signatures [HashAlgorithm]. */
val PGPSignature.signatureHashAlgorithm: HashAlgorithm
get() = HashAlgorithm.requireFromId(hashAlgorithm)
/**
* Return true if the signature has the given [SignatureType].
*
* @param type signature type
* @return true if the signature type matches the signatures type
*/
fun PGPSignature.isOfType(type: SignatureType): Boolean =
SignatureType.fromCode(signatureType) == type

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.InputStream
import java.util.*
import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.openpgp.*
import org.bouncycastle.openpgp.api.OpenPGPCertificate
import org.bouncycastle.openpgp.api.OpenPGPKey
import org.bouncycastle.openpgp.api.OpenPGPKeyMaterialProvider.OpenPGPCertificateProvider
import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory
import org.pgpainless.bouncycastle.extensions.getPublicKeyFor
import org.pgpainless.PGPainless
import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy
import org.pgpainless.key.SubkeyIdentifier
@ -19,7 +24,7 @@ import org.pgpainless.util.Passphrase
import org.pgpainless.util.SessionKey
/** Options for decryption and signature verification. */
class ConsumerOptions {
class ConsumerOptions(private val api: PGPainless) {
private var ignoreMDCErrors = false
var isDisableAsciiArmorCRC = false
@ -27,17 +32,18 @@ class ConsumerOptions {
private var verifyNotBefore: Date? = null
private var verifyNotAfter: Date? = Date()
private val certificates = CertificateSource()
private val certificates = CertificateSource(api)
private val detachedSignatures = mutableSetOf<PGPSignature>()
private var missingCertificateCallback: MissingPublicKeyCallback? = null
private var missingCertificateCallback: OpenPGPCertificateProvider? = null
private var sessionKey: SessionKey? = null
private val customDecryptorFactories =
mutableMapOf<SubkeyIdentifier, PublicKeyDataDecryptorFactory>()
private val decryptionKeys = mutableMapOf<PGPSecretKeyRing, SecretKeyRingProtector>()
private val decryptionKeys = mutableMapOf<OpenPGPKey, SecretKeyRingProtector>()
private val decryptionPassphrases = mutableSetOf<Passphrase>()
private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE
private var multiPassStrategy: MultiPassStrategy = InMemoryMultiPassStrategy()
private var allowDecryptionWithNonEncryptionKey: Boolean = false
/**
* Consider signatures on the message made before the given timestamp invalid. Null means no
@ -65,14 +71,26 @@ class ConsumerOptions {
fun getVerifyNotAfter() = verifyNotAfter
fun addVerificationCert(verificationCert: OpenPGPCertificate): ConsumerOptions = apply {
this.certificates.addCertificate(verificationCert)
}
fun addVerificationCerts(verificationCerts: Collection<OpenPGPCertificate>): ConsumerOptions =
apply {
for (cert in verificationCerts) {
addVerificationCert(cert)
}
}
/**
* Add a certificate (public key ring) for signature verification.
*
* @param verificationCert certificate for signature verification
* @return options
*/
@Deprecated("Pass OpenPGPCertificate instead.")
fun addVerificationCert(verificationCert: PGPPublicKeyRing): ConsumerOptions = apply {
this.certificates.addCertificate(verificationCert)
this.certificates.addCertificate(api.toCertificate(verificationCert))
}
/**
@ -81,10 +99,11 @@ class ConsumerOptions {
* @param verificationCerts certificates for signature verification
* @return options
*/
@Deprecated("Use of methods taking PGPPublicKeyRingCollections is discouraged.")
fun addVerificationCerts(verificationCerts: PGPPublicKeyRingCollection): ConsumerOptions =
apply {
for (cert in verificationCerts) {
addVerificationCert(cert)
addVerificationCert(api.toCertificate(cert))
}
}
@ -117,6 +136,14 @@ class ConsumerOptions {
}
}
fun addVerificationOfDetachedSignature(signature: OpenPGPDocumentSignature): ConsumerOptions =
apply {
if (signature.issuerCertificate != null) {
addVerificationCert(signature.issuerCertificate)
}
addVerificationOfDetachedSignature(signature.signature)
}
/**
* Add a detached signature for the signature verification process.
*
@ -137,9 +164,10 @@ class ConsumerOptions {
* @param callback callback
* @return options
*/
fun setMissingCertificateCallback(callback: MissingPublicKeyCallback): ConsumerOptions = apply {
this.missingCertificateCallback = callback
}
fun setMissingCertificateCallback(callback: OpenPGPCertificateProvider): ConsumerOptions =
apply {
this.missingCertificateCallback = callback
}
/**
* Attempt decryption using a session key.
@ -155,52 +183,45 @@ class ConsumerOptions {
fun getSessionKey() = sessionKey
@JvmOverloads
fun addDecryptionKey(
key: OpenPGPKey,
protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()
) = apply { decryptionKeys[key] = protector }
/**
* Add a key for message decryption. If the key is encrypted, the [SecretKeyRingProtector] is
* used to decrypt it when needed.
*
* @param key key
* @param keyRingProtector protector for the secret key
* @param protector protector for the secret key
* @return options
*/
@JvmOverloads
@Deprecated("Pass OpenPGPKey instead.")
fun addDecryptionKey(
key: PGPSecretKeyRing,
protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()
) = apply { decryptionKeys[key] = protector }
) = addDecryptionKey(api.toKey(key), protector)
/**
* Add the keys in the provided key collection for message decryption.
*
* @param keys key collection
* @param keyRingProtector protector for encrypted secret keys
* @param protector protector for encrypted secret keys
* @return options
*/
@JvmOverloads
@Deprecated("Pass OpenPGPKey instances instead.")
fun addDecryptionKeys(
keys: PGPSecretKeyRingCollection,
protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()
) = apply {
for (key in keys) {
addDecryptionKey(key, protector)
addDecryptionKey(api.toKey(key), protector)
}
}
/**
* Add a passphrase for message decryption. This passphrase will be used to try to decrypt
* messages which were symmetrically encrypted for a passphrase.
*
* See
* [Symmetrically Encrypted Data Packet](https://datatracker.ietf.org/doc/html/rfc4880#section-5.7)
*
* @param passphrase passphrase
* @return options
*/
@Deprecated(
"Deprecated in favor of addMessagePassphrase",
ReplaceWith("addMessagePassphrase(passphrase)"))
fun addDecryptionPassphrase(passphrase: Passphrase) = addMessagePassphrase(passphrase)
/**
* Add a passphrase for message decryption. This passphrase will be used to try to decrypt
* messages which were symmetrically encrypted for a passphrase.
@ -240,21 +261,21 @@ class ConsumerOptions {
*
* @return decryption keys
*/
fun getDecryptionKeys() = decryptionKeys.keys.toSet()
fun getDecryptionKeys(): Set<OpenPGPKey> = decryptionKeys.keys.toSet()
/**
* Return the set of available message 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 certificate source
*/
fun getCertificateSource() = certificates
fun getCertificateSource(): CertificateSource = certificates
/**
* Return the callback that gets called when a certificate for signature verification is
@ -262,7 +283,7 @@ class ConsumerOptions {
*
* @return missing public key callback
*/
fun getMissingCertificateCallback() = missingCertificateCallback
fun getMissingCertificateCallback(): OpenPGPCertificateProvider? = missingCertificateCallback
/**
* Return the [SecretKeyRingProtector] for the given [PGPSecretKeyRing].
@ -270,7 +291,7 @@ class ConsumerOptions {
* @param decryptionKeyRing secret key
* @return protector for that particular secret key
*/
fun getSecretKeyProtector(decryptionKeyRing: PGPSecretKeyRing): SecretKeyRingProtector? {
fun getSecretKeyProtector(decryptionKeyRing: OpenPGPKey): SecretKeyRingProtector? {
return decryptionKeys[decryptionKeyRing]
}
@ -306,7 +327,15 @@ class ConsumerOptions {
this.ignoreMDCErrors = ignoreMDCErrors
}
fun isIgnoreMDCErrors() = ignoreMDCErrors
fun isIgnoreMDCErrors(): Boolean = ignoreMDCErrors
fun setAllowDecryptionWithMissingKeyFlags(): ConsumerOptions = apply {
allowDecryptionWithNonEncryptionKey = true
}
fun getAllowDecryptionWithNonEncryptionKey(): Boolean {
return allowDecryptionWithNonEncryptionKey
}
/**
* Force PGPainless to handle the data provided by the [InputStream] as non-OpenPGP data. This
@ -322,7 +351,7 @@ class ConsumerOptions {
*
* @return true if non-OpenPGP data is forced
*/
fun isForceNonOpenPgpData() = forceNonOpenPgpData
fun isForceNonOpenPgpData(): Boolean = forceNonOpenPgpData
/**
* Specify the [MissingKeyPassphraseStrategy]. This strategy defines, how missing passphrases
@ -377,15 +406,25 @@ class ConsumerOptions {
* Source for OpenPGP certificates. When verifying signatures on a message, this object holds
* available signer certificates.
*/
class CertificateSource {
private val explicitCertificates: MutableSet<PGPPublicKeyRing> = mutableSetOf()
class CertificateSource(private val api: PGPainless) {
private val explicitCertificates: MutableSet<OpenPGPCertificate> = mutableSetOf()
/**
* Add a certificate as verification cert explicitly.
*
* @param certificate certificate
*/
@Deprecated("Pass in an OpenPGPCertificate instead.")
fun addCertificate(certificate: PGPPublicKeyRing) {
explicitCertificates.add(api.toCertificate(certificate))
}
/**
* Add a certificate as explicitly provided verification cert.
*
* @param certificate explicit verification cert
*/
fun addCertificate(certificate: OpenPGPCertificate) {
explicitCertificates.add(certificate)
}
@ -394,7 +433,7 @@ class ConsumerOptions {
*
* @return explicitly set verification certs
*/
fun getExplicitCertificates(): Set<PGPPublicKeyRing> {
fun getExplicitCertificates(): Set<OpenPGPCertificate> {
return explicitCertificates.toSet()
}
@ -406,15 +445,31 @@ class ConsumerOptions {
* @param keyId key id
* @return certificate
*/
fun getCertificate(keyId: Long): PGPPublicKeyRing? {
return explicitCertificates.firstOrNull { it.getPublicKey(keyId) != null }
@Deprecated("Pass in a KeyIdentifier instead.")
fun getCertificate(keyId: Long): OpenPGPCertificate? {
return getCertificate(KeyIdentifier(keyId))
}
fun getCertificate(signature: PGPSignature): PGPPublicKeyRing? =
explicitCertificates.firstOrNull { it.getPublicKeyFor(signature) != null }
/**
* Return a certificate which contains a component key for the given [identifier]. This
* method first checks all explicitly provided verification certs and if no cert is found it
* consults the certificate stores.
*
* @param identifier key identifier
* @return certificate or null if no match is found
*/
fun getCertificate(identifier: KeyIdentifier): OpenPGPCertificate? {
return explicitCertificates.firstOrNull { it.getKey(identifier) != null }
}
/** Find a certificate containing the issuer component key for the given [signature]. */
fun getCertificate(signature: PGPSignature): OpenPGPCertificate? =
explicitCertificates.firstOrNull { it.getSigningKeyFor(signature) != null }
}
companion object {
@JvmStatic fun get() = ConsumerOptions()
@JvmOverloads
@JvmStatic
fun get(api: PGPainless = PGPainless.getInstance()) = ConsumerOptions(api)
}
}

View file

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

View file

@ -6,6 +6,7 @@ package org.pgpainless.decryption_verification
import kotlin.jvm.Throws
import org.bouncycastle.bcpg.AEADEncDataPacket
import org.bouncycastle.bcpg.KeyIdentifier
import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket
import org.bouncycastle.openpgp.PGPException
import org.bouncycastle.openpgp.PGPSessionKey
@ -33,12 +34,36 @@ class HardwareSecurity {
* @return decrypted session key
* @throws HardwareSecurityException exception
*/
@Deprecated("Pass in a KeyIdentifier instead of a Long keyId.")
@Throws(HardwareSecurityException::class)
fun decryptSessionKey(
keyId: Long,
keyAlgorithm: Int,
sessionKeyData: ByteArray,
pkeskVersion: Int
): ByteArray =
decryptSessionKey(KeyIdentifier(keyId), keyAlgorithm, sessionKeyData, pkeskVersion)
/**
* Delegate decryption of a Public-Key-Encrypted-Session-Key (PKESK) to an external API for
* dealing with hardware security modules such as smartcards or TPMs.
*
* If decryption fails for some reason, a subclass of the [HardwareSecurityException] is
* thrown.
*
* @param keyIdentifier identifier of the encryption component key
* @param keyAlgorithm algorithm
* @param sessionKeyData encrypted session key
* @param pkeskVersion version of the Public-Key-Encrypted-Session-Key packet (3 or 6)
* @return decrypted session key
* @throws HardwareSecurityException exception
*/
@Throws(HardwareSecurityException::class)
fun decryptSessionKey(
keyIdentifier: KeyIdentifier,
keyAlgorithm: Int,
sessionKeyData: ByteArray,
pkeskVersion: Int
): ByteArray
}
@ -84,7 +109,7 @@ class HardwareSecurity {
): ByteArray {
return try {
callback.decryptSessionKey(
subkeyIdentifier.subkeyId, keyAlgorithm, secKeyData[0], pkeskVersion)
subkeyIdentifier.keyIdentifier, keyAlgorithm, secKeyData[0], pkeskVersion)
} catch (e: HardwareSecurityException) {
throw PGPException("Hardware-backed decryption failed.", e)
}

View file

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

View file

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

View file

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

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

View file

@ -5,22 +5,23 @@
package org.pgpainless.decryption_verification
import org.bouncycastle.openpgp.PGPSignature
import org.pgpainless.decryption_verification.SignatureVerification.Failure
import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature
import org.pgpainless.exception.SignatureValidationException
import org.pgpainless.key.SubkeyIdentifier
import org.pgpainless.signature.SignatureUtils
/**
* Tuple of a signature and an identifier of its corresponding verification key. Semantic meaning of
* the signature verification (success, failure) is merely given by context. E.g.
* [MessageMetadata.getVerifiedInlineSignatures] contains verified verifications, while the class
* [Failure] contains failed verifications.
* An evaluated document signature.
*
* @param signature PGPSignature object
* @param signingKey [SubkeyIdentifier] of the (sub-) key that is used for signature verification.
* Note, that this might be null, e.g. in case of a [Failure] due to missing verification key.
* @param documentSignature OpenPGPDocumentSignature object
*/
data class SignatureVerification(val signature: PGPSignature, val signingKey: SubkeyIdentifier) {
data class SignatureVerification(val documentSignature: OpenPGPDocumentSignature) {
/** Underlying [PGPSignature]. */
val signature: PGPSignature = documentSignature.signature
/** [SubkeyIdentifier] of the component key that created the signature. */
val signingKey: SubkeyIdentifier = SubkeyIdentifier(documentSignature.issuer)
override fun toString(): String {
return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)};" +
@ -31,20 +32,27 @@ data class SignatureVerification(val signature: PGPSignature, val signingKey: Su
* Tuple object of a [SignatureVerification] and the corresponding
* [SignatureValidationException] that caused the verification to fail.
*
* @param signatureVerification verification (tuple of [PGPSignature] and corresponding
* [SubkeyIdentifier])
* @param documentSignature signature that could not be verified
* @param validationException exception that caused the verification to fail
*/
data class Failure(
val signature: PGPSignature,
val signingKey: SubkeyIdentifier?,
val documentSignature: OpenPGPDocumentSignature,
val validationException: SignatureValidationException
) {
/** Underlying [PGPSignature]. */
val signature: PGPSignature = documentSignature.signature
/**
* [SubkeyIdentifier] of the component key that created the signature. Note: In case of a
* missing verification key, this might be null.
*/
val signingKey: SubkeyIdentifier? = documentSignature.issuer?.let { SubkeyIdentifier(it) }
constructor(
verification: SignatureVerification,
validationException: SignatureValidationException
) : this(verification.signature, verification.signingKey, validationException)
) : this(verification.documentSignature, validationException)
override fun toString(): String {
return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)}; Key: ${signingKey?.toString() ?: "null"}; Failure: ${validationException.message}"

View file

@ -8,9 +8,9 @@ import java.io.*
import kotlin.jvm.Throws
import org.bouncycastle.bcpg.ArmoredInputStream
import org.bouncycastle.openpgp.PGPSignatureList
import org.bouncycastle.openpgp.api.OpenPGPImplementation
import org.bouncycastle.util.Strings
import org.pgpainless.exception.WrongConsumingMethodException
import org.pgpainless.implementation.ImplementationFactory
import org.pgpainless.util.ArmoredInputStreamFactory
/**
@ -72,7 +72,7 @@ class ClearsignedMessageUtil {
}
}
val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(input)
val objectFactory = OpenPGPImplementation.getInstance().pgpObjectFactory(input)
val next = objectFactory.nextObject() ?: PGPSignatureList(arrayOf())
return next as PGPSignatureList
}

View file

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

View file

@ -5,69 +5,25 @@
package org.pgpainless.encryption_signing
import java.io.OutputStream
import org.pgpainless.PGPainless.Companion.getPolicy
import org.pgpainless.algorithm.CompressionAlgorithm
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity
import org.pgpainless.PGPainless
import org.pgpainless.util.NullOutputStream
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class EncryptionBuilder : EncryptionBuilderInterface {
class EncryptionBuilder(private val api: PGPainless) : EncryptionBuilderInterface {
override fun onOutputStream(
outputStream: OutputStream
): EncryptionBuilderInterface.WithOptions {
return WithOptionsImpl(outputStream)
return WithOptionsImpl(outputStream, api)
}
override fun discardOutput(): EncryptionBuilderInterface.WithOptions {
return onOutputStream(NullOutputStream())
}
class WithOptionsImpl(val outputStream: OutputStream) : EncryptionBuilderInterface.WithOptions {
class WithOptionsImpl(val outputStream: OutputStream, private val api: PGPainless) :
EncryptionBuilderInterface.WithOptions {
override fun withOptions(options: ProducerOptions): EncryptionStream {
return EncryptionStream(outputStream, options)
}
}
companion object {
@JvmStatic val LOGGER: Logger = LoggerFactory.getLogger(EncryptionBuilder::class.java)
/**
* Negotiate the [SymmetricKeyAlgorithm] used for message encryption.
*
* @param encryptionOptions encryption options
* @return negotiated symmetric key algorithm
*/
@JvmStatic
fun negotiateSymmetricEncryptionAlgorithm(
encryptionOptions: EncryptionOptions
): SymmetricKeyAlgorithm {
val preferences =
encryptionOptions.keyViews.values
.map { it.preferredSymmetricKeyAlgorithms }
.toList()
val algorithm =
byPopularity()
.negotiate(
getPolicy().symmetricKeyEncryptionAlgorithmPolicy,
encryptionOptions.encryptionAlgorithmOverride,
preferences)
LOGGER.debug(
"Negotiation resulted in {} being the symmetric encryption algorithm of choice.",
algorithm)
return algorithm
}
@JvmStatic
fun negotiateCompressionAlgorithm(producerOptions: ProducerOptions): CompressionAlgorithm {
val compressionAlgorithmOverride = producerOptions.compressionAlgorithmOverride
return compressionAlgorithmOverride
?: getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm()
// TODO: Negotiation
return EncryptionStream(outputStream, options, api)
}
}
}

View file

@ -5,55 +5,69 @@
package org.pgpainless.encryption_signing
import java.util.*
import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.PGPPublicKeyRing
import org.bouncycastle.openpgp.api.MessageEncryptionMechanism
import org.bouncycastle.openpgp.api.OpenPGPCertificate
import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey
import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator
import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator
import org.pgpainless.PGPainless
import org.pgpainless.algorithm.EncryptionPurpose
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
import org.pgpainless.algorithm.negotiation.EncryptionMechanismNegotiator
import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity
import org.pgpainless.authentication.CertificateAuthority
import org.pgpainless.encryption_signing.EncryptionOptions.EncryptionKeySelector
import org.pgpainless.exception.KeyException
import org.pgpainless.exception.KeyException.*
import org.pgpainless.implementation.ImplementationFactory
import org.pgpainless.key.OpenPgpFingerprint
import org.pgpainless.exception.KeyException.ExpiredKeyException
import org.pgpainless.exception.KeyException.UnacceptableEncryptionKeyException
import org.pgpainless.exception.KeyException.UnacceptableSelfSignatureException
import org.pgpainless.key.SubkeyIdentifier
import org.pgpainless.key.info.KeyAccessor
import org.pgpainless.key.info.KeyRingInfo
import org.pgpainless.util.Passphrase
class EncryptionOptions(private val purpose: EncryptionPurpose) {
class EncryptionOptions(private val purpose: EncryptionPurpose, private val api: PGPainless) {
var encryptionMechanismNegotiator: EncryptionMechanismNegotiator =
EncryptionMechanismNegotiator.modificationDetectionOrBetter(byPopularity())
private val _encryptionMethods: MutableSet<PGPKeyEncryptionMethodGenerator> = mutableSetOf()
private val _encryptionKeyIdentifiers: MutableSet<SubkeyIdentifier> = mutableSetOf()
private val keysAndAccessors: MutableMap<OpenPGPComponentKey, KeyAccessor> = mutableMapOf()
private val _keyRingInfo: MutableMap<SubkeyIdentifier, KeyRingInfo> = mutableMapOf()
private val _keyViews: MutableMap<SubkeyIdentifier, KeyAccessor> = mutableMapOf()
private val encryptionKeySelector: EncryptionKeySelector = encryptToAllCapableSubkeys()
private var allowEncryptionWithMissingKeyFlags = false
private var evaluationDate = Date()
private var _encryptionAlgorithmOverride: SymmetricKeyAlgorithm? = null
private var _encryptionMechanismOverride: MessageEncryptionMechanism? = null
val encryptionMethods
get() = _encryptionMethods.toSet()
val encryptionKeyIdentifiers
get() = _encryptionKeyIdentifiers.toSet()
get() = keysAndAccessors.keys.map { SubkeyIdentifier(it) }
val keyRingInfo
get() = _keyRingInfo.toMap()
val keyViews
get() = _keyViews.toMap()
val encryptionKeys
get() = keysAndAccessors.keys.toSet()
@Deprecated(
"Deprecated in favor of encryptionMechanismOverride",
replaceWith = ReplaceWith("encryptionMechanismOverride"))
// TODO: Remove in 2.1
val encryptionAlgorithmOverride
get() = _encryptionAlgorithmOverride
get() =
_encryptionMechanismOverride?.let {
SymmetricKeyAlgorithm.requireFromId(it.symmetricKeyAlgorithm)
}
constructor() : this(EncryptionPurpose.ANY)
val encryptionMechanismOverride
get() = _encryptionMechanismOverride
constructor(api: PGPainless) : this(EncryptionPurpose.ANY, api)
/**
* Factory method to create an [EncryptionOptions] object which will encrypt for keys which
* carry the flag [org.pgpainless.algorithm.KeyFlag.ENCRYPT_COMMS].
* Set the evaluation date for certificate evaluation.
*
* @return encryption options
* @param evaluationDate reference time
* @return this
*/
fun setEvaluationDate(evaluationDate: Date) = apply { this.evaluationDate = evaluationDate }
@ -89,11 +103,14 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
/**
* Add all key rings in the provided [Iterable] (e.g.
* [org.bouncycastle.openpgp.PGPPublicKeyRingCollection]) as recipients.
* [org.bouncycastle.openpgp.PGPPublicKeyRingCollection]) as recipients. Note: This method is
* deprecated. Instead, repeatedly call [addRecipient], passing in individual
* [OpenPGPCertificate] instances.
*
* @param keys keys
* @return this
*/
@Deprecated("Repeatedly pass OpenPGPCertificate instances instead.")
fun addRecipients(keys: Iterable<PGPPublicKeyRing>) = apply {
keys.toList().let {
require(it.isNotEmpty()) { "Set of recipient keys cannot be empty." }
@ -104,12 +121,15 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
/**
* Add all key rings in the provided [Iterable] (e.g.
* [org.bouncycastle.openpgp.PGPPublicKeyRingCollection]) as recipients. Per key ring, the
* selector is applied to select one or more encryption subkeys.
* selector is applied to select one or more encryption subkeys. Note: This method is
* deprecated. Instead, repeatedly call [addRecipient], passing in individual
* [OpenPGPCertificate] instances.
*
* @param keys keys
* @param selector encryption key selector
* @return this
*/
@Deprecated("Repeatedly pass OpenPGPCertificate instances instead.")
fun addRecipients(keys: Iterable<PGPPublicKeyRing>, selector: EncryptionKeySelector) = apply {
keys.toList().let {
require(it.isNotEmpty()) { "Set of recipient keys cannot be empty." }
@ -117,71 +137,175 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
}
}
/**
* Encrypt the message to the recipients [OpenPGPCertificate].
*
* @param cert recipient certificate
* @return this
*/
fun addRecipient(cert: OpenPGPCertificate) = addRecipient(cert, encryptionKeySelector)
/**
* Add a recipient by providing a key.
*
* @param key key ring
* @return this
*/
@Deprecated(
"Pass in OpenPGPCertificate instead.",
replaceWith =
ReplaceWith("addRecipient(key.toOpenPGPCertificate(), encryptionKeySelector)"))
fun addRecipient(key: PGPPublicKeyRing) = addRecipient(key, encryptionKeySelector)
/**
* Encrypt the message for the given recipients [OpenPGPCertificate], sourcing algorithm
* preferences by inspecting the binding signature on the passed [userId].
*
* @param cert recipient certificate
* @param userId recipient user-id
* @return this
*/
fun addRecipient(cert: OpenPGPCertificate, userId: CharSequence) =
addRecipient(cert, userId, encryptionKeySelector)
/**
* Add a recipient by providing a key and recipient user-id. The user-id is used to determine
* the recipients preferences (algorithms etc.).
* the recipients preferences (algorithms etc.). Note: This method is deprecated. Replace the
* [PGPPublicKeyRing] instance with an [OpenPGPCertificate].
*
* @param key key ring
* @param userId user id
* @return this
*/
@Deprecated(
"Pass in OpenPGPCertificate instead.",
replaceWith = ReplaceWith("addRecipient(key.toOpenPGPCertificate(), userId)"))
fun addRecipient(key: PGPPublicKeyRing, userId: CharSequence) =
addRecipient(key, userId, encryptionKeySelector)
/**
* Encrypt the message for the given recipients [OpenPGPCertificate], sourcing algorithm
* preferences by inspecting the binding signature on the given [userId] and filtering the
* recipient subkeys through the given [EncryptionKeySelector].
*
* @param cert recipient certificate
* @param userId user-id for sourcing algorithm preferences
* @param encryptionKeySelector decides which subkeys to encrypt for
* @return this
*/
fun addRecipient(
key: PGPPublicKeyRing,
cert: OpenPGPCertificate,
userId: CharSequence,
encryptionKeySelector: EncryptionKeySelector
) = apply {
val info = KeyRingInfo(key, evaluationDate)
val info = api.inspect(cert, evaluationDate)
val subkeys =
encryptionKeySelector.selectEncryptionSubkeys(
info.getEncryptionSubkeys(userId, purpose))
if (subkeys.isEmpty()) {
throw KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key))
throw UnacceptableEncryptionKeyException(cert)
}
for (subkey in subkeys) {
val keyId = SubkeyIdentifier(key, subkey.keyID)
val keyId = SubkeyIdentifier(subkey)
_keyRingInfo[keyId] = info
_keyViews[keyId] = KeyAccessor.ViaUserId(info, keyId, userId.toString())
addRecipientKey(key, subkey, false)
val accessor = KeyAccessor.ViaUserId(subkey, cert.getUserId(userId.toString()))
addRecipientKey(subkey, accessor, false)
}
}
fun addRecipient(key: PGPPublicKeyRing, encryptionKeySelector: EncryptionKeySelector) = apply {
addAsRecipient(key, encryptionKeySelector, false)
}
/**
* Encrypt the message for the given recipients public key, sourcing algorithm preferences by
* inspecting the binding signature on the given [userId] and filtering the recipient subkeys
* through the given [EncryptionKeySelector].
*
* @param key recipient public key
* @param userId user-id for sourcing algorithm preferences
* @param encryptionKeySelector decides which subkeys to encrypt for
* @return this
*/
@Deprecated(
"Pass in OpenPGPCertificate instead.",
replaceWith =
ReplaceWith("addRecipient(key.toOpenPGPCertificate(), userId, encryptionKeySelector)"))
fun addRecipient(
key: PGPPublicKeyRing,
userId: CharSequence,
encryptionKeySelector: EncryptionKeySelector
) = addRecipient(api.toCertificate(key), userId, encryptionKeySelector)
/**
* Encrypt the message for the given recipients [OpenPGPCertificate], filtering encryption
* subkeys through the given [EncryptionKeySelector].
*
* @param cert recipient certificate
* @param encryptionKeySelector decides, which subkeys to encrypt for
* @return this
*/
fun addRecipient(cert: OpenPGPCertificate, encryptionKeySelector: EncryptionKeySelector) =
addAsRecipient(cert, encryptionKeySelector, false)
/**
* Encrypt the message for the given recipients public key, filtering encryption subkeys through
* the given [EncryptionKeySelector].
*
* @param key recipient public key
* @param encryptionKeySelector decides, which subkeys to encrypt for
* @return this
*/
@Deprecated(
"Pass in OpenPGPCertificate instead.",
replaceWith =
ReplaceWith("addRecipient(key.toOpenPGPCertificate(), encryptionKeySelector)"))
fun addRecipient(key: PGPPublicKeyRing, encryptionKeySelector: EncryptionKeySelector) =
addRecipient(api.toCertificate(key), encryptionKeySelector)
/**
* Encrypt the message for the recipients [OpenPGPCertificate], keeping the recipient anonymous
* by setting a wildcard key-id / fingerprint.
*
* @param cert recipient certificate
* @param selector decides, which subkeys to encrypt for
* @return this
*/
@JvmOverloads
fun addHiddenRecipient(
cert: OpenPGPCertificate,
selector: EncryptionKeySelector = encryptionKeySelector
) = addAsRecipient(cert, selector, true)
/**
* Encrypt the message for the recipients public key, keeping the recipient anonymous by setting
* a wildcard key-id / fingerprint.
*
* @param key recipient public key
* @param selector decides, which subkeys to encrypt for
* @return this
*/
@JvmOverloads
@Deprecated(
"Pass in an OpenPGPCertificate instead.",
replaceWith = ReplaceWith("addHiddenRecipient(key.toOpenPGPCertificate(), selector)"))
fun addHiddenRecipient(
key: PGPPublicKeyRing,
selector: EncryptionKeySelector = encryptionKeySelector
) = apply { addAsRecipient(key, selector, true) }
) = addHiddenRecipient(api.toCertificate(key), selector)
private fun addAsRecipient(
key: PGPPublicKeyRing,
cert: OpenPGPCertificate,
selector: EncryptionKeySelector,
wildcardKeyId: Boolean
) = apply {
val info = KeyRingInfo(key, evaluationDate)
val info = api.inspect(cert, evaluationDate)
val primaryKeyExpiration =
try {
info.primaryKeyExpirationDate
} catch (e: NoSuchElementException) {
throw UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key))
throw UnacceptableSelfSignatureException(cert)
}
if (primaryKeyExpiration != null && primaryKeyExpiration < evaluationDate) {
throw ExpiredKeyException(OpenPgpFingerprint.of(key), primaryKeyExpiration)
throw ExpiredKeyException(cert, primaryKeyExpiration)
}
var encryptionSubkeys = selector.selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose))
@ -193,31 +317,31 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
if (encryptionSubkeys.isEmpty() && allowEncryptionWithMissingKeyFlags) {
encryptionSubkeys =
info.validSubkeys
.filter { it.isEncryptionKey }
.filter { info.getKeyFlagsOf(it.keyID).isEmpty() }
.filter { it.pgpPublicKey.isEncryptionKey }
.filter { info.getKeyFlagsOf(it.keyIdentifier).isEmpty() }
}
if (encryptionSubkeys.isEmpty()) {
throw UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key))
throw UnacceptableEncryptionKeyException(cert)
}
for (subkey in encryptionSubkeys) {
val keyId = SubkeyIdentifier(key, subkey.keyID)
val keyId = SubkeyIdentifier(subkey)
_keyRingInfo[keyId] = info
_keyViews[keyId] = KeyAccessor.ViaKeyId(info, keyId)
addRecipientKey(key, subkey, wildcardKeyId)
val accessor = KeyAccessor.ViaKeyIdentifier(subkey)
addRecipientKey(subkey, accessor, wildcardKeyId)
}
}
private fun addRecipientKey(
certificate: PGPPublicKeyRing,
key: PGPPublicKey,
wildcardKeyId: Boolean
key: OpenPGPComponentKey,
accessor: KeyAccessor,
wildcardRecipient: Boolean
) {
_encryptionKeyIdentifiers.add(SubkeyIdentifier(certificate, key.keyID))
keysAndAccessors[key] = accessor
addEncryptionMethod(
ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(key).also {
it.setUseWildcardKeyID(wildcardKeyId)
api.implementation.publicKeyKeyEncryptionMethodGenerator(key.pgpPublicKey).also {
it.setUseWildcardRecipient(wildcardRecipient)
})
}
@ -241,7 +365,7 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
fun addMessagePassphrase(passphrase: Passphrase) = apply {
require(!passphrase.isEmpty) { "Passphrase MUST NOT be empty." }
addEncryptionMethod(
ImplementationFactory.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase))
api.implementation.pbeKeyEncryptionMethodGenerator(passphrase.getChars()))
}
/**
@ -270,11 +394,27 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
* @param encryptionAlgorithm encryption algorithm override
* @return this
*/
@Deprecated(
"Deprecated in favor of overrideEncryptionMechanism",
replaceWith =
ReplaceWith(
"overrideEncryptionMechanism(MessageEncryptionMechanism.integrityProtected(encryptionAlgorithm.algorithmId))"))
// TODO: Remove in 2.1
fun overrideEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply {
require(encryptionAlgorithm != SymmetricKeyAlgorithm.NULL) {
"Encryption algorithm override cannot be NULL."
}
_encryptionAlgorithmOverride = encryptionAlgorithm
overrideEncryptionMechanism(
MessageEncryptionMechanism.integrityProtected(encryptionAlgorithm.algorithmId))
}
fun overrideEncryptionMechanism(encryptionMechanism: MessageEncryptionMechanism) = apply {
require(
api.algorithmPolicy.messageEncryptionAlgorithmPolicy.isAcceptable(
encryptionMechanism)) {
"Provided symmetric encryption algorithm is not acceptable."
}
_encryptionMechanismOverride = encryptionMechanism
}
/**
@ -291,16 +431,50 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
fun hasEncryptionMethod() = _encryptionMethods.isNotEmpty()
fun usesOnlyPasswordBasedEncryption() =
_encryptionMethods.all { it is PBEKeyEncryptionMethodGenerator }
internal fun negotiateEncryptionMechanism(): MessageEncryptionMechanism {
if (encryptionMechanismOverride != null) {
return encryptionMechanismOverride!!
}
val features = keysAndAccessors.values.map { it.features }.toList()
val aeadAlgorithms = keysAndAccessors.values.map { it.preferredAEADCipherSuites }.toList()
val symmetricKeyAlgorithms =
keysAndAccessors.values.map { it.preferredSymmetricKeyAlgorithms }.toList()
val mechanism =
encryptionMechanismNegotiator.negotiate(
api.algorithmPolicy,
encryptionMechanismOverride,
features,
aeadAlgorithms,
symmetricKeyAlgorithms)
return mechanism
}
fun interface EncryptionKeySelector {
fun selectEncryptionSubkeys(encryptionCapableKeys: List<PGPPublicKey>): List<PGPPublicKey>
fun selectEncryptionSubkeys(
encryptionCapableKeys: List<OpenPGPComponentKey>
): List<OpenPGPComponentKey>
}
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.

View file

@ -8,23 +8,45 @@ import java.util.*
import org.bouncycastle.openpgp.PGPLiteralData
import org.bouncycastle.openpgp.PGPPublicKeyRing
import org.bouncycastle.openpgp.PGPSignature
import org.bouncycastle.openpgp.api.MessageEncryptionMechanism
import org.bouncycastle.openpgp.api.OpenPGPCertificate
import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature
import org.pgpainless.algorithm.CompressionAlgorithm
import org.pgpainless.algorithm.StreamEncoding
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
import org.pgpainless.bouncycastle.extensions.matches
import org.pgpainless.key.SubkeyIdentifier
import org.pgpainless.util.MultiMap
import org.pgpainless.util.SessionKey
data class EncryptionResult(
val encryptionAlgorithm: SymmetricKeyAlgorithm,
val encryptionMechanism: MessageEncryptionMechanism,
val sessionKey: SessionKey?,
val compressionAlgorithm: CompressionAlgorithm,
val detachedSignatures: MultiMap<SubkeyIdentifier, PGPSignature>,
val detachedDocumentSignatures: OpenPGPSignatureSet<OpenPGPDocumentSignature>,
val recipients: Set<SubkeyIdentifier>,
val fileName: String,
val modificationDate: Date,
val fileEncoding: StreamEncoding
) {
@Deprecated(
"Use encryptionMechanism instead.", replaceWith = ReplaceWith("encryptionMechanism"))
val encryptionAlgorithm: SymmetricKeyAlgorithm?
get() = SymmetricKeyAlgorithm.fromId(encryptionMechanism.symmetricKeyAlgorithm)
@Deprecated(
"Use detachedSignatures instead", replaceWith = ReplaceWith("detachedDocumentSignatures"))
// TODO: Remove in 2.1
val detachedSignatures: MultiMap<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
* setting the filename "_CONSOLE".
@ -34,6 +56,9 @@ data class EncryptionResult(
val isForYourEyesOnly: Boolean
get() = PGPLiteralData.CONSOLE == fileName
fun isEncryptedFor(certificate: OpenPGPCertificate) =
recipients.any { certificate.getKey(it.keyIdentifier) != null }
/**
* Returns true, if the message was encrypted for at least one subkey of the given certificate.
*
@ -52,17 +77,19 @@ data class EncryptionResult(
}
class Builder {
var _encryptionAlgorithm: SymmetricKeyAlgorithm? = null
var _encryptionMechanism: MessageEncryptionMechanism =
MessageEncryptionMechanism.unencrypted()
var _compressionAlgorithm: CompressionAlgorithm? = null
val detachedSignatures: MultiMap<SubkeyIdentifier, PGPSignature> = MultiMap()
val detachedSignatures: MutableList<OpenPGPDocumentSignature> = mutableListOf()
val recipients: Set<SubkeyIdentifier> = mutableSetOf()
private var _fileName = ""
private var _modificationDate = Date(0)
private var _encoding = StreamEncoding.BINARY
private var _sessionKey: SessionKey? = null
fun setEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply {
_encryptionAlgorithm = encryptionAlgorithm
fun setEncryptionMechanism(mechanism: MessageEncryptionMechanism): Builder = apply {
_encryptionMechanism = mechanism
}
fun setCompressionAlgorithm(compressionAlgorithm: CompressionAlgorithm) = apply {
@ -81,19 +108,20 @@ data class EncryptionResult(
(recipients as MutableSet).add(recipient)
}
fun addDetachedSignature(
signingSubkeyIdentifier: SubkeyIdentifier,
detachedSignature: PGPSignature
) = apply { detachedSignatures.put(signingSubkeyIdentifier, detachedSignature) }
fun setSessionKey(sessionKey: SessionKey) = apply { _sessionKey = sessionKey }
fun addDetachedSignature(signature: OpenPGPDocumentSignature): Builder = apply {
detachedSignatures.add(signature)
}
fun build(): EncryptionResult {
checkNotNull(_encryptionAlgorithm) { "Encryption algorithm not set." }
checkNotNull(_compressionAlgorithm) { "Compression algorithm not set." }
return EncryptionResult(
_encryptionAlgorithm!!,
_encryptionMechanism,
_sessionKey,
_compressionAlgorithm!!,
detachedSignatures,
OpenPGPSignatureSet(detachedSignatures),
recipients,
_fileName,
_modificationDate,

View file

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

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

View file

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

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