Compare commits

...

273 Commits
1.5.7 ... main

Author SHA1 Message Date
Paul Schaub dd3ef89a5c
Add (failing) test for extracting certificate from key with unknown secret key encryption method 2024-04-10 10:47:13 +02:00
Paul Schaub a6f3a223b1
Reject data signatures made by non-signing primary key 2024-04-10 10:38:50 +02:00
Paul Schaub 741d72eadc
Document nature of tests in pgpainless-sop 2024-03-30 19:20:12 +01:00
Paul Schaub 0b7511a223
Remove tests for armor --label 2024-03-30 19:07:12 +01:00
Paul Schaub eeb5986890
Remove notice about armor's label() option 2024-03-30 19:06:42 +01:00
Paul Schaub 32d62c6610
Update pgpainless-cli usage documentation 2024-03-30 18:52:49 +01:00
Paul Schaub 1f9b65e3d2
Fix missing readthedocs theme 2024-03-30 00:37:51 +01:00
Paul Schaub b96f22d0a9
Add EncryptionBuilder.discardOutput()
Also move NullOutputStream from pgpainless-sop to pgpainless-core
2024-03-29 20:37:24 +01:00
Paul Schaub 80cf1a7446
Merge branch 'sopKotlin' 2024-03-24 16:43:43 +01:00
Paul Schaub fe80b1185e
Update man pages 2024-03-24 16:43:27 +01:00
Paul Schaub b393a90da4
Port pgpainless-sop to Kotlin 2024-03-24 16:16:29 +01:00
Paul Schaub 8066650584
Add comments 2024-03-24 11:00:16 +01:00
Paul Schaub bd1949871a
Update CHANGELOG 2024-03-24 10:52:16 +01:00
Paul Schaub 194e4e1458
Bump sop-java to 10.0.0 2024-03-24 10:52:15 +01:00
Paul Schaub 44be5aa981
Delegate verification operations to SOPVImpl 2024-03-24 10:52:15 +01:00
Paul Schaub 3ac273757a
Bump sop-java to 10.0.0-SNAPSHOT and implement sopv interface subset 2024-03-24 10:52:15 +01:00
Paul Schaub fa5bdfcd82
Throw BadData if KEYS are passed where CERTS are expected 2024-03-24 10:52:14 +01:00
Paul Schaub 89038ebedf
Update CHANGELOG 2024-03-21 14:13:58 +01:00
Paul Schaub 337b5d68b6
Add Automatic-Module-Name to pgpainless-core and pgpainless-sop 2024-03-19 15:56:49 +01:00
Paul Schaub 265f72d99f
Fix OOM when detached signing large files
Fixes #432
2024-03-17 17:29:01 +01:00
Paul Schaub a9cec16dc6
Fix badge showing SOP Spec revision to show 8 instead of 7 2024-03-17 15:50:40 +01:00
Paul Schaub cbbd980554
Spotless apply 2024-03-05 21:30:28 +01:00
Paul Schaub c2abc89d5e
Add tests for PGPKeyRingExtensions 2024-03-05 21:29:47 +01:00
Paul Schaub c89c47c491
Add tests for PGPPublicKeyExtensions 2024-03-05 21:17:03 +01:00
Paul Schaub e561d58562
Add tests for PGPSecretKeyExtensions 2024-03-05 21:05:34 +01:00
Paul Schaub dfbc56fe24
Add tests for PGPSecretKeyRingExtensions 2024-03-05 20:54:15 +01:00
Paul Schaub 60ea98df00
Add Github Issue Templates to dep5 file 2024-03-05 20:38:12 +01:00
Paul Schaub 11cb7e2107
Update issue templates 2024-02-26 11:15:18 +01:00
Paul Schaub 020d411417
Move CachingBcPublicKeyDataDecryptorFactory to org.pgpainless.decryption_verification package 2024-02-21 15:12:29 +01:00
Paul Schaub 252c520ca2
Move org.bouncycastle classes to org.pgpainless.bouncycastle in order to avoid split package
See https://github.com/pgpainless/pgpainless/issues/428 for more background information
2024-02-21 14:43:38 +01:00
Paul Schaub 412f804eee
PGPainless 1.6.6 2024-02-03 11:56:53 +01:00
Paul Schaub 842c8980b9
Downgrade logback to 1.2.13 2024-02-02 22:11:38 +01:00
Paul Schaub bd26268533
Add syntactic sugar for SignatureSubpacketCallback factory methods 2024-01-24 18:59:35 +01:00
Paul Schaub b103f3ecc2
Update changelog 2024-01-24 11:33:29 +01:00
Paul Schaub acd7f15744
Rename LibrePGP features 2024-01-24 11:30:20 +01:00
Paul Schaub ce51f4b8cc
Add documentation to AEAD Algorithms 2024-01-24 11:28:35 +01:00
Paul Schaub de9a161252
Accept certification signatures using SHA-1 before 2023-02-01
This commit introduces a dedicated SignatureHashAlgorithmPolicy for certification signatures.
The default configuration will accept SHA-1 on sigs created before 2023-02-01.
2024-01-04 18:20:09 +01:00
Paul Schaub 5053221e93
Bump logback-core to 1.4.14 2023-12-15 18:30:04 +01:00
Paul Schaub fc45c9450a
Update SECURITY.md 2023-12-15 18:25:18 +01:00
Paul Schaub 69f1028fd9
Add method to change expiration time of subkeys
Port of e06f60f62c to kotlin
2023-12-15 18:20:51 +01:00
Paul Schaub d7b6dfc8d4
Update changelog 2023-12-15 17:50:27 +01:00
Paul Schaub 7b37f206d6
Update CHANGELOG 2023-11-30 20:58:41 +01:00
Paul Schaub a5a9153692
Bump logback to 1.4.13 2023-11-30 20:56:30 +01:00
Paul Schaub b8b46a3ab2
Bump BC to 1.77 2023-11-30 20:56:18 +01:00
Paul Schaub 97455aa256
Add test for handling key with unknown signature subpacket 2023-11-30 19:36:44 +01:00
Paul Schaub 74c7b025a0
Do not choke on unknown signature subpackets
Fixes #418
2023-11-30 19:36:01 +01:00
Paul Schaub f39d2c5566
Prevent subkey binding signature from predating subkey
Fixes #419
2023-11-30 17:58:10 +01:00
Paul Schaub 49de608785
Update changelog 2023-11-27 13:31:31 +01:00
Paul Schaub b0caa95378
Properly feed an EOS token to the push down automata in OpenPgpMessageInputStream.read() 2023-11-27 13:27:23 +01:00
Paul Schaub 1e33408098
Please the checkstyle checker 2023-11-27 13:26:41 +01:00
Daniel Gultsch 9ab0c35b78 add unit test to read decryption stream beyond end 2023-11-26 10:55:47 +01:00
Paul Schaub 6999f4d241
Update changelog 2023-11-23 14:45:23 +01:00
Paul Schaub 417684f55d
Update changelog 2023-11-23 14:04:11 +01:00
Paul Schaub f2448890e1
Bump sop-java to 8.0.1 2023-11-23 13:32:52 +01:00
Paul Schaub b58826f907
Update changelog 2023-11-15 19:26:27 +01:00
Paul Schaub 481dfac455
Revert PassphraseProvider API change 2023-11-15 19:23:52 +01:00
Paul Schaub cf638da130
Bump sop spec to 8 2023-11-15 19:09:15 +01:00
Paul Schaub 8203bd58c7
Bump sop-java to 8.0.0 2023-11-15 19:05:39 +01:00
Paul Schaub 1c10dd5bce
Adapt changes from sop-java 8.0.0 2023-11-15 13:40:22 +01:00
Paul Schaub 71431b7b0a
Bump sop-java to 8.0.0-SNAPSHOT 2023-11-15 13:40:00 +01:00
Paul Schaub 608ec0b7b0
Annotate methods with @Nonnull 2023-11-15 13:39:26 +01:00
Paul Schaub f07063d55f
Kotlin conversion: SignatureBuilder classes 2023-11-13 16:21:08 +01:00
Paul Schaub 3bb25a62a2
Remove unused CRCingArmoredInputStreamWrapper class 2023-11-13 14:09:42 +01:00
Paul Schaub 620c1fc96a
Ensure proper compatibility with keys with missing direct-key or certification self-sigs 2023-11-08 15:16:41 +01:00
Paul Schaub 6fdf136024
Merge pull request #404 from pgpainless/pgpainless2
PGPainless 2.0 - Kotlin Conversion
2023-10-26 13:03:16 +02:00
Paul Schaub f4bfb9dc04
Remove test with expired key 2023-10-26 12:52:21 +02:00
Paul Schaub 19b45644ae
Kotlin conversion: SignatureVerifier 2023-10-26 12:52:04 +02:00
Paul Schaub dc05a492f5
Fix reuse 2023-10-25 19:08:03 +02:00
Paul Schaub e9d8ddc57b
Kotlin conversion: SignatureValidator 2023-10-25 19:07:52 +02:00
Paul Schaub 733d5d6b82
Decrease continuation_indent from 8 to 4 2023-10-24 10:21:10 +02:00
Paul Schaub 4f64868914
Add dropbox-style editorconfig
Source: https://github.com/facebook/ktfmt/blob/main/docs/editorconfig/.editorconfig-dropbox
2023-10-24 10:15:16 +02:00
Paul Schaub 29fe4faeed
Add .git-blame-ignore-revs file 2023-10-23 14:27:54 +02:00
Paul Schaub 51e9bfc67f
Apply new formatting from 'gradle spotlessApply' 2023-10-23 14:24:31 +02:00
Paul Schaub 633048fd2a
Merge pull request #412 from msfjarvis/pgpainless2
build: add ktfmt via Spotless
2023-10-23 14:22:49 +02:00
Harsh Shandilya 384347ed1a build: add `ktfmt` via Spotless 2023-10-23 17:36:00 +05:30
Paul Schaub b72f95b46c
Kotlin conversion: SignatureSubpacketsHelper 2023-10-20 14:55:51 +02:00
Paul Schaub 0effc84fac
Kotlin conversion: SignatureSubpackets + subclasses 2023-10-20 14:10:37 +02:00
Paul Schaub 4fc513fa25
Kotlin conversion: SignatureCreationDateComparator, SignatureValidityComparator 2023-10-17 18:58:37 +02:00
Paul Schaub 70e1b40cd2
Fix ArmorUtil header 2023-10-13 13:41:50 +02:00
Paul Schaub efae652a66
Kotlin conversion: CertificateValidator 2023-10-13 13:38:45 +02:00
Paul Schaub 853e3de472
Clean up unused casts from EncryptionOptions 2023-10-10 13:00:01 +02:00
Paul Schaub 11c1c54111
Kotlin conversion: ProviderFactory 2023-10-09 12:49:17 +02:00
Paul Schaub 8351223614
Kotlin conversion: PublicKeyParameterValidationUtil 2023-10-09 12:49:17 +02:00
Paul Schaub 1cdce5c93a
Kotlin conversion: ImplementationFactory classes 2023-10-09 12:49:17 +02:00
Paul Schaub d707dcf74a
Move now unused utility classes to test directory 2023-10-09 12:49:17 +02:00
Paul Schaub 8382da923d
Add TODO to CRCinArmoredInputStreamWrapper 2023-10-09 12:49:17 +02:00
Paul Schaub aca884e936
Kotlin conversion: ArmoredOutputStreamFactory
Also allow configuration of CRC calculation for both input and output streams
2023-10-09 12:49:16 +02:00
Paul Schaub e16376ca68
Kotlin conversion: ArmoredInputStreamFactory 2023-10-09 12:49:16 +02:00
Paul Schaub 6b397a0d56
Kotlin conversion: SignaturePicker 2023-10-09 12:49:16 +02:00
Paul Schaub 841b386226
Kotlin conversion: MultiMap
Warning: This commit changes the semantics of MultiMap.put()
put() now replaces values, while plus() adds them.
2023-10-09 12:49:16 +02:00
Paul Schaub b324742a62
Kotlin conversion: ArmorUtils 2023-10-09 12:49:16 +02:00
Paul Schaub 9a917f7fdb
Kotlin conversion: DateUtil 2023-10-09 12:49:15 +02:00
Paul Schaub 4c237d55ed
Add note about deprecation to BaseSignatureSubpackets 2023-10-09 12:49:15 +02:00
Paul Schaub c9f988b2d1
Kotlin conversion: SelectUserId 2023-10-09 12:49:15 +02:00
Paul Schaub 33037b9743
Kotlin conversion: Passphrase 2023-10-09 12:49:15 +02:00
Paul Schaub 53b1e3ff71
Kotlin conversion: HashContextSigning 2023-10-09 12:49:15 +02:00
Paul Schaub a50be47fa4
Kotlin conversion: CRLFGeneratorStream 2023-10-09 12:49:14 +02:00
Paul Schaub 068aa0ec27
Kotlin conversion: SignatureGenerationStream 2023-10-09 12:49:14 +02:00
Paul Schaub 0fa09065cf
Kotlin conversion: TeeBCPGInputStream 2023-10-09 12:49:14 +02:00
Paul Schaub befb1c8c0f
Kotlin conversion: MessageInspector 2023-10-09 12:49:14 +02:00
Paul Schaub ea57c4aec0
Kotlin conversion: EncryptionStream 2023-10-09 12:49:14 +02:00
Paul Schaub 9ee29f7a53
Kotlin conversion: IntegrityProtectedInputStream 2023-10-09 12:49:13 +02:00
Paul Schaub a6198aadb3
Kotlin conversion: RevocationAttributes 2023-10-09 12:49:13 +02:00
Paul Schaub 68ac5af255
Kotlin conversion: UserId 2023-10-09 12:49:13 +02:00
Paul Schaub ec8ae3eff0
Kotlin conversion: SecretKeyRingEditor 2023-10-09 12:49:13 +02:00
Paul Schaub 4719d6ccea
Migrate further to extension methods 2023-10-09 12:49:13 +02:00
Paul Schaub 68af0a4f0e
Introduce more extension methods 2023-10-09 12:49:12 +02:00
Paul Schaub bb796143ff
Improve public/secret key selection 2023-10-09 12:49:12 +02:00
Paul Schaub 76cf6173e8
Add test for OpenPgpFingerprint.getBytes() 2023-10-09 12:49:12 +02:00
Paul Schaub de3ea580e3
Add extension methods to PGPKeyRing, PGPSecretKeyRing and PGPSignature 2023-10-09 12:49:12 +02:00
Paul Schaub a0b01f121a
Remove KeyRingUtils.unlockSecretKey() 2023-10-09 12:49:12 +02:00
Paul Schaub 19063454cb
Add PGPSecretKey.unlock() methods 2023-10-09 12:49:11 +02:00
Paul Schaub 6f9e692474
Kotlin conversion: KeyRingInfo 2023-10-09 12:49:04 +02:00
Paul Schaub 85e2fe956a
Add test for SubkeyIdentifier.isPrimaryKey() 2023-10-09 12:45:47 +02:00
Paul Schaub 9ee0f09b8d
Fix bug caused by false field comparison in SubkeyIdentifier 2023-10-09 12:45:47 +02:00
Paul Schaub 8fe9d250a8
Kotlin conversion: KeyInfo 2023-10-09 12:45:47 +02:00
Paul Schaub b6e47d7739
Kotlin conversion: KeyAccessor 2023-10-09 12:45:47 +02:00
Paul Schaub ad734ca1b4
Kotlin conversion: XDH 2023-10-09 12:45:46 +02:00
Paul Schaub 521424c23a
Kotlin conversion: XDHSpec 2023-10-09 12:45:46 +02:00
Paul Schaub ac245fb56b
Kotlin conversion: RSA 2023-10-09 12:45:46 +02:00
Paul Schaub ca3ff6acce
Kotlin conversion: RsaLength 2023-10-09 12:45:46 +02:00
Paul Schaub 2d755be10e
Kotlin conversion: ElGamal 2023-10-09 12:45:46 +02:00
Paul Schaub 72147b685e
Kotlin conversion: ElGamalLength 2023-10-09 12:45:45 +02:00
Paul Schaub f8abb28a81
Turn KeyLength method into val 2023-10-09 12:45:45 +02:00
Paul Schaub 4382c1f20e
Kotlin conversion: EdDSA 2023-10-09 12:45:45 +02:00
Paul Schaub 8f49b01d51
Kotlin conversion: EdDSACurve 2023-10-09 12:45:45 +02:00
Paul Schaub 89b73895f5
Kotlin conversion: ECDSA 2023-10-09 12:45:45 +02:00
Paul Schaub 9e7a25ffe1
Kotlin conversion: ECDH 2023-10-09 12:45:45 +02:00
Paul Schaub 7f96272152
Kotlin conversion: EllipticCurve 2023-10-09 12:45:44 +02:00
Paul Schaub 13082215d6
Fix property access 2023-10-09 12:45:37 +02:00
Paul Schaub b3f4ba052a
Remove whitespace 2023-10-09 12:44:25 +02:00
Paul Schaub 472d5c4beb
Kotlin conversion: KeyType 2023-10-09 12:44:25 +02:00
Paul Schaub 1ebf8e1e6f
Kotlin conversion: KeyLength 2023-10-09 12:44:25 +02:00
Paul Schaub 0b071ff8e1
Kotlin conversion: CachingBcPublicKeyDataDecryptorFactory 2023-10-09 12:44:24 +02:00
Paul Schaub e8fef1f1f3
Add PGPKeyRingExtensions class and make use of it 2023-10-09 12:44:24 +02:00
Paul Schaub 6a23016104
Kotlin conversion: EncryptionResult 2023-10-09 12:44:24 +02:00
Paul Schaub a4cd965967
Kotlin conversion: ProducerOptions 2023-10-09 12:44:24 +02:00
Paul Schaub 5441993baf
Kotlin conversion: SigningOptions 2023-10-09 12:44:24 +02:00
Paul Schaub 6e653f3c92
Kotlin conversion: MissingKeyPassphraseStrategy 2023-10-09 12:44:23 +02:00
Paul Schaub 1234c8800a
Kotlin conversion: MissingPublicKeyCallback 2023-10-09 12:44:23 +02:00
Paul Schaub a268cfd51a
Add missing license header 2023-10-09 12:44:23 +02:00
Paul Schaub 4cfdcca2e0
Kotlin conversion: MessageMetadata 2023-10-09 12:44:23 +02:00
Paul Schaub b09979fa45
Kotlin conversion: HardwareSecurity 2023-10-09 12:44:23 +02:00
Paul Schaub 3115e13bc2
Kotlin conversion: CustomPublicKeyDataDecryptorFactory 2023-10-09 12:44:22 +02:00
Paul Schaub d8df6c35d0
Rename heyKeyId -> openPgpKeyId 2023-10-09 12:44:22 +02:00
Paul Schaub 39e170064c
Kotlin conversion: NotationRegistry 2023-10-09 12:44:22 +02:00
Paul Schaub dc064d1727
Kotlin conversion: KeyRingUtils 2023-10-09 12:44:22 +02:00
Paul Schaub ab42a7503f
Replace usage of KeyIdUtil.formatKeyId() in Kotlin classes with Long.hexKeyId() 2023-10-09 12:44:22 +02:00
Paul Schaub 44c22f9044
Kotlin conversion: KeyIdUtil
This PR also introduces LongExtensions.kt which provides extension methods to
parse Long from Hex KeyIDs and to format Longs as Hex KeyIDs.
2023-10-09 12:44:22 +02:00
Paul Schaub d075ed6637
Kotlin conversion: PGPKeyRingCollection 2023-10-09 12:44:21 +02:00
Paul Schaub 0e6a146594
Add missing license header 2023-10-09 12:44:21 +02:00
Paul Schaub f3ea9f62e1
Kotlin conversion: EncryptionOptions 2023-10-09 12:44:21 +02:00
Paul Schaub e9caa4af1f
Kotlin conversion: EncryptionBuilder + Interface 2023-10-09 12:44:21 +02:00
Paul Schaub bbd956dbb7
Kotlin conversion: KeyRingReader 2023-10-09 12:44:21 +02:00
Paul Schaub e3f51fbf56
Kotlin conversion: SecretKeyPassphraseProvider and subclasses
This commit also adds a workaround to build.gradle which enables proper Java interop for
Kotlin interfaces with default implementations
2023-10-09 12:44:20 +02:00
Paul Schaub 5cb6d6e41d
Kotlin conversion: S2KUsageFix 2023-10-09 12:44:20 +02:00
Paul Schaub 873db12125
Kotlin conversion: UnlockSecretKey 2023-10-09 12:44:20 +02:00
Paul Schaub 2547f1c687
Kotlin conversion: KeyRingProtectionSettings 2023-10-09 12:44:20 +02:00
Paul Schaub ee32d9e446
Kotlin conversion: CachingSecretKeyRingProtector 2023-10-09 12:44:20 +02:00
Paul Schaub b9c601b996
Kotlin conversion: PasswordBasedSecretKeyRingProtector 2023-10-09 12:44:19 +02:00
Paul Schaub b125333c89
Kotlin conversion: UnprotectedKeysProtector 2023-10-09 12:44:19 +02:00
Paul Schaub a8bc929f24
Kotlin conversion: BaseSecretKeyRingProtector 2023-10-09 12:44:19 +02:00
Paul Schaub 5fce443ad9
Kotlin conversion: SecretKeyRingProtector and subclasses 2023-10-09 12:44:19 +02:00
Paul Schaub c40e2ba6c2
Move const to companion object 2023-10-09 12:44:19 +02:00
Paul Schaub b343b4ad17
Kotlin conversion: KeyRingTemplates 2023-10-09 12:44:19 +02:00
Paul Schaub 41f56bdf99
Kotlin conversion: KeySpec 2023-10-09 12:44:15 +02:00
Paul Schaub bb17c627ce
Kotlin conversion: KeySpecBuilder 2023-10-09 12:43:16 +02:00
Paul Schaub eaef1fe44a
Kotlin conversion: KeyRingBuilder 2023-10-09 12:33:45 +02:00
Paul Schaub 43335cbcd3
Kotlin conversion: SessionKey 2023-10-09 12:29:34 +02:00
Paul Schaub 02511ac1ca
Kotlin conversion: SignatureVerification 2023-10-09 12:29:34 +02:00
Paul Schaub 48af91efbf
Kotlin conversion: Cleartext Signature Framework 2023-10-09 12:29:34 +02:00
Paul Schaub 8d67820f50
Kotlin conversion: OnePassSignatureCheck 2023-10-09 12:29:34 +02:00
Paul Schaub 145555997c
Kotlin conversion: SignatureCheck 2023-10-09 12:29:33 +02:00
Paul Schaub 1a701333e3
Remove deprecated OpenPgpMetadata class 2023-10-09 12:29:33 +02:00
Paul Schaub 8c25b59c8b
Add missing utility methods to MessageMetadata class 2023-10-09 12:29:33 +02:00
Paul Schaub 23f8777c34
Kotlin conversion: DecryptionStream 2023-10-09 12:29:33 +02:00
Paul Schaub 9988ba9940
Kotlin conversion: DecryptionBuilder 2023-10-09 12:29:33 +02:00
Paul Schaub 4a19e6ca20
WIP: Kotlin conversion: ConsumerOptions 2023-10-09 12:29:32 +02:00
Paul Schaub cc63095ab0
Kotlin conversion: SignatureSubpacketsUtil 2023-10-09 12:29:32 +02:00
Paul Schaub 6dc08e7445
Improve SignatureUtils readability 2023-10-09 12:29:32 +02:00
Paul Schaub fa765fdb0d
Add documentation to PGPSignatureExtensions 2023-10-09 12:29:32 +02:00
Paul Schaub 85b1ffe2e9
Add PGPSignatureExtensions file 2023-10-09 12:29:32 +02:00
Paul Schaub b33ee90845
Kotlin conversion: SignatureUtils 2023-10-09 12:29:32 +02:00
Paul Schaub fca5c88d09
Kotlin conversion: OpenPgpMessageInputStream 2023-10-09 12:29:31 +02:00
Paul Schaub 1ab5377f70
Rename syntax checker enums to screaming snake case 2023-10-09 12:29:31 +02:00
Paul Schaub 603f07d014
Kotlin conversion: Syntax checking 2023-10-09 12:29:31 +02:00
Paul Schaub a1a090028d
Kotlin conversion: PDA 2023-10-09 12:29:31 +02:00
Paul Schaub de73b3b00b
Kotlin conversion: CertifyCertificate 2023-10-09 12:29:27 +02:00
Paul Schaub 39c5d12096
Use IntRange for Trustworthiness range check 2023-10-09 12:24:49 +02:00
Paul Schaub e57e74163c
Finish Policy conversion and move kotlin classes to src/kotlin/ 2023-10-09 12:24:49 +02:00
Paul Schaub b68061373d
Wip: Kolin conversion of Policy 2023-10-09 12:24:49 +02:00
Paul Schaub ca6e18d701
Kotlin conversion: SubkeyIdentifier 2023-10-09 12:24:49 +02:00
Paul Schaub d36e878bf9
Kotlin conversion: OpenPgpFingerprints 2023-10-09 12:24:49 +02:00
Paul Schaub 344421a0f4
Kolin conversion: Some OpenPgpFingerprint classes 2023-10-09 12:24:49 +02:00
Paul Schaub 84e554fc0d
Kotlin conversion: CertificateAuthority 2023-10-09 12:24:48 +02:00
Paul Schaub 58951ce19a
Get rid of animalsniffer 2023-10-09 12:24:48 +02:00
Paul Schaub 075ca4baa3
Kotlin conversion: CertificateAuthenticity 2023-10-09 12:24:48 +02:00
Paul Schaub b91e19fc39
Kotlin conversion: PGPainless 2023-10-09 12:24:48 +02:00
Paul Schaub 5a5b604411
Add missing license header 2023-10-09 12:24:48 +02:00
Paul Schaub cf7a7f3aca
Kotlin conversion: SymmetricKeyAlgorithmNegotiator 2023-10-09 12:24:47 +02:00
Paul Schaub fb235d7810
Kotlin conversion: HashAlgorithmNegotiator 2023-10-09 12:24:47 +02:00
Paul Schaub b5d8126e48
Kotlin conversion: AlgorithmSuite 2023-10-09 12:24:47 +02:00
Paul Schaub 382817dc5f
Kotlin conversion: Trustworthiness 2023-10-09 12:24:47 +02:00
Paul Schaub 3a62b39197
Kotlin conversion: SymmetricKeyAlgorithm 2023-10-09 12:24:47 +02:00
Paul Schaub 5da58904b6
Kotlin conversion: StreamEncoding 2023-10-09 12:24:47 +02:00
Paul Schaub 595b9c7379
Kotlin conversion: SignatureType 2023-10-09 12:24:46 +02:00
Paul Schaub f0082d3fb7
Kotlin conversion: SignatureSubpacket 2023-10-09 12:24:46 +02:00
Paul Schaub 2fb7e6c3a3
Kotlin conversion: RevocationStateType 2023-10-09 12:24:46 +02:00
Paul Schaub c8bfcc807b
Kotlin conversion: RevocationState 2023-10-09 12:24:46 +02:00
Paul Schaub 608edc2378
Kotlin conversion: PublicKeyAlgorithm 2023-10-09 12:24:46 +02:00
Paul Schaub eb07b94bcb
Kotlin conversion: OpenPgpPacket 2023-10-09 12:24:45 +02:00
Paul Schaub 294c469a29
Kotlin conversion: KeyFlag 2023-10-09 12:24:40 +02:00
Paul Schaub 2c38490742
Kotlin conversion: HashAlgorithm 2023-10-09 12:22:04 +02:00
Paul Schaub 98e9c0934d
Kotlin conversion: Feature 2023-10-09 12:22:04 +02:00
Paul Schaub eb94aa6063
Kotlin conversion: EncryptionPurpose 2023-10-09 12:22:03 +02:00
Paul Schaub e9dceb4553
Kotlin conversion: DocumentSignatureType 2023-10-09 12:22:03 +02:00
Paul Schaub dfb33a5ecb
Kotlin conversion: CompressionAlgorithm 2023-10-09 12:22:03 +02:00
Paul Schaub 644700c8ee
Kotlin conversion: CertificationType 2023-10-09 12:22:03 +02:00
Paul Schaub 94ad4cfbe7
Kotlin conversion: AEADAlgorithm 2023-10-09 12:22:03 +02:00
Paul Schaub c9dce319f8
Prepare for Kotlin conversion 2023-10-09 12:22:03 +02:00
Paul Schaub 64dfefc9e7
Remove usage of PublicKeyAlgorithm.EC 2023-10-09 12:22:02 +02:00
Paul Schaub bf6c89af64
Test usability of keyflag-less key 2023-10-09 12:09:22 +02:00
Paul Schaub 1b96919d84
Allow generation of keys with empty key flags.
Forbid certification of thirdparty certificates if CERTIFY_OTHERS flag is missing
2023-10-09 12:02:10 +02:00
Paul Schaub e7e269d7ce
Documentation: Add section on reading certificates 2023-10-06 12:46:17 +02:00
Paul Schaub 41dfe71994
Pad long KeyIDs with zeros to 16 chars 2023-09-04 14:18:13 +02:00
Paul Schaub 9ac681d88c
Update SECURITY.md 2023-08-30 13:18:39 +02:00
Paul Schaub 71ea2ce8c1
Remove sop-java and sop-java-picocli notices 2023-08-30 13:17:56 +02:00
Paul Schaub 682042f563
Update codeql action to v2 2023-08-30 12:23:11 +02:00
Paul Schaub 32b4a49102
PGPainless 1.6.3-SNAPSHOT 2023-08-30 12:19:13 +02:00
Paul Schaub ea7e5e8022
PGPainless 1.6.2 2023-08-30 12:14:20 +02:00
Paul Schaub 3eb1c60991
Update changelog 2023-08-29 16:59:43 +02:00
Paul Schaub 29165f3df4
Switch bcpg and bcprov artifacts from -jdk15to18 variant to -jdk18on 2023-08-29 16:57:05 +02:00
Paul Schaub 1c3cc19ff7
Add documentation to OpenPgpInputStream 2023-08-03 15:20:24 +02:00
Paul Schaub db7e1ce942
Allow overriding evluation date in SigningOptions 2023-08-03 14:57:31 +02:00
Paul Schaub 16a4836f8a
Override evaluation date in test with expiring key 2023-08-03 14:51:43 +02:00
Paul Schaub f0e59ecef5
EncryptionOptions: Allow overriding evaluation date for recipient keys 2023-08-03 14:48:57 +02:00
Paul Schaub d08bc6bd4b
Update changelog 2023-08-03 14:12:58 +02:00
Paul Schaub 975d59c5a9
Add method to allow for encryption for keys with missing keyflags.
There are legacy keys around, which do not carry any key flags.
This commit adds a method to EncryptionOptions that allow PGPainless to encrypt
for such keys.

Fixes #400
2023-08-03 14:04:40 +02:00
Paul Schaub 0d8db24b1a
Fix typo 2023-08-02 16:02:01 +02:00
Paul Schaub 1df6dcce13
Update sop quickstart document 2023-08-02 14:46:35 +02:00
Paul Schaub e167fa37f3
Make use of new ArmoredOutputStream.Builder 2023-08-01 16:53:55 +02:00
Paul Schaub 8cdb7ee4e0
Add more tests for V6 fingerprints 2023-08-01 15:29:24 +02:00
Paul Schaub 15af265e3f
Update changelog 2023-08-01 14:31:09 +02:00
Paul Schaub 8bc2338463
Bump BC to 1.76 2023-08-01 14:27:49 +02:00
Paul Schaub 789fa507d1
PGPainless 1.6.2-SNAPSHOT 2023-07-22 15:21:10 +02:00
Paul Schaub 1768156899
PGPainless 1.6.1 2023-07-22 15:19:17 +02:00
Paul Schaub b2ea55a67d
Update changelog 2023-07-22 15:13:01 +02:00
Paul Schaub 28e4bc44a1
Further integration of pgpainless-wot 2023-07-22 00:30:52 +02:00
Paul Schaub 616820fe0f
Update ecosystem diagram 2023-07-22 00:30:39 +02:00
Paul Schaub 6ac019a420
Add isAuthenticatablySignedBy() to MessageMetadata 2023-07-21 17:30:11 +02:00
Paul Schaub 44690d063c
Rename CertificateAuthority methods 2023-07-21 17:11:56 +02:00
Paul Schaub c26ddc116e
Add identify API endpoint 2023-07-21 17:08:48 +02:00
Paul Schaub ccbf4ab84d
Add documentation to CertificateAuthority 2023-07-21 16:55:21 +02:00
Paul Schaub 8926ff9dfb
s/identify/lookup 2023-07-21 16:50:49 +02:00
Paul Schaub bf9bf94fb0
Integrate WoT by adding EncryptionOptions.addAuthenticatableRecipients() method 2023-07-21 16:38:34 +02:00
Paul Schaub 9d93c0f5ae
Add CertificateAuthority interface to enable integration with pgpainless-wot 2023-07-21 16:25:29 +02:00
Paul Schaub 22b4b93be8
Replace jetbrains annotations package with jsr305 2023-07-19 12:43:23 +02:00
Paul Schaub 59fa51bdf3
Expose SignatureValidator methods 2023-07-19 11:47:53 +02:00
Paul Schaub f46790be00
Require UTF8 for KeyRingBuilder.addUserId(byte[]) 2023-07-12 16:49:38 +02:00
Paul Schaub 0fa62991ec
PGPainless 1.6.1-SNAPSHOT 2023-07-12 16:25:31 +02:00
Paul Schaub d6dfcbf4b5
PGPainless 1.6.0 2023-07-12 16:23:42 +02:00
Paul Schaub e4589bf147
Update CHANGELOG 2023-07-12 16:13:57 +02:00
Paul Schaub 8f53952c7c
Update man pages 2023-07-12 16:00:54 +02:00
Paul Schaub 06fd04ac76
Fix error message in rewriteManPages script 2023-07-12 15:59:38 +02:00
Paul Schaub 50787708f5
Bump SOP-Java dependency from 7.0.0-SNAPSHOT to 7.0.0 2023-07-12 15:45:56 +02:00
Paul Schaub f3980304ed
SOP-Java now produces hard-revocations 2023-07-12 15:36:09 +02:00
Paul Schaub c69af33588
revoke-key: Generate hard instead of soft revocation 2023-07-12 15:27:36 +02:00
Paul Schaub 6e9d276309
Add complex change-key-password test 2023-07-12 15:25:12 +02:00
Paul Schaub e5539a810d
Use KeyReader class when reading public or secret keys 2023-07-12 15:25:03 +02:00
Paul Schaub 744c679e0c
Bump SOP_VERSION to 7 2023-07-12 01:37:19 +02:00
Paul Schaub 9c216e1ff4
Implement '--signing-only' option for 'generate-key' subcommand 2023-07-12 01:07:29 +02:00
Paul Schaub d3fe850c95
Initial implementation of 'change-key-password' command of SOP-07 2023-07-12 00:40:59 +02:00
Paul Schaub 37bbe8bb39
Initial implementation of the new revoke-key command from SOP-07 2023-07-11 23:15:22 +02:00
497 changed files with 24953 additions and 29491 deletions

View File

@ -1,513 +1,51 @@
# SPDX-FileCopyrightText: 2021 Paul Schaub <info@pgpainless.org>
# This .editorconfig section approximates ktfmt's formatting rules. You can include it in an
# existing .editorconfig file or use it standalone by copying it to <project root>/.editorconfig
# and making sure your editor is set to read settings from .editorconfig files.
#
# SPDX-License-Identifier: CC0-1.0
# It includes editor-specific config options for IntelliJ IDEA.
#
# If any option is wrong, PR are welcome
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
[{*.kt,*.kts}]
indent_style = space
insert_final_newline = false
max_line_length = 120
tab_width = 4
ij_continuation_indent_size = 8
ij_formatter_off_tag = @formatter:off
ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = false
ij_smart_tabs = false
ij_visual_guides = none
ij_wrap_on_typing = false
[*.java]
ij_java_align_consecutive_assignments = false
ij_java_align_consecutive_variable_declarations = false
ij_java_align_group_field_declarations = false
ij_java_align_multiline_annotation_parameters = false
ij_java_align_multiline_array_initializer_expression = false
ij_java_align_multiline_assignment = false
ij_java_align_multiline_binary_operation = false
ij_java_align_multiline_chained_methods = false
ij_java_align_multiline_extends_list = false
ij_java_align_multiline_for = true
ij_java_align_multiline_method_parentheses = false
ij_java_align_multiline_parameters = true
ij_java_align_multiline_parameters_in_calls = false
ij_java_align_multiline_parenthesized_expression = false
ij_java_align_multiline_records = true
ij_java_align_multiline_resources = true
ij_java_align_multiline_ternary_operation = false
ij_java_align_multiline_text_blocks = false
ij_java_align_multiline_throws_list = false
ij_java_align_subsequent_simple_methods = false
ij_java_align_throws_keyword = false
ij_java_annotation_parameter_wrap = off
ij_java_array_initializer_new_line_after_left_brace = false
ij_java_array_initializer_right_brace_on_new_line = false
ij_java_array_initializer_wrap = off
ij_java_assert_statement_colon_on_next_line = false
ij_java_assert_statement_wrap = off
ij_java_assignment_wrap = off
ij_java_binary_operation_sign_on_next_line = false
ij_java_binary_operation_wrap = off
ij_java_blank_lines_after_anonymous_class_header = 0
ij_java_blank_lines_after_class_header = 0
ij_java_blank_lines_after_imports = 1
ij_java_blank_lines_after_package = 1
ij_java_blank_lines_around_class = 1
ij_java_blank_lines_around_field = 0
ij_java_blank_lines_around_field_in_interface = 0
ij_java_blank_lines_around_initializer = 1
ij_java_blank_lines_around_method = 1
ij_java_blank_lines_around_method_in_interface = 1
ij_java_blank_lines_before_class_end = 0
ij_java_blank_lines_before_imports = 1
ij_java_blank_lines_before_method_body = 0
ij_java_blank_lines_before_package = 0
ij_java_block_brace_style = end_of_line
ij_java_block_comment_at_first_column = true
ij_java_builder_methods = none
ij_java_call_parameters_new_line_after_left_paren = false
ij_java_call_parameters_right_paren_on_new_line = false
ij_java_call_parameters_wrap = off
ij_java_case_statement_on_separate_line = true
ij_java_catch_on_new_line = false
ij_java_class_annotation_wrap = split_into_lines
ij_java_class_brace_style = end_of_line
ij_java_class_count_to_use_import_on_demand = 10000
ij_java_class_names_in_javadoc = 1
ij_java_do_not_indent_top_level_class_members = false
ij_java_do_not_wrap_after_single_annotation = false
ij_java_do_while_brace_force = never
ij_java_doc_add_blank_line_after_description = true
ij_java_doc_add_blank_line_after_param_comments = false
ij_java_doc_add_blank_line_after_return = false
ij_java_doc_add_p_tag_on_empty_lines = true
ij_java_doc_align_exception_comments = true
ij_java_doc_align_param_comments = true
ij_java_doc_do_not_wrap_if_one_line = false
ij_java_doc_enable_formatting = true
ij_java_doc_enable_leading_asterisks = true
ij_java_doc_indent_on_continuation = false
ij_java_doc_keep_empty_lines = true
ij_java_doc_keep_empty_parameter_tag = true
ij_java_doc_keep_empty_return_tag = true
ij_java_doc_keep_empty_throws_tag = true
ij_java_doc_keep_invalid_tags = true
ij_java_doc_param_description_on_new_line = false
ij_java_doc_preserve_line_breaks = false
ij_java_doc_use_throws_not_exception_tag = true
ij_java_else_on_new_line = false
ij_java_enum_constants_wrap = off
ij_java_extends_keyword_wrap = off
ij_java_extends_list_wrap = off
ij_java_field_annotation_wrap = split_into_lines
ij_java_finally_on_new_line = false
ij_java_for_brace_force = never
ij_java_for_statement_new_line_after_left_paren = false
ij_java_for_statement_right_paren_on_new_line = false
ij_java_for_statement_wrap = off
ij_java_generate_final_locals = false
ij_java_generate_final_parameters = false
ij_java_if_brace_force = never
ij_java_imports_layout = $*,|,java.**,javax.**,|,*
ij_java_indent_case_from_switch = true
ij_java_insert_inner_class_imports = false
ij_java_insert_override_annotation = true
ij_java_keep_blank_lines_before_right_brace = 2
ij_java_keep_blank_lines_between_package_declaration_and_header = 2
ij_java_keep_blank_lines_in_code = 2
ij_java_keep_blank_lines_in_declarations = 2
ij_java_keep_builder_methods_indents = false
ij_java_keep_control_statement_in_one_line = true
ij_java_keep_first_column_comment = true
ij_java_keep_indents_on_empty_lines = false
ij_java_keep_line_breaks = true
ij_java_keep_multiple_expressions_in_one_line = false
ij_java_keep_simple_blocks_in_one_line = false
ij_java_keep_simple_classes_in_one_line = false
ij_java_keep_simple_lambdas_in_one_line = false
ij_java_keep_simple_methods_in_one_line = false
ij_java_label_indent_absolute = false
ij_java_label_indent_size = 0
ij_java_lambda_brace_style = end_of_line
ij_java_layout_static_imports_separately = true
ij_java_line_comment_add_space = false
ij_java_line_comment_at_first_column = true
ij_java_method_annotation_wrap = split_into_lines
ij_java_method_brace_style = end_of_line
ij_java_method_call_chain_wrap = off
ij_java_method_parameters_new_line_after_left_paren = false
ij_java_method_parameters_right_paren_on_new_line = false
ij_java_method_parameters_wrap = off
ij_java_modifier_list_wrap = false
ij_java_names_count_to_use_import_on_demand = 1000
ij_java_new_line_after_lparen_in_record_header = false
ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.*
ij_java_parameter_annotation_wrap = off
ij_java_parentheses_expression_new_line_after_left_paren = false
ij_java_parentheses_expression_right_paren_on_new_line = false
ij_java_place_assignment_sign_on_next_line = false
ij_java_prefer_longer_names = true
ij_java_prefer_parameters_wrap = false
ij_java_record_components_wrap = normal
ij_java_repeat_synchronized = true
ij_java_replace_instanceof_and_cast = false
ij_java_replace_null_check = true
ij_java_replace_sum_lambda_with_method_ref = true
ij_java_resource_list_new_line_after_left_paren = false
ij_java_resource_list_right_paren_on_new_line = false
ij_java_resource_list_wrap = off
ij_java_rparen_on_new_line_in_record_header = false
ij_java_space_after_closing_angle_bracket_in_type_argument = false
ij_java_space_after_colon = true
ij_java_space_after_comma = true
ij_java_space_after_comma_in_type_arguments = true
ij_java_space_after_for_semicolon = true
ij_java_space_after_quest = true
ij_java_space_after_type_cast = true
ij_java_space_before_annotation_array_initializer_left_brace = false
ij_java_space_before_annotation_parameter_list = false
ij_java_space_before_array_initializer_left_brace = false
ij_java_space_before_catch_keyword = true
ij_java_space_before_catch_left_brace = true
ij_java_space_before_catch_parentheses = true
ij_java_space_before_class_left_brace = true
ij_java_space_before_colon = true
ij_java_space_before_colon_in_foreach = true
ij_java_space_before_comma = false
ij_java_space_before_do_left_brace = true
ij_java_space_before_else_keyword = true
ij_java_space_before_else_left_brace = true
ij_java_space_before_finally_keyword = true
ij_java_space_before_finally_left_brace = true
ij_java_space_before_for_left_brace = true
ij_java_space_before_for_parentheses = true
ij_java_space_before_for_semicolon = false
ij_java_space_before_if_left_brace = true
ij_java_space_before_if_parentheses = true
ij_java_space_before_method_call_parentheses = false
ij_java_space_before_method_left_brace = true
ij_java_space_before_method_parentheses = false
ij_java_space_before_opening_angle_bracket_in_type_parameter = false
ij_java_space_before_quest = true
ij_java_space_before_switch_left_brace = true
ij_java_space_before_switch_parentheses = true
ij_java_space_before_synchronized_left_brace = true
ij_java_space_before_synchronized_parentheses = true
ij_java_space_before_try_left_brace = true
ij_java_space_before_try_parentheses = true
ij_java_space_before_type_parameter_list = false
ij_java_space_before_while_keyword = true
ij_java_space_before_while_left_brace = true
ij_java_space_before_while_parentheses = true
ij_java_space_inside_one_line_enum_braces = false
ij_java_space_within_empty_array_initializer_braces = false
ij_java_space_within_empty_method_call_parentheses = false
ij_java_space_within_empty_method_parentheses = false
ij_java_spaces_around_additive_operators = true
ij_java_spaces_around_assignment_operators = true
ij_java_spaces_around_bitwise_operators = true
ij_java_spaces_around_equality_operators = true
ij_java_spaces_around_lambda_arrow = true
ij_java_spaces_around_logical_operators = true
ij_java_spaces_around_method_ref_dbl_colon = false
ij_java_spaces_around_multiplicative_operators = true
ij_java_spaces_around_relational_operators = true
ij_java_spaces_around_shift_operators = true
ij_java_spaces_around_type_bounds_in_type_parameters = true
ij_java_spaces_around_unary_operator = false
ij_java_spaces_within_angle_brackets = false
ij_java_spaces_within_annotation_parentheses = false
ij_java_spaces_within_array_initializer_braces = false
ij_java_spaces_within_braces = false
ij_java_spaces_within_brackets = false
ij_java_spaces_within_cast_parentheses = false
ij_java_spaces_within_catch_parentheses = false
ij_java_spaces_within_for_parentheses = false
ij_java_spaces_within_if_parentheses = false
ij_java_spaces_within_method_call_parentheses = false
ij_java_spaces_within_method_parentheses = false
ij_java_spaces_within_parentheses = false
ij_java_spaces_within_record_header = false
ij_java_spaces_within_switch_parentheses = false
ij_java_spaces_within_synchronized_parentheses = false
ij_java_spaces_within_try_parentheses = false
ij_java_spaces_within_while_parentheses = false
ij_java_special_else_if_treatment = true
ij_java_subclass_name_suffix = Impl
ij_java_ternary_operation_signs_on_next_line = false
ij_java_ternary_operation_wrap = off
ij_java_test_name_suffix = Test
ij_java_throws_keyword_wrap = off
ij_java_throws_list_wrap = off
ij_java_use_external_annotations = false
ij_java_use_fq_class_names = false
ij_java_use_relative_indents = false
ij_java_use_single_class_imports = true
ij_java_variable_annotation_wrap = off
ij_java_visibility = public
ij_java_while_brace_force = never
ij_java_while_on_new_line = false
ij_java_wrap_comments = false
ij_java_wrap_first_method_in_call_chain = false
ij_java_wrap_long_lines = false
[*.properties]
ij_properties_align_group_field_declarations = false
ij_properties_keep_blank_lines = false
ij_properties_key_value_delimiter = equals
ij_properties_spaces_around_key_value_delimiter = false
[.editorconfig]
ij_editorconfig_align_group_field_declarations = false
ij_editorconfig_space_after_colon = false
ij_editorconfig_space_after_comma = true
ij_editorconfig_space_before_colon = false
ij_editorconfig_space_before_comma = false
ij_editorconfig_spaces_around_assignment_operators = true
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.jspx,*.pom,*.rng,*.tagx,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
ij_xml_align_attributes = true
ij_xml_align_text = false
ij_xml_attribute_wrap = normal
ij_xml_block_comment_at_first_column = true
ij_xml_keep_blank_lines = 2
ij_xml_keep_indents_on_empty_lines = false
ij_xml_keep_line_breaks = true
ij_xml_keep_line_breaks_in_text = true
ij_xml_keep_whitespaces = false
ij_xml_keep_whitespaces_around_cdata = preserve
ij_xml_keep_whitespaces_inside_cdata = false
ij_xml_line_comment_at_first_column = true
ij_xml_space_after_tag_name = false
ij_xml_space_around_equals_in_attribute = false
ij_xml_space_inside_empty_tag = false
ij_xml_text_wrap = normal
ij_xml_use_custom_settings = false
[{*.bash,*.sh,*.zsh}]
indent_size = 2
tab_width = 2
ij_shell_binary_ops_start_line = false
ij_shell_keep_column_alignment_padding = false
ij_shell_minify_program = false
ij_shell_redirect_followed_by_space = false
ij_shell_switch_cases_indented = false
ij_shell_use_unix_line_separator = true
[{*.gant,*.gradle,*.groovy,*.gy}]
ij_groovy_align_group_field_declarations = false
ij_groovy_align_multiline_array_initializer_expression = false
ij_groovy_align_multiline_assignment = false
ij_groovy_align_multiline_binary_operation = false
ij_groovy_align_multiline_chained_methods = false
ij_groovy_align_multiline_extends_list = false
ij_groovy_align_multiline_for = true
ij_groovy_align_multiline_list_or_map = true
ij_groovy_align_multiline_method_parentheses = false
ij_groovy_align_multiline_parameters = true
ij_groovy_align_multiline_parameters_in_calls = false
ij_groovy_align_multiline_resources = true
ij_groovy_align_multiline_ternary_operation = false
ij_groovy_align_multiline_throws_list = false
ij_groovy_align_named_args_in_map = true
ij_groovy_align_throws_keyword = false
ij_groovy_array_initializer_new_line_after_left_brace = false
ij_groovy_array_initializer_right_brace_on_new_line = false
ij_groovy_array_initializer_wrap = off
ij_groovy_assert_statement_wrap = off
ij_groovy_assignment_wrap = off
ij_groovy_binary_operation_wrap = off
ij_groovy_blank_lines_after_class_header = 0
ij_groovy_blank_lines_after_imports = 1
ij_groovy_blank_lines_after_package = 1
ij_groovy_blank_lines_around_class = 1
ij_groovy_blank_lines_around_field = 0
ij_groovy_blank_lines_around_field_in_interface = 0
ij_groovy_blank_lines_around_method = 1
ij_groovy_blank_lines_around_method_in_interface = 1
ij_groovy_blank_lines_before_imports = 1
ij_groovy_blank_lines_before_method_body = 0
ij_groovy_blank_lines_before_package = 0
ij_groovy_block_brace_style = end_of_line
ij_groovy_block_comment_at_first_column = true
ij_groovy_call_parameters_new_line_after_left_paren = false
ij_groovy_call_parameters_right_paren_on_new_line = false
ij_groovy_call_parameters_wrap = off
ij_groovy_catch_on_new_line = false
ij_groovy_class_annotation_wrap = split_into_lines
ij_groovy_class_brace_style = end_of_line
ij_groovy_class_count_to_use_import_on_demand = 5
ij_groovy_do_while_brace_force = never
ij_groovy_else_on_new_line = false
ij_groovy_enum_constants_wrap = off
ij_groovy_extends_keyword_wrap = off
ij_groovy_extends_list_wrap = off
ij_groovy_field_annotation_wrap = split_into_lines
ij_groovy_finally_on_new_line = false
ij_groovy_for_brace_force = never
ij_groovy_for_statement_new_line_after_left_paren = false
ij_groovy_for_statement_right_paren_on_new_line = false
ij_groovy_for_statement_wrap = off
ij_groovy_if_brace_force = never
ij_groovy_import_annotation_wrap = 2
ij_groovy_imports_layout = *,|,javax.**,java.**,|,$*
ij_groovy_indent_case_from_switch = true
ij_groovy_indent_label_blocks = true
ij_groovy_insert_inner_class_imports = false
ij_groovy_keep_blank_lines_before_right_brace = 2
ij_groovy_keep_blank_lines_in_code = 2
ij_groovy_keep_blank_lines_in_declarations = 2
ij_groovy_keep_control_statement_in_one_line = true
ij_groovy_keep_first_column_comment = true
ij_groovy_keep_indents_on_empty_lines = false
ij_groovy_keep_line_breaks = true
ij_groovy_keep_multiple_expressions_in_one_line = false
ij_groovy_keep_simple_blocks_in_one_line = false
ij_groovy_keep_simple_classes_in_one_line = true
ij_groovy_keep_simple_lambdas_in_one_line = true
ij_groovy_keep_simple_methods_in_one_line = true
ij_groovy_label_indent_absolute = false
ij_groovy_label_indent_size = 0
ij_groovy_lambda_brace_style = end_of_line
ij_groovy_layout_static_imports_separately = true
ij_groovy_line_comment_add_space = false
ij_groovy_line_comment_at_first_column = true
ij_groovy_method_annotation_wrap = split_into_lines
ij_groovy_method_brace_style = end_of_line
ij_groovy_method_call_chain_wrap = off
ij_groovy_method_parameters_new_line_after_left_paren = false
ij_groovy_method_parameters_right_paren_on_new_line = false
ij_groovy_method_parameters_wrap = off
ij_groovy_modifier_list_wrap = false
ij_groovy_names_count_to_use_import_on_demand = 3
ij_groovy_parameter_annotation_wrap = off
ij_groovy_parentheses_expression_new_line_after_left_paren = false
ij_groovy_parentheses_expression_right_paren_on_new_line = false
ij_groovy_prefer_parameters_wrap = false
ij_groovy_resource_list_new_line_after_left_paren = false
ij_groovy_resource_list_right_paren_on_new_line = false
ij_groovy_resource_list_wrap = off
ij_groovy_space_after_assert_separator = true
ij_groovy_space_after_colon = true
ij_groovy_space_after_comma = true
ij_groovy_space_after_comma_in_type_arguments = true
ij_groovy_space_after_for_semicolon = true
ij_groovy_space_after_quest = true
ij_groovy_space_after_type_cast = true
ij_groovy_space_before_annotation_parameter_list = false
ij_groovy_space_before_array_initializer_left_brace = false
ij_groovy_space_before_assert_separator = false
ij_groovy_space_before_catch_keyword = true
ij_groovy_space_before_catch_left_brace = true
ij_groovy_space_before_catch_parentheses = true
ij_groovy_space_before_class_left_brace = true
ij_groovy_space_before_closure_left_brace = true
ij_groovy_space_before_colon = true
ij_groovy_space_before_comma = false
ij_groovy_space_before_do_left_brace = true
ij_groovy_space_before_else_keyword = true
ij_groovy_space_before_else_left_brace = true
ij_groovy_space_before_finally_keyword = true
ij_groovy_space_before_finally_left_brace = true
ij_groovy_space_before_for_left_brace = true
ij_groovy_space_before_for_parentheses = true
ij_groovy_space_before_for_semicolon = false
ij_groovy_space_before_if_left_brace = true
ij_groovy_space_before_if_parentheses = true
ij_groovy_space_before_method_call_parentheses = false
ij_groovy_space_before_method_left_brace = true
ij_groovy_space_before_method_parentheses = false
ij_groovy_space_before_quest = true
ij_groovy_space_before_switch_left_brace = true
ij_groovy_space_before_switch_parentheses = true
ij_groovy_space_before_synchronized_left_brace = true
ij_groovy_space_before_synchronized_parentheses = true
ij_groovy_space_before_try_left_brace = true
ij_groovy_space_before_try_parentheses = true
ij_groovy_space_before_while_keyword = true
ij_groovy_space_before_while_left_brace = true
ij_groovy_space_before_while_parentheses = true
ij_groovy_space_in_named_argument = true
ij_groovy_space_in_named_argument_before_colon = false
ij_groovy_space_within_empty_array_initializer_braces = false
ij_groovy_space_within_empty_method_call_parentheses = false
ij_groovy_spaces_around_additive_operators = true
ij_groovy_spaces_around_assignment_operators = true
ij_groovy_spaces_around_bitwise_operators = true
ij_groovy_spaces_around_equality_operators = true
ij_groovy_spaces_around_lambda_arrow = true
ij_groovy_spaces_around_logical_operators = true
ij_groovy_spaces_around_multiplicative_operators = true
ij_groovy_spaces_around_regex_operators = true
ij_groovy_spaces_around_relational_operators = true
ij_groovy_spaces_around_shift_operators = true
ij_groovy_spaces_within_annotation_parentheses = false
ij_groovy_spaces_within_array_initializer_braces = false
ij_groovy_spaces_within_braces = true
ij_groovy_spaces_within_brackets = false
ij_groovy_spaces_within_cast_parentheses = false
ij_groovy_spaces_within_catch_parentheses = false
ij_groovy_spaces_within_for_parentheses = false
ij_groovy_spaces_within_gstring_injection_braces = false
ij_groovy_spaces_within_if_parentheses = false
ij_groovy_spaces_within_list_or_map = false
ij_groovy_spaces_within_method_call_parentheses = false
ij_groovy_spaces_within_method_parentheses = false
ij_groovy_spaces_within_parentheses = false
ij_groovy_spaces_within_switch_parentheses = false
ij_groovy_spaces_within_synchronized_parentheses = false
ij_groovy_spaces_within_try_parentheses = false
ij_groovy_spaces_within_tuple_expression = false
ij_groovy_spaces_within_while_parentheses = false
ij_groovy_special_else_if_treatment = true
ij_groovy_ternary_operation_wrap = off
ij_groovy_throws_keyword_wrap = off
ij_groovy_throws_list_wrap = off
ij_groovy_use_flying_geese_braces = false
ij_groovy_use_fq_class_names = false
ij_groovy_use_fq_class_names_in_javadoc = true
ij_groovy_use_relative_indents = false
ij_groovy_use_single_class_imports = true
ij_groovy_variable_annotation_wrap = off
ij_groovy_while_brace_force = never
ij_groovy_while_on_new_line = false
ij_groovy_wrap_long_lines = false
[{*.gradle.kts,*.kt,*.kts,*.main.kts,*.space.kts}]
insert_final_newline = true
max_line_length = 100
indent_size = 4
ij_continuation_indent_size = 4 # was 8
ij_java_names_count_to_use_import_on_demand = 9999
ij_kotlin_align_in_columns_case_branch = false
ij_kotlin_align_multiline_binary_operation = false
ij_kotlin_align_multiline_extends_list = false
ij_kotlin_align_multiline_method_parentheses = false
ij_kotlin_align_multiline_parameters = true
ij_kotlin_align_multiline_parameters_in_calls = false
ij_kotlin_allow_trailing_comma = false
ij_kotlin_allow_trailing_comma_on_call_site = false
ij_kotlin_assignment_wrap = off
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_assignment_wrap = normal
ij_kotlin_blank_lines_after_class_header = 0
ij_kotlin_blank_lines_around_block_when_branches = 0
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
ij_kotlin_block_comment_at_first_column = true
ij_kotlin_call_parameters_new_line_after_left_paren = false
ij_kotlin_call_parameters_new_line_after_left_paren = true
ij_kotlin_call_parameters_right_paren_on_new_line = false
ij_kotlin_call_parameters_wrap = off
ij_kotlin_call_parameters_wrap = on_every_item
ij_kotlin_catch_on_new_line = false
ij_kotlin_class_annotation_wrap = split_into_lines
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
ij_kotlin_continuation_indent_for_chained_calls = true
ij_kotlin_continuation_indent_for_expression_bodies = true
ij_kotlin_continuation_indent_in_argument_lists = true
ij_kotlin_continuation_indent_in_elvis = true
ij_kotlin_continuation_indent_in_if_conditions = true
ij_kotlin_continuation_indent_in_parameter_lists = true
ij_kotlin_continuation_indent_in_supertype_lists = true
ij_kotlin_continuation_indent_in_elvis = false
ij_kotlin_continuation_indent_in_if_conditions = false
ij_kotlin_continuation_indent_in_parameter_lists = false
ij_kotlin_continuation_indent_in_supertype_lists = false
ij_kotlin_else_on_new_line = false
ij_kotlin_enum_constants_wrap = off
ij_kotlin_extends_list_wrap = off
ij_kotlin_extends_list_wrap = normal
ij_kotlin_field_annotation_wrap = split_into_lines
ij_kotlin_finally_on_new_line = false
ij_kotlin_if_rparen_on_new_line = false
ij_kotlin_import_nested_classes = false
ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
ij_kotlin_keep_blank_lines_before_right_brace = 2
ij_kotlin_keep_blank_lines_in_code = 2
@ -519,13 +57,13 @@ ij_kotlin_lbrace_on_next_line = false
ij_kotlin_line_comment_add_space = false
ij_kotlin_line_comment_at_first_column = true
ij_kotlin_method_annotation_wrap = split_into_lines
ij_kotlin_method_call_chain_wrap = off
ij_kotlin_method_parameters_new_line_after_left_paren = false
ij_kotlin_method_parameters_right_paren_on_new_line = false
ij_kotlin_method_parameters_wrap = off
ij_kotlin_name_count_to_use_star_import = 5
ij_kotlin_name_count_to_use_star_import_for_members = 3
ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.**
ij_kotlin_method_call_chain_wrap = normal
ij_kotlin_method_parameters_new_line_after_left_paren = true
ij_kotlin_method_parameters_right_paren_on_new_line = true
ij_kotlin_method_parameters_wrap = on_every_item
ij_kotlin_name_count_to_use_star_import = 9999
ij_kotlin_name_count_to_use_star_import_for_members = 9999
ij_java_names_count_to_use_import_on_demand = 9999
ij_kotlin_parameter_annotation_wrap = off
ij_kotlin_space_after_comma = true
ij_kotlin_space_after_extend_colon = true
@ -552,72 +90,5 @@ ij_kotlin_spaces_around_when_arrow = true
ij_kotlin_variable_annotation_wrap = off
ij_kotlin_while_on_new_line = false
ij_kotlin_wrap_elvis_expressions = 1
ij_kotlin_wrap_expression_body_functions = 0
ij_kotlin_wrap_expression_body_functions = 1
ij_kotlin_wrap_first_method_in_call_chain = false
[{*.har,*.json}]
indent_size = 2
ij_json_keep_blank_lines_in_code = 0
ij_json_keep_indents_on_empty_lines = false
ij_json_keep_line_breaks = true
ij_json_space_after_colon = true
ij_json_space_after_comma = true
ij_json_space_before_colon = true
ij_json_space_before_comma = false
ij_json_spaces_within_braces = false
ij_json_spaces_within_brackets = false
ij_json_wrap_long_lines = false
[{*.htm,*.html,*.sht,*.shtm,*.shtml}]
ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3
ij_html_align_attributes = true
ij_html_align_text = false
ij_html_attribute_wrap = normal
ij_html_block_comment_at_first_column = true
ij_html_do_not_align_children_of_min_lines = 0
ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p
ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot
ij_html_enforce_quotes = false
ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var
ij_html_keep_blank_lines = 2
ij_html_keep_indents_on_empty_lines = false
ij_html_keep_line_breaks = true
ij_html_keep_line_breaks_in_text = true
ij_html_keep_whitespaces = false
ij_html_keep_whitespaces_inside = span,pre,textarea
ij_html_line_comment_at_first_column = true
ij_html_new_line_after_last_attribute = never
ij_html_new_line_before_first_attribute = never
ij_html_quote_style = double
ij_html_remove_new_line_before_tags = br
ij_html_space_after_tag_name = false
ij_html_space_around_equality_in_attribute = false
ij_html_space_inside_empty_tag = false
ij_html_text_wrap = normal
ij_html_uniform_ident = false
[{*.markdown,*.md}]
ij_markdown_force_one_space_after_blockquote_symbol = true
ij_markdown_force_one_space_after_header_symbol = true
ij_markdown_force_one_space_after_list_bullet = true
ij_markdown_force_one_space_between_words = true
ij_markdown_keep_indents_on_empty_lines = false
ij_markdown_max_lines_around_block_elements = 1
ij_markdown_max_lines_around_header = 1
ij_markdown_max_lines_between_paragraphs = 1
ij_markdown_min_lines_around_block_elements = 1
ij_markdown_min_lines_around_header = 1
ij_markdown_min_lines_between_paragraphs = 1
[{*.yaml,*.yml}]
indent_size = 2
ij_yaml_align_values_properties = do_not_align
ij_yaml_autoinsert_sequence_marker = true
ij_yaml_block_mapping_on_new_line = false
ij_yaml_indent_sequence_value = true
ij_yaml_keep_indents_on_empty_lines = false
ij_yaml_keep_line_breaks = true
ij_yaml_sequence_on_new_line = false
ij_yaml_space_before_colon = false
ij_yaml_spaces_within_braces = true
ij_yaml_spaces_within_brackets = true

2
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,2 @@
# Ignore initial spotlessApply using ktfmt
51e9bfc67f19e16a69790a8d92bd6b1c86a76a5f

View File

@ -0,0 +1,35 @@
---
name: CLI Application
about: Report an issue with the pgpainless-cli utility
title: ''
labels: 'module: cli'
assignees: ''
---
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**Version**
<!-- What version of the software are you using? -->
- `pgpainless-cli`:
**Installation Source**
<!-- Where did you install / build pgpainless-cli from? -->
- Debian Repository
- Built locally (`gradle build...`)
**To Reproduce**
<!-- Steps to reproduce the behavior: -->
1. `pgpainless-cli foo bar [...]`'
2. ...
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Additional context**
<!-- Add any other context (test keys, test messages) about the problem here. -->
```
-----BEGIN PGP FOO BAR-----
...
```

28
.github/ISSUE_TEMPLATE/library.md vendored Normal file
View File

@ -0,0 +1,28 @@
---
name: Library
about: Report an issue with the libraries pgpainless-core or pgpainless-sop
title: ''
labels: 'module: core'
assignees: ''
---
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**Version**
<!-- What version of the software are you using? Delete lines which are not applicable. -->
- `pgpainless-core`:
- `pgpainless-sop`:
**To Reproduce**
<!-- Steps to reproduce the behavior: -->
```
Example Code Block with your Code
```
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Additional context**
<!-- Add any other context about the problem here. -->

View File

@ -46,7 +46,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -57,7 +57,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -71,4 +71,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

View File

@ -9,6 +9,11 @@ Source: https://pgpainless.org
# Copyright: $YEAR $NAME <$CONTACT>
# License: ...
# GitBlameIgnore
Files: .git-blame-ignore-revs
Copyright: 2023 Paul Schaub <info@pgpainless.org>
License: CC0-1.0
# Documentation
Files: docs/*
Copyright: 2022 Paul Schaub <info@pgpainless.org>
@ -23,6 +28,11 @@ Files: gradle*
Copyright: 2015 the original author or authors.
License: Apache-2.0
# Editorconfig
Files: .editorconfig
Copyright: Facebook
License: Apache-2.0
# PGPainless Logo
Files: assets/repository-open-graph.png
Copyright: 2021 Paul Schaub <info@pgpainless.org>
@ -69,3 +79,8 @@ License: Apache-2.0
Files: pgpainless-cli/packaging/man/*
Copyright: 2022 Paul Schaub <info@pgpainless.org>
License: Apache-2.0
# Github Issue Templates
Files: .github/ISSUE_TEMPLATE/*
Copyright: 2024 Paul Schaub <info@pgpainless.org>
License: CC0-1.0

View File

@ -5,6 +5,78 @@ SPDX-License-Identifier: CC0-1.0
# PGPainless Changelog
## 2.0.0-SNAPSHOT
- Bump `bcpg-jdk8on` to `1.77`
- Bump `bcprov-jdk18on` to `1.77`
- Bump `logback-core` and `logback-classic` to `1.4.13`
- `pgpainless-core`
- Rewrote most of the codebase in Kotlin
- Removed `OpenPgpMetadata` (`decryptionStream.getResult()`) in favor of `MessageMetadata` (`decryptionStream.getMetadata()`)
- `pgpainless-sop`, `pgpainless-cli`
- Bump `sop-java` to `10.0.0`, implementing [SOP Spec Revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html)
- Change API of `sop.encrypt` to return a `ReadyWithResult<EncryptionResult>` to expose the session key
- `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty
- Separate signature verification operations into `SOPV` interface
- Add `version --sopv` option
- Throw `BadData` error when passing KEYS where CERTS are expected.
- Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice)
- Do not choke on unknown signature subpackets (thanks @Jerbell)
- Prevent timing issues resuting in subkey binding signatures predating the subkey (@thanks Jerbell)
- Rename LibrePGP-related `Feature` enums:
- `GNUPG_AEAD_ENCRYPTED_DATA` -> `LIBREPGP_OCB_ENCRYPTED_DATA`
- `GNUPG_VERSION_5_PUBLIC_KEY` -> `LIBREPGP_VERSION_5_PUBLIC_KEY`
## 1.6.7
- SOP: Fix OOM error when detached-signing large amounts of data (fix #432)
- Move `CachingBcPublicKeyDataDecryptorFactory` from `org.bouncycastle` packet to `org.pgpainless.decryption_verification` to avoid package split (partially addresses #428)
- Basic support for Java Modules for `pgpainless-core` and `pgpainless-sop`
- Added `Automatic-Module-Name` directive to gradle build files
## 1.6.6
- Downgrade `logback-core` and `logback-classic` to `1.2.13` to fix #426
## 1.6.5
- Add `SecretKeyRingEditor.setExpirationDateOfSubkey()`
## 1.6.4
- Bump `bcpg-jdk8on` to `1.77`
- Bump `bcprov-jdk18on` to `1.77`
- Bump `logback-core` and `logback-classic` to `1.4.13`
- Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice)
- Do not choke on unknown signature subpackets (thanks @Jerbell)
- Prevent timing issues resuting in subkey binding signatures predating the subkey (@thanks Jerbell)
## 1.6.3
- Bump `sop-java` to `7.0.1`
- `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty
## 1.6.2
- Switch `bcpg` and `bcprov` artifacts from `-jdk15to18`variant to `-jdk18on`
- Bump `bcpg-jdk8on` to `1.76`
- Bump `bcprov-jdk18on` to `1.76`
- Add `EncryptionOptions.setAllowEncryptionWithMissingKeyFlags()` to properly allow
encrypting to legacy keys which do not carry any key flags.
- Allow overriding of reference time in `EncryptionOptions` and `SigningOptions`.
## 1.6.1
- `KeyRingBuilder`: Require UTF8 when adding user-ID via `addUserId(byte[])`
- `pgpainless-sop`: Remove dependency on jetbrains annotations
- Add `CertificateAuthority` interface to allow integration with [`pgpainless-wot`](https://github.com/pgpainless/pgpainless-wot)
- Add `EncryptionOptions.addAuthenticatableRecipients()` method
- Add `MessageMetadata.isAuthenticatablySignedBy()` method
## 1.6.0
- Bump `sop-java` to `7.0.0`, implementing [SOP Spec Revision 07](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-07.html)
- Implement `revoke-key` subcommand and API
- Implement `change-key-password` subcommand and API
- `generate-key`: Add support for new `--signing-only` option
- Move some methods related to password changing from `SecretKeyRingEditor` to `KeyRingUtils`
## 1.5.7
- Bump `sop-java` to `6.1.1`
- `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty
## 1.5.6
- Bump `jacoco` to `0.8.8` (thanks @hkos)
- Ignore malformed, non-UTF8 user-IDs on certificates
@ -81,6 +153,10 @@ SPDX-License-Identifier: CC0-1.0
- Add profile `rfc4880` to reflect status quo
- `version`: Add support for `--sop-spec` option
## 1.4.6
- Bump `sop-java` to `4.1.2`
- Fix `decrypt --verify-with` to not throw `NoSignature` exception (exit code 3) if `VERIFICAIONS` is empty.
## 1.4.5
- Bugfix: Direct-Key signatures are calculated over the signee key only, not the signer key + signee key
- Security: Fix faulty bit-strength policy check for signing subkeys
@ -154,6 +230,10 @@ SPDX-License-Identifier: CC0-1.0
- Add `KeyRingUtils.publicKeys(PGPKeyRing keys)`
- Remove `BCUtil` class
## 1.3.18
- Bump `sop-java` to `4.1.2`
- Fix `decrypt --verify-with XYZ` not throwing `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty (#415)
## 1.3.17
- Bugfix: Direct-Key signatures are calculated over the signee key only, not the signer key + signee key
- Security: Fix faulty bit-strength policy check for signing subkeys

View File

@ -191,7 +191,7 @@ repositories {
}
dependencies {
implementation 'org.pgpainless:pgpainless-core:1.5.6'
implementation 'org.pgpainless:pgpainless-core:1.6.6'
}
```

View File

@ -12,12 +12,13 @@ SPDX-License-Identifier: Apache-2.0
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
|---------|--------------------|
| 1.5.X | :white_check_mark: |
| 1.4.X | :white_check_mark: |
| 1.3.X | :white_check_mark: |
| < 1.3.X | :x: |
| Version | Supported | Note |
|---------|--------------------|------------|
| 1.6.X | :white_check_mark: | LTS branch |
| 1.5.X | :white_check_mark: | |
| 1.4.X | :white_check_mark: | |
| 1.3.X | :white_check_mark: | LTS branch |
| < 1.3.X | :x: | |
## Reporting a Vulnerability

View File

@ -18,7 +18,8 @@ buildscript {
}
plugins {
id 'ru.vyarus.animalsniffer' version '1.5.3'
id 'org.jetbrains.kotlin.jvm' version "1.8.10"
id 'com.diffplug.spotless' version '6.22.0' apply false
}
apply from: 'version.gradle'
@ -29,6 +30,8 @@ allprojects {
apply plugin: 'eclipse'
apply plugin: 'jacoco'
apply plugin: 'checkstyle'
apply plugin: 'kotlin'
apply plugin: 'com.diffplug.spotless'
compileJava {
options.release = 8
@ -41,23 +44,17 @@ allprojects {
onlyIf { !sourceSets.main.allSource.files.isEmpty() }
}
// For library modules, enable android api compatibility check
if (it.name != 'pgpainless-cli') {
// animalsniffer
apply plugin: 'ru.vyarus.animalsniffer'
dependencies {
signature "net.sf.androidscents.signature:android-api-level-${pgpainlessMinAndroidSdk}:2.3.3_r2@signature"
}
animalsniffer {
sourceSets = [sourceSets.main]
}
}
// checkstyle
checkstyle {
toolVersion = '10.12.1'
}
spotless {
kotlin {
ktfmt().dropboxStyle()
}
}
group 'org.pgpainless'
description = "Simple to use OpenPGP API for Java based on Bouncycastle"
version = shortVersion
@ -78,6 +75,13 @@ allprojects {
fileMode = 0644
}
// Compatibility of default implementations in kotlin interfaces with Java implementations.
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += ["-Xjvm-default=all-compatibility"]
}
}
project.ext {
rootConfigDir = new File(rootDir, 'config')
gitCommit = getGitCommit()

View File

@ -1,2 +1,3 @@
myst-parser>=0.17
sphinxcontrib-mermaid>=0.7.1
sphinx_rtd_theme>=2.0.0

View File

@ -47,6 +47,13 @@ The diagram below shows, how the different projects relate to one another.
* `pgpainless-cert-d` - PGPainless-based implementation of `pgp-cert-d-java`
* `pgpainless-cert-d-cli` - CLI frontend for `pgpainless-cert-d`
* {{ '[PGPainless-WOT](https://{}/pgpainless/pgpainless-wot)'.format(repo_host) }}
Implementation of the [OpenPGP Web of Trust specification](https://sequoia-pgp.gitlab.io/sequoia-wot/) using PGPainless.
* `pgpainless-wot` - Parse OpenPGP keyrings into a generic `Network` object
* `wot-dijkstra` - Perform queries to find paths inside a `Network` object
* `pgpainless-wot-cli` - CLI frontend for `pgpainless-wot` and `wot-dijkstra`
* `wot-test-suite` - Test vectors ported from [Sequoia-PGPs WoT implementation](https://gitlab.com/sequoia-pgp/sequoia-wot/-/tree/main/tests/data)
* {{ '[PGPeasy](https://{}/pgpainless/pgpeasy)'.format(repo_host) }}
Prototypical, comprehensive OpenPGP CLI application
* `pgpeasy` - CLI application

View File

@ -27,6 +27,13 @@ flowchart LR
subgraph VKS-JAVA
vks-java-cli-->vks-java
end
subgraph PGPAINLESS-WOT
wot-test-suite-->pgpainless-wot
pgpainless-wot-->wot-dijkstra
pgpainless-wot-cli-->pgpainless-wot
pgpainless-wot-->pgpainless-core
pgpainless-wot-cli-->pgpainless-cert-d
end
subgraph PGPEASY
pgpeasy-->pgpainless-cli
pgpeasy-->wkd-java-cli

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 110 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -82,23 +82,26 @@ Stateless OpenPGP Protocol
Usage: pgpainless-cli [--stacktrace] [COMMAND]
Options:
--stacktrace Print Stacktrace
--stacktrace Print stacktrace
Commands:
help Display usage information for the specified subcommand
armor Add ASCII Armor to standard input
dearmor Remove ASCII Armor from standard input
decrypt Decrypt a message from standard input
inline-detach Split signatures from a clearsigned message
encrypt Encrypt a message from standard input
extract-cert Extract a public key certificate from a secret key from
standard input
generate-key Generate a secret key
sign Create a detached signature on the data from standard input
verify Verify a detached signature over the data from standard input
inline-sign Create an inline-signed message from data on standard input
inline-verify Verify inline-signed data from standard input
version Display version information about the tool
version Display version information about the tool
list-profiles Emit a list of profiles supported by the identified
subcommand
generate-key Generate a secret key
change-key-password Update the password of a key
revoke-key Generate revocation certificates
extract-cert Extract a public key certificate from a secret key
sign Create a detached message signature
verify Verify a detached signature
encrypt Encrypt a message from standard input
decrypt Decrypt a message
inline-detach Split signatures from a clearsigned message
inline-sign Create an inline-signed message
inline-verify Verify an inline-signed message
armor Add ASCII Armor to standard input
dearmor Remove ASCII Armor from standard input
help Display usage information for the specified subcommand
Exit Codes:
0 Successful program execution
@ -120,6 +123,9 @@ Exit Codes:
71 Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter
73 Ambiguous input (a filename matching the designator already exists)
79 Key is not signing capable
83 Options were supplied that are incompatible with each other
89 The requested profile is unsupported, or the indicated subcommand does
not accept profiles
```
To get help on a subcommand, e.g. `encrypt`, just call the help subcommand followed by the subcommand you

View File

@ -50,9 +50,15 @@ There is a very good chance that you can find code examples there that fit your
Reading keys from ASCII armored strings or from binary files is easy:
```java
// Secret Keys
String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"...;
PGPSecretKeyRing secretKey = PGPainless.readKeyRing()
.secretKeyRing(key);
// Certificates (Public Keys)
String cert = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...";
PGPPublicKeyRing certificate = PGPainless.readKeyRing()
.publicKeyRing(cert);
```
Similarly, keys or certificates can quickly be exported:

View File

@ -114,6 +114,56 @@ To disable ASCII armoring, call `noArmor()` before calling `key(_)`.
In our example, `certificateBytes` can now safely be shared with anyone.
### Change Key Password
OpenPGP keys can (but don't need to) be password protected.
The `changeKeyPassword()` API can be used to add, change or remove password protection from OpenPGP keys.
While the input to this operation can be keys with different per-subkey passwords, the output will use at most one password.
Via `oldKeyPassphrase()`, multiple decryption passphrase candidates can be provided.
These are tried one after another to unlock protected subkeys.
In order to successfully change the passphrase of an OpenPGP key, all of its subkeys needs to be successfully decrypted.
If one or more subkeys cannot be decrypted, the operation fails with a `KeyIsProtected` exception.
The result is either fully encrypted for a single passphrase (passed via `newKeyPassphrase()`),
or unprotected if the new key passphrase is omitted.
```java
byte[] keyBefore = ...
byte[] keyAfter = sop.changeKeyPassword()
// Provide old passphrases - all subkeys need to be decryptable,
// otherwise KeyIsProtected exception will be thrown
.oldKeyPassphrase("4d4m5m1th")
.oldKeyPassphrase("d4v1dR1c4rd0")
// Provide the new passphrase - if omitted, key will be unprotected
.newKeyPassphrase("fr1edr1ch3n93l5")
.keys(keyBefore)
.getBytes();
```
### Generate Revocation Certificates
You might want to generate a revocation certificate for your OpenPGP key.
This certificate can be published to a key server to let your contacts known that your key is no longer
trustworthy.
The `revokeKey()` API can be used to generate a "hard-revocation", which retroactively invalidates all
signatures previously issued by the key.
If the input secret key is an OpenPGP v6 key, the result will be a minimal revocation certificate,
consisting of only the bare primary public key and a revocation signature. For v4 keys, the result
will consist of the whole public certificate plus a revocation signature.
```java
byte[] keys = ...
byte[] revoked = sop.revokeKey()
// primary key password(s) if the key(s) are protected
.withKeyPassword("5w0rdf1sh")
// one or more secret keys
.keys(keys)
.getBytes();
```
### Apply / Remove ASCII Armor
Perhaps you want to print your secret key onto a piece of paper for backup purposes,
@ -130,14 +180,6 @@ byte[] armoredData = sop.armor()
The `data(_)` method can either be called by providing a byte array, or an `InputStream`.
:::{note}
There is a `label(ArmorLabel label)` method, which could theoretically be used to define the label used in the
ASCII armor header.
However, this method is not (yet?) supported by `pgpainless-sop` and will currently throw an `UnsupportedOption`
exception.
Instead, the implementation will figure out the data type and set the respective label on its own.
:::
To remove ASCII armor from armored data, simply use the `dearmor()` API:
```java

View File

@ -30,16 +30,12 @@
pgpainless\-cli\-armor \- Add ASCII Armor to standard input
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP] [\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP]
\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP]
.SH "DESCRIPTION"
.SH "OPTIONS"
.sp
\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP
.RS 4
Label to be used in the header and tail of the armoring
.RE
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE

View File

@ -0,0 +1,67 @@
'\" t
.\" Title: pgpainless-cli-change-key-password
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-CHANGE\-KEY\-PASSWORD" "1" "" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
.nh
.ad l
.de URL
\fI\\$2\fP <\\$1>\\$3
..
.als MTO URL
.if \n[.g] \{\
. mso www.tmac
. am URL
. ad l
. .
. am MTO
. ad l
. .
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-change\-key\-password \- Update the password of a key
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli change\-key\-password\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-new\-key\-password\fP
[=\fIPASSWORD\fP]] [\fB\-\-old\-key\-password\fP=\fIPASSWORD\fP]...
.SH "DESCRIPTION"
.sp
Unlock all secret keys from STDIN using the given old passwords and emit them re\-locked using the new password to STDOUT.
If any (sub\-) key cannot be unlocked, this operation will exit with error code 67.
.SH "OPTIONS"
.sp
\fB\-\-new\-key\-password\fP[=\fIPASSWORD\fP]
.RS 4
New password to lock the keys with.
.sp
If no new password is passed in, the keys will be emitted unlocked.
.sp
Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
.RE
.sp
\fB\-\-[no\-]armor\fP
.RS 4
ASCII armor the output
.RE
.sp
\fB\-\-old\-key\-password\fP=\fIPASSWORD\fP
.RS 4
Old passwords to unlock the keys with.
.sp
Multiple passwords can be passed in, which are tested sequentially to unlock locked subkeys.
.sp
Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
.RE
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE

View File

@ -37,4 +37,5 @@ pgpainless\-cli\-dearmor \- Remove ASCII Armor from standard input
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE

View File

@ -27,12 +27,12 @@
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-decrypt \- Decrypt a message from standard input
pgpainless\-cli\-decrypt \- Decrypt a message
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP]
[\fB\-\-verify\-not\-after\fP=\fIDATE\fP] [\fB\-\-verify\-not\-before\fP=\fIDATE\fP]
[\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP] [\fB\-\-verify\-with\fP=\fICERT\fP]...
[\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP] [\fB\-\-verify\-not\-after\fP=\fIDATE\fP]
[\fB\-\-verify\-not\-before\fP=\fIDATE\fP] [\fB\-\-verify\-with\fP=\fICERT\fP]...
[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]...
[\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP]... [\fIKEY\fP...]
.SH "DESCRIPTION"
@ -44,9 +44,29 @@ pgpainless\-cli\-decrypt \- Decrypt a message from standard input
Can be used to learn the session key on successful decryption
.RE
.sp
\fB\-\-stacktrace\fP, \fB\-\-verify\-not\-after\fP=\fIDATE\fP, \fB\-\-verify\-not\-before\fP=\fIDATE\fP, \fB\-\-verify\-out, \-\-verifications\-out\fP=\fIVERIFICATIONS\fP
\fB\-\-stacktrace\fP
.RS 4
Emits signature verification status to the designated output
Print stacktrace
.RE
.sp
\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP, \fB\-\-verify\-not\-after\fP=\fIDATE\fP
.RS 4
ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z)
.sp
Reject signatures with a creation date not in range.
.sp
Defaults to current system time (\(aqnow\(aq).
.sp
Accepts special value \(aq\-\(aq for end of time.
.RE
.sp
\fB\-\-verify\-not\-before\fP=\fIDATE\fP
.RS 4
ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z)
.sp
Reject signatures with a creation date not in range.
.sp
Defaults to beginning of time (\(aq\-\(aq).
.RE
.sp
\fB\-\-verify\-with\fP=\fICERT\fP

View File

@ -31,9 +31,9 @@ pgpainless\-cli\-encrypt \- Encrypt a message from standard input
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli encrypt\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP]
[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-sign\-with\fP=\fIKEY\fP]...
[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]...
[\fICERTS\fP...]
[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP]
[\fB\-\-sign\-with\fP=\fIKEY\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]...
[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fICERTS\fP...]
.SH "DESCRIPTION"
.SH "OPTIONS"
@ -53,12 +53,17 @@ ASCII armor the output
Profile identifier to switch between profiles
.RE
.sp
\fB\-\-sign\-with\fP=\fIKEY\fP
\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP, \fB\-\-sign\-with\fP=\fIKEY\fP
.RS 4
Sign the output with a private key
.RE
.sp
\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE
.sp
\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP
.RS 4
Passphrase to unlock the secret key(s).
.sp

View File

@ -27,12 +27,13 @@
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-extract\-cert \- Extract a public key certificate from a secret key from standard input
pgpainless\-cli\-extract\-cert \- Extract a public key certificate from a secret key
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli extract\-cert\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP]
.SH "DESCRIPTION"
.sp
Read a secret key from STDIN and emit the public key certificate to STDOUT.
.SH "OPTIONS"
.sp
\fB\-\-[no\-]armor\fP
@ -42,4 +43,5 @@ ASCII armor the output
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE

View File

@ -30,8 +30,8 @@
pgpainless\-cli\-generate\-key \- Generate a secret key
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli generate\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-profile\fP=\fIPROFILE\fP]
[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP] [\fIUSERID\fP...]
\fBpgpainless\-cli generate\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-signing\-only\fP] [\fB\-\-stacktrace\fP]
[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP] [\fIUSERID\fP...]
.SH "DESCRIPTION"
.SH "OPTIONS"
@ -46,7 +46,17 @@ ASCII armor the output
Profile identifier to switch between profiles
.RE
.sp
\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP
\fB\-\-signing\-only\fP
.RS 4
Generate a key that can only be used for signing
.RE
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE
.sp
\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP
.RS 4
Password to protect the private key with
.sp

View File

@ -47,4 +47,5 @@ Destination to which a detached signatures block will be written
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE

View File

@ -27,7 +27,7 @@
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-inline\-sign \- Create an inline\-signed message from data on standard input
pgpainless\-cli\-inline\-sign \- Create an inline\-signed message
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli inline\-sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text|clearsigned}\fP]
@ -54,7 +54,12 @@ If \(aq\-\-as=text\(aq and the input data is not valid UTF\-8, inline\-sign fail
ASCII armor the output
.RE
.sp
\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE
.sp
\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP
.RS 4
Passphrase to unlock the secret key(s).
.sp

View File

@ -27,11 +27,11 @@
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-inline\-verify \- Verify inline\-signed data from standard input
pgpainless\-cli\-inline\-verify \- Verify an inline\-signed message
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli inline\-verify\fP [\fB\-\-stacktrace\fP] [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP]
[\fB\-\-verifications\-out\fP=\fI<verificationsOut>\fP] [\fICERT\fP...]
[\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP] [\fICERT\fP...]
.SH "DESCRIPTION"
.SH "OPTIONS"
@ -56,7 +56,12 @@ Reject signatures with a creation date not in range.
Defaults to beginning of time ("\-").
.RE
.sp
\fB\-\-stacktrace\fP, \fB\-\-verifications\-out\fP=\fI<verificationsOut>\fP
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE
.sp
\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP
.RS 4
File to write details over successful verifications to
.RE

View File

@ -37,6 +37,7 @@ pgpainless\-cli\-list\-profiles \- Emit a list of profiles supported by the iden
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE
.SH "ARGUMENTS"
.sp

View File

@ -0,0 +1,54 @@
'\" t
.\" Title: pgpainless-cli-revoke-key
.\" Author: [see the "AUTHOR(S)" section]
.\" Generator: Asciidoctor 2.0.10
.\" Manual: PGPainless-CLI Manual
.\" Source:
.\" Language: English
.\"
.TH "PGPAINLESS\-CLI\-REVOKE\-KEY" "1" "" "" "PGPainless\-CLI Manual"
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.ss \n[.ss] 0
.nh
.ad l
.de URL
\fI\\$2\fP <\\$1>\\$3
..
.als MTO URL
.if \n[.g] \{\
. mso www.tmac
. am URL
. ad l
. .
. am MTO
. ad l
. .
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-revoke\-key \- Generate revocation certificates
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli revoke\-key\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]
.SH "DESCRIPTION"
.sp
Emit revocation certificates for secret keys from STDIN to STDOUT.
.SH "OPTIONS"
.sp
\fB\-\-[no\-]armor\fP
.RS 4
ASCII armor the output
.RE
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE
.sp
\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP
.RS 4
Passphrase to unlock the secret key(s).
.sp
Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
.RE

View File

@ -27,7 +27,7 @@
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-sign \- Create a detached signature on the data from standard input
pgpainless\-cli\-sign \- Create a detached message signature
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli sign\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP]
@ -55,7 +55,12 @@ Emits the digest algorithm used to the specified file in a way that can be used
ASCII armor the output
.RE
.sp
\fB\-\-stacktrace\fP, \fB\-\-with\-key\-password\fP=\fIPASSWORD\fP
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE
.sp
\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP
.RS 4
Passphrase to unlock the secret key(s).
.sp

View File

@ -27,13 +27,14 @@
. LINKSTYLE blue R < >
.\}
.SH "NAME"
pgpainless\-cli\-verify \- Verify a detached signature over the data from standard input
pgpainless\-cli\-verify \- Verify a detached signature
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli verify\fP [\fB\-\-stacktrace\fP] [\fB\-\-not\-after\fP=\fIDATE\fP] [\fB\-\-not\-before\fP=\fIDATE\fP] \fISIGNATURE\fP
\fICERT\fP...
.SH "DESCRIPTION"
.sp
Verify a detached signature over some data from STDIN.
.SH "OPTIONS"
.sp
\fB\-\-not\-after\fP=\fIDATE\fP
@ -58,6 +59,7 @@ Defaults to beginning of time ("\-").
.sp
\fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE
.SH "ARGUMENTS"
.sp
@ -68,4 +70,5 @@ Detached signature
.sp
\fICERT\fP...
.RS 4
Public key certificates for signature verification
.RE

View File

@ -30,7 +30,7 @@
pgpainless\-cli\-version \- Display version information about the tool
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\fP]
\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\fP | \fB\-\-sopv\fP]
.SH "DESCRIPTION"
.SH "OPTIONS"
@ -50,6 +50,7 @@ Print an extended version string
Print the latest revision of the SOP specification targeted by the implementation
.RE
.sp
\fB\-\-stacktrace\fP
\fB\-\-sopv\fP, \fB\-\-stacktrace\fP
.RS 4
Print stacktrace
.RE

View File

@ -41,9 +41,69 @@ Print stacktrace
.RE
.SH "COMMANDS"
.sp
\fBhelp\fP
\fBversion\fP
.RS 4
Stateless OpenPGP Protocol
Display version information about the tool
.RE
.sp
\fBlist\-profiles\fP
.RS 4
Emit a list of profiles supported by the identified subcommand
.RE
.sp
\fBgenerate\-key\fP
.RS 4
Generate a secret key
.RE
.sp
\fBchange\-key\-password\fP
.RS 4
Update the password of a key
.RE
.sp
\fBrevoke\-key\fP
.RS 4
Generate revocation certificates
.RE
.sp
\fBextract\-cert\fP
.RS 4
Extract a public key certificate from a secret key
.RE
.sp
\fBsign\fP
.RS 4
Create a detached message signature
.RE
.sp
\fBverify\fP
.RS 4
Verify a detached signature
.RE
.sp
\fBencrypt\fP
.RS 4
Encrypt a message from standard input
.RE
.sp
\fBdecrypt\fP
.RS 4
Decrypt a message
.RE
.sp
\fBinline\-detach\fP
.RS 4
Split signatures from a clearsigned message
.RE
.sp
\fBinline\-sign\fP
.RS 4
Create an inline\-signed message
.RE
.sp
\fBinline\-verify\fP
.RS 4
Verify an inline\-signed message
.RE
.sp
\fBarmor\fP
@ -56,59 +116,9 @@ Add ASCII Armor to standard input
Remove ASCII Armor from standard input
.RE
.sp
\fBdecrypt\fP
\fBhelp\fP
.RS 4
Decrypt a message from standard input
.RE
.sp
\fBinline\-detach\fP
.RS 4
Split signatures from a clearsigned message
.RE
.sp
\fBencrypt\fP
.RS 4
Encrypt a message from standard input
.RE
.sp
\fBextract\-cert\fP
.RS 4
Extract a public key certificate from a secret key from standard input
.RE
.sp
\fBgenerate\-key\fP
.RS 4
Generate a secret key
.RE
.sp
\fBsign\fP
.RS 4
Create a detached signature on the data from standard input
.RE
.sp
\fBverify\fP
.RS 4
Verify a detached signature over the data from standard input
.RE
.sp
\fBinline\-sign\fP
.RS 4
Create an inline\-signed message from data on standard input
.RE
.sp
\fBinline\-verify\fP
.RS 4
Verify inline\-signed data from standard input
.RE
.sp
\fBlist\-profiles\fP
.RS 4
Emit a list of profiles supported by the identified subcommand
.RE
.sp
\fBversion\fP
.RS 4
Display version information about the tool
Stateless OpenPGP Protocol
.RE
.sp
\fBgenerate\-completion\fP

View File

@ -4,7 +4,7 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
SOP_DIR=$(realpath $SCRIPT_DIR/../../sop-java)
[ ! -d "$SOP_DIR" ] && echo "sop-java repository MUST be cloned next to pgpainless repo" && exit 1;
SRC_DIR=$SOP_DIR/sop-java-picocli/build/docs/manpage
[ ! -d "$SRC_DIR" ] && echo "No sop manpages found. Please run `gradle asciidoctor` in the sop-java repo." && exit 1;
[ ! -d "$SRC_DIR" ] && echo "No sop manpages found. Please run 'gradle asciidoctor' in the sop-java repo." && exit 1;
DEST_DIR=$SCRIPT_DIR/packaging/man
mkdir -p $DEST_DIR
@ -13,12 +13,14 @@ do
SRC="${page##*/}"
DEST="${SRC/sop/pgpainless-cli}"
sed \
-e 's/sopv/PLACEHOLDERV/g' \
-e 's#.\\" Title: sop#.\\" Title: pgpainless-cli#g' \
-e 's/Manual: Sop Manual/Manual: PGPainless-CLI Manual/g' \
-e 's/.TH "SOP/.TH "PGPAINLESS\\-CLI/g' \
-e 's/"Sop Manual"/"PGPainless\\-CLI Manual"/g' \
-e 's/\\fBsop/\\fBpgpainless\\-cli/g' \
-e 's/sop/pgpainless\\-cli/g' \
-e 's/PLACEHOLDERV/sopv/g' \
$page > $DEST_DIR/$DEST
done

View File

@ -16,7 +16,6 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.slf4j.LoggerFactory;
import sop.exception.SOPGPException;
public class ArmorCmdTest extends CLITest {
@ -89,15 +88,6 @@ public class ArmorCmdTest extends CLITest {
assertTrue(armored.contains("SGVsbG8sIFdvcmxkIQo="));
}
@Test
public void labelNotYetSupported() throws IOException {
pipeStringToStdin("Hello, World!\n");
ByteArrayOutputStream out = pipeStdoutToStream();
int exitCode = executeCommand("armor", "--label", "Message");
assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, exitCode);
assertEquals(0, out.size());
}
@Test
public void armorAlreadyArmoredDataIsIdempotent() throws IOException {
pipeStringToStdin(key);

View File

@ -20,10 +20,17 @@ dependencies {
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
// Bouncy Castle
api "org.bouncycastle:bcprov-jdk15to18:$bouncyCastleVersion"
api "org.bouncycastle:bcpg-jdk15to18:$bouncyPgVersion"
api "org.bouncycastle:bcprov-jdk18on:$bouncyCastleVersion"
api "org.bouncycastle:bcpg-jdk18on:$bouncyPgVersion"
// api(files("../libs/bcpg-jdk18on-1.70.jar"))
// @Nullable, @Nonnull annotations
implementation "com.google.code.findbugs:jsr305:3.0.2"
}
// https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_modular_auto
tasks.named('jar') {
manifest {
attributes('Automatic-Module-Name': 'org.pgpainless.core')
}
}

View File

@ -1,95 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.bouncycastle;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;
import org.pgpainless.decryption_verification.CustomPublicKeyDataDecryptorFactory;
import org.pgpainless.key.SubkeyIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
/**
* Implementation of the {@link PublicKeyDataDecryptorFactory} which caches decrypted session keys.
* That way, if a message needs to be decrypted multiple times, expensive private key operations can be omitted.
*
* This implementation changes the behavior or {@link #recoverSessionData(int, byte[][])} to first return any
* cache hits.
* If no hit is found, the method call is delegated to the underlying {@link PublicKeyDataDecryptorFactory}.
* The result of that is then placed in the cache and returned.
*/
public class CachingBcPublicKeyDataDecryptorFactory
extends BcPublicKeyDataDecryptorFactory
implements CustomPublicKeyDataDecryptorFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(CachingBcPublicKeyDataDecryptorFactory.class);
private final Map<String, byte[]> cachedSessionKeys = new HashMap<>();
private final SubkeyIdentifier decryptionKey;
public CachingBcPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey, SubkeyIdentifier decryptionKey) {
super(privateKey);
this.decryptionKey = decryptionKey;
}
@Override
public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException {
byte[] sessionKey = lookupSessionKeyData(secKeyData);
if (sessionKey == null) {
LOGGER.debug("Cache miss for encrypted session key " + Hex.toHexString(secKeyData[0]));
sessionKey = costlyRecoverSessionData(keyAlgorithm, secKeyData);
cacheSessionKeyData(secKeyData, sessionKey);
} else {
LOGGER.debug("Cache hit for encrypted session key " + Hex.toHexString(secKeyData[0]));
}
return sessionKey;
}
public byte[] costlyRecoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException {
return super.recoverSessionData(keyAlgorithm, secKeyData);
}
private byte[] lookupSessionKeyData(byte[][] secKeyData) {
String key = toKey(secKeyData);
byte[] sessionKey = cachedSessionKeys.get(key);
return copy(sessionKey);
}
private void cacheSessionKeyData(byte[][] secKeyData, byte[] sessionKey) {
String key = toKey(secKeyData);
cachedSessionKeys.put(key, copy(sessionKey));
}
private static String toKey(byte[][] secKeyData) {
byte[] sk = secKeyData[0];
String key = Base64.toBase64String(sk);
return key;
}
private static byte[] copy(byte[] bytes) {
if (bytes == null) {
return null;
}
byte[] copy = new byte[bytes.length];
System.arraycopy(bytes, 0, copy, 0, copy.length);
return copy;
}
public void clear() {
cachedSessionKeys.clear();
}
@Override
public SubkeyIdentifier getSubkeyIdentifier() {
return decryptionKey;
}
}

View File

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Classes which could be upstreamed to BC at some point.
*/
package org.bouncycastle;

View File

@ -1,238 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
import javax.annotation.Nonnull;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.decryption_verification.DecryptionBuilder;
import org.pgpainless.decryption_verification.DecryptionStream;
import org.pgpainless.encryption_signing.EncryptionBuilder;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.key.certification.CertifyCertificate;
import org.pgpainless.key.generation.KeyRingBuilder;
import org.pgpainless.key.generation.KeyRingTemplates;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor;
import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface;
import org.pgpainless.key.parsing.KeyRingReader;
import org.pgpainless.key.util.KeyRingUtils;
import org.pgpainless.policy.Policy;
import org.pgpainless.util.ArmorUtils;
public final class PGPainless {
private PGPainless() {
}
/**
* Generate a fresh OpenPGP key ring from predefined templates.
* @return templates
*/
@Nonnull
public static KeyRingTemplates generateKeyRing() {
return new KeyRingTemplates();
}
/**
* Build a custom OpenPGP key ring.
*
* @return builder
*/
@Nonnull
public static KeyRingBuilder buildKeyRing() {
return new KeyRingBuilder();
}
/**
* Read an existing OpenPGP key ring.
* @return builder
*/
@Nonnull
public static KeyRingReader readKeyRing() {
return new KeyRingReader();
}
/**
* Extract a public key certificate from a secret key.
*
* @param secretKey secret key
* @return public key certificate
*/
@Nonnull
public static PGPPublicKeyRing extractCertificate(@Nonnull PGPSecretKeyRing secretKey) {
return KeyRingUtils.publicKeyRingFrom(secretKey);
}
/**
* Merge two copies of the same certificate (e.g. an old copy, and one retrieved from a key server) together.
*
* @param originalCopy local, older copy of the cert
* @param updatedCopy updated, newer copy of the cert
* @return merged certificate
* @throws PGPException in case of an error
*/
@Nonnull
public static PGPPublicKeyRing mergeCertificate(
@Nonnull PGPPublicKeyRing originalCopy,
@Nonnull PGPPublicKeyRing updatedCopy)
throws PGPException {
return PGPPublicKeyRing.join(originalCopy, updatedCopy);
}
/**
* Wrap a key or certificate in ASCII armor.
*
* @param key key or certificate
* @return ascii armored string
*
* @throws IOException in case of an error in the {@link ArmoredOutputStream}
*/
@Nonnull
public static String asciiArmor(@Nonnull PGPKeyRing key)
throws IOException {
if (key instanceof PGPSecretKeyRing) {
return ArmorUtils.toAsciiArmoredString((PGPSecretKeyRing) key);
} else {
return ArmorUtils.toAsciiArmoredString((PGPPublicKeyRing) key);
}
}
/**
* Wrap the detached signature in ASCII armor.
*
* @param signature detached signature
* @return ascii armored string
*
* @throws IOException in case of an error in the {@link ArmoredOutputStream}
*/
@Nonnull
public static String asciiArmor(@Nonnull PGPSignature signature)
throws IOException {
return ArmorUtils.toAsciiArmoredString(signature);
}
/**
* Wrap a key of certificate in ASCII armor and write the result into the given {@link OutputStream}.
*
* @param key key or certificate
* @param outputStream output stream
*
* @throws IOException in case of an error ion the {@link ArmoredOutputStream}
*/
public static void asciiArmor(@Nonnull PGPKeyRing key, @Nonnull OutputStream outputStream)
throws IOException {
ArmoredOutputStream armorOut = ArmorUtils.toAsciiArmoredStream(key, outputStream);
key.encode(armorOut);
armorOut.close();
}
/**
* Create an {@link EncryptionStream}, which can be used to encrypt and/or sign data using OpenPGP.
*
* @return builder
*/
@Nonnull
public static EncryptionBuilder encryptAndOrSign() {
return new EncryptionBuilder();
}
/**
* Create a {@link DecryptionStream}, which can be used to decrypt and/or verify data using OpenPGP.
*
* @return builder
*/
@Nonnull
public static DecryptionBuilder decryptAndOrVerify() {
return new DecryptionBuilder();
}
/**
* Make changes to a secret key.
* This method can be used to change key expiration dates and passphrases, or add/revoke subkeys.
* <p>
* After making the desired changes in the builder, the modified key ring can be extracted using {@link SecretKeyRingEditorInterface#done()}.
*
* @param secretKeys secret key ring
* @return builder
*/
@Nonnull
public static SecretKeyRingEditorInterface modifyKeyRing(@Nonnull PGPSecretKeyRing secretKeys) {
return modifyKeyRing(secretKeys, new Date());
}
/**
* Make changes to a secret key at the given reference time.
* This method can be used to change key expiration dates and passphrases, or add/revoke user-ids and subkeys.
* <p>
* After making the desired changes in the builder, the modified key can be extracted using {@link SecretKeyRingEditorInterface#done()}.
*
* @param secretKeys secret key ring
* @param referenceTime reference time used as signature creation date
* @return builder
*/
@Nonnull
public static SecretKeyRingEditorInterface modifyKeyRing(@Nonnull PGPSecretKeyRing secretKeys,
@Nonnull Date referenceTime) {
return new SecretKeyRingEditor(secretKeys, referenceTime);
}
/**
* Quickly access information about a {@link org.bouncycastle.openpgp.PGPPublicKeyRing} / {@link PGPSecretKeyRing}.
* This method can be used to determine expiration dates, key flags and other information about a key.
* <p>
* To evaluate a key at a given date (e.g. to determine if the key was allowed to create a certain signature)
* use {@link #inspectKeyRing(PGPKeyRing, Date)} instead.
*
* @param keyRing key ring
* @return access object
*/
@Nonnull
public static KeyRingInfo inspectKeyRing(@Nonnull PGPKeyRing keyRing) {
return new KeyRingInfo(keyRing);
}
/**
* Quickly access information about a {@link org.bouncycastle.openpgp.PGPPublicKeyRing} / {@link PGPSecretKeyRing}.
* This method can be used to determine expiration dates, key flags and other information about a key at a specific time.
*
* @param keyRing key ring
* @param referenceTime date of inspection
* @return access object
*/
@Nonnull
public static KeyRingInfo inspectKeyRing(@Nonnull PGPKeyRing keyRing, @Nonnull Date referenceTime) {
return new KeyRingInfo(keyRing, referenceTime);
}
/**
* Access, and make changes to PGPainless policy on acceptable/default algorithms etc.
*
* @return policy
*/
@Nonnull
public static Policy getPolicy() {
return Policy.getInstance();
}
/**
* Create different kinds of signatures on other keys.
*
* @return builder
*/
@Nonnull
public static CertifyCertificate certify() {
return new CertifyCertificate();
}
}

View File

@ -1,100 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
/**
* List of AEAD algorithms defined in crypto-refresh-06.
*
* @see <a href="https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-06.html#name-aead-algorithms">
* Crypto-Refresh-06 §9.6 - AEAD Algorithms</a>
*/
public enum AEADAlgorithm {
EAX(1, 16, 16),
OCB(2, 15, 16),
GCM(3, 12, 16),
;
private final int algorithmId;
private final int ivLength;
private final int tagLength;
private static final Map<Integer, AEADAlgorithm> MAP = new HashMap<>();
static {
for (AEADAlgorithm h : AEADAlgorithm.values()) {
MAP.put(h.algorithmId, h);
}
}
AEADAlgorithm(int id, int ivLength, int tagLength) {
this.algorithmId = id;
this.ivLength = ivLength;
this.tagLength = tagLength;
}
/**
* Return the ID of the AEAD algorithm.
*
* @return algorithm ID
*/
public int getAlgorithmId() {
return algorithmId;
}
/**
* Return the length (in octets) of the IV.
*
* @return iv length
*/
public int getIvLength() {
return ivLength;
}
/**
* Return the length (in octets) of the authentication tag.
*
* @return tag length
*/
public int getTagLength() {
return tagLength;
}
/**
* Return the {@link AEADAlgorithm} value that corresponds to the provided algorithm id.
* If an invalid algorithm id was provided, null is returned.
*
* @param id numeric id
* @return enum value
*/
@Nullable
public static AEADAlgorithm fromId(int id) {
return MAP.get(id);
}
/**
* Return the {@link AEADAlgorithm} value that corresponds to the provided algorithm id.
* If an invalid algorithm id was provided, throw a {@link NoSuchElementException}.
*
* @param id algorithm id
* @return enum value
* @throws NoSuchElementException in case of an unknown algorithm id
*/
@Nonnull
public static AEADAlgorithm requireFromId(int id) {
AEADAlgorithm algorithm = fromId(id);
if (algorithm == null) {
throw new NoSuchElementException("No AEADAlgorithm found for id " + id);
}
return algorithm;
}
}

View File

@ -1,63 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* The {@link AlgorithmSuite} class is consulted when new OpenPGP keys are being generated to set
* preferred algorithms on the key.
*/
public class AlgorithmSuite {
private static final AlgorithmSuite defaultAlgorithmSuite = new AlgorithmSuite(
Arrays.asList(
SymmetricKeyAlgorithm.AES_256,
SymmetricKeyAlgorithm.AES_192,
SymmetricKeyAlgorithm.AES_128),
Arrays.asList(
HashAlgorithm.SHA512,
HashAlgorithm.SHA384,
HashAlgorithm.SHA256,
HashAlgorithm.SHA224),
Arrays.asList(
CompressionAlgorithm.ZLIB,
CompressionAlgorithm.BZIP2,
CompressionAlgorithm.ZIP,
CompressionAlgorithm.UNCOMPRESSED)
);
private final Set<SymmetricKeyAlgorithm> symmetricKeyAlgorithms;
private final Set<HashAlgorithm> hashAlgorithms;
private final Set<CompressionAlgorithm> compressionAlgorithms;
public AlgorithmSuite(List<SymmetricKeyAlgorithm> symmetricKeyAlgorithms,
List<HashAlgorithm> hashAlgorithms,
List<CompressionAlgorithm> compressionAlgorithms) {
this.symmetricKeyAlgorithms = Collections.unmodifiableSet(new LinkedHashSet<>(symmetricKeyAlgorithms));
this.hashAlgorithms = Collections.unmodifiableSet(new LinkedHashSet<>(hashAlgorithms));
this.compressionAlgorithms = Collections.unmodifiableSet(new LinkedHashSet<>(compressionAlgorithms));
}
public Set<SymmetricKeyAlgorithm> getSymmetricKeyAlgorithms() {
return new LinkedHashSet<>(symmetricKeyAlgorithms);
}
public Set<HashAlgorithm> getHashAlgorithms() {
return new LinkedHashSet<>(hashAlgorithms);
}
public Set<CompressionAlgorithm> getCompressionAlgorithms() {
return new LinkedHashSet<>(compressionAlgorithms);
}
public static AlgorithmSuite getDefaultAlgorithmSuite() {
return defaultAlgorithmSuite;
}
}

View File

@ -1,46 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import javax.annotation.Nonnull;
/**
* Subset of {@link SignatureType}, reduced to certification types.
*/
public enum CertificationType {
/**
* The issuer of this certification does not make any particular assertion as to how well the certifier has
* checked that the owner of the key is in fact the person described by the User ID.
*/
GENERIC(SignatureType.GENERIC_CERTIFICATION),
/**
* The issuer of this certification has not done any verification of the claim that the owner of this key is
* the User ID specified.
*/
NONE(SignatureType.NO_CERTIFICATION),
/**
* The issuer of this certification has done some casual verification of the claim of identity.
*/
CASUAL(SignatureType.CASUAL_CERTIFICATION),
/**
* The issuer of this certification has done some casual verification of the claim of identity.
*/
POSITIVE(SignatureType.POSITIVE_CERTIFICATION),
;
private final SignatureType signatureType;
CertificationType(@Nonnull SignatureType signatureType) {
this.signatureType = signatureType;
}
public @Nonnull SignatureType asSignatureType() {
return signatureType;
}
}

View File

@ -1,79 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import org.bouncycastle.bcpg.CompressionAlgorithmTags;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Enumeration of possible compression algorithms.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-9.3">RFC4880: Compression Algorithm Tags</a>
*/
public enum CompressionAlgorithm {
UNCOMPRESSED (CompressionAlgorithmTags.UNCOMPRESSED),
ZIP (CompressionAlgorithmTags.ZIP),
ZLIB (CompressionAlgorithmTags.ZLIB),
BZIP2 (CompressionAlgorithmTags.BZIP2),
;
private static final Map<Integer, CompressionAlgorithm> MAP = new ConcurrentHashMap<>();
static {
for (CompressionAlgorithm c : CompressionAlgorithm.values()) {
MAP.put(c.algorithmId, c);
}
}
/**
* Return the {@link CompressionAlgorithm} value that corresponds to the provided numerical id.
* If an invalid id is provided, null is returned.
*
* @param id id
* @return compression algorithm
*/
@Nullable
public static CompressionAlgorithm fromId(int id) {
return MAP.get(id);
}
/**
* Return the {@link CompressionAlgorithm} value that corresponds to the provided numerical id.
* If an invalid id is provided, thrown an {@link NoSuchElementException}.
*
* @param id id
* @return compression algorithm
* @throws NoSuchElementException in case of an unmapped id
*/
@Nonnull
public static CompressionAlgorithm requireFromId(int id) {
CompressionAlgorithm algorithm = fromId(id);
if (algorithm == null) {
throw new NoSuchElementException("No CompressionAlgorithm found for id " + id);
}
return algorithm;
}
private final int algorithmId;
CompressionAlgorithm(int id) {
this.algorithmId = id;
}
/**
* Return the numerical algorithm tag corresponding to this compression algorithm.
* @return id
*/
public int getAlgorithmId() {
return algorithmId;
}
}

View File

@ -1,35 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
/**
* Subset of {@link SignatureType}, used for signatures over documents.
*/
public enum DocumentSignatureType {
/**
* Signature is calculated over the unchanged binary data.
*/
BINARY_DOCUMENT(SignatureType.BINARY_DOCUMENT),
/**
* The signature is calculated over the text data with its line endings converted to
* <pre>
* {@code &lt;CR&gt;&lt;LF&gt;}
* </pre>.
*/
CANONICAL_TEXT_DOCUMENT(SignatureType.CANONICAL_TEXT_DOCUMENT),
;
final SignatureType signatureType;
DocumentSignatureType(SignatureType signatureType) {
this.signatureType = signatureType;
}
public SignatureType getSignatureType() {
return signatureType;
}
}

View File

@ -1,22 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
public enum EncryptionPurpose {
/**
* The stream will encrypt communication that goes over the wire.
* E.g. EMail, Chat...
*/
COMMUNICATIONS,
/**
* The stream will encrypt data at rest.
* E.g. Encrypted backup...
*/
STORAGE,
/**
* The stream will use keys with either flags to encrypt the data.
*/
ANY
}

View File

@ -1,152 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import org.bouncycastle.bcpg.sig.Features;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* An enumeration of features that may be set in the {@link Features} subpacket.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.24">RFC4880: Features</a>
*/
public enum Feature {
/**
* Support for Symmetrically Encrypted Integrity Protected Data Packets (version 1) using Modification
* Detection Code Packets.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.14">
* RFC-4880 §5.14: Modification Detection Code Packet</a>
*/
MODIFICATION_DETECTION(Features.FEATURE_MODIFICATION_DETECTION),
/**
* Support for Authenticated Encryption with Additional Data (AEAD).
* If a key announces this feature, it signals support for consuming AEAD Encrypted Data Packets.
*
* NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!!
* NOTE: This value is currently RESERVED.
*
* @see <a href="https://openpgp-wg.gitlab.io/rfc4880bis/#name-aead-encrypted-data-packet-">
* AEAD Encrypted Data Packet</a>
*/
GNUPG_AEAD_ENCRYPTED_DATA(Features.FEATURE_AEAD_ENCRYPTED_DATA),
/**
* If a key announces this feature, it is a version 5 public key.
* The version 5 format is similar to the version 4 format except for the addition of a count for the key material.
* This count helps to parse 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 5 keys are calculated differently from version 4 keys.
*
* NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!!
* NOTE: This value is currently RESERVED.
*
* @see <a href="https://openpgp-wg.gitlab.io/rfc4880bis/#name-public-key-packet-formats">
* Public-Key Packet Formats</a>
*/
GNUPG_VERSION_5_PUBLIC_KEY(Features.FEATURE_VERSION_5_PUBLIC_KEY),
/**
* Support for Symmetrically Encrypted Integrity Protected Data packet version 2.
*
* @see <a href="https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-06.html#version-two-seipd">
* crypto-refresh-06 §5.13.2. Version 2 Sym. Encrypted Integrity Protected Data Packet Format</a>
*/
MODIFICATION_DETECTION_2((byte) 0x08),
;
private static final Map<Byte, Feature> MAP = new ConcurrentHashMap<>();
static {
for (Feature f : Feature.values()) {
MAP.put(f.featureId, f);
}
}
/**
* Return the {@link Feature} encoded by the given id.
* If the id does not match any known features, return null.
*
* @param id feature id
* @return feature
*/
@Nullable
public static Feature fromId(byte id) {
return MAP.get(id);
}
/**
* Return the {@link Feature} encoded by the given id.
* If the id does not match any known features, throw an {@link NoSuchElementException}.
*
* @param id feature id
* @return feature
* @throws NoSuchElementException if an unmatched feature id is encountered
*/
@Nonnull
public static Feature requireFromId(byte id) {
Feature feature = fromId(id);
if (feature == null) {
throw new NoSuchElementException("Unknown feature id encountered: " + id);
}
return feature;
}
private final byte featureId;
Feature(byte featureId) {
this.featureId = featureId;
}
/**
* Return the id of the feature.
*
* @return feature id
*/
public byte getFeatureId() {
return featureId;
}
/**
* Convert a bitmask into a list of {@link KeyFlag KeyFlags}.
*
* @param bitmask bitmask
* @return list of key flags encoded by the bitmask
*/
@Nonnull
public static List<Feature> fromBitmask(int bitmask) {
List<Feature> features = new ArrayList<>();
for (Feature f : Feature.values()) {
if ((bitmask & f.featureId) != 0) {
features.add(f);
}
}
return features;
}
/**
* Encode a list of {@link KeyFlag KeyFlags} into a bitmask.
*
* @param features list of flags
* @return bitmask
*/
public static byte toBitmask(Feature... features) {
byte mask = 0;
for (Feature f : features) {
mask |= f.featureId;
}
return mask;
}
}

View File

@ -1,118 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* An enumeration of different hashing algorithms.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-9.4">RFC4880: Hash Algorithms</a>
*/
public enum HashAlgorithm {
@Deprecated
MD5 (HashAlgorithmTags.MD5, "MD5"),
SHA1 (HashAlgorithmTags.SHA1, "SHA1"),
RIPEMD160 (HashAlgorithmTags.RIPEMD160, "RIPEMD160"),
SHA256 (HashAlgorithmTags.SHA256, "SHA256"),
SHA384 (HashAlgorithmTags.SHA384, "SHA384"),
SHA512 (HashAlgorithmTags.SHA512, "SHA512"),
SHA224 (HashAlgorithmTags.SHA224, "SHA224"),
SHA3_256 (12, "SHA3-256"),
SHA3_512 (14, "SHA3-512"),
;
private static final Map<Integer, HashAlgorithm> ID_MAP = new HashMap<>();
private static final Map<String, HashAlgorithm> NAME_MAP = new HashMap<>();
static {
for (HashAlgorithm h : HashAlgorithm.values()) {
ID_MAP.put(h.algorithmId, h);
NAME_MAP.put(h.name, h);
}
}
/**
* Return the {@link HashAlgorithm} value that corresponds to the provided algorithm id.
* If an invalid algorithm id was provided, null is returned.
*
* @param id numeric id
* @return enum value
*/
@Nullable
public static HashAlgorithm fromId(int id) {
return ID_MAP.get(id);
}
/**
* Return the {@link HashAlgorithm} value that corresponds to the provided algorithm id.
* If an invalid algorithm id was provided, throw a {@link NoSuchElementException}.
*
* @param id algorithm id
* @return enum value
* @throws NoSuchElementException in case of an unknown algorithm id
*/
@Nonnull
public static HashAlgorithm requireFromId(int id) {
HashAlgorithm algorithm = fromId(id);
if (algorithm == null) {
throw new NoSuchElementException("No HashAlgorithm found for id " + id);
}
return algorithm;
}
/**
* Return the {@link HashAlgorithm} value that corresponds to the provided name.
* If an invalid algorithm name was provided, null is returned.
*
* @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-9.4">RFC4880: §9.4 Hash Algorithms</a>
* for a list of algorithms and names.
*
* @param name text name
* @return enum value
*/
@Nullable
public static HashAlgorithm fromName(String name) {
String algorithmName = name.toUpperCase();
HashAlgorithm algorithm = NAME_MAP.get(algorithmName);
if (algorithm == null) {
algorithm = NAME_MAP.get(algorithmName.replace("-", ""));
}
return algorithm;
}
private final int algorithmId;
private final String name;
HashAlgorithm(int id, String name) {
this.algorithmId = id;
this.name = name;
}
/**
* Return the numeric algorithm id of the hash algorithm.
*
* @return numeric id
*/
public int getAlgorithmId() {
return algorithmId;
}
/**
* Return the text name of the hash algorithm.
*
* @return text name
*/
public String getAlgorithmName() {
return name;
}
}

View File

@ -1,121 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import java.util.ArrayList;
import java.util.List;
import org.bouncycastle.bcpg.sig.KeyFlags;
/**
* Enumeration of different key flags.
* Key flags denote different capabilities of a key pair.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.21">RFC4880: Key Flags</a>
*/
public enum KeyFlag {
/**
* This key may be used to certify other keys.
*/
CERTIFY_OTHER (KeyFlags.CERTIFY_OTHER),
/**
* This key may be used to sign data.
*/
SIGN_DATA (KeyFlags.SIGN_DATA),
/**
* This key may be used to encrypt communications.
*/
ENCRYPT_COMMS (KeyFlags.ENCRYPT_COMMS),
/**
* This key may be used to encrypt storage.
*/
ENCRYPT_STORAGE(KeyFlags.ENCRYPT_STORAGE),
/**
* The private component of this key may have been split by a secret-sharing mechanism.
*/
SPLIT (KeyFlags.SPLIT),
/**
* This key may be used for authentication.
*/
AUTHENTICATION (KeyFlags.AUTHENTICATION),
/**
* The private component of this key may be in the possession of more than one person.
*/
SHARED (KeyFlags.SHARED),
;
private final int flag;
KeyFlag(int flag) {
this.flag = flag;
}
/**
* Return the numeric id of the {@link KeyFlag}.
*
* @return numeric id
*/
public int getFlag() {
return flag;
}
/**
* Convert a bitmask into a list of {@link KeyFlag KeyFlags}.
*
* @param bitmask bitmask
* @return list of key flags encoded by the bitmask
*/
public static List<KeyFlag> fromBitmask(int bitmask) {
List<KeyFlag> flags = new ArrayList<>();
for (KeyFlag f : KeyFlag.values()) {
if ((bitmask & f.flag) != 0) {
flags.add(f);
}
}
return flags;
}
/**
* Encode a list of {@link KeyFlag KeyFlags} into a bitmask.
*
* @param flags list of flags
* @return bitmask
*/
public static int toBitmask(KeyFlag... flags) {
int mask = 0;
for (KeyFlag f : flags) {
mask |= f.getFlag();
}
return mask;
}
/**
* Return true if the provided bitmask has the bit for the provided flag set.
* Return false if the mask does not contain the flag.
*
* @param mask bitmask
* @param flag flag to be tested for
* @return true if flag is set, false otherwise
*/
public static boolean hasKeyFlag(int mask, KeyFlag flag) {
return (mask & flag.getFlag()) == flag.getFlag();
}
public static boolean containsAny(int mask, KeyFlag... flags) {
for (KeyFlag flag : flags) {
if (hasKeyFlag(mask, flag)) {
return true;
}
}
return false;
}
}

View File

@ -1,71 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import org.bouncycastle.bcpg.PacketTags;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
public enum OpenPgpPacket {
PKESK(PacketTags.PUBLIC_KEY_ENC_SESSION),
SIG(PacketTags.SIGNATURE),
SKESK(PacketTags.SYMMETRIC_KEY_ENC_SESSION),
OPS(PacketTags.ONE_PASS_SIGNATURE),
SK(PacketTags.SECRET_KEY),
PK(PacketTags.PUBLIC_KEY),
SSK(PacketTags.SECRET_SUBKEY),
COMP(PacketTags.COMPRESSED_DATA),
SED(PacketTags.SYMMETRIC_KEY_ENC),
MARKER(PacketTags.MARKER),
LIT(PacketTags.LITERAL_DATA),
TRUST(PacketTags.TRUST),
UID(PacketTags.USER_ID),
PSK(PacketTags.PUBLIC_SUBKEY),
UATTR(PacketTags.USER_ATTRIBUTE),
SEIPD(PacketTags.SYM_ENC_INTEGRITY_PRO),
MDC(PacketTags.MOD_DETECTION_CODE),
EXP_1(PacketTags.EXPERIMENTAL_1),
EXP_2(PacketTags.EXPERIMENTAL_2),
EXP_3(PacketTags.EXPERIMENTAL_3),
EXP_4(PacketTags.EXPERIMENTAL_4),
;
static final Map<Integer, OpenPgpPacket> MAP = new HashMap<>();
static {
for (OpenPgpPacket p : OpenPgpPacket.values()) {
MAP.put(p.getTag(), p);
}
}
final int tag;
@Nullable
public static OpenPgpPacket fromTag(int tag) {
return MAP.get(tag);
}
@Nonnull
public static OpenPgpPacket requireFromTag(int tag) {
OpenPgpPacket p = fromTag(tag);
if (p == null) {
throw new NoSuchElementException("No OpenPGP packet known for tag " + tag);
}
return p;
}
OpenPgpPacket(int tag) {
this.tag = tag;
}
int getTag() {
return tag;
}
}

View File

@ -1,163 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Enumeration of public key algorithms as defined in RFC4880.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-9.1">RFC4880: Public-Key Algorithms</a>
*/
public enum PublicKeyAlgorithm {
/**
* RSA capable of encryption and signatures.
*/
RSA_GENERAL (PublicKeyAlgorithmTags.RSA_GENERAL, true, true),
/**
* RSA with usage encryption.
*
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.5">Deprecation notice</a>
*/
@Deprecated
RSA_ENCRYPT (PublicKeyAlgorithmTags.RSA_ENCRYPT, false, true),
/**
* RSA with usage of creating signatures.
*
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.5">Deprecation notice</a>
*/
@Deprecated
RSA_SIGN (PublicKeyAlgorithmTags.RSA_SIGN, true, false),
/**
* ElGamal with usage encryption.
*/
ELGAMAL_ENCRYPT (PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT, false, true),
/**
* Digital Signature Algorithm.
*/
DSA (PublicKeyAlgorithmTags.DSA, true, false),
/**
* EC is deprecated.
* @deprecated use {@link #ECDH} instead.
*/
@Deprecated
EC (PublicKeyAlgorithmTags.EC, false, true),
/**
* Elliptic Curve Diffie-Hellman.
*/
ECDH (PublicKeyAlgorithmTags.ECDH, false, true),
/**
* Elliptic Curve Digital Signature Algorithm.
*/
ECDSA (PublicKeyAlgorithmTags.ECDSA, true, false),
/**
* ElGamal General.
*
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.8">Deprecation notice</a>
*/
@Deprecated
ELGAMAL_GENERAL (PublicKeyAlgorithmTags.ELGAMAL_GENERAL, true, true),
/**
* Diffie-Hellman key exchange algorithm.
*/
DIFFIE_HELLMAN (PublicKeyAlgorithmTags.DIFFIE_HELLMAN, false, true),
/**
* Digital Signature Algorithm based on twisted Edwards Curves.
*/
EDDSA (PublicKeyAlgorithmTags.EDDSA, true, false),
;
private static final Map<Integer, PublicKeyAlgorithm> MAP = new ConcurrentHashMap<>();
static {
for (PublicKeyAlgorithm p : PublicKeyAlgorithm.values()) {
MAP.put(p.algorithmId, p);
}
}
/**
* Return the {@link PublicKeyAlgorithm} that corresponds to the provided algorithm id.
* If an invalid id is provided, null is returned.
*
* @param id numeric algorithm id
* @return algorithm or null
*/
@Nullable
public static PublicKeyAlgorithm fromId(int id) {
return MAP.get(id);
}
/**
* Return the {@link PublicKeyAlgorithm} that corresponds to the provided algorithm id.
* If an invalid id is provided, throw a {@link NoSuchElementException}.
*
* @param id numeric algorithm id
* @return algorithm
* @throws NoSuchElementException in case of an unmatched algorithm id
*/
@Nonnull
public static PublicKeyAlgorithm requireFromId(int id) {
PublicKeyAlgorithm algorithm = fromId(id);
if (algorithm == null) {
throw new NoSuchElementException("No PublicKeyAlgorithm found for id " + id);
}
return algorithm;
}
private final int algorithmId;
private final boolean signingCapable;
private final boolean encryptionCapable;
PublicKeyAlgorithm(int algorithmId, boolean signingCapable, boolean encryptionCapable) {
this.algorithmId = algorithmId;
this.signingCapable = signingCapable;
this.encryptionCapable = encryptionCapable;
}
/**
* Return the numeric identifier of the public key algorithm.
*
* @return id
*/
public int getAlgorithmId() {
return algorithmId;
}
/**
* Return true if this public key algorithm is able to create signatures.
*
* @return true if the algorithm can sign
*/
public boolean isSigningCapable() {
return signingCapable;
}
/**
* Return true if this public key algorithm can be used as an encryption algorithm.
*
* @return true if the algorithm can encrypt
*/
public boolean isEncryptionCapable() {
return encryptionCapable;
}
}

View File

@ -1,131 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import org.pgpainless.util.DateUtil;
import javax.annotation.Nonnull;
import java.util.Date;
import java.util.NoSuchElementException;
public final class RevocationState implements Comparable<RevocationState> {
private final RevocationStateType type;
private final Date date;
private RevocationState(RevocationStateType type) {
this(type, null);
}
private RevocationState(RevocationStateType type, Date date) {
this.type = type;
if (type == RevocationStateType.softRevoked && date == null) {
throw new NullPointerException("If type is 'softRevoked' then date cannot be null.");
}
this.date = date;
}
public static RevocationState notRevoked() {
return new RevocationState(RevocationStateType.notRevoked);
}
public static RevocationState softRevoked(@Nonnull Date date) {
return new RevocationState(RevocationStateType.softRevoked, date);
}
public static RevocationState hardRevoked() {
return new RevocationState(RevocationStateType.hardRevoked);
}
public RevocationStateType getType() {
return type;
}
public @Nonnull Date getDate() {
if (!isSoftRevocation()) {
throw new NoSuchElementException("RevocationStateType is not equal to 'softRevoked'. Cannot extract date.");
}
return date;
}
public boolean isHardRevocation() {
return getType() == RevocationStateType.hardRevoked;
}
public boolean isSoftRevocation() {
return getType() == RevocationStateType.softRevoked;
}
public boolean isNotRevoked() {
return getType() == RevocationStateType.notRevoked;
}
@Override
public String toString() {
String out = getType().toString();
if (isSoftRevocation()) {
out = out + " (" + DateUtil.formatUTCDate(date) + ")";
}
return out;
}
@Override
public int compareTo(@Nonnull RevocationState o) {
switch (getType()) {
case notRevoked:
if (o.isNotRevoked()) {
return 0;
} else {
return -1;
}
case softRevoked:
if (o.isNotRevoked()) {
return 1;
} else if (o.isSoftRevocation()) {
// Compare soft dates in reverse
return o.getDate().compareTo(getDate());
} else {
return -1;
}
case hardRevoked:
if (o.isHardRevocation()) {
return 0;
} else {
return 1;
}
default:
throw new AssertionError("Unknown type: " + type);
}
}
@Override
public int hashCode() {
return type.hashCode() * 31 + (isSoftRevocation() ? getDate().hashCode() : 0);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof RevocationState)) {
return false;
}
RevocationState other = (RevocationState) obj;
if (getType() != other.getType()) {
return false;
}
if (isSoftRevocation()) {
return DateUtil.toSecondsPrecision(getDate()).getTime() == DateUtil.toSecondsPrecision(other.getDate()).getTime();
}
return true;
}
}

View File

@ -1,23 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
public enum RevocationStateType {
/**
* Certificate is not revoked.
*/
notRevoked,
/**
* Certificate is revoked with a soft revocation.
*/
softRevoked,
/**
* Certificate is revoked with a hard revocation.
*/
hardRevoked
}

View File

@ -1,462 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.ATTESTED_CERTIFICATIONS;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.CREATION_TIME;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.EMBEDDED_SIGNATURE;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.EXPIRE_TIME;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.EXPORTABLE;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.FEATURES;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.INTENDED_RECIPIENT_FINGERPRINT;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.ISSUER_FINGERPRINT;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.ISSUER_KEY_ID;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.KEY_EXPIRE_TIME;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.KEY_FLAGS;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.KEY_SERVER_PREFS;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.NOTATION_DATA;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.PLACEHOLDER;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.POLICY_URL;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_AEAD_ALGORITHMS;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_COMP_ALGS;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_HASH_ALGS;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_KEY_SERV;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_SYM_ALGS;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.PRIMARY_USER_ID;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.REG_EXP;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCABLE;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_KEY;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_REASON;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.SIGNATURE_TARGET;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.SIGNER_USER_ID;
import static org.bouncycastle.bcpg.SignatureSubpacketTags.TRUST_SIG;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
/**
* Enumeration of possible subpackets that might be found in the hashed and unhashed area of an OpenPGP signature.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.1">RFC4880: Signature Subpacket Specification</a>
*/
public enum SignatureSubpacket {
/**
* The time the signature was made.
* MUST be present in the hashed area of the signature.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.4">Signature Creation Time</a>
*/
signatureCreationTime(CREATION_TIME),
/**
* The validity period of the signature. This is the number of seconds
* after the signature creation time that the signature expires. If
* this is not present or has a value of zero, it never expires.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.10">Signature Expiration Time</a>
*/
signatureExpirationTime(EXPIRE_TIME),
/**
* Denotes whether the signature is exportable for other users.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.11">Exportable Certification</a>
*/
exportableCertification(EXPORTABLE),
/**
* Signer asserts that the key is not only valid but also trustworthy at
* the specified level. Level 0 has the same meaning as an ordinary
* validity signature. Level 1 means that the signed key is asserted to
* be a valid, trusted introducer, with the 2nd octet of the body
* specifying the degree of trust. Level 2 means that the signed key is
* asserted to be trusted to issue level 1 trust signatures, i.e., that
* it is a "meta introducer". Generally, a level n trust signature
* asserts that a key is trusted to issue level n-1 trust signatures.
* The trust amount is in a range from 0-255, interpreted such that
* values less than 120 indicate partial trust and values of 120 or
* greater indicate complete trust. Implementations SHOULD emit values
* of 60 for partial trust and 120 for complete trust.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.13">Trust Signature</a>
*/
trustSignature(TRUST_SIG),
/**
* Used in conjunction with trust Signature packets (of level greater 0) to
* limit the scope of trust that is extended. Only signatures by the
* target key on User IDs that match the regular expression in the body
* of this packet have trust extended by the trust Signature subpacket.
* The regular expression uses the same syntax as the Henry Spencer's
* "almost public domain" regular expression [REGEX] package. A
* description of the syntax is found in Section 8 below.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.14">Regular Expression</a>
*/
regularExpression(REG_EXP),
/**
* Signature's revocability status. The packet body contains a Boolean
* flag indicating whether the signature is revocable. Signatures that
* are not revocable have any later revocation signatures ignored. They
* represent a commitment by the signer that he cannot revoke his
* signature for the life of his key. If this packet is not present,
* the signature is revocable.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.12">Revocable</a>
*/
revocable(REVOCABLE),
/**
* The validity period of the key. This is the number of seconds after
* the key creation time that the key expires. If this is not present
* or has a value of zero, the key never expires. This is found only on
* a self-signature.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.6">Key Expiration Time</a>
*/
keyExpirationTime(KEY_EXPIRE_TIME),
/**
* Placeholder for backwards compatibility.
*/
placeholder(PLACEHOLDER),
/**
* Symmetric algorithm numbers that indicate which algorithms the keyholder
* prefers to use. The subpackets body is an ordered list of
* octets with the most preferred listed first. It is assumed that only
* algorithms listed are supported by the recipient's software.
* This is only found on a self-signature.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.7">Preferred Symmetric Algorithms</a>
*/
preferredSymmetricAlgorithms(PREFERRED_SYM_ALGS),
/**
* Authorizes the specified key to issue revocation signatures for this
* key. Class octet must have bit 0x80 set. If the bit 0x40 is set,
* then this means that the revocation information is sensitive. Other
* bits are for future expansion to other kinds of authorizations. This
* is found on a self-signature.
*
* If the "sensitive" flag is set, the keyholder feels this subpacket
* contains private trust information that describes a real-world
* sensitive relationship. If this flag is set, implementations SHOULD
* NOT export this signature to other users except in cases where the
* data needs to be available: when the signature is being sent to the
* designated revoker, or when it is accompanied by a revocation
* signature from that revoker. Note that it may be appropriate to
* isolate this subpacket within a separate signature so that it is not
* combined with other subpackets that need to be exported.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.15">Revocation Key</a>
*/
revocationKey(REVOCATION_KEY),
/**
* The OpenPGP Key ID of the key issuing the signature.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.5">Issuer Key ID</a>
*/
issuerKeyId(ISSUER_KEY_ID),
/**
* This subpacket describes a "notation" on the signature that the
* issuer wishes to make. The notation has a name and a value, each of
* which are strings of octets. There may be more than one notation in
* a signature. Notations can be used for any extension the issuer of
* the signature cares to make. The "flags" field holds four octets of
* flags.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.16">Notation Data</a>
*/
notationData(NOTATION_DATA),
/**
* Message digest algorithm numbers that indicate which algorithms the
* keyholder prefers to receive. Like the preferred symmetric
* algorithms, the list is ordered.
* This is only found on a self-signature.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.8">Preferred Hash Algorithms</a>
*/
preferredHashAlgorithms(PREFERRED_HASH_ALGS),
/**
* Compression algorithm numbers that indicate which algorithms the
* keyholder prefers to use. Like the preferred symmetric algorithms, the
* list is ordered. If this subpacket is not included, ZIP is preferred.
* A zero denotes that uncompressed data is preferred; the keyholder's
* software might have no compression software in that implementation.
* This is only found on a self-signature.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.9">Preferred Compressio Algorithms</a>
*/
preferredCompressionAlgorithms(PREFERRED_COMP_ALGS),
/**
* This is a list of one-bit flags that indicate preferences that the
* keyholder has about how the key is handled on a key server. All
* undefined flags MUST be zero.
* This is found only on a self-signature.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.17">Key Server Preferences</a>
*/
keyServerPreferences(KEY_SERVER_PREFS),
/**
* This is a URI of a key server that the keyholder prefers be used for
* updates. Note that keys with multiple User IDs can have a preferred
* key server for each User ID. Note also that since this is a URI, the
* key server can actually be a copy of the key retrieved by ftp, http,
* finger, etc.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.18">Preferred Key Server</a>
*/
preferredKeyServers(PREFERRED_KEY_SERV),
/**
* This is a flag in a User ID's self-signature that states whether this
* User ID is the main User ID for this key. It is reasonable for an
* implementation to resolve ambiguities in preferences, etc. by
* referring to the primary User ID. If this flag is absent, its value
* is zero. If more than one User ID in a key is marked as primary, the
* implementation may resolve the ambiguity in any way it sees fit, but
* it is RECOMMENDED that priority be given to the User ID with the most
* recent self-signature.
*
* When appearing on a self-signature on a User ID packet, this
* subpacket applies only to User ID packets. When appearing on a
* self-signature on a User Attribute packet, this subpacket applies
* only to User Attribute packets. That is to say, there are two
* different and independent "primaries" -- one for User IDs, and one
* for User Attributes.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.19">Primary User-ID</a>
*/
primaryUserId(PRIMARY_USER_ID),
/**
* This subpacket contains a URI of a document that describes the policy
* under which the signature was issued.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.20">Policy URL</a>
*/
policyUrl(POLICY_URL),
/**
* This subpacket contains a list of binary flags that hold information
* about a key. It is a string of octets, and an implementation MUST
* NOT assume a fixed size. This is so it can grow over time. If a
* list is shorter than an implementation expects, the unstated flags
* are considered to be zero.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.21">Key Flags</a>
*/
keyFlags(KEY_FLAGS),
/**
* This subpacket allows a keyholder to state which User ID is
* responsible for the signing. Many keyholders use a single key for
* different purposes, such as business communications as well as
* personal communications. This subpacket allows such a keyholder to
* state which of their roles is making a signature.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.22">Signer's User ID</a>
*/
signerUserId(SIGNER_USER_ID),
/**
* This subpacket is used only in key revocation and certification
* revocation signatures. It describes the reason why the key or
* certificate was revoked.
*
* The first octet contains a machine-readable code that denotes the
* reason for the revocation:
*
* 0 - No reason specified (key revocations or cert revocations)
* 1 - Key is superseded (key revocations)
* 2 - Key material has been compromised (key revocations)
* 3 - Key is retired and no longer used (key revocations)
* 32 - User ID information is no longer valid (cert revocations)
* 100-110 - Private Use
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.23">Reason for Revocation</a>
*/
revocationReason(REVOCATION_REASON),
/**
* The Features subpacket denotes which advanced OpenPGP features a
* user's implementation supports. This is so that as features are
* added to OpenPGP that cannot be backwards-compatible, a user can
* state that they can use that feature. The flags are single bits that
* indicate that a given feature is supported.
*
* This subpacket is similar to a preferences subpacket, and only
* appears in a self-signature.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.24">Features</a>
*/
features(FEATURES),
/**
* This subpacket identifies a specific target signature to which a
* signature refers. For revocation signatures, this subpacket
* provides explicit designation of which signature is being revoked.
* For a third-party or timestamp signature, this designates what
* signature is signed. All arguments are an identifier of that target
* signature.
*
* The N octets of hash data MUST be the size of the hash of the
* signature. For example, a target signature with a SHA-1 hash MUST
* have 20 octets of hash data.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.25">Signature Target</a>
*/
signatureTarget(SIGNATURE_TARGET),
/**
* This subpacket contains a complete Signature packet body as
* specified in Section 5.2 above. It is useful when one signature
* needs to refer to, or be incorporated in, another signature.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.2.3.26">Embedded Signature</a>
*/
embeddedSignature(EMBEDDED_SIGNATURE),
/**
* The OpenPGP Key fingerprint of the key issuing the signature. This
* subpacket SHOULD be included in all signatures. If the version of
* the issuing key is 4 and an Issuer subpacket is also included in the
* signature, the key ID of the Issuer subpacket MUST match the low 64
* bits of the fingerprint.
*
* Note that the length N of the fingerprint for a version 4 key is 20
* octets; for a version 5 key N is 32.
*
* @see <a href="https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.28">Issuer Fingerprint</a>
*/
issuerFingerprint(ISSUER_FINGERPRINT),
/**
* AEAD algorithm numbers that indicate which AEAD algorithms the
* keyholder prefers to use. The subpackets body is an ordered list of
* octets with the most preferred listed first. It is assumed that only
* algorithms listed are supported by the recipient's software.
* This is only found on a self-signature.
* Note that support for the AEAD Encrypted Data packet in the general
* is indicated by a Feature Flag.
*
* @see <a href="https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.8">Preferred AEAD Algorithms</a>
*/
preferredAEADAlgorithms(PREFERRED_AEAD_ALGORITHMS),
/**
* The OpenPGP Key fingerprint of the intended recipient primary key.
* If one or more subpackets of this type are included in a signature,
* it SHOULD be considered valid only in an encrypted context, where the
* key it was encrypted to is one of the indicated primary keys, or one
* of their subkeys. This can be used to prevent forwarding a signature
* outside its intended, encrypted context.
*
* Note that the length N of the fingerprint for a version 4 key is 20
* octets; for a version 5 key N is 32.
*
* @see <a href="https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.29">Intended Recipient Fingerprint</a>
*/
intendedRecipientFingerprint(INTENDED_RECIPIENT_FINGERPRINT),
/**
* This subpacket MUST only appear as a hashed subpacket of an
* Attestation Key Signature. It has no meaning in any other signature
* type. It is used by the primary key to attest to a set of third-
* party certifications over the associated User ID or User Attribute.
* This enables the holder of an OpenPGP primary key to mark specific
* third-party certifications as re-distributable with the rest of the
* Transferable Public Key (see the "No-modify" flag in "Key Server
* Preferences", above). Implementations MUST include exactly one
* Attested Certification subpacket in any generated Attestation Key
* Signature.
*
* @see <a href="https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.30">Attested Certification</a>
*/
attestedCertification(ATTESTED_CERTIFICATIONS)
;
private static final Map<Integer, SignatureSubpacket> MAP = new ConcurrentHashMap<>();
static {
for (SignatureSubpacket p : values()) {
MAP.put(p.code, p);
}
}
private final int code;
SignatureSubpacket(int code) {
this.code = code;
}
/**
* Return the numerical identifier of the {@link SignatureSubpacket}.
* @return id
*/
public int getCode() {
return code;
}
/**
* Return the {@link SignatureSubpacket} that corresponds to the provided id.
* If an unmatched code is presented, return null.
*
* @param code id
* @return signature subpacket
*/
@Nullable
public static SignatureSubpacket fromCode(int code) {
return MAP.get(code);
}
/**
* Return the {@link SignatureSubpacket} that corresponds to the provided code.
*
* @param code code
* @return signature subpacket
* @throws NoSuchElementException in case of an unmatched subpacket tag
*/
@Nonnull
public static SignatureSubpacket requireFromCode(int code) {
SignatureSubpacket tag = fromCode(code);
if (tag == null) {
throw new NoSuchElementException("No SignatureSubpacket tag found with code " + code);
}
return tag;
}
/**
* Convert an array of signature subpacket tags into a list of {@link SignatureSubpacket SignatureSubpackets}.
*
* @param codes array of codes
* @return list of subpackets
*/
public static List<SignatureSubpacket> fromCodes(int[] codes) {
List<SignatureSubpacket> tags = new ArrayList<>();
for (int code : codes) {
try {
tags.add(requireFromCode(code));
} catch (NoSuchElementException e) {
// skip
}
}
return tags;
}
}

View File

@ -1,225 +0,0 @@
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import org.bouncycastle.openpgp.PGPSignature;
import javax.annotation.Nonnull;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Enum that enlists all the Signature Types defined in rfc4880 section 5.2.1
* See {@link org.bouncycastle.openpgp.PGPSignature} for comparison.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.11">rfc4880 §5.2.1. Signature Types</a>
*/
public enum SignatureType {
/**
* Signature of a binary document.
* This means the signer owns it, created it, or certifies that it
* has not been modified.
*/
BINARY_DOCUMENT(PGPSignature.BINARY_DOCUMENT),
/**
* Signature of a canonical text document.
* This means the signer owns it, created it, or certifies that it
* has not been modified. The signature is calculated over the text
* data with its line endings converted to {@code <CR><LF>}.
*/
CANONICAL_TEXT_DOCUMENT(PGPSignature.CANONICAL_TEXT_DOCUMENT),
/**
* Standalone signature.
* This signature is a signature of only its own subpacket contents.
* It is calculated identically to a signature over a zero-length
* binary document. Note that it doesn't make sense to have a V3
* standalone signature.
*/
STANDALONE(PGPSignature.STAND_ALONE),
/**
* Generic certification of a User ID and Public-Key packet.
* The issuer of this certification does not make any particular
* assertion as to how well the certifier has checked that the owner
* of the key is in fact the person described by the User ID.
*/
GENERIC_CERTIFICATION(PGPSignature.DEFAULT_CERTIFICATION),
/**
* Persona certification of a User ID and Public-Key packet.
* The issuer of this certification has not done any verification of
* the claim that the owner of this key is the User ID specified.
*/
NO_CERTIFICATION(PGPSignature.NO_CERTIFICATION),
/**
* Casual certification of a User ID and Public-Key packet.
* The issuer of this certification has done some casual
* verification of the claim of identity.
*/
CASUAL_CERTIFICATION(PGPSignature.CASUAL_CERTIFICATION),
/**
* Positive certification of a User ID and Public-Key packet.
* The issuer of this certification has done substantial
* verification of the claim of identity.
*/
POSITIVE_CERTIFICATION(PGPSignature.POSITIVE_CERTIFICATION),
/**
* Subkey Binding Signature.
* This signature is a statement by the top-level signing key that
* indicates that it owns the subkey. This signature is calculated
* directly on the primary key and subkey, and not on any User ID or
* other packets. A signature that binds a signing subkey MUST have
* an Embedded Signature subpacket in this binding signature that
* contains a {@link #PRIMARYKEY_BINDING} signature made by the
* signing subkey on the primary key and subkey.
*/
SUBKEY_BINDING(PGPSignature.SUBKEY_BINDING),
/**
* Primary Key Binding Signature
* This signature is a statement by a signing subkey, indicating
* that it is owned by the primary key and subkey. This signature
* is calculated the same way as a {@link #SUBKEY_BINDING} signature:
* directly on the primary key and subkey, and not on any User ID or
* other packets.
*/
PRIMARYKEY_BINDING(PGPSignature.PRIMARYKEY_BINDING),
/**
* Signature directly on a key
* This signature is calculated directly on a key. It binds the
* information in the Signature subpackets to the key, and is
* appropriate to be used for subpackets that provide information
* about the key, such as the Revocation Key subpacket. It is also
* appropriate for statements that non-self certifiers want to make
* about the key itself, rather than the binding between a key and a
* name.
*/
DIRECT_KEY(PGPSignature.DIRECT_KEY),
/**
* Key revocation signature
* The signature is calculated directly on the key being revoked. A
* revoked key is not to be used. Only revocation signatures by the
* key being revoked, or by an authorized revocation key, should be
* considered valid revocation signatures.
*/
KEY_REVOCATION(PGPSignature.KEY_REVOCATION),
/**
* Subkey revocation signature
* The signature is calculated directly on the subkey being revoked.
* A revoked subkey is not to be used. Only revocation signatures
* by the top-level signature key that is bound to this subkey, or
* by an authorized revocation key, should be considered valid
* revocation signatures.
*/
SUBKEY_REVOCATION(PGPSignature.SUBKEY_REVOCATION),
/**
* Certification revocation signature
* This signature revokes an earlier User ID certification signature
* (signature class 0x10 through 0x13) or signature {@link #DIRECT_KEY}.
* It should be issued by the same key that issued the
* revoked signature or an authorized revocation key. The signature
* is computed over the same data as the certificate that it
* revokes, and should have a later creation date than that
* certificate.
*/
CERTIFICATION_REVOCATION(PGPSignature.CERTIFICATION_REVOCATION),
/**
* Timestamp signature.
* This signature is only meaningful for the timestamp contained in
* it.
*/
TIMESTAMP(PGPSignature.TIMESTAMP),
/**
* Third-Party Confirmation signature.
* This signature is a signature over some other OpenPGP Signature
* packet(s). It is analogous to a notary seal on the signed data.
* A third-party signature SHOULD include Signature Target
* subpacket(s) to give easy identification. Note that we really do
* mean SHOULD. There are plausible uses for this (such as a blind
* party that only sees the signature, not the key or source
* document) that cannot include a target subpacket.
*/
THIRD_PARTY_CONFIRMATION(0x50)
;
private static final Map<Integer, SignatureType> map = new ConcurrentHashMap<>();
static {
for (SignatureType sigType : SignatureType.values()) {
map.put(sigType.getCode(), sigType);
}
}
/**
* Convert a numerical id into a {@link SignatureType}.
*
* @param code numeric id
* @return signature type enum
* @throws IllegalArgumentException in case of an unmatched signature type code
*/
@Nonnull
public static SignatureType valueOf(int code) {
SignatureType type = map.get(code);
if (type != null) {
return type;
}
throw new IllegalArgumentException("Signature type 0x" + Integer.toHexString(code) + " appears to be invalid.");
}
private final int code;
SignatureType(int code) {
this.code = code;
}
/**
* Return the numeric id of the signature type enum.
*
* @return numeric id
*/
public int getCode() {
return code;
}
public static boolean isRevocationSignature(int signatureType) {
return isRevocationSignature(SignatureType.valueOf(signatureType));
}
public static boolean isRevocationSignature(SignatureType signatureType) {
switch (signatureType) {
case BINARY_DOCUMENT:
case CANONICAL_TEXT_DOCUMENT:
case STANDALONE:
case GENERIC_CERTIFICATION:
case NO_CERTIFICATION:
case CASUAL_CERTIFICATION:
case POSITIVE_CERTIFICATION:
case SUBKEY_BINDING:
case PRIMARYKEY_BINDING:
case DIRECT_KEY:
case TIMESTAMP:
case THIRD_PARTY_CONFIRMATION:
return false;
case KEY_REVOCATION:
case SUBKEY_REVOCATION:
case CERTIFICATION_REVOCATION:
return true;
default:
throw new IllegalArgumentException("Unknown signature type: " + signatureType);
}
}
}

View File

@ -1,101 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import org.bouncycastle.openpgp.PGPLiteralData;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Enumeration of possible encoding formats of the content of the literal data packet.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-5.9">RFC4880: Literal Data Packet</a>
*/
public enum StreamEncoding {
/**
* The Literal packet contains binary data.
*/
BINARY(PGPLiteralData.BINARY),
/**
* The Literal packet contains text data, and thus may need line ends converted to local form, or other
* text-mode changes.
*/
TEXT(PGPLiteralData.TEXT),
/**
* Indication that the implementation believes that the literal data contains UTF-8 text.
*/
UTF8(PGPLiteralData.UTF8),
/**
* Early versions of PGP also defined a value of 'l' as a 'local' mode for machine-local conversions.
* RFC 1991 [RFC1991] incorrectly stated this local mode flag as '1' (ASCII numeral one).
* Both of these local modes are deprecated.
*/
@Deprecated
LOCAL('l'),
;
private final char code;
private static final Map<Character, StreamEncoding> MAP = new ConcurrentHashMap<>();
static {
for (StreamEncoding f : StreamEncoding.values()) {
MAP.put(f.code, f);
}
// RFC 1991 [RFC1991] incorrectly stated local mode flag as '1', see doc of LOCAL.
MAP.put('1', LOCAL);
}
StreamEncoding(char code) {
this.code = code;
}
/**
* Return the code identifier of the encoding.
*
* @return identifier
*/
public char getCode() {
return code;
}
/**
* Return the {@link StreamEncoding} corresponding to the provided code identifier.
* If no matching encoding is found, return null.
*
* @param code identifier
* @return encoding enum
*/
@Nullable
public static StreamEncoding fromCode(int code) {
return MAP.get((char) code);
}
/**
* Return the {@link StreamEncoding} corresponding to the provided code identifier.
* If no matching encoding is found, throw a {@link NoSuchElementException}.
*
* @param code identifier
* @return encoding enum
*
* @throws NoSuchElementException in case of an unmatched identifier
*/
@Nonnull
public static StreamEncoding requireFromCode(int code) {
StreamEncoding encoding = fromCode(code);
if (encoding == null) {
throw new NoSuchElementException("No StreamEncoding found for code " + code);
}
return encoding;
}
}

View File

@ -1,150 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Enumeration of possible symmetric encryption algorithms.
*
* @see <a href="https://tools.ietf.org/html/rfc4880#section-9.2">RFC4880: Symmetric-Key Algorithms</a>
*/
public enum SymmetricKeyAlgorithm {
/**
* Plaintext or unencrypted data.
*/
NULL (SymmetricKeyAlgorithmTags.NULL),
/**
* IDEA is deprecated.
* @deprecated use a different algorithm.
*/
@Deprecated
IDEA (SymmetricKeyAlgorithmTags.IDEA),
/**
* TripleDES (DES-EDE - 168 bit key derived from 192).
*/
TRIPLE_DES (SymmetricKeyAlgorithmTags.TRIPLE_DES),
/**
* CAST5 (128-bit key, as per RFC2144).
*/
CAST5 (SymmetricKeyAlgorithmTags.CAST5),
/**
* Blowfish (128-bit key, 16 rounds).
*/
BLOWFISH (SymmetricKeyAlgorithmTags.BLOWFISH),
/**
* Reserved in RFC4880.
* SAFER-SK128 (13 rounds)
*/
SAFER (SymmetricKeyAlgorithmTags.SAFER),
/**
* Reserved in RFC4880.
* Reserved for DES/SK
*/
DES (SymmetricKeyAlgorithmTags.DES),
/**
* AES with 128-bit key.
*/
AES_128 (SymmetricKeyAlgorithmTags.AES_128),
/**
* AES with 192-bit key.
*/
AES_192 (SymmetricKeyAlgorithmTags.AES_192),
/**
* AES with 256-bit key.
*/
AES_256 (SymmetricKeyAlgorithmTags.AES_256),
/**
* Twofish with 256-bit key.
*/
TWOFISH (SymmetricKeyAlgorithmTags.TWOFISH),
/**
* Reserved for Camellia with 128-bit key.
*/
CAMELLIA_128 (SymmetricKeyAlgorithmTags.CAMELLIA_128),
/**
* Reserved for Camellia with 192-bit key.
*/
CAMELLIA_192 (SymmetricKeyAlgorithmTags.CAMELLIA_192),
/**
* Reserved for Camellia with 256-bit key.
*/
CAMELLIA_256 (SymmetricKeyAlgorithmTags.CAMELLIA_256),
;
private static final Map<Integer, SymmetricKeyAlgorithm> MAP = new ConcurrentHashMap<>();
static {
for (SymmetricKeyAlgorithm s : SymmetricKeyAlgorithm.values()) {
MAP.put(s.algorithmId, s);
}
}
/**
* Return the {@link SymmetricKeyAlgorithm} enum that corresponds to the provided numeric id.
* If an invalid id is provided, null is returned.
*
* @param id numeric algorithm id
* @return symmetric key algorithm enum
*/
@Nullable
public static SymmetricKeyAlgorithm fromId(int id) {
return MAP.get(id);
}
/**
* Return the {@link SymmetricKeyAlgorithm} enum that corresponds to the provided numeric id.
* If an invalid id is provided, throw a {@link NoSuchElementException}.
*
* @param id numeric algorithm id
* @return symmetric key algorithm enum
*
* @throws NoSuchElementException if an unmatched id is provided
*/
@Nonnull
public static SymmetricKeyAlgorithm requireFromId(int id) {
SymmetricKeyAlgorithm algorithm = fromId(id);
if (algorithm == null) {
throw new NoSuchElementException("No SymmetricKeyAlgorithm found for id " + id);
}
return algorithm;
}
private final int algorithmId;
SymmetricKeyAlgorithm(int algorithmId) {
this.algorithmId = algorithmId;
}
/**
* Return the numeric algorithm id of the enum.
*
* @return numeric id
*/
public int getAlgorithmId() {
return algorithmId;
}
}

View File

@ -1,188 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm;
/**
* Facade class for {@link org.bouncycastle.bcpg.sig.TrustSignature}.
* A trust signature subpacket marks the trustworthiness of a certificate and defines its capabilities to act
* as a trusted introducer.
*/
public class Trustworthiness {
private final int amount;
private final int depth;
public static final int THRESHOLD_FULLY_CONVINCED = 120; // greater or equal is fully trusted
public static final int MARGINALLY_CONVINCED = 60; // default value for marginally convinced
public static final int NOT_TRUSTED = 0; // 0 is not trusted
public Trustworthiness(int amount, int depth) {
this.amount = capAmount(amount);
this.depth = capDepth(depth);
}
/**
* Get the trust amount.
* This value means how confident the issuer of the signature is in validity of the binding.
*
* @return trust amount
*/
public int getAmount() {
return amount;
}
/**
* Get the depth of the trust signature.
* This value controls, whether the certificate can act as a trusted introducer.
*
* @return depth
*/
public int getDepth() {
return depth;
}
/**
* Returns true, if the trust amount is equal to 0.
* This means the key is not trusted.
*
* Otherwise return false
* @return true if untrusted
*/
public boolean isNotTrusted() {
return getAmount() == NOT_TRUSTED;
}
/**
* Return true if the certificate is at least marginally trusted.
* That is the case, if the trust amount is greater than 0.
*
* @return true if the cert is at least marginally trusted
*/
public boolean isMarginallyTrusted() {
return getAmount() > NOT_TRUSTED;
}
/**
* Return true if the certificate is fully trusted. That is the case if the trust amount is
* greater than or equal to 120.
*
* @return true if the cert is fully trusted
*/
public boolean isFullyTrusted() {
return getAmount() >= THRESHOLD_FULLY_CONVINCED;
}
/**
* Return true, if the cert is an introducer. That is the case if the depth is greater 0.
*
* @return true if introducer
*/
public boolean isIntroducer() {
return getDepth() >= 1;
}
/**
* Return true, if the certified cert can introduce certificates with trust depth of <pre>otherDepth</pre>.
*
* @param otherDepth other certifications trust depth
* @return true if the cert can introduce the other
*/
public boolean canIntroduce(int otherDepth) {
return getDepth() > otherDepth;
}
/**
* Return true, if the certified cert can introduce certificates with the given <pre>other</pre> trust depth.
*
* @param other other certificates trust depth
* @return true if the cert can introduce the other
*/
public boolean canIntroduce(Trustworthiness other) {
return canIntroduce(other.getDepth());
}
/**
* This means that we are fully convinced of the trustworthiness of the key.
*
* @return builder
*/
public static Builder fullyTrusted() {
return new Builder(THRESHOLD_FULLY_CONVINCED);
}
/**
* This means that we are marginally (partially) convinced of the trustworthiness of the key.
*
* @return builder
*/
public static Builder marginallyTrusted() {
return new Builder(MARGINALLY_CONVINCED);
}
/**
* This means that we do not trust the key.
* Can be used to overwrite previous trust.
*
* @return builder
*/
public static Builder untrusted() {
return new Builder(NOT_TRUSTED);
}
public static final class Builder {
private final int amount;
private Builder(int amount) {
this.amount = amount;
}
/**
* The key is a trusted introducer (depth 1).
* Certifications made by this key are considered trustworthy.
*
* @return trust
*/
public Trustworthiness introducer() {
return new Trustworthiness(amount, 1);
}
/**
* The key is a meta introducer (depth 2).
* This key can introduce trusted introducers of depth 1.
*
* @return trust
*/
public Trustworthiness metaIntroducer() {
return new Trustworthiness(amount, 2);
}
/**
* The key is a meta introducer of depth <pre>n</pre>.
* This key can introduce meta introducers of depth <pre>n - 1</pre>.
*
* @param n depth
* @return trust
*/
public Trustworthiness metaIntroducerOfDepth(int n) {
return new Trustworthiness(amount, n);
}
}
private static int capAmount(int amount) {
if (amount < 0 || amount > 255) {
throw new IllegalArgumentException("Trust amount MUST be a value between 0 and 255");
}
return amount;
}
private static int capDepth(int depth) {
if (depth < 0 || depth > 255) {
throw new IllegalArgumentException("Trust depth MUST be a value between 0 and 255");
}
return depth;
}
}

View File

@ -1,70 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm.negotiation;
import java.util.Set;
import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.policy.Policy;
/**
* Interface for a class that negotiates {@link HashAlgorithm HashAlgorithms}.
*
* You can provide your own implementation using custom logic by implementing the
* {@link #negotiateHashAlgorithm(Set)} method.
*/
public interface HashAlgorithmNegotiator {
/**
* Pick one {@link HashAlgorithm} from the ordered set of acceptable algorithms.
*
* @param orderedHashAlgorithmPreferencesSet hash algorithm preferences
* @return picked algorithms
*/
HashAlgorithm negotiateHashAlgorithm(Set<HashAlgorithm> orderedHashAlgorithmPreferencesSet);
/**
* Return an instance that negotiates {@link HashAlgorithm HashAlgorithms} used for non-revocation signatures
* based on the given {@link Policy}.
*
* @param policy algorithm policy
* @return negotiator
*/
static HashAlgorithmNegotiator negotiateSignatureHashAlgorithm(Policy policy) {
return negotiateByPolicy(policy.getSignatureHashAlgorithmPolicy());
}
/**
* Return an instance that negotiates {@link HashAlgorithm HashAlgorithms} used for revocation signatures
* based on the given {@link Policy}.
*
* @param policy algorithm policy
* @return negotiator
*/
static HashAlgorithmNegotiator negotiateRevocationSignatureAlgorithm(Policy policy) {
return negotiateByPolicy(policy.getRevocationSignatureHashAlgorithmPolicy());
}
/**
* Return an instance that negotiates {@link HashAlgorithm HashAlgorithms} based on the given
* {@link Policy.HashAlgorithmPolicy}.
*
* @param hashAlgorithmPolicy algorithm policy for hash algorithms
* @return negotiator
*/
static HashAlgorithmNegotiator negotiateByPolicy(Policy.HashAlgorithmPolicy hashAlgorithmPolicy) {
return new HashAlgorithmNegotiator() {
@Override
public HashAlgorithm negotiateHashAlgorithm(Set<HashAlgorithm> orderedPreferencesSet) {
for (HashAlgorithm preference : orderedPreferencesSet) {
if (hashAlgorithmPolicy.isAcceptable(preference)) {
return preference;
}
}
return hashAlgorithmPolicy.defaultHashAlgorithm();
}
};
}
}

View File

@ -1,105 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm.negotiation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.policy.Policy;
/**
* Interface for symmetric key algorithm negotiation.
*/
public interface SymmetricKeyAlgorithmNegotiator {
/**
* Negotiate a symmetric encryption algorithm.
* If the override is non-null, it will be returned instead of performing an actual negotiation.
* Otherwise, the list of ordered sets containing the preferences of different recipient keys will be
* used to determine a suitable symmetric encryption algorithm.
*
* @param policy algorithm policy
* @param override algorithm override (if not null, return this)
* @param keyPreferences list of preferences per key
* @return negotiated algorithm
*/
SymmetricKeyAlgorithm negotiate(
Policy.SymmetricKeyAlgorithmPolicy policy,
SymmetricKeyAlgorithm override,
List<Set<SymmetricKeyAlgorithm>> keyPreferences);
/**
* Return an instance that negotiates a {@link SymmetricKeyAlgorithm} by selecting the most popular acceptable
* algorithm from the list of preferences.
*
* This negotiator has the best chances to select an algorithm which is understood by all recipients.
*
* @return negotiator that selects by popularity
*/
static SymmetricKeyAlgorithmNegotiator byPopularity() {
return new SymmetricKeyAlgorithmNegotiator() {
@Override
public SymmetricKeyAlgorithm negotiate(
Policy.SymmetricKeyAlgorithmPolicy policy,
SymmetricKeyAlgorithm override,
List<Set<SymmetricKeyAlgorithm>> preferences) {
if (override == SymmetricKeyAlgorithm.NULL) {
throw new IllegalArgumentException("Algorithm override cannot be NULL (plaintext).");
}
if (override != null) {
return override;
}
// Count score (occurrences) of each algorithm
Map<SymmetricKeyAlgorithm, Integer> supportWeight = new LinkedHashMap<>();
for (Set<SymmetricKeyAlgorithm> keyPreferences : preferences) {
for (SymmetricKeyAlgorithm preferred : keyPreferences) {
if (supportWeight.containsKey(preferred)) {
supportWeight.put(preferred, supportWeight.get(preferred) + 1);
} else {
supportWeight.put(preferred, 1);
}
}
}
// Pivot the score map
Map<Integer, List<SymmetricKeyAlgorithm>> byScore = new HashMap<>();
for (SymmetricKeyAlgorithm algorithm : supportWeight.keySet()) {
int score = supportWeight.get(algorithm);
List<SymmetricKeyAlgorithm> withSameScore = byScore.get(score);
if (withSameScore == null) {
withSameScore = new ArrayList<>();
byScore.put(score, withSameScore);
}
withSameScore.add(algorithm);
}
List<Integer> scores = new ArrayList<>(byScore.keySet());
// Sort map and iterate from highest to lowest score
Collections.sort(scores);
for (int i = scores.size() - 1; i >= 0; i--) {
int score = scores.get(i);
List<SymmetricKeyAlgorithm> withSameScore = byScore.get(score);
// Select best algorithm
SymmetricKeyAlgorithm best = policy.selectBest(withSameScore);
if (best != null) {
return best;
}
}
// If no algorithm is acceptable, choose fallback
return policy.getDefaultSymmetricKeyAlgorithm();
}
};
}
}

View File

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

View File

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Enums which map to OpenPGP's algorithm IDs.
*/
package org.pgpainless.algorithm;

View File

@ -1,508 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy;
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.signature.SignatureUtils;
import org.pgpainless.util.Passphrase;
import org.pgpainless.util.SessionKey;
/**
* Options for decryption and signature verification.
*/
public class ConsumerOptions {
private boolean ignoreMDCErrors = false;
private boolean forceNonOpenPgpData = false;
private Date verifyNotBefore = null;
private Date verifyNotAfter = new Date();
private final CertificateSource certificates = new CertificateSource();
private final Set<PGPSignature> detachedSignatures = new HashSet<>();
private MissingPublicKeyCallback missingCertificateCallback = null;
// Session key for decryption without passphrase/key
private SessionKey sessionKey = null;
private final Map<SubkeyIdentifier, PublicKeyDataDecryptorFactory> customPublicKeyDataDecryptorFactories =
new HashMap<>();
private final Map<PGPSecretKeyRing, SecretKeyRingProtector> decryptionKeys = new HashMap<>();
private final Set<Passphrase> decryptionPassphrases = new HashSet<>();
private MissingKeyPassphraseStrategy missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE;
private MultiPassStrategy multiPassStrategy = new InMemoryMultiPassStrategy();
public static ConsumerOptions get() {
return new ConsumerOptions();
}
/**
* Consider signatures on the message made before the given timestamp invalid.
* Null means no limitation.
*
* @param timestamp timestamp
* @return options
*/
public ConsumerOptions verifyNotBefore(Date timestamp) {
this.verifyNotBefore = timestamp;
return this;
}
/**
* Return the earliest creation date on which signatures on the message are considered valid.
* Signatures made earlier than this date are considered invalid.
*
* @return earliest allowed signature creation date or null
*/
public @Nullable Date getVerifyNotBefore() {
return verifyNotBefore;
}
/**
* Consider signatures on the message made after the given timestamp invalid.
* Null means no limitation.
*
* @param timestamp timestamp
* @return options
*/
public ConsumerOptions verifyNotAfter(Date timestamp) {
this.verifyNotAfter = timestamp;
return this;
}
/**
* Return the latest possible creation date on which signatures made on the message are considered valid.
* Signatures made later than this date are considered invalid.
*
* @return Latest possible creation date or null.
*/
public Date getVerifyNotAfter() {
return verifyNotAfter;
}
/**
* Add a certificate (public key ring) for signature verification.
*
* @param verificationCert certificate for signature verification
* @return options
*/
public ConsumerOptions addVerificationCert(PGPPublicKeyRing verificationCert) {
this.certificates.addCertificate(verificationCert);
return this;
}
/**
* Add a set of certificates (public key rings) for signature verification.
*
* @param verificationCerts certificates for signature verification
* @return options
*/
public ConsumerOptions addVerificationCerts(PGPPublicKeyRingCollection verificationCerts) {
for (PGPPublicKeyRing certificate : verificationCerts) {
addVerificationCert(certificate);
}
return this;
}
/**
* Add some detached signatures from the given {@link InputStream} for verification.
*
* @param signatureInputStream input stream of detached signatures
* @return options
*
* @throws IOException in case of an IO error
* @throws PGPException in case of an OpenPGP error
*/
public ConsumerOptions addVerificationOfDetachedSignatures(InputStream signatureInputStream)
throws IOException, PGPException {
List<PGPSignature> signatures = SignatureUtils.readSignatures(signatureInputStream);
return addVerificationOfDetachedSignatures(signatures);
}
/**
* Add some detached signatures for verification.
*
* @param detachedSignatures detached signatures
* @return options
*/
public ConsumerOptions addVerificationOfDetachedSignatures(List<PGPSignature> detachedSignatures) {
for (PGPSignature signature : detachedSignatures) {
addVerificationOfDetachedSignature(signature);
}
return this;
}
/**
* Add a detached signature for the signature verification process.
*
* @param detachedSignature detached signature
* @return options
*/
public ConsumerOptions addVerificationOfDetachedSignature(PGPSignature detachedSignature) {
detachedSignatures.add(detachedSignature);
return this;
}
/**
* Set a callback that's used when a certificate (public key) is missing for signature verification.
*
* @param callback callback
* @return options
*/
public ConsumerOptions setMissingCertificateCallback(MissingPublicKeyCallback callback) {
this.missingCertificateCallback = callback;
return this;
}
/**
* Attempt decryption using a session key.
*
* Note: PGPainless does not yet support decryption with session keys.
*
* @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-2.1">RFC4880 on Session Keys</a>
*
* @param sessionKey session key
* @return options
*/
public ConsumerOptions setSessionKey(@Nonnull SessionKey sessionKey) {
this.sessionKey = sessionKey;
return this;
}
/**
* Return the session key.
*
* @return session key or null
*/
public @Nullable SessionKey getSessionKey() {
return sessionKey;
}
/**
* Add a key for message decryption.
* The key is expected to be unencrypted.
*
* @param key unencrypted key
* @return options
*/
public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key) {
return addDecryptionKey(key, SecretKeyRingProtector.unprotectedKeys());
}
/**
* Add a key for message decryption. If the key is encrypted, the {@link SecretKeyRingProtector}
* is used to decrypt it when needed.
*
* @param key key
* @param keyRingProtector protector for the secret key
* @return options
*/
public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key,
@Nonnull SecretKeyRingProtector keyRingProtector) {
decryptionKeys.put(key, keyRingProtector);
return this;
}
/**
* Add the keys in the provided key collection for message decryption.
*
* @param keys key collection
* @param keyRingProtector protector for encrypted secret keys
* @return options
*/
public ConsumerOptions addDecryptionKeys(@Nonnull PGPSecretKeyRingCollection keys,
@Nonnull SecretKeyRingProtector keyRingProtector) {
for (PGPSecretKeyRing key : keys) {
addDecryptionKey(key, keyRingProtector);
}
return this;
}
/**
* Add a passphrase for message decryption.
* This passphrase will be used to try to decrypt messages which were symmetrically encrypted for a passphrase.
*
* @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.7">Symmetrically Encrypted Data Packet</a>
*
* @param passphrase passphrase
* @return options
*/
public ConsumerOptions addDecryptionPassphrase(@Nonnull Passphrase passphrase) {
decryptionPassphrases.add(passphrase);
return this;
}
/**
* Add a custom {@link PublicKeyDataDecryptorFactory} which enable decryption of messages, e.g. using
* hardware-backed secret keys.
* (See e.g. {@link org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory}).
*
* @param factory decryptor factory
* @return options
*/
public ConsumerOptions addCustomDecryptorFactory(@Nonnull CustomPublicKeyDataDecryptorFactory factory) {
this.customPublicKeyDataDecryptorFactories.put(factory.getSubkeyIdentifier(), factory);
return this;
}
/**
* Return the custom {@link PublicKeyDataDecryptorFactory PublicKeyDataDecryptorFactories} that were
* set by the user.
* These factories can be used to decrypt session keys using a custom logic.
*
* @return custom decryptor factories
*/
Map<SubkeyIdentifier, PublicKeyDataDecryptorFactory> getCustomDecryptorFactories() {
return new HashMap<>(customPublicKeyDataDecryptorFactories);
}
/**
* Return the set of available decryption keys.
*
* @return decryption keys
*/
public @Nonnull Set<PGPSecretKeyRing> getDecryptionKeys() {
return Collections.unmodifiableSet(decryptionKeys.keySet());
}
/**
* Return the set of available message decryption passphrases.
*
* @return decryption passphrases
*/
public @Nonnull Set<Passphrase> getDecryptionPassphrases() {
return Collections.unmodifiableSet(decryptionPassphrases);
}
/**
* Return the explicitly set verification certificates.
*
* @deprecated use {@link #getCertificateSource()} instead.
* @return verification certs
*/
@Deprecated
public @Nonnull Set<PGPPublicKeyRing> getCertificates() {
return certificates.getExplicitCertificates();
}
/**
* Return an object holding available certificates for signature verification.
*
* @return certificate source
*/
public @Nonnull CertificateSource getCertificateSource() {
return certificates;
}
/**
* Return the callback that gets called when a certificate for signature verification is missing.
* This method might return <pre>null</pre> if the users hasn't set a callback.
*
* @return missing public key callback
*/
public @Nullable MissingPublicKeyCallback getMissingCertificateCallback() {
return missingCertificateCallback;
}
/**
* Return the {@link SecretKeyRingProtector} for the given {@link PGPSecretKeyRing}.
*
* @param decryptionKeyRing secret key
* @return protector for that particular secret key
*/
public @Nonnull SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) {
return decryptionKeys.get(decryptionKeyRing);
}
/**
* Return the set of detached signatures the user provided.
*
* @return detached signatures
*/
public @Nonnull Set<PGPSignature> getDetachedSignatures() {
return Collections.unmodifiableSet(detachedSignatures);
}
/**
* By default, PGPainless will require encrypted messages to make use of SEIP data packets.
* Those are Symmetrically Encrypted Integrity Protected Data packets.
* Symmetrically Encrypted Data Packets without integrity protection are rejected by default.
* Furthermore, PGPainless will throw an exception if verification of the MDC error detection
* code of the SEIP packet fails.
*
* Failure of MDC verification indicates a tampered ciphertext, which might be the cause of an
* attack or data corruption.
*
* This method can be used to ignore MDC errors and allow PGPainless to consume encrypted data
* without integrity protection.
* If the flag <pre>ignoreMDCErrors</pre> is set to true, PGPainless will
* <ul>
* <li>not throw exceptions for SEIP packets with tampered ciphertext</li>
* <li>not throw exceptions for SEIP packets with tampered MDC</li>
* <li>not throw exceptions for MDCs with bad CTB</li>
* <li>not throw exceptions for MDCs with bad length</li>
* </ul>
*
* It will however still throw an exception if it encounters a SEIP packet with missing or truncated MDC
*
* @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.13">
* Sym. Encrypted Integrity Protected Data Packet</a>
* @param ignoreMDCErrors true if MDC errors or missing MDCs shall be ignored, false otherwise.
* @return options
*/
@Deprecated
public ConsumerOptions setIgnoreMDCErrors(boolean ignoreMDCErrors) {
this.ignoreMDCErrors = ignoreMDCErrors;
return this;
}
/**
* Return true, if PGPainless is ignoring MDC errors.
*
* @return ignore mdc errors
*/
boolean isIgnoreMDCErrors() {
return ignoreMDCErrors;
}
/**
* Force PGPainless to handle the data provided by the {@link InputStream} as non-OpenPGP data.
* This workaround might come in handy if PGPainless accidentally mistakes the data for binary OpenPGP data.
*
* @return options
*/
public ConsumerOptions forceNonOpenPgpData() {
this.forceNonOpenPgpData = true;
return this;
}
/**
* Return true, if the ciphertext should be handled as binary non-OpenPGP data.
*
* @return true if non-OpenPGP data is forced
*/
boolean isForceNonOpenPgpData() {
return forceNonOpenPgpData;
}
/**
* Specify the {@link MissingKeyPassphraseStrategy}.
* This strategy defines, how missing passphrases for unlocking secret keys are handled.
* In interactive mode ({@link MissingKeyPassphraseStrategy#INTERACTIVE}) PGPainless will try to obtain missing
* passphrases for secret keys via the {@link SecretKeyRingProtector SecretKeyRingProtectors}
* {@link org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider} callback.
*
* In non-interactice mode ({@link MissingKeyPassphraseStrategy#THROW_EXCEPTION}, PGPainless will instead
* throw a {@link org.pgpainless.exception.MissingPassphraseException} containing the ids of all keys for which
* there are missing passphrases.
*
* @param strategy strategy
* @return options
*/
public ConsumerOptions setMissingKeyPassphraseStrategy(MissingKeyPassphraseStrategy strategy) {
this.missingKeyPassphraseStrategy = strategy;
return this;
}
/**
* Return the currently configured {@link MissingKeyPassphraseStrategy}.
*
* @return missing key passphrase strategy
*/
MissingKeyPassphraseStrategy getMissingKeyPassphraseStrategy() {
return missingKeyPassphraseStrategy;
}
/**
* Set a custom multi-pass strategy for processing cleartext-signed messages.
* Uses {@link InMemoryMultiPassStrategy} by default.
*
* @param multiPassStrategy multi-pass caching strategy
* @return builder
*/
public ConsumerOptions setMultiPassStrategy(@Nonnull MultiPassStrategy multiPassStrategy) {
this.multiPassStrategy = multiPassStrategy;
return this;
}
/**
* Return the currently configured {@link MultiPassStrategy}.
* Defaults to {@link InMemoryMultiPassStrategy}.
*
* @return multi-pass strategy
*/
public MultiPassStrategy getMultiPassStrategy() {
return multiPassStrategy;
}
/**
* Source for OpenPGP certificates.
* When verifying signatures on a message, this object holds available signer certificates.
*/
public static class CertificateSource {
private Set<PGPPublicKeyRing> explicitCertificates = new HashSet<>();
/**
* Add a certificate as verification cert explicitly.
*
* @param certificate certificate
*/
public void addCertificate(PGPPublicKeyRing certificate) {
this.explicitCertificates.add(certificate);
}
/**
* Return the set of explicitly set verification certificates.
* @return explicitly set verification certs
*/
public Set<PGPPublicKeyRing> getExplicitCertificates() {
return Collections.unmodifiableSet(explicitCertificates);
}
/**
* Return a certificate which contains a subkey with the given keyId.
* This method first checks all explicitly set verification certs and if no cert is found it consults
* the certificate stores.
*
* @param keyId key id
* @return certificate
*/
public PGPPublicKeyRing getCertificate(long keyId) {
for (PGPPublicKeyRing cert : explicitCertificates) {
if (cert.getPublicKey(keyId) != null) {
return cert;
}
}
return null;
}
}
}

View File

@ -1,27 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import org.pgpainless.key.SubkeyIdentifier;
/**
* Custom {@link PublicKeyDataDecryptorFactory} which can enable customized implementations of message decryption
* using public keys.
* This class can for example be used to implement message encryption using hardware tokens like smartcards or
* TPMs.
* @see ConsumerOptions#addCustomDecryptorFactory(CustomPublicKeyDataDecryptorFactory)
*/
public interface CustomPublicKeyDataDecryptorFactory extends PublicKeyDataDecryptorFactory {
/**
* Return the {@link SubkeyIdentifier} for which this particular {@link CustomPublicKeyDataDecryptorFactory}
* is intended.
*
* @return subkey identifier
*/
SubkeyIdentifier getSubkeyIdentifier();
}

View File

@ -1,42 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nonnull;
import org.bouncycastle.openpgp.PGPException;
/**
* Builder class that takes an {@link InputStream} of ciphertext (or plaintext signed data)
* and combines it with a configured {@link ConsumerOptions} object to form a {@link DecryptionStream} which
* can be used to decrypt an OpenPGP message or verify signatures.
*/
public class DecryptionBuilder implements DecryptionBuilderInterface {
@Override
public DecryptWith onInputStream(@Nonnull InputStream inputStream) {
return new DecryptWithImpl(inputStream);
}
static class DecryptWithImpl implements DecryptWith {
private final InputStream inputStream;
DecryptWithImpl(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public DecryptionStream withOptions(ConsumerOptions consumerOptions) throws PGPException, IOException {
if (consumerOptions == null) {
throw new IllegalArgumentException("Consumer options cannot be null.");
}
return OpenPgpMessageInputStream.create(inputStream, consumerOptions);
}
}
}

View File

@ -1,36 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nonnull;
import org.bouncycastle.openpgp.PGPException;
public interface DecryptionBuilderInterface {
/**
* Create a {@link DecryptionStream} on an {@link InputStream} which contains the encrypted and/or signed data.
*
* @param inputStream encrypted and/or signed data.
* @return api handle
*/
DecryptWith onInputStream(@Nonnull InputStream inputStream);
interface DecryptWith {
/**
* Add options for decryption / signature verification, such as keys, passphrases etc.
*
* @param consumerOptions consumer options
* @return decryption stream
* @throws PGPException in case of an OpenPGP related error
* @throws IOException in case of an IO error
*/
DecryptionStream withOptions(ConsumerOptions consumerOptions) throws PGPException, IOException;
}
}

View File

@ -1,33 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import java.io.InputStream;
/**
* Abstract definition of an {@link InputStream} which can be used to decrypt / verify OpenPGP messages.
*/
public abstract class DecryptionStream extends InputStream {
/**
* Return {@link MessageMetadata metadata} about the decrypted / verified message.
* The {@link DecryptionStream} MUST be closed via {@link #close()} before the metadata object can be accessed.
*
* @return message metadata
*/
public abstract MessageMetadata getMetadata();
/**
* Return a {@link OpenPgpMetadata} object containing information about the decrypted / verified message.
* The {@link DecryptionStream} MUST be closed via {@link #close()} before the metadata object can be accessed.
*
* @return message metadata
* @deprecated use {@link #getMetadata()} instead.
*/
@Deprecated
public OpenPgpMetadata getResult() {
return getMetadata().toLegacyMetadata();
}
}

View File

@ -1,105 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import org.bouncycastle.bcpg.AEADEncDataPacket;
import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPSessionKey;
import org.bouncycastle.openpgp.operator.PGPDataDecryptor;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory;
import org.pgpainless.key.SubkeyIdentifier;
/**
* Enable integration of hardware-backed OpenPGP keys.
*/
public class HardwareSecurity {
public interface DecryptionCallback {
/**
* Delegate decryption of a Public-Key-Encrypted-Session-Key (PKESK) to an external API for dealing with
* hardware security modules such as smartcards or TPMs.
*
* If decryption fails for some reason, a subclass of the {@link HardwareSecurityException} is thrown.
*
* @param keyId id of the key
* @param keyAlgorithm algorithm
* @param sessionKeyData encrypted session key
*
* @return decrypted session key
* @throws HardwareSecurityException exception
*/
byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData)
throws HardwareSecurityException;
}
/**
* Implementation of {@link PublicKeyDataDecryptorFactory} which delegates decryption of encrypted session keys
* to a {@link DecryptionCallback}.
* Users can provide such a callback to delegate decryption of messages to hardware security SDKs.
*/
public static class HardwareDataDecryptorFactory implements CustomPublicKeyDataDecryptorFactory {
private final DecryptionCallback callback;
// luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument.
private final PublicKeyDataDecryptorFactory factory =
new BcPublicKeyDataDecryptorFactory(null);
private final SubkeyIdentifier subkey;
/**
* Create a new {@link HardwareDataDecryptorFactory}.
*
* @param subkeyIdentifier identifier of the decryption subkey
* @param callback decryption callback
*/
public HardwareDataDecryptorFactory(SubkeyIdentifier subkeyIdentifier, DecryptionCallback callback) {
this.callback = callback;
this.subkey = subkeyIdentifier;
}
@Override
public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData)
throws PGPException {
try {
// delegate decryption to the callback
return callback.decryptSessionKey(subkey.getSubkeyId(), keyAlgorithm, secKeyData[0]);
} catch (HardwareSecurityException e) {
throw new PGPException("Hardware-backed decryption failed.", e);
}
}
@Override
public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key)
throws PGPException {
return factory.createDataDecryptor(withIntegrityPacket, encAlgorithm, key);
}
@Override
public PGPDataDecryptor createDataDecryptor(AEADEncDataPacket aeadEncDataPacket, PGPSessionKey sessionKey)
throws PGPException {
return factory.createDataDecryptor(aeadEncDataPacket, sessionKey);
}
@Override
public PGPDataDecryptor createDataDecryptor(SymmetricEncIntegrityPacket seipd, PGPSessionKey sessionKey)
throws PGPException {
return factory.createDataDecryptor(seipd, sessionKey);
}
@Override
public SubkeyIdentifier getSubkeyIdentifier() {
return subkey;
}
}
public static class HardwareSecurityException
extends Exception {
}
}

View File

@ -1,60 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nonnull;
import org.bouncycastle.openpgp.PGPEncryptedData;
import org.bouncycastle.openpgp.PGPException;
import org.pgpainless.exception.ModificationDetectionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class IntegrityProtectedInputStream extends InputStream {
private static final Logger LOGGER = LoggerFactory.getLogger(IntegrityProtectedInputStream.class);
private final InputStream inputStream;
private final PGPEncryptedData encryptedData;
private final ConsumerOptions options;
private boolean closed = false;
public IntegrityProtectedInputStream(InputStream inputStream, PGPEncryptedData encryptedData, ConsumerOptions options) {
this.inputStream = inputStream;
this.encryptedData = encryptedData;
this.options = options;
}
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public int read(@Nonnull byte[] b, int offset, int length) throws IOException {
return inputStream.read(b, offset, length);
}
@Override
public void close() throws IOException {
if (closed) {
return;
}
closed = true;
if (encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) {
try {
if (!encryptedData.verify()) {
throw new ModificationDetectionException();
}
LOGGER.debug("Integrity Protection check passed");
} catch (PGPException e) {
throw new IOException("Data appears to not be integrity protected.", e);
}
}
}
}

View File

@ -1,146 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPEncryptedData;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPPBEEncryptedData;
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
import org.bouncycastle.openpgp.PGPUtil;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.util.ArmorUtils;
/**
* Inspect an OpenPGP message to determine IDs of its encryption keys or whether it is passphrase protected.
*/
public final class MessageInspector {
public static class EncryptionInfo {
private final List<Long> keyIds = new ArrayList<>();
private boolean isPassphraseEncrypted = false;
private boolean isSignedOnly = false;
/**
* Return a list of recipient key ids for whom the message is encrypted.
* @return recipient key ids
*/
public List<Long> getKeyIds() {
return Collections.unmodifiableList(keyIds);
}
public boolean isPassphraseEncrypted() {
return isPassphraseEncrypted;
}
/**
* Return true, if the message is encrypted.
*
* @return true if encrypted
*/
public boolean isEncrypted() {
return isPassphraseEncrypted || !keyIds.isEmpty();
}
/**
* Return true, if the message is not encrypted, but signed using {@link org.bouncycastle.openpgp.PGPOnePassSignature OnePassSignatures}.
*
* @return true if message is signed only
*/
public boolean isSignedOnly() {
return isSignedOnly;
}
}
private MessageInspector() {
}
/**
* 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
*/
public static EncryptionInfo determineEncryptionInfoForMessage(String message) throws PGPException, IOException {
@SuppressWarnings("CharsetObjectCanBeUsed")
Charset charset = Charset.forName("UTF-8");
return determineEncryptionInfoForMessage(new ByteArrayInputStream(message.getBytes(charset)));
}
/**
* 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 dataIn openpgp message
* @return encryption information
*
* @throws IOException in case of an IO error
* @throws PGPException if the message is broken
*/
public static EncryptionInfo determineEncryptionInfoForMessage(InputStream dataIn) throws IOException, PGPException {
InputStream decoded = ArmorUtils.getDecoderStream(dataIn);
EncryptionInfo info = new EncryptionInfo();
processMessage(decoded, info);
return info;
}
private static void processMessage(InputStream dataIn, EncryptionInfo info) throws PGPException, IOException {
PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(dataIn);
Object next;
while ((next = objectFactory.nextObject()) != null) {
if (next instanceof PGPOnePassSignatureList) {
PGPOnePassSignatureList signatures = (PGPOnePassSignatureList) next;
if (!signatures.isEmpty()) {
info.isSignedOnly = true;
return;
}
}
if (next instanceof PGPEncryptedDataList) {
PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) next;
for (PGPEncryptedData encryptedData : encryptedDataList) {
if (encryptedData instanceof PGPPublicKeyEncryptedData) {
PGPPublicKeyEncryptedData pubKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData;
info.keyIds.add(pubKeyEncryptedData.getKeyID());
} else if (encryptedData instanceof PGPPBEEncryptedData) {
info.isPassphraseEncrypted = true;
}
}
// Data is encrypted, we cannot go deeper
return;
}
if (next instanceof PGPCompressedData) {
PGPCompressedData compressed = (PGPCompressedData) next;
InputStream decompressed = compressed.getDataStream();
InputStream decoded = PGPUtil.getDecoderStream(decompressed);
objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoded);
}
if (next instanceof PGPLiteralData) {
return;
}
}
}
}

View File

@ -1,823 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.bouncycastle.openpgp.PGPKeyRing;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.util.SessionKey;
/**
* View for extracting metadata about a {@link Message}.
*/
public class MessageMetadata {
protected Message message;
public MessageMetadata(@Nonnull Message message) {
this.message = message;
}
/**
* Convert this {@link MessageMetadata} object into a legacy {@link OpenPgpMetadata} object.
* This method is intended to be used for a transition period between the 1.3 / 1.4+ branches.
* TODO: Remove in 1.6.X
*
* @return converted {@link OpenPgpMetadata} object
*/
public @Nonnull OpenPgpMetadata toLegacyMetadata() {
OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder();
resultBuilder.setCompressionAlgorithm(getCompressionAlgorithm());
resultBuilder.setModificationDate(getModificationDate());
resultBuilder.setFileName(getFilename());
resultBuilder.setFileEncoding(getLiteralDataEncoding());
resultBuilder.setSessionKey(getSessionKey());
resultBuilder.setDecryptionKey(getDecryptionKey());
for (SignatureVerification accepted : getVerifiedDetachedSignatures()) {
resultBuilder.addVerifiedDetachedSignature(accepted);
}
for (SignatureVerification.Failure rejected : getRejectedDetachedSignatures()) {
resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException());
}
for (SignatureVerification accepted : getVerifiedInlineSignatures()) {
resultBuilder.addVerifiedInbandSignature(accepted);
}
for (SignatureVerification.Failure rejected : getRejectedInlineSignatures()) {
resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException());
}
if (message.isCleartextSigned()) {
resultBuilder.setCleartextSigned();
}
return resultBuilder.build();
}
public boolean isUsingCleartextSignatureFramework() {
return message.isCleartextSigned();
}
public boolean isEncrypted() {
SymmetricKeyAlgorithm algorithm = getEncryptionAlgorithm();
return algorithm != null && algorithm != SymmetricKeyAlgorithm.NULL;
}
public boolean isEncryptedFor(@Nonnull PGPKeyRing keys) {
Iterator<EncryptedData> encryptionLayers = getEncryptionLayers();
while (encryptionLayers.hasNext()) {
EncryptedData encryptedData = encryptionLayers.next();
for (long recipient : encryptedData.getRecipients()) {
PGPPublicKey key = keys.getPublicKey(recipient);
if (key != null) {
return true;
}
}
}
return false;
}
/**
* Return a list containing all recipient keyIDs.
*
* @return list of recipients
*/
public List<Long> getRecipientKeyIds() {
List<Long> keyIds = new ArrayList<>();
Iterator<EncryptedData> encLayers = getEncryptionLayers();
while (encLayers.hasNext()) {
EncryptedData layer = encLayers.next();
keyIds.addAll(layer.getRecipients());
}
return keyIds;
}
public @Nonnull Iterator<EncryptedData> getEncryptionLayers() {
return new LayerIterator<EncryptedData>(message) {
@Override
public boolean matches(Packet layer) {
return layer instanceof EncryptedData;
}
@Override
public EncryptedData getProperty(Layer last) {
return (EncryptedData) last;
}
};
}
/**
* Return the {@link SymmetricKeyAlgorithm} of the outermost encrypted data packet, or null if message is
* unencrypted.
*
* @return encryption algorithm
*/
public @Nullable SymmetricKeyAlgorithm getEncryptionAlgorithm() {
return firstOrNull(getEncryptionAlgorithms());
}
/**
* Return an {@link Iterator} of {@link SymmetricKeyAlgorithm SymmetricKeyAlgorithms} encountered in the message.
* The first item returned by the iterator is the algorithm of the outermost encrypted data packet, the next item
* that of the next nested encrypted data packet and so on.
* The iterator might also be empty, in case of an unencrypted message.
*
* @return iterator of symmetric encryption algorithms
*/
public @Nonnull Iterator<SymmetricKeyAlgorithm> getEncryptionAlgorithms() {
return map(getEncryptionLayers(), encryptedData -> encryptedData.algorithm);
}
public @Nonnull Iterator<CompressedData> getCompressionLayers() {
return new LayerIterator<CompressedData>(message) {
@Override
boolean matches(Packet layer) {
return layer instanceof CompressedData;
}
@Override
CompressedData getProperty(Layer last) {
return (CompressedData) last;
}
};
}
/**
* Return the {@link CompressionAlgorithm} of the outermost compressed data packet, or null, if the message
* does not contain any compressed data packets.
*
* @return compression algorithm
*/
public @Nullable CompressionAlgorithm getCompressionAlgorithm() {
return firstOrNull(getCompressionAlgorithms());
}
/**
* Return an {@link Iterator} of {@link CompressionAlgorithm CompressionAlgorithms} encountered in the message.
* The first item returned by the iterator is the algorithm of the outermost compressed data packet, the next
* item that of the next nested compressed data packet and so on.
* The iterator might also be empty, in case of a message without any compressed data packets.
*
* @return iterator of compression algorithms
*/
public @Nonnull Iterator<CompressionAlgorithm> getCompressionAlgorithms() {
return map(getCompressionLayers(), compressionLayer -> compressionLayer.algorithm);
}
/**
* Return the {@link SessionKey} of the outermost encrypted data packet.
* If the message was unencrypted, this method returns <pre>null</pre>.
*
* @return session key of the message
*/
public @Nullable SessionKey getSessionKey() {
return firstOrNull(getSessionKeys());
}
/**
* Return an {@link Iterator} of {@link SessionKey SessionKeys} for all encrypted data packets in the message.
* The first item returned by the iterator is the session key of the outermost encrypted data packet,
* the next item that of the next nested encrypted data packet and so on.
* The iterator might also be empty, in case of an unencrypted message.
*
* @return iterator of session keys
*/
public @Nonnull Iterator<SessionKey> getSessionKeys() {
return map(getEncryptionLayers(), encryptedData -> encryptedData.sessionKey);
}
public boolean isVerifiedSignedBy(@Nonnull PGPKeyRing keys) {
return isVerifiedInlineSignedBy(keys) || isVerifiedDetachedSignedBy(keys);
}
public List<SignatureVerification> getVerifiedSignatures() {
List<SignatureVerification> allVerifiedSignatures = getVerifiedInlineSignatures();
allVerifiedSignatures.addAll(getVerifiedDetachedSignatures());
return allVerifiedSignatures;
}
public boolean isVerifiedDetachedSignedBy(@Nonnull PGPKeyRing keys) {
return containsSignatureBy(getVerifiedDetachedSignatures(), keys);
}
/**
* Return a list of all verified detached signatures.
* This list contains all acceptable, correct detached signatures.
*
* @return verified detached signatures
*/
public @Nonnull List<SignatureVerification> getVerifiedDetachedSignatures() {
return message.getVerifiedDetachedSignatures();
}
/**
* Return a list of all rejected detached signatures.
*
* @return rejected detached signatures
*/
public @Nonnull List<SignatureVerification.Failure> getRejectedDetachedSignatures() {
return message.getRejectedDetachedSignatures();
}
public boolean isVerifiedInlineSignedBy(@Nonnull PGPKeyRing keys) {
return containsSignatureBy(getVerifiedInlineSignatures(), keys);
}
/**
* Return a list of all verified inline-signatures.
* This list contains all acceptable, correct signatures that were part of the message itself.
*
* @return verified inline signatures
*/
public @Nonnull List<SignatureVerification> getVerifiedInlineSignatures() {
List<SignatureVerification> verifications = new ArrayList<>();
Iterator<List<SignatureVerification>> verificationsByLayer = getVerifiedInlineSignaturesByLayer();
while (verificationsByLayer.hasNext()) {
verifications.addAll(verificationsByLayer.next());
}
return verifications;
}
/**
* Return an {@link Iterator} of {@link List Lists} of verified inline-signatures of the message.
* Since signatures might occur in different layers within a message, this method can be used to gain more detailed
* insights into what signatures were encountered at what layers of the message structure.
* Each item of the {@link Iterator} represents a layer of the message and contains only signatures from
* this layer.
* An empty list means no (or no acceptable) signatures were encountered in that layer.
*
* @return iterator of lists of signatures by-layer.
*/
public @Nonnull Iterator<List<SignatureVerification>> getVerifiedInlineSignaturesByLayer() {
return new LayerIterator<List<SignatureVerification>>(message) {
@Override
boolean matches(Packet layer) {
return layer instanceof Layer;
}
@Override
List<SignatureVerification> getProperty(Layer last) {
List<SignatureVerification> list = new ArrayList<>();
list.addAll(last.getVerifiedOnePassSignatures());
list.addAll(last.getVerifiedPrependedSignatures());
return list;
}
};
}
/**
* Return a list of all rejected inline-signatures of the message.
*
* @return list of rejected inline-signatures
*/
public @Nonnull List<SignatureVerification.Failure> getRejectedInlineSignatures() {
List<SignatureVerification.Failure> rejected = new ArrayList<>();
Iterator<List<SignatureVerification.Failure>> rejectedByLayer = getRejectedInlineSignaturesByLayer();
while (rejectedByLayer.hasNext()) {
rejected.addAll(rejectedByLayer.next());
}
return rejected;
}
/**
* Similar to {@link #getVerifiedInlineSignaturesByLayer()}, this method returns all rejected inline-signatures
* of the message, but organized by layer.
*
* @return rejected inline-signatures by-layer
*/
public @Nonnull Iterator<List<SignatureVerification.Failure>> getRejectedInlineSignaturesByLayer() {
return new LayerIterator<List<SignatureVerification.Failure>>(message) {
@Override
boolean matches(Packet layer) {
return layer instanceof Layer;
}
@Override
List<SignatureVerification.Failure> getProperty(Layer last) {
List<SignatureVerification.Failure> list = new ArrayList<>();
list.addAll(last.getRejectedOnePassSignatures());
list.addAll(last.getRejectedPrependedSignatures());
return list;
}
};
}
private static boolean containsSignatureBy(@Nonnull List<SignatureVerification> verifications,
@Nonnull PGPKeyRing keys) {
for (SignatureVerification verification : verifications) {
SubkeyIdentifier issuer = verification.getSigningKey();
if (issuer == null) {
// No issuer, shouldn't happen, but better be safe and skip...
continue;
}
if (keys.getPublicKey().getKeyID() != issuer.getPrimaryKeyId()) {
// Wrong cert
continue;
}
if (keys.getPublicKey(issuer.getSubkeyId()) != null) {
// Matching cert and signing key
return true;
}
}
return false;
}
/**
* Return the value of the literal data packet's filename field.
* This value can be used to store a decrypted file under its original filename,
* but since this field is not necessarily part of the signed data of a message, usage of this field is
* discouraged.
*
* @return filename
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-5.9">RFC4880 §5.9. Literal Data Packet</a>
*/
public @Nullable String getFilename() {
LiteralData literalData = findLiteralData();
if (literalData == null) {
return null;
}
return literalData.getFileName();
}
/**
* Returns true, if the filename of the literal data packet indicates that the data is intended for your eyes only.
*
* @return isForYourEyesOnly
*/
public boolean isForYourEyesOnly() {
return PGPLiteralData.CONSOLE.equals(getFilename());
}
/**
* Return the value of the literal data packets modification date field.
* This value can be used to restore the modification date of a decrypted file,
* but since this field is not necessarily part of the signed data, its use is discouraged.
*
* @return modification date
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-5.9">RFC4880 §5.9. Literal Data Packet</a>
*/
public @Nullable Date getModificationDate() {
LiteralData literalData = findLiteralData();
if (literalData == null) {
return null;
}
return literalData.getModificationDate();
}
/**
* Return the value of the format field of the literal data packet.
* This value indicates what format (text, binary data, ...) the data has.
* Since this field is not necessarily part of the signed data of a message, its usage is discouraged.
*
* @return format
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-5.9">RFC4880 §5.9. Literal Data Packet</a>
*/
public @Nullable StreamEncoding getLiteralDataEncoding() {
LiteralData literalData = findLiteralData();
if (literalData == null) {
return null;
}
return literalData.getFormat();
}
/**
* Find the {@link LiteralData} layer of an OpenPGP message.
* Usually, every message has a literal data packet, but for malformed messages this method might still
* return <pre>null</pre>.
*
* @return literal data
*/
private @Nullable LiteralData findLiteralData() {
Nested nested = message.getChild();
if (nested == null) {
return null;
}
while (nested != null && nested.hasNestedChild()) {
Layer layer = (Layer) nested;
nested = layer.getChild();
}
return (LiteralData) nested;
}
/**
* Return the {@link SubkeyIdentifier} of the decryption key that was used to decrypt the outermost encryption
* layer.
* If the message was unencrypted, this might return <pre>null</pre>.
*
* @return decryption key
*/
public SubkeyIdentifier getDecryptionKey() {
return firstOrNull(map(getEncryptionLayers(), encryptedData -> encryptedData.decryptionKey));
}
public boolean isVerifiedSigned() {
return !getVerifiedSignatures().isEmpty();
}
public interface Packet {
}
public abstract static class Layer implements Packet {
public static final int MAX_LAYER_DEPTH = 16;
protected final int depth;
protected final List<SignatureVerification> verifiedDetachedSignatures = new ArrayList<>();
protected final List<SignatureVerification.Failure> rejectedDetachedSignatures = new ArrayList<>();
protected final List<SignatureVerification> verifiedOnePassSignatures = new ArrayList<>();
protected final List<SignatureVerification.Failure> rejectedOnePassSignatures = new ArrayList<>();
protected final List<SignatureVerification> verifiedPrependedSignatures = new ArrayList<>();
protected final List<SignatureVerification.Failure> rejectedPrependedSignatures = new ArrayList<>();
protected Nested child;
public Layer(int depth) {
this.depth = depth;
if (depth > MAX_LAYER_DEPTH) {
throw new MalformedOpenPgpMessageException("Maximum packet nesting depth (" + MAX_LAYER_DEPTH + ") exceeded.");
}
}
/**
* Return the nested child element of this layer.
* Might return <pre>null</pre>, if this layer does not have a child element
* (e.g. if this is a {@link LiteralData} packet).
*
* @return child element
*/
public @Nullable Nested getChild() {
return child;
}
/**
* Set the nested child element for this layer.
*
* @param child child element
*/
void setChild(Nested child) {
this.child = child;
}
/**
* Return a list of all verified detached signatures of this layer.
*
* @return all verified detached signatures of this layer
*/
public List<SignatureVerification> getVerifiedDetachedSignatures() {
return new ArrayList<>(verifiedDetachedSignatures);
}
/**
* Return a list of all rejected detached signatures of this layer.
*
* @return all rejected detached signatures of this layer
*/
public List<SignatureVerification.Failure> getRejectedDetachedSignatures() {
return new ArrayList<>(rejectedDetachedSignatures);
}
/**
* Add a verified detached signature for this layer.
*
* @param signatureVerification verified detached signature
*/
void addVerifiedDetachedSignature(SignatureVerification signatureVerification) {
verifiedDetachedSignatures.add(signatureVerification);
}
/**
* Add a rejected detached signature for this layer.
*
* @param failure rejected detached signature
*/
void addRejectedDetachedSignature(SignatureVerification.Failure failure) {
rejectedDetachedSignatures.add(failure);
}
/**
* Return a list of all verified one-pass-signatures of this layer.
*
* @return all verified one-pass-signatures of this layer
*/
public List<SignatureVerification> getVerifiedOnePassSignatures() {
return new ArrayList<>(verifiedOnePassSignatures);
}
/**
* Return a list of all rejected one-pass-signatures of this layer.
*
* @return all rejected one-pass-signatures of this layer
*/
public List<SignatureVerification.Failure> getRejectedOnePassSignatures() {
return new ArrayList<>(rejectedOnePassSignatures);
}
/**
* Add a verified one-pass-signature for this layer.
*
* @param verifiedOnePassSignature verified one-pass-signature for this layer
*/
void addVerifiedOnePassSignature(SignatureVerification verifiedOnePassSignature) {
this.verifiedOnePassSignatures.add(verifiedOnePassSignature);
}
/**
* Add a rejected one-pass-signature for this layer.
*
* @param rejected rejected one-pass-signature for this layer
*/
void addRejectedOnePassSignature(SignatureVerification.Failure rejected) {
this.rejectedOnePassSignatures.add(rejected);
}
/**
* Return a list of all verified prepended signatures of this layer.
*
* @return all verified prepended signatures of this layer
*/
public List<SignatureVerification> getVerifiedPrependedSignatures() {
return new ArrayList<>(verifiedPrependedSignatures);
}
/**
* Return a list of all rejected prepended signatures of this layer.
*
* @return all rejected prepended signatures of this layer
*/
public List<SignatureVerification.Failure> getRejectedPrependedSignatures() {
return new ArrayList<>(rejectedPrependedSignatures);
}
/**
* Add a verified prepended signature for this layer.
*
* @param verified verified prepended signature
*/
void addVerifiedPrependedSignature(SignatureVerification verified) {
this.verifiedPrependedSignatures.add(verified);
}
/**
* Add a rejected prepended signature for this layer.
*
* @param rejected rejected prepended signature
*/
void addRejectedPrependedSignature(SignatureVerification.Failure rejected) {
this.rejectedPrependedSignatures.add(rejected);
}
}
public interface Nested extends Packet {
boolean hasNestedChild();
}
public static class Message extends Layer {
protected boolean cleartextSigned;
public Message() {
super(0);
}
/**
* Returns true, is the message is a signed message using the cleartext signature framework.
*
* @return <pre>true</pre> if message is cleartext-signed, <pre>false</pre> otherwise
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-7">RFC4880 §7. Cleartext Signature Framework</a>
*/
public boolean isCleartextSigned() {
return cleartextSigned;
}
}
public static class LiteralData implements Nested {
protected String fileName;
protected Date modificationDate;
protected StreamEncoding format;
public LiteralData() {
this("", new Date(0L), StreamEncoding.BINARY);
}
public LiteralData(@Nonnull String fileName,
@Nonnull Date modificationDate,
@Nonnull StreamEncoding format) {
this.fileName = fileName;
this.modificationDate = modificationDate;
this.format = format;
}
/**
* Return the value of the filename field.
* An empty String <pre>""</pre> indicates no filename.
*
* @return filename
*/
public @Nonnull String getFileName() {
return fileName;
}
/**
* Return the value of the modification date field.
* A special date <pre>{@code new Date(0L)}</pre> indicates no modification date.
*
* @return modification date
*/
public @Nonnull Date getModificationDate() {
return modificationDate;
}
/**
* Return the value of the format field.
*
* @return format
*/
public @Nonnull StreamEncoding getFormat() {
return format;
}
@Override
public boolean hasNestedChild() {
// A literal data packet MUST NOT have a child element, as its content is the plaintext
return false;
}
}
public static class CompressedData extends Layer implements Nested {
protected final CompressionAlgorithm algorithm;
public CompressedData(@Nonnull CompressionAlgorithm zip, int depth) {
super(depth);
this.algorithm = zip;
}
/**
* Return the {@link CompressionAlgorithm} used to compress the packet.
* @return compression algorithm
*/
public @Nonnull CompressionAlgorithm getAlgorithm() {
return algorithm;
}
@Override
public boolean hasNestedChild() {
// A compressed data packet MUST have a child element
return true;
}
}
public static class EncryptedData extends Layer implements Nested {
protected final SymmetricKeyAlgorithm algorithm;
protected SubkeyIdentifier decryptionKey;
protected SessionKey sessionKey;
protected List<Long> recipients;
public EncryptedData(@Nonnull SymmetricKeyAlgorithm algorithm, int depth) {
super(depth);
this.algorithm = algorithm;
}
/**
* Return the {@link SymmetricKeyAlgorithm} used to encrypt the packet.
* @return symmetric encryption algorithm
*/
public @Nonnull SymmetricKeyAlgorithm getAlgorithm() {
return algorithm;
}
/**
* Return the {@link SessionKey} used to decrypt the packet.
*
* @return session key
*/
public @Nonnull SessionKey getSessionKey() {
return sessionKey;
}
/**
* Return a list of all recipient key ids to which the packet was encrypted for.
*
* @return recipients
*/
public @Nonnull List<Long> getRecipients() {
if (recipients == null) {
return new ArrayList<>();
}
return new ArrayList<>(recipients);
}
@Override
public boolean hasNestedChild() {
// An encrypted data packet MUST have a child element
return true;
}
}
private abstract static class LayerIterator<O> implements Iterator<O> {
private Nested current;
Layer last = null;
Message parent;
LayerIterator(@Nonnull Message message) {
super();
this.parent = message;
this.current = message.getChild();
if (matches(current)) {
last = (Layer) current;
}
}
@Override
public boolean hasNext() {
if (parent != null && matches(parent)) {
return true;
}
if (last == null) {
findNext();
}
return last != null;
}
@Override
public O next() {
if (parent != null && matches(parent)) {
O property = getProperty(parent);
parent = null;
return property;
}
if (last == null) {
findNext();
}
if (last != null) {
O property = getProperty(last);
last = null;
return property;
}
throw new NoSuchElementException();
}
private void findNext() {
while (current != null && current instanceof Layer) {
current = ((Layer) current).getChild();
if (matches(current)) {
last = (Layer) current;
break;
}
}
}
abstract boolean matches(Packet layer);
abstract O getProperty(Layer last);
}
private static <A,B> Iterator<B> map(Iterator<A> from, Function<A, B> mapping) {
return new Iterator<B>() {
@Override
public boolean hasNext() {
return from.hasNext();
}
@Override
public B next() {
return mapping.apply(from.next());
}
};
}
public interface Function<A, B> {
B apply(A item);
}
private static @Nullable <A> A firstOrNull(Iterator<A> iterator) {
if (iterator.hasNext()) {
return iterator.next();
}
return null;
}
private static @Nonnull <A> A firstOr(Iterator<A> iterator, A item) {
if (iterator.hasNext()) {
return iterator.next();
}
return item;
}
}

View File

@ -1,21 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
/**
* Strategy defining how missing secret key passphrases are handled.
*/
public enum MissingKeyPassphraseStrategy {
/**
* Try to interactively obtain key passphrases one-by-one via callbacks,
* eg {@link org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider}.
*/
INTERACTIVE,
/**
* Do not try to obtain passphrases interactively and instead throw a
* {@link org.pgpainless.exception.MissingPassphraseException} listing all keys with missing passphrases.
*/
THROW_EXCEPTION
}

View File

@ -1,33 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
public 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 {@link 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>
*/
@Nullable PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId);
}

View File

@ -45,6 +45,9 @@ 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")

View File

@ -1,380 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.key.OpenPgpFingerprint;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.util.SessionKey;
/**
* Legacy class containing metadata about an OpenPGP message.
* It is advised to use {@link MessageMetadata} instead.
*
* TODO: Remove in 1.6.X
*/
public class OpenPgpMetadata {
private final Set<Long> recipientKeyIds;
private final SubkeyIdentifier decryptionKey;
private final List<SignatureVerification> verifiedInbandSignatures;
private final List<SignatureVerification.Failure> invalidInbandSignatures;
private final List<SignatureVerification> verifiedDetachedSignatures;
private final List<SignatureVerification.Failure> invalidDetachedSignatures;
private final SessionKey sessionKey;
private final CompressionAlgorithm compressionAlgorithm;
private final String fileName;
private final Date modificationDate;
private final StreamEncoding fileEncoding;
private final boolean cleartextSigned;
public OpenPgpMetadata(Set<Long> recipientKeyIds,
SubkeyIdentifier decryptionKey,
SessionKey sessionKey,
CompressionAlgorithm algorithm,
List<SignatureVerification> verifiedInbandSignatures,
List<SignatureVerification.Failure> invalidInbandSignatures,
List<SignatureVerification> verifiedDetachedSignatures,
List<SignatureVerification.Failure> invalidDetachedSignatures,
String fileName,
Date modificationDate,
StreamEncoding fileEncoding,
boolean cleartextSigned) {
this.recipientKeyIds = Collections.unmodifiableSet(recipientKeyIds);
this.decryptionKey = decryptionKey;
this.sessionKey = sessionKey;
this.compressionAlgorithm = algorithm;
this.verifiedInbandSignatures = Collections.unmodifiableList(verifiedInbandSignatures);
this.invalidInbandSignatures = Collections.unmodifiableList(invalidInbandSignatures);
this.verifiedDetachedSignatures = Collections.unmodifiableList(verifiedDetachedSignatures);
this.invalidDetachedSignatures = Collections.unmodifiableList(invalidDetachedSignatures);
this.fileName = fileName;
this.modificationDate = modificationDate;
this.fileEncoding = fileEncoding;
this.cleartextSigned = cleartextSigned;
}
/**
* Return a set of key-ids the messages was encrypted for.
*
* @return recipient ids
*/
public @Nonnull Set<Long> getRecipientKeyIds() {
return recipientKeyIds;
}
/**
* Return true, if the message was encrypted.
*
* @return true if encrypted, false otherwise
*/
public boolean isEncrypted() {
return sessionKey != null && sessionKey.getAlgorithm() != SymmetricKeyAlgorithm.NULL;
}
/**
* Return the {@link SubkeyIdentifier} of the key that was used to decrypt the message.
* This can be null if the message was decrypted using a {@link org.pgpainless.util.Passphrase}, or if it was not
* encrypted at all (e.g. signed only).
*
* @return subkey identifier of decryption key
*/
public @Nullable SubkeyIdentifier getDecryptionKey() {
return decryptionKey;
}
/**
* Return the algorithm that was used to symmetrically encrypt the message.
*
* @return encryption algorithm
*/
public @Nullable SymmetricKeyAlgorithm getSymmetricKeyAlgorithm() {
return sessionKey == null ? null : sessionKey.getAlgorithm();
}
public @Nullable SessionKey getSessionKey() {
return sessionKey;
}
/**
* Return the {@link CompressionAlgorithm} that was used to compress the message.
*
* @return compression algorithm
*/
public @Nullable CompressionAlgorithm getCompressionAlgorithm() {
return compressionAlgorithm;
}
/**
* Return a set of all signatures on the message.
* Note: This method returns just the signatures. There is no guarantee that the signatures are verified or even correct.
*
* Use {@link #getVerifiedSignatures()} instead to get all verified signatures.
* @return unverified and verified signatures
*/
public @Nonnull Set<PGPSignature> getSignatures() {
Set<PGPSignature> signatures = new HashSet<>();
for (SignatureVerification v : getVerifiedDetachedSignatures()) {
signatures.add(v.getSignature());
}
for (SignatureVerification v : getVerifiedInbandSignatures()) {
signatures.add(v.getSignature());
}
for (SignatureVerification.Failure f : getInvalidDetachedSignatures()) {
signatures.add(f.getSignatureVerification().getSignature());
}
for (SignatureVerification.Failure f : getInvalidInbandSignatures()) {
signatures.add(f.getSignatureVerification().getSignature());
}
return signatures;
}
/**
* Return true if the message contained at least one signature.
*
* Note: This method does not reflect, whether the signature on the message is correct.
* Use {@link #isVerified()} instead to determine, if the message carries a verifiable signature.
*
* @return true if message contains at least one unverified or verified signature, false otherwise.
*/
public boolean isSigned() {
return !getSignatures().isEmpty();
}
/**
* Return a map of all verified signatures on the message.
* The map contains verified signatures as value, with the {@link SubkeyIdentifier} of the key that was used to verify
* the signature as the maps keys.
*
* @return verified detached and one-pass signatures
*/
public Map<SubkeyIdentifier, PGPSignature> getVerifiedSignatures() {
Map<SubkeyIdentifier, PGPSignature> verifiedSignatures = new ConcurrentHashMap<>();
for (SignatureVerification detachedSignature : getVerifiedDetachedSignatures()) {
verifiedSignatures.put(detachedSignature.getSigningKey(), detachedSignature.getSignature());
}
for (SignatureVerification inbandSignatures : verifiedInbandSignatures) {
verifiedSignatures.put(inbandSignatures.getSigningKey(), inbandSignatures.getSignature());
}
return verifiedSignatures;
}
public List<SignatureVerification> getVerifiedInbandSignatures() {
return verifiedInbandSignatures;
}
public List<SignatureVerification> getVerifiedDetachedSignatures() {
return verifiedDetachedSignatures;
}
public List<SignatureVerification.Failure> getInvalidInbandSignatures() {
return invalidInbandSignatures;
}
public List<SignatureVerification.Failure> getInvalidDetachedSignatures() {
return invalidDetachedSignatures;
}
/**
* Return true, if the message is signed and at least one signature on the message was verified successfully.
*
* @return true if message is verified, false otherwise
*/
public boolean isVerified() {
return !getVerifiedSignatures().isEmpty();
}
/**
* Return true, if the message contains at least one verified signature made by a key in the
* given certificate.
*
* @param certificate certificate
* @return true if message was signed by the certificate (and the signature is valid), false otherwise
*/
public boolean containsVerifiedSignatureFrom(PGPPublicKeyRing certificate) {
for (PGPPublicKey key : certificate) {
OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(key);
if (containsVerifiedSignatureFrom(fingerprint)) {
return true;
}
}
return false;
}
/**
* Return true, if the message contains at least one valid signature made by the key with the given
* fingerprint, false otherwise.
*
* The fingerprint might be of the signing subkey, or the primary key of the signing certificate.
*
* @param fingerprint fingerprint of primary key or signing subkey
* @return true if validly signed, false otherwise
*/
public boolean containsVerifiedSignatureFrom(OpenPgpFingerprint fingerprint) {
for (SubkeyIdentifier verifiedSigningKey : getVerifiedSignatures().keySet()) {
if (verifiedSigningKey.getPrimaryKeyFingerprint().equals(fingerprint) ||
verifiedSigningKey.getSubkeyFingerprint().equals(fingerprint)) {
return true;
}
}
return false;
}
/**
* Return the name of the encrypted / signed file.
*
* @return file name
*/
public String getFileName() {
return fileName;
}
/**
* Return true, if the encrypted data is intended for your eyes only.
*
* @return true if for-your-eyes-only
*/
public boolean isForYourEyesOnly() {
return PGPLiteralData.CONSOLE.equals(getFileName());
}
/**
* Return the modification date of the encrypted / signed file.
*
* @return modification date
*/
public Date getModificationDate() {
return modificationDate;
}
/**
* Return the encoding format of the encrypted / signed file.
*
* @return encoding
*/
public StreamEncoding getFileEncoding() {
return fileEncoding;
}
/**
* Return true if the message was signed using the cleartext signature framework.
*
* @return true if cleartext signed.
*/
public boolean isCleartextSigned() {
return cleartextSigned;
}
public static Builder getBuilder() {
return new Builder();
}
public static class Builder {
private final Set<Long> recipientFingerprints = new HashSet<>();
private SessionKey sessionKey;
private SubkeyIdentifier decryptionKey;
private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED;
private String fileName;
private StreamEncoding fileEncoding;
private Date modificationDate;
private boolean cleartextSigned = false;
private final List<SignatureVerification> verifiedInbandSignatures = new ArrayList<>();
private final List<SignatureVerification> verifiedDetachedSignatures = new ArrayList<>();
private final List<SignatureVerification.Failure> invalidInbandSignatures = new ArrayList<>();
private final List<SignatureVerification.Failure> invalidDetachedSignatures = new ArrayList<>();
public Builder addRecipientKeyId(Long keyId) {
this.recipientFingerprints.add(keyId);
return this;
}
public Builder setDecryptionKey(SubkeyIdentifier decryptionKey) {
this.decryptionKey = decryptionKey;
return this;
}
public Builder setSessionKey(SessionKey sessionKey) {
this.sessionKey = sessionKey;
return this;
}
public Builder setCompressionAlgorithm(CompressionAlgorithm algorithm) {
this.compressionAlgorithm = algorithm;
return this;
}
public Builder setFileName(@Nullable String fileName) {
this.fileName = fileName;
return this;
}
public Builder setModificationDate(Date modificationDate) {
this.modificationDate = modificationDate;
return this;
}
public Builder setFileEncoding(StreamEncoding encoding) {
this.fileEncoding = encoding;
return this;
}
public Builder addVerifiedInbandSignature(SignatureVerification signatureVerification) {
this.verifiedInbandSignatures.add(signatureVerification);
return this;
}
public Builder addVerifiedDetachedSignature(SignatureVerification signatureVerification) {
this.verifiedDetachedSignatures.add(signatureVerification);
return this;
}
public Builder addInvalidInbandSignature(SignatureVerification signatureVerification, SignatureValidationException e) {
this.invalidInbandSignatures.add(new SignatureVerification.Failure(signatureVerification, e));
return this;
}
public Builder addInvalidDetachedSignature(SignatureVerification signatureVerification, SignatureValidationException e) {
this.invalidDetachedSignatures.add(new SignatureVerification.Failure(signatureVerification, e));
return this;
}
public Builder setCleartextSigned() {
this.cleartextSigned = true;
return this;
}
public OpenPgpMetadata build() {
return new OpenPgpMetadata(
recipientFingerprints, decryptionKey,
sessionKey, compressionAlgorithm,
verifiedInbandSignatures, invalidInbandSignatures,
verifiedDetachedSignatures, invalidDetachedSignatures,
fileName, modificationDate, fileEncoding, cleartextSigned);
}
}
}

View File

@ -1,106 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import javax.annotation.Nullable;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.util.encoders.Hex;
import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.key.SubkeyIdentifier;
/**
* 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. {@link OpenPgpMetadata#getVerifiedInbandSignatures()} contains verified verifications,
* while the class {@link Failure} contains failed verifications.
*/
public class SignatureVerification {
private final PGPSignature signature;
private final SubkeyIdentifier signingKey;
/**
* Construct a verification tuple.
*
* @param signature PGPSignature object
* @param signingKey identifier of the signing key
*/
public SignatureVerification(PGPSignature signature, @Nullable SubkeyIdentifier signingKey) {
this.signature = signature;
this.signingKey = signingKey;
}
/**
* Return the {@link PGPSignature}.
*
* @return signature
*/
public PGPSignature getSignature() {
return signature;
}
/**
* Return a {@link SubkeyIdentifier} of the (sub-) key that is used for signature verification.
* Note, that this method might return null, e.g. in case of a {@link Failure} due to missing verification key.
*
* @return verification key identifier
*/
@Nullable
public SubkeyIdentifier getSigningKey() {
return signingKey;
}
@Override
public String toString() {
return "Signature: " + (signature != null ? Hex.toHexString(signature.getDigestPrefix()) : "null")
+ "; Key: " + (signingKey != null ? signingKey.toString() : "null") + ";";
}
/**
* Tuple object of a {@link SignatureVerification} and the corresponding {@link SignatureValidationException}
* that caused the verification to fail.
*/
public static class Failure {
private final SignatureVerification signatureVerification;
private final SignatureValidationException validationException;
/**
* Construct a signature verification failure object.
*
* @param verification verification
* @param validationException exception that caused the verification to fail
*/
public Failure(SignatureVerification verification, SignatureValidationException validationException) {
this.signatureVerification = verification;
this.validationException = validationException;
}
/**
* Return the verification (tuple of {@link PGPSignature} and corresponding {@link SubkeyIdentifier})
* of the signing/verification key.
*
* @return verification
*/
public SignatureVerification getSignatureVerification() {
return signatureVerification;
}
/**
* Return the {@link SignatureValidationException} that caused the verification to fail.
*
* @return exception
*/
public SignatureValidationException getValidationException() {
return validationException;
}
@Override
public String toString() {
return signatureVerification.toString() + " Failure: " + getValidationException().getMessage();
}
}
}

View File

@ -1,159 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.bouncycastle.bcpg.BCPGInputStream;
import org.bouncycastle.bcpg.MarkerPacket;
import org.bouncycastle.bcpg.Packet;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPOnePassSignature;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.algorithm.OpenPgpPacket;
import javax.annotation.Nonnull;
/**
* Since we need to update signatures with data from the underlying stream, this class is used to tee out the data.
* Unfortunately we cannot simply override {@link BCPGInputStream#read()} to tee the data out though, since
* {@link BCPGInputStream#readPacket()} inconsistently calls a mix of {@link BCPGInputStream#read()} and
* {@link InputStream#read()} of the underlying stream. This would cause the second length byte to get swallowed up.
*
* Therefore, this class delegates the teeing to an {@link DelayedTeeInputStream} which wraps the underlying
* stream. Since calling {@link BCPGInputStream#nextPacketTag()} reads up to and including the next packets tag,
* we need to delay teeing out that byte to signature verifiers.
* Hence, the reading methods of the {@link TeeBCPGInputStream} handle pushing this byte to the output stream using
* {@link DelayedTeeInputStream#squeeze()}.
*/
public class TeeBCPGInputStream {
protected final DelayedTeeInputStream delayedTee;
// InputStream of OpenPGP packets of the current layer
protected final BCPGInputStream packetInputStream;
public TeeBCPGInputStream(BCPGInputStream inputStream, OutputStream outputStream) {
this.delayedTee = new DelayedTeeInputStream(inputStream, outputStream);
this.packetInputStream = BCPGInputStream.wrap(delayedTee);
}
public OpenPgpPacket nextPacketTag() throws IOException {
int tag = packetInputStream.nextPacketTag();
if (tag == -1) {
return null;
}
return OpenPgpPacket.requireFromTag(tag);
}
public Packet readPacket() throws IOException {
return packetInputStream.readPacket();
}
public PGPCompressedData readCompressedData() throws IOException {
delayedTee.squeeze();
PGPCompressedData compressedData = new PGPCompressedData(packetInputStream);
return compressedData;
}
public PGPLiteralData readLiteralData() throws IOException {
delayedTee.squeeze();
return new PGPLiteralData(packetInputStream);
}
public PGPEncryptedDataList readEncryptedDataList() throws IOException {
delayedTee.squeeze();
return new PGPEncryptedDataList(packetInputStream);
}
public PGPOnePassSignature readOnePassSignature() throws PGPException, IOException {
PGPOnePassSignature onePassSignature = new PGPOnePassSignature(packetInputStream);
delayedTee.squeeze();
return onePassSignature;
}
public PGPSignature readSignature() throws PGPException, IOException {
PGPSignature signature = new PGPSignature(packetInputStream);
delayedTee.squeeze();
return signature;
}
public MarkerPacket readMarker() throws IOException {
MarkerPacket markerPacket = (MarkerPacket) readPacket();
delayedTee.squeeze();
return markerPacket;
}
public void close() throws IOException {
this.packetInputStream.close();
}
public static class DelayedTeeInputStream extends InputStream {
private int last = -1;
private final InputStream inputStream;
private final OutputStream outputStream;
public DelayedTeeInputStream(InputStream inputStream, OutputStream outputStream) {
this.inputStream = inputStream;
this.outputStream = outputStream;
}
@Override
public int read() throws IOException {
if (last != -1) {
outputStream.write(last);
}
try {
last = inputStream.read();
return last;
} catch (IOException e) {
if (e.getMessage().contains("crc check failed in armored message")) {
throw e;
}
return -1;
}
}
@Override
public int read(@Nonnull byte[] b, int off, int len) throws IOException {
if (last != -1) {
outputStream.write(last);
}
int r = inputStream.read(b, off, len);
if (r > 0) {
outputStream.write(b, off, r - 1);
last = b[off + r - 1];
} else {
last = -1;
}
return r;
}
/**
* Squeeze the last byte out and update the output stream.
*
* @throws IOException in case of an IO error
*/
public void squeeze() throws IOException {
if (last != -1) {
outputStream.write(last);
}
last = -1;
}
@Override
public void close() throws IOException {
inputStream.close();
outputStream.close();
}
}
}

View File

@ -1,169 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.cleartext_signatures;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.util.Strings;
import org.pgpainless.exception.WrongConsumingMethodException;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.util.ArmoredInputStreamFactory;
/**
* Utility class to deal with cleartext-signed messages.
* Based on Bouncycastle's {@link org.bouncycastle.openpgp.examples.ClearSignedFileProcessor}.
*/
public final class ClearsignedMessageUtil {
private ClearsignedMessageUtil() {
}
/**
* Dearmor a clearsigned message, detach the inband signatures and write the plaintext message to the provided
* messageOutputStream.
*
* @param clearsignedInputStream input stream containing a clearsigned message
* @param messageOutputStream output stream to which the dearmored message shall be written
* @return signatures
*
* @throws IOException if the message is not clearsigned or some other IO error happens
* @throws WrongConsumingMethodException in case the armored message is not cleartext signed
*/
public static PGPSignatureList detachSignaturesFromInbandClearsignedMessage(InputStream clearsignedInputStream,
OutputStream messageOutputStream)
throws IOException, WrongConsumingMethodException {
ArmoredInputStream in;
if (clearsignedInputStream instanceof ArmoredInputStream) {
in = (ArmoredInputStream) clearsignedInputStream;
} else {
in = ArmoredInputStreamFactory.get(clearsignedInputStream);
}
if (!in.isClearText()) {
throw new WrongConsumingMethodException("Message is not using the Cleartext Signature Framework.");
}
OutputStream out = new BufferedOutputStream(messageOutputStream);
try {
ByteArrayOutputStream lineOut = new ByteArrayOutputStream();
int lookAhead = readInputLine(lineOut, in);
byte[] lineSep = getLineSeparator();
if (lookAhead != -1 && in.isClearText()) {
byte[] line = lineOut.toByteArray();
out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line));
while (lookAhead != -1 && in.isClearText()) {
lookAhead = readInputLine(lineOut, lookAhead, in);
line = lineOut.toByteArray();
out.write(lineSep);
out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line));
}
} else {
if (lookAhead != -1) {
byte[] line = lineOut.toByteArray();
out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line));
}
}
} finally {
out.close();
}
PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(in);
PGPSignatureList signatures = (PGPSignatureList) objectFactory.nextObject();
return signatures;
}
public static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn)
throws IOException {
bOut.reset();
int lookAhead = -1;
int ch;
while ((ch = fIn.read()) >= 0) {
bOut.write(ch);
if (ch == '\r' || ch == '\n') {
lookAhead = readPassedEOL(bOut, ch, fIn);
break;
}
}
return lookAhead;
}
public static int readInputLine(ByteArrayOutputStream bOut, int lookAhead, InputStream fIn)
throws IOException {
bOut.reset();
int ch = lookAhead;
do {
bOut.write(ch);
if (ch == '\r' || ch == '\n') {
lookAhead = readPassedEOL(bOut, ch, fIn);
break;
}
}
while ((ch = fIn.read()) >= 0);
if (ch < 0) {
lookAhead = -1;
}
return lookAhead;
}
private static int readPassedEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn)
throws IOException {
int lookAhead = fIn.read();
if (lastCh == '\r' && lookAhead == '\n') {
bOut.write(lookAhead);
lookAhead = fIn.read();
}
return lookAhead;
}
private static byte[] getLineSeparator() {
String nl = Strings.lineSeparator();
byte[] nlBytes = new byte[nl.length()];
for (int i = 0; i != nlBytes.length; i++) {
nlBytes[i] = (byte) nl.charAt(i);
}
return nlBytes;
}
private static int getLengthWithoutSeparatorOrTrailingWhitespace(byte[] line) {
int end = line.length - 1;
while (end >= 0 && isWhiteSpace(line[end])) {
end--;
}
return end + 1;
}
private static boolean isLineEnding(byte b) {
return b == '\r' || b == '\n';
}
private static boolean isWhiteSpace(byte b) {
return isLineEnding(b) || b == '\t' || b == ' ';
}
}

View File

@ -1,35 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.cleartext_signatures;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
/**
* Implementation of the {@link MultiPassStrategy}.
* This class keeps the read data in memory by caching the data inside a {@link ByteArrayOutputStream}.
*
* Note, that this class is suitable and efficient for processing small amounts of data.
* For larger data like encrypted files, use of the {@link WriteToFileMultiPassStrategy} is recommended to
* prevent {@link OutOfMemoryError OutOfMemoryErrors} and other issues.
*/
public class InMemoryMultiPassStrategy implements MultiPassStrategy {
private final ByteArrayOutputStream cache = new ByteArrayOutputStream();
@Override
public ByteArrayOutputStream getMessageOutputStream() {
return cache;
}
@Override
public ByteArrayInputStream getMessageInputStream() {
return new ByteArrayInputStream(getBytes());
}
public byte[] getBytes() {
return getMessageOutputStream().toByteArray();
}
}

View File

@ -1,70 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.cleartext_signatures;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Since for verification of cleartext signed messages, we need to read the whole data twice in order to verify signatures,
* a strategy for how to cache the read data is required.
* Otherwise, large data kept in memory could cause {@link OutOfMemoryError OutOfMemoryErrors} or other issues.
*
* This is an Interface that describes a strategy to deal with the fact that detached signatures require multiple passes
* to do verification.
*
* This interface can be used to write the signed data stream out via {@link #getMessageOutputStream()} and later
* get access to the data again via {@link #getMessageInputStream()}.
* Thereby the detail where the data is being stored (memory, file, etc.) can be abstracted away.
*/
public interface MultiPassStrategy {
/**
* Provide an {@link OutputStream} into which the signed data can be read into.
*
* @return output stream
* @throws IOException io error
*/
OutputStream getMessageOutputStream() throws IOException;
/**
* Provide an {@link InputStream} which contains the data that was previously written away in
* {@link #getMessageOutputStream()}.
*
* As there may be multiple signatures that need to be processed, each call of this method MUST return
* a new {@link InputStream}.
*
* @return input stream
* @throws IOException io error
*/
InputStream getMessageInputStream() throws IOException;
/**
* Write the message content out to a file and re-read it to verify signatures.
* This strategy is best suited for larger messages (e.g. plaintext signed files) which might not fit into memory.
* After the message has been processed completely, the messages content are available at the provided file.
*
* @param file target file
* @return strategy
*/
static MultiPassStrategy writeMessageToFile(File file) {
return new WriteToFileMultiPassStrategy(file);
}
/**
* Read the message content into memory.
* This strategy is best suited for small messages which fit into memory.
* After the message has been processed completely, the message content can be accessed by calling
* {@link ByteArrayOutputStream#toByteArray()} on {@link #getMessageOutputStream()}.
*
* @return strategy
*/
static InMemoryMultiPassStrategy keepMessageInMemory() {
return new InMemoryMultiPassStrategy();
}
}

View File

@ -1,54 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.cleartext_signatures;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Implementation of the {@link MultiPassStrategy}.
* When processing signed data the first time, the data is being written out into a file.
* For the second pass, that file is being read again.
*
* This strategy is recommended when larger amounts of data need to be processed.
* For smaller files, {@link InMemoryMultiPassStrategy} yields higher efficiency.
*/
public class WriteToFileMultiPassStrategy implements MultiPassStrategy {
private final File file;
/**
* Create a {@link MultiPassStrategy} which writes data to a file.
* Note that {@link #getMessageOutputStream()} will create the file if necessary.
*
* @param file file to write the data to and read from
*/
public WriteToFileMultiPassStrategy(File file) {
this.file = file;
}
@Override
public OutputStream getMessageOutputStream() throws IOException {
if (!file.exists()) {
boolean created = file.createNewFile();
if (!created) {
throw new IOException("New file '" + file.getAbsolutePath() + "' was not created.");
}
}
return new FileOutputStream(file);
}
@Override
public InputStream getMessageInputStream() throws IOException {
if (!file.exists()) {
throw new IOException("File '" + file.getAbsolutePath() + "' does no longer exist.");
}
return new FileInputStream(file);
}
}

View File

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Classes related to cleartext signature verification.
*/
package org.pgpainless.decryption_verification.cleartext_signatures;

View File

@ -1,45 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPSignatureList;
public enum InputSymbol {
/**
* A {@link PGPLiteralData} packet.
*/
LiteralData,
/**
* A {@link PGPSignatureList} object.
*/
Signature,
/**
* A {@link PGPOnePassSignatureList} object.
*/
OnePassSignature,
/**
* A {@link PGPCompressedData} packet.
* The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify
* its nested packet sequence.
*/
CompressedData,
/**
* A {@link PGPEncryptedDataList} object.
* This object combines multiple ESKs and the corresponding Symmetrically Encrypted
* (possibly Integrity Protected) Data packet.
*/
EncryptedData,
/**
* Marks the end of a (sub-) sequence.
* This input is given if the end of an OpenPGP message is reached.
* This might be the case for the end of the whole ciphertext, or the end of a packet with nested contents
* (e.g. the end of a Compressed Data packet).
*/
EndOfSequence
}

View File

@ -1,142 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* This class describes the syntax for OpenPGP messages as specified by rfc4880.
*
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-11.3">
* rfc4880 - §11.3. OpenPGP Messages</a>
* @see <a href="https://blog.jabberhead.tk/2022/09/14/using-pushdown-automata-to-verify-packet-sequences/">
* Blog post about theoretic background and translation of grammar to PDA syntax</a>
* @see <a href="https://blog.jabberhead.tk/2022/10/26/implementing-packet-sequence-validation-using-pushdown-automata/">
* Blog post about practically implementing the PDA for packet syntax validation</a>
*/
public class OpenPgpMessageSyntax implements Syntax {
@Override
public @Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem)
throws MalformedOpenPgpMessageException {
switch (from) {
case OpenPgpMessage:
return fromOpenPgpMessage(input, stackItem);
case LiteralMessage:
return fromLiteralMessage(input, stackItem);
case CompressedMessage:
return fromCompressedMessage(input, stackItem);
case EncryptedMessage:
return fromEncryptedMessage(input, stackItem);
case Valid:
return fromValid(input, stackItem);
}
throw new MalformedOpenPgpMessageException(from, input, stackItem);
}
@Nonnull
Transition fromOpenPgpMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem)
throws MalformedOpenPgpMessageException {
if (stackItem != StackSymbol.msg) {
throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem);
}
switch (input) {
case LiteralData:
return new Transition(State.LiteralMessage);
case Signature:
return new Transition(State.OpenPgpMessage, StackSymbol.msg);
case OnePassSignature:
return new Transition(State.OpenPgpMessage, StackSymbol.ops, StackSymbol.msg);
case CompressedData:
return new Transition(State.CompressedMessage);
case EncryptedData:
return new Transition(State.EncryptedMessage);
case EndOfSequence:
default:
throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem);
}
}
@Nonnull
Transition fromLiteralMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem)
throws MalformedOpenPgpMessageException {
switch (input) {
case Signature:
if (stackItem == StackSymbol.ops) {
return new Transition(State.LiteralMessage);
}
break;
case EndOfSequence:
if (stackItem == StackSymbol.terminus) {
return new Transition(State.Valid);
}
break;
}
throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem);
}
@Nonnull
Transition fromCompressedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem)
throws MalformedOpenPgpMessageException {
switch (input) {
case Signature:
if (stackItem == StackSymbol.ops) {
return new Transition(State.CompressedMessage);
}
break;
case EndOfSequence:
if (stackItem == StackSymbol.terminus) {
return new Transition(State.Valid);
}
break;
}
throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem);
}
@Nonnull
Transition fromEncryptedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem)
throws MalformedOpenPgpMessageException {
switch (input) {
case Signature:
if (stackItem == StackSymbol.ops) {
return new Transition(State.EncryptedMessage);
}
break;
case EndOfSequence:
if (stackItem == StackSymbol.terminus) {
return new Transition(State.Valid);
}
break;
}
throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem);
}
@Nonnull
Transition fromValid(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem)
throws MalformedOpenPgpMessageException {
if (input == InputSymbol.EndOfSequence) {
// allow subsequent read() calls.
return new Transition(State.Valid);
}
// There is no applicable transition rule out of Valid
throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem);
}
}

View File

@ -1,156 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.msg;
import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.terminus;
/**
* Pushdown Automaton for validating context-free languages.
* In PGPainless, this class is used to validate OpenPGP message packet sequences against the allowed syntax.
*
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-11.3">OpenPGP Message Syntax</a>
*/
public class PDA {
private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class);
// right now we implement what rfc4880 specifies.
// TODO: Consider implementing what we proposed here:
// https://mailarchive.ietf.org/arch/msg/openpgp/uepOF6XpSegMO4c59tt9e5H1i4g/
private final Syntax syntax;
private final Stack<StackSymbol> stack = new Stack<>();
private final List<InputSymbol> inputs = new ArrayList<>(); // Track inputs for debugging / error reporting
private State state;
/**
* Default constructor which initializes the PDA to work with the {@link OpenPgpMessageSyntax}.
*/
public PDA() {
this(new OpenPgpMessageSyntax(), State.OpenPgpMessage, terminus, msg);
}
/**
* Construct a PDA with a custom {@link Syntax}, initial {@link State} and initial {@link StackSymbol StackSymbols}.
*
* @param syntax syntax
* @param initialState initial state
* @param initialStack zero or more initial stack items (get pushed onto the stack in order of appearance)
*/
public PDA(@Nonnull Syntax syntax, @Nonnull State initialState, @Nonnull StackSymbol... initialStack) {
this.syntax = syntax;
this.state = initialState;
for (StackSymbol symbol : initialStack) {
pushStack(symbol);
}
}
/**
* Process the next {@link InputSymbol}.
* This will either leave the PDA in the next state, or throw a {@link MalformedOpenPgpMessageException} if the
* input symbol is rejected.
*
* @param input input symbol
* @throws MalformedOpenPgpMessageException if the input symbol is rejected
*/
public void next(@Nonnull InputSymbol input)
throws MalformedOpenPgpMessageException {
StackSymbol stackSymbol = popStack();
try {
Transition transition = syntax.transition(state, input, stackSymbol);
state = transition.getNewState();
for (StackSymbol item : transition.getPushedItems()) {
pushStack(item);
}
inputs.add(input);
} catch (MalformedOpenPgpMessageException e) {
MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException(
"Malformed message: After reading packet sequence " + Arrays.toString(inputs.toArray()) +
", token '" + input + "' is not allowed." +
"\nNo transition from state '" + state + "' with stack " + Arrays.toString(stack.toArray()) +
(stackSymbol != null ? "||'" + stackSymbol + "'." : "."), e);
LOGGER.debug("Invalid input '" + input + "'", wrapped);
throw wrapped;
}
}
/**
* Return the current state of the PDA.
*
* @return state
*/
public @Nonnull State getState() {
return state;
}
/**
* Peek at the stack, returning the topmost stack item without changing the stack.
*
* @return topmost stack item, or null if stack is empty
*/
public @Nullable StackSymbol peekStack() {
if (stack.isEmpty()) {
return null;
}
return stack.peek();
}
/**
* Return true, if the PDA is in a valid state (the OpenPGP message is valid).
*
* @return true if valid, false otherwise
*/
public boolean isValid() {
return getState() == State.Valid && stack.isEmpty();
}
/**
* Throw a {@link MalformedOpenPgpMessageException} if the pda is not in a valid state right now.
*
* @throws MalformedOpenPgpMessageException if the pda is not in an acceptable state
*/
public void assertValid() throws MalformedOpenPgpMessageException {
if (!isValid()) {
throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString());
}
}
/**
* Pop an item from the stack.
*
* @return stack item
*/
private StackSymbol popStack() {
if (stack.isEmpty()) {
return null;
}
return stack.pop();
}
/**
* Push an item onto the stack.
*
* @param item item
*/
private void pushStack(StackSymbol item) {
stack.push(item);
}
@Override
public String toString() {
return "State: " + state + " Stack: " + stack;
}
}

View File

@ -1,20 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
public enum StackSymbol {
/**
* OpenPGP Message.
*/
msg,
/**
* OnePassSignature (in case of BC this represents a OnePassSignatureList).
*/
ops,
/**
* Special symbol representing the end of the message.
*/
terminus
}

View File

@ -1,16 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
/**
* Set of states of the automaton.
*/
public enum State {
OpenPgpMessage,
LiteralMessage,
CompressedMessage,
EncryptedMessage,
Valid
}

View File

@ -1,33 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
import org.pgpainless.exception.MalformedOpenPgpMessageException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* This interface can be used to define a custom syntax for the {@link PDA}.
*/
public interface Syntax {
/**
* Describe a transition rule from {@link State} <pre>from</pre> for {@link InputSymbol} <pre>input</pre>
* with {@link StackSymbol} <pre>stackItem</pre> from the top of the {@link PDA PDAs} stack.
* The resulting {@link Transition} contains the new {@link State}, as well as a list of
* {@link StackSymbol StackSymbols} that get pushed onto the stack by the transition rule.
* If there is no applicable rule, a {@link MalformedOpenPgpMessageException} is thrown, since in this case
* the {@link InputSymbol} must be considered illegal.
*
* @param from current state of the PDA
* @param input input symbol
* @param stackItem item that got popped from the top of the stack
* @return applicable transition rule containing the new state and pushed stack symbols
* @throws MalformedOpenPgpMessageException if there is no applicable transition rule (the input symbol is illegal)
*/
@Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem)
throws MalformedOpenPgpMessageException;
}

View File

@ -1,48 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification.syntax_check;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Result of applying a transition rule.
* Transition rules can be described by implementing the {@link Syntax} interface.
*/
public class Transition {
private final List<StackSymbol> pushedItems = new ArrayList<>();
private final State newState;
public Transition(@Nonnull State newState, @Nonnull StackSymbol... pushedItems) {
this.newState = newState;
this.pushedItems.addAll(Arrays.asList(pushedItems));
}
/**
* Return the new {@link State} that is reached by applying the transition.
*
* @return new state
*/
@Nonnull
public State getNewState() {
return newState;
}
/**
* Return a list of {@link StackSymbol StackSymbols} that are pushed onto the stack
* by applying the transition.
* The list contains items in the order in which they are pushed onto the stack.
* The list may be empty.
*
* @return list of items to be pushed onto the stack
*/
@Nonnull
public List<StackSymbol> getPushedItems() {
return new ArrayList<>(pushedItems);
}
}

View File

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Pushdown Automaton to verify validity of packet sequences according to the OpenPGP Message format.
*/
package org.pgpainless.decryption_verification.syntax_check;

View File

@ -1,66 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.encryption_signing;
import java.security.MessageDigest;
import java.util.List;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.SignatureType;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.key.protection.UnlockSecretKey;
import javax.annotation.Nonnull;
public class BcHashContextSigner {
public static PGPSignature signHashContext(@Nonnull MessageDigest hashContext,
@Nonnull SignatureType signatureType,
@Nonnull PGPSecretKeyRing secretKeys,
@Nonnull SecretKeyRingProtector protector)
throws PGPException {
KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys);
List<PGPPublicKey> signingSubkeyCandidates = info.getSigningSubkeys();
PGPSecretKey signingKey = null;
for (PGPPublicKey signingKeyCandidate : signingSubkeyCandidates) {
signingKey = secretKeys.getSecretKey(signingKeyCandidate.getKeyID());
if (signingKey != null) {
break;
}
}
if (signingKey == null) {
throw new PGPException("Key does not contain suitable signing subkey.");
}
PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(signingKey, protector);
return signHashContext(hashContext, signatureType, privateKey);
}
/**
* Create an OpenPGP Signature over the given {@link MessageDigest} hash context.
*
* @param hashContext hash context
* @param privateKey signing-capable key
* @return signature
* @throws PGPException in case of an OpenPGP error
*/
static PGPSignature signHashContext(MessageDigest hashContext, SignatureType signatureType, PGPPrivateKey privateKey)
throws PGPException {
PGPSignatureGenerator sigGen = new PGPSignatureGenerator(
new BcPGPHashContextContentSignerBuilder(hashContext)
);
sigGen.init(signatureType.getCode(), privateKey);
return sigGen.generate();
}
}

View File

@ -1,174 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.encryption_signing;
import java.io.OutputStream;
import java.security.MessageDigest;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.Signer;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.signers.DSADigestSigner;
import org.bouncycastle.crypto.signers.DSASigner;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import org.bouncycastle.crypto.signers.Ed448Signer;
import org.bouncycastle.crypto.signers.RSADigestSigner;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.operator.PGPContentSigner;
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter;
import org.bouncycastle.util.Arrays;
import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.algorithm.PublicKeyAlgorithm;
/**
* Implementation of {@link PGPContentSignerBuilder} using the BC API, which can be used to sign hash contexts.
* This can come in handy to sign data, which was already processed to calculate the hash context, without the
* need to process it again to calculate the OpenPGP signature.
*/
class BcPGPHashContextContentSignerBuilder extends PGPHashContextContentSignerBuilder {
private final BcPGPKeyConverter keyConverter = new BcPGPKeyConverter();
private final MessageDigest messageDigest;
private final HashAlgorithm hashAlgorithm;
BcPGPHashContextContentSignerBuilder(MessageDigest messageDigest) {
this.messageDigest = messageDigest;
this.hashAlgorithm = requireFromName(messageDigest.getAlgorithm());
}
private static HashAlgorithm requireFromName(String digestName) {
HashAlgorithm hashAlgorithm = HashAlgorithm.fromName(digestName);
if (hashAlgorithm == null) {
throw new IllegalArgumentException("Cannot recognize OpenPGP Hash Algorithm: " + digestName);
}
return hashAlgorithm;
}
@Override
public PGPContentSigner build(int signatureType, PGPPrivateKey privateKey) throws PGPException {
PublicKeyAlgorithm keyAlgorithm = PublicKeyAlgorithm.requireFromId(privateKey.getPublicKeyPacket().getAlgorithm());
AsymmetricKeyParameter privKeyParam = keyConverter.getPrivateKey(privateKey);
final Signer signer = createSigner(keyAlgorithm, messageDigest, privKeyParam);
signer.init(true, privKeyParam);
return new PGPContentSigner() {
public int getType() {
return signatureType;
}
public int getHashAlgorithm() {
return hashAlgorithm.getAlgorithmId();
}
public int getKeyAlgorithm() {
return keyAlgorithm.getAlgorithmId();
}
public long getKeyID() {
return privateKey.getKeyID();
}
public OutputStream getOutputStream() {
return new PGPHashContextContentSignerBuilder.SignerOutputStream(signer);
}
public byte[] getSignature() {
try {
return signer.generateSignature();
} catch (CryptoException e) {
throw new IllegalStateException("unable to create signature");
}
}
public byte[] getDigest() {
return messageDigest.digest();
}
};
}
static Signer createSigner(
PublicKeyAlgorithm keyAlgorithm,
MessageDigest messageDigest,
CipherParameters keyParam)
throws PGPException {
ExistingMessageDigest staticDigest = new ExistingMessageDigest(messageDigest);
switch (keyAlgorithm.getAlgorithmId()) {
case PublicKeyAlgorithmTags.RSA_GENERAL:
case PublicKeyAlgorithmTags.RSA_SIGN:
return new RSADigestSigner(staticDigest);
case PublicKeyAlgorithmTags.DSA:
return new DSADigestSigner(new DSASigner(), staticDigest);
case PublicKeyAlgorithmTags.ECDSA:
return new DSADigestSigner(new ECDSASigner(), staticDigest);
case PublicKeyAlgorithmTags.EDDSA:
if (keyParam instanceof Ed25519PrivateKeyParameters || keyParam instanceof Ed25519PublicKeyParameters) {
return new EdDsaSigner(new Ed25519Signer(), staticDigest);
}
return new EdDsaSigner(new Ed448Signer(new byte[0]), staticDigest);
default:
throw new PGPException("cannot recognise keyAlgorithm: " + keyAlgorithm);
}
}
// Copied from BCs BcImplProvider - required since BCs class is package visible only :/
private static class EdDsaSigner
implements Signer {
private final Signer signer;
private final Digest digest;
private final byte[] digBuf;
EdDsaSigner(Signer signer, Digest digest) {
this.signer = signer;
this.digest = digest;
this.digBuf = new byte[digest.getDigestSize()];
}
public void init(boolean forSigning, CipherParameters param) {
this.signer.init(forSigning, param);
this.digest.reset();
}
public void update(byte b) {
this.digest.update(b);
}
public void update(byte[] in, int off, int len) {
this.digest.update(in, off, len);
}
public byte[] generateSignature()
throws CryptoException, DataLengthException {
digest.doFinal(digBuf, 0);
signer.update(digBuf, 0, digBuf.length);
return signer.generateSignature();
}
public boolean verifySignature(byte[] signature) {
digest.doFinal(digBuf, 0);
signer.update(digBuf, 0, digBuf.length);
return signer.verifySignature(signature);
}
public void reset() {
Arrays.clear(digBuf);
signer.reset();
digest.reset();
}
}
}

View File

@ -1,55 +0,0 @@
// SPDX-FileCopyrightText: 2021 David Hook <dgh@cryptoworkshop.com>
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.encryption_signing;
import org.pgpainless.algorithm.StreamEncoding;
import java.io.IOException;
import java.io.OutputStream;
/**
* {@link OutputStream} which applies CR-LF encoding of its input data, based on the desired {@link StreamEncoding}.
* This implementation originates from the Bouncy Castle library.
*/
public class CRLFGeneratorStream extends OutputStream {
protected final OutputStream crlfOut;
private final boolean isBinary;
private int lastB = 0;
public CRLFGeneratorStream(OutputStream crlfOut, StreamEncoding encoding) {
this.crlfOut = crlfOut;
this.isBinary = encoding == StreamEncoding.BINARY;
}
public void write(int b) throws IOException {
if (!isBinary) {
if (b == '\n' && lastB != '\r') { // Unix
crlfOut.write('\r');
} else if (lastB == '\r') { // MAC
if (b != '\n') {
crlfOut.write('\n');
}
}
lastB = b;
}
crlfOut.write(b);
}
public void close() throws IOException {
if (!isBinary && lastB == '\r') { // MAC
crlfOut.write('\n');
}
crlfOut.close();
}
@Override
public void flush() throws IOException {
super.flush();
crlfOut.flush();
}
}

View File

@ -1,77 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.encryption_signing;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.annotation.Nonnull;
import org.bouncycastle.openpgp.PGPException;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator;
import org.pgpainless.key.SubkeyIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EncryptionBuilder implements EncryptionBuilderInterface {
private static final Logger LOGGER = LoggerFactory.getLogger(EncryptionBuilder.class);
private OutputStream outputStream;
@Override
public WithOptions onOutputStream(@Nonnull OutputStream outputStream) {
this.outputStream = outputStream;
return new WithOptionsImpl();
}
class WithOptionsImpl implements WithOptions {
@Override
public EncryptionStream withOptions(ProducerOptions options) throws PGPException, IOException {
if (options == null) {
throw new NullPointerException("ProducerOptions cannot be null.");
}
return new EncryptionStream(outputStream, options);
}
}
/**
* Negotiate the {@link SymmetricKeyAlgorithm} used for message encryption.
*
* @param encryptionOptions encryption options
* @return negotiated symmetric key algorithm
*/
public static SymmetricKeyAlgorithm negotiateSymmetricEncryptionAlgorithm(EncryptionOptions encryptionOptions) {
List<Set<SymmetricKeyAlgorithm>> preferences = new ArrayList<>();
for (SubkeyIdentifier key : encryptionOptions.getKeyViews().keySet()) {
preferences.add(encryptionOptions.getKeyViews().get(key).getPreferredSymmetricKeyAlgorithms());
}
SymmetricKeyAlgorithm algorithm = SymmetricKeyAlgorithmNegotiator
.byPopularity()
.negotiate(
PGPainless.getPolicy().getSymmetricKeyEncryptionAlgorithmPolicy(),
encryptionOptions.getEncryptionAlgorithmOverride(),
preferences);
LOGGER.debug("Negotiation resulted in {} being the symmetric encryption algorithm of choice.", algorithm);
return algorithm;
}
public static CompressionAlgorithm negotiateCompressionAlgorithm(ProducerOptions producerOptions) {
CompressionAlgorithm compressionAlgorithmOverride = producerOptions.getCompressionAlgorithmOverride();
if (compressionAlgorithmOverride != null) {
return compressionAlgorithmOverride;
}
// TODO: Negotiation
return PGPainless.getPolicy().getCompressionAlgorithmPolicy().defaultCompressionAlgorithm();
}
}

View File

@ -1,38 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.encryption_signing;
import java.io.IOException;
import java.io.OutputStream;
import javax.annotation.Nonnull;
import org.bouncycastle.openpgp.PGPException;
public interface EncryptionBuilderInterface {
/**
* Create a {@link EncryptionStream} on an {@link OutputStream} that contains the plain data that
* shall be encrypted and or signed.
*
* @param outputStream output stream of the plain data.
* @return api handle
*/
WithOptions onOutputStream(@Nonnull OutputStream outputStream);
interface WithOptions {
/**
* Create an {@link EncryptionStream} with the given options (recipients, signers, algorithms...).
*
* @param options options
* @return encryption stream
*
* @throws PGPException if something goes wrong during encryption stream preparation
* @throws IOException if something goes wrong during encryption stream preparation (writing headers)
*/
EncryptionStream withOptions(ProducerOptions options) throws PGPException, IOException;
}
}

View File

@ -1,391 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.encryption_signing;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.annotation.Nonnull;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator;
import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator;
import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator;
import org.pgpainless.algorithm.EncryptionPurpose;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.exception.KeyException;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.OpenPgpFingerprint;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.key.info.KeyAccessor;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.util.Passphrase;
/**
* Options for the encryption process.
* This class can be used to set encryption parameters, like encryption keys and passphrases, algorithms etc.
* <p>
* A typical use might look like follows:
* <pre>
* {@code
* EncryptionOptions opt = new EncryptionOptions();
* opt.addRecipient(aliceKey, "Alice <alice@wonderland.lit>");
* opt.addPassphrase(Passphrase.fromPassword("AdditionalDecryptionPassphrase123"));
* }
* </pre>
*<p>
* To use a custom symmetric encryption algorithm, use {@link #overrideEncryptionAlgorithm(SymmetricKeyAlgorithm)}.
* This will cause PGPainless to use the provided algorithm for message encryption, instead of negotiating an algorithm
* by inspecting the provided recipient keys.
* <p>
* By default, PGPainless will encrypt to all suitable, encryption capable subkeys on each recipient's certificate.
* This behavior can be changed per recipient, e.g. by calling
* <pre>
* {@code
* opt.addRecipient(aliceKey, EncryptionOptions.encryptToFirstSubkey());
* }
* </pre>
* when adding the recipient key.
*/
public class EncryptionOptions {
private final EncryptionPurpose purpose;
private final Set<PGPKeyEncryptionMethodGenerator> encryptionMethods = new LinkedHashSet<>();
private final Set<SubkeyIdentifier> encryptionKeys = new LinkedHashSet<>();
private final Map<SubkeyIdentifier, KeyRingInfo> keyRingInfo = new HashMap<>();
private final Map<SubkeyIdentifier, KeyAccessor> keyViews = new HashMap<>();
private final EncryptionKeySelector encryptionKeySelector = encryptToAllCapableSubkeys();
private SymmetricKeyAlgorithm encryptionAlgorithmOverride = null;
/**
* Encrypt to keys both carrying the key flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}
* or {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}.
*/
public EncryptionOptions() {
this(EncryptionPurpose.ANY);
}
public EncryptionOptions(@Nonnull EncryptionPurpose purpose) {
this.purpose = purpose;
}
/**
* Factory method to create an {@link EncryptionOptions} object which will encrypt for keys
* which carry either the {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS} or
* {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE} flag.
* <p>
* Use this if you are not sure.
*
* @return encryption options
*/
public static EncryptionOptions get() {
return new EncryptionOptions();
}
/**
* Factory method to create an {@link EncryptionOptions} object which will encrypt for keys
* which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}.
*
* @return encryption options
*/
public static EncryptionOptions encryptCommunications() {
return new EncryptionOptions(EncryptionPurpose.COMMUNICATIONS);
}
/**
* Factory method to create an {@link EncryptionOptions} object which will encrypt for keys
* which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}.
*
* @return encryption options
*/
public static EncryptionOptions encryptDataAtRest() {
return new EncryptionOptions(EncryptionPurpose.STORAGE);
}
/**
* Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients.
*
* @param keys keys
* @return this
*/
public EncryptionOptions addRecipients(@Nonnull Iterable<PGPPublicKeyRing> keys) {
if (!keys.iterator().hasNext()) {
throw new IllegalArgumentException("Set of recipient keys cannot be empty.");
}
for (PGPPublicKeyRing key : keys) {
addRecipient(key);
}
return this;
}
/**
* Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients.
* Per key ring, the selector is applied to select one or more encryption subkeys.
*
* @param keys keys
* @param selector encryption key selector
* @return this
*/
public EncryptionOptions addRecipients(@Nonnull Iterable<PGPPublicKeyRing> keys, @Nonnull EncryptionKeySelector selector) {
if (!keys.iterator().hasNext()) {
throw new IllegalArgumentException("Set of recipient keys cannot be empty.");
}
for (PGPPublicKeyRing key : keys) {
addRecipient(key, selector);
}
return this;
}
/**
* Add a recipient by providing a key and recipient user-id.
* The user-id is used to determine the recipients preferences (algorithms etc.).
*
* @param key key ring
* @param userId user id
* @return this
*/
public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, @Nonnull CharSequence userId) {
return addRecipient(key, userId, encryptionKeySelector);
}
/**
* Add a recipient by providing a key and recipient user-id, as well as a strategy for selecting one or multiple
* encryption capable subkeys from the key.
*
* @param key key
* @param userId user-id
* @param encryptionKeySelectionStrategy strategy to select one or more encryption subkeys to encrypt to
* @return this
*/
public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key,
@Nonnull CharSequence userId,
@Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) {
KeyRingInfo info = new KeyRingInfo(key, new Date());
List<PGPPublicKey> encryptionSubkeys = encryptionKeySelectionStrategy
.selectEncryptionSubkeys(info.getEncryptionSubkeys(userId.toString(), purpose));
if (encryptionSubkeys.isEmpty()) {
throw new KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key));
}
for (PGPPublicKey encryptionSubkey : encryptionSubkeys) {
SubkeyIdentifier keyId = new SubkeyIdentifier(key, encryptionSubkey.getKeyID());
keyRingInfo.put(keyId, info);
keyViews.put(keyId, new KeyAccessor.ViaUserId(info, keyId, userId.toString()));
addRecipientKey(key, encryptionSubkey, false);
}
return this;
}
/**
* Add a recipient by providing a key.
*
* @param key key ring
* @return this
*/
public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key) {
return addRecipient(key, encryptionKeySelector);
}
/**
* Add a recipient by providing a key and an encryption key selection strategy.
*
* @param key key ring
* @param encryptionKeySelectionStrategy strategy used to select one or multiple encryption subkeys.
* @return this
*/
public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key,
@Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) {
return addAsRecipient(key, encryptionKeySelectionStrategy, false);
}
/**
* Add a certificate as hidden recipient.
* The recipients key-id will be obfuscated by setting a wildcard key ID.
*
* @param key recipient key
* @return this
*/
public EncryptionOptions addHiddenRecipient(@Nonnull PGPPublicKeyRing key) {
return addHiddenRecipient(key, encryptionKeySelector);
}
/**
* Add a certificate as hidden recipient, using the provided {@link EncryptionKeySelector} to select recipient subkeys.
* The recipients key-ids will be obfuscated by setting a wildcard key ID instead.
*
* @param key recipient key
* @param encryptionKeySelectionStrategy strategy to select recipient (sub) keys.
* @return this
*/
public EncryptionOptions addHiddenRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy) {
return addAsRecipient(key, encryptionKeySelectionStrategy, true);
}
private EncryptionOptions addAsRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy, boolean wildcardKeyId) {
Date evaluationDate = new Date();
KeyRingInfo info;
info = new KeyRingInfo(key, evaluationDate);
Date primaryKeyExpiration;
try {
primaryKeyExpiration = info.getPrimaryKeyExpirationDate();
} catch (NoSuchElementException e) {
throw new KeyException.UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key));
}
if (primaryKeyExpiration != null && primaryKeyExpiration.before(evaluationDate)) {
throw new KeyException.ExpiredKeyException(OpenPgpFingerprint.of(key), primaryKeyExpiration);
}
List<PGPPublicKey> encryptionSubkeys = encryptionKeySelectionStrategy
.selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose));
if (encryptionSubkeys.isEmpty()) {
throw new KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key));
}
for (PGPPublicKey encryptionSubkey : encryptionSubkeys) {
SubkeyIdentifier keyId = new SubkeyIdentifier(key, encryptionSubkey.getKeyID());
keyRingInfo.put(keyId, info);
keyViews.put(keyId, new KeyAccessor.ViaKeyId(info, keyId));
addRecipientKey(key, encryptionSubkey, wildcardKeyId);
}
return this;
}
private void addRecipientKey(@Nonnull PGPPublicKeyRing keyRing,
@Nonnull PGPPublicKey key,
boolean wildcardKeyId) {
encryptionKeys.add(new SubkeyIdentifier(keyRing, key.getKeyID()));
PublicKeyKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory
.getInstance().getPublicKeyKeyEncryptionMethodGenerator(key);
encryptionMethod.setUseWildcardKeyID(wildcardKeyId);
addEncryptionMethod(encryptionMethod);
}
/**
* Add a symmetric passphrase which the message will be encrypted to.
*
* @param passphrase passphrase
* @return this
*/
public EncryptionOptions addPassphrase(@Nonnull Passphrase passphrase) {
if (passphrase.isEmpty()) {
throw new IllegalArgumentException("Passphrase must not be empty.");
}
PBEKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory
.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase);
return addEncryptionMethod(encryptionMethod);
}
/**
* Add an {@link PGPKeyEncryptionMethodGenerator} which will be used to encrypt the message.
* Method generators are either {@link PBEKeyEncryptionMethodGenerator} (passphrase)
* or {@link PGPKeyEncryptionMethodGenerator} (public key).
*
* This method is intended for advanced users to allow encryption for specific subkeys.
* This can come in handy for example if data needs to be encrypted to a subkey that's ignored by PGPainless.
*
* @param encryptionMethod encryption method
* @return this
*/
public EncryptionOptions addEncryptionMethod(@Nonnull PGPKeyEncryptionMethodGenerator encryptionMethod) {
encryptionMethods.add(encryptionMethod);
return this;
}
Set<PGPKeyEncryptionMethodGenerator> getEncryptionMethods() {
return new HashSet<>(encryptionMethods);
}
Map<SubkeyIdentifier, KeyRingInfo> getKeyRingInfo() {
return new HashMap<>(keyRingInfo);
}
Set<SubkeyIdentifier> getEncryptionKeyIdentifiers() {
return new HashSet<>(encryptionKeys);
}
Map<SubkeyIdentifier, KeyAccessor> getKeyViews() {
return new HashMap<>(keyViews);
}
SymmetricKeyAlgorithm getEncryptionAlgorithmOverride() {
return encryptionAlgorithmOverride;
}
/**
* Override the used symmetric encryption algorithm.
* The symmetric encryption algorithm is used to encrypt the message itself,
* while the used symmetric key will be encrypted to all recipients using public key
* cryptography.
*
* If the algorithm is not overridden, a suitable algorithm will be negotiated.
*
* @param encryptionAlgorithm encryption algorithm override
* @return this
*/
public EncryptionOptions overrideEncryptionAlgorithm(@Nonnull SymmetricKeyAlgorithm encryptionAlgorithm) {
if (encryptionAlgorithm == SymmetricKeyAlgorithm.NULL) {
throw new IllegalArgumentException("Plaintext encryption can only be used to denote unencrypted secret keys.");
}
this.encryptionAlgorithmOverride = encryptionAlgorithm;
return this;
}
/**
* Return <pre>true</pre> iff the user specified at least one encryption method,
* <pre>false</pre> otherwise.
*
* @return encryption methods is not empty
*/
public boolean hasEncryptionMethod() {
return !encryptionMethods.isEmpty();
}
public interface EncryptionKeySelector {
List<PGPPublicKey> selectEncryptionSubkeys(@Nonnull List<PGPPublicKey> encryptionCapableKeys);
}
/**
* Only encrypt to the first valid encryption capable subkey we stumble upon.
*
* @return encryption key selector
*/
public static EncryptionKeySelector encryptToFirstSubkey() {
return new EncryptionKeySelector() {
@Override
public List<PGPPublicKey> selectEncryptionSubkeys(@Nonnull List<PGPPublicKey> encryptionCapableKeys) {
return encryptionCapableKeys.isEmpty() ? Collections.emptyList() : Collections.singletonList(encryptionCapableKeys.get(0));
}
};
}
/**
* Encrypt to any valid, encryption capable subkey on the key ring.
*
* @return encryption key selector
*/
public static EncryptionKeySelector encryptToAllCapableSubkeys() {
return new EncryptionKeySelector() {
@Override
public List<PGPPublicKey> selectEncryptionSubkeys(@Nonnull List<PGPPublicKey> encryptionCapableKeys) {
return encryptionCapableKeys;
}
};
}
// TODO: Create encryptToBestSubkey() method
}

View File

@ -1,209 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.encryption_signing;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.Nonnull;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.util.MultiMap;
public final class EncryptionResult {
private final SymmetricKeyAlgorithm encryptionAlgorithm;
private final CompressionAlgorithm compressionAlgorithm;
private final MultiMap<SubkeyIdentifier, PGPSignature> detachedSignatures;
private final Set<SubkeyIdentifier> recipients;
private final String fileName;
private final Date modificationDate;
private final StreamEncoding fileEncoding;
private EncryptionResult(SymmetricKeyAlgorithm encryptionAlgorithm,
CompressionAlgorithm compressionAlgorithm,
MultiMap<SubkeyIdentifier, PGPSignature> detachedSignatures,
Set<SubkeyIdentifier> recipients,
String fileName,
Date modificationDate,
StreamEncoding encoding) {
this.encryptionAlgorithm = encryptionAlgorithm;
this.compressionAlgorithm = compressionAlgorithm;
this.detachedSignatures = detachedSignatures;
this.recipients = Collections.unmodifiableSet(recipients);
this.fileName = fileName;
this.modificationDate = modificationDate;
this.fileEncoding = encoding;
}
/**
* Return the symmetric encryption algorithm used to encrypt the message.
*
* @return symmetric encryption algorithm
* */
public SymmetricKeyAlgorithm getEncryptionAlgorithm() {
return encryptionAlgorithm;
}
/**
* Return the compression algorithm that was used to compress the message before encryption/signing.
*
* @return compression algorithm
*/
public CompressionAlgorithm getCompressionAlgorithm() {
return compressionAlgorithm;
}
/**
* Return a {@link MultiMap} of key identifiers and detached signatures that were generated for the message.
* Each key of the map represents a signing key, which has one or more detached signatures associated with it.
*
* @return detached signatures
*/
public MultiMap<SubkeyIdentifier, PGPSignature> getDetachedSignatures() {
return detachedSignatures;
}
/**
* Return the set of recipient encryption keys.
*
* @return recipients
*/
public Set<SubkeyIdentifier> getRecipients() {
return recipients;
}
/**
* Return the file name of the encrypted/signed data.
*
* @return filename
*/
public String getFileName() {
return fileName;
}
/**
* Return the modification date of the encrypted/signed file.
*
* @return modification date
*/
public Date getModificationDate() {
return modificationDate;
}
/**
* Return the encoding format of the encrypted/signed data.
*
* @return encoding format
*/
public StreamEncoding getFileEncoding() {
return fileEncoding;
}
/**
* Return true, if the message is marked as for-your-eyes-only.
* This is typically done by setting the filename "_CONSOLE".
*
* @return is message for your eyes only?
*/
public boolean isForYourEyesOnly() {
return PGPLiteralData.CONSOLE.equals(getFileName());
}
/**
* Returns true, if the message was encrypted for at least one subkey of the given certificate.
*
* @param certificate certificate
* @return true if encrypted for 1+ subkeys, false otherwise.
*/
public boolean isEncryptedFor(PGPPublicKeyRing certificate) {
for (SubkeyIdentifier recipient : recipients) {
if (certificate.getPublicKey().getKeyID() != recipient.getPrimaryKeyId()) {
continue;
}
if (certificate.getPublicKey(recipient.getSubkeyId()) != null) {
return true;
}
}
return false;
}
/**
* Create a builder for the encryption result class.
*
* @return builder
*/
public static Builder builder() {
return new Builder();
}
public static class Builder {
private SymmetricKeyAlgorithm encryptionAlgorithm;
private CompressionAlgorithm compressionAlgorithm;
private final MultiMap<SubkeyIdentifier, PGPSignature> detachedSignatures = new MultiMap<>();
private final Set<SubkeyIdentifier> recipients = new HashSet<>();
private String fileName = "";
private Date modificationDate = new Date(0L); // NOW
private StreamEncoding encoding = StreamEncoding.BINARY;
public Builder setEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) {
this.encryptionAlgorithm = encryptionAlgorithm;
return this;
}
public Builder setCompressionAlgorithm(CompressionAlgorithm compressionAlgorithm) {
this.compressionAlgorithm = compressionAlgorithm;
return this;
}
public Builder addRecipient(SubkeyIdentifier recipient) {
this.recipients.add(recipient);
return this;
}
public Builder addDetachedSignature(SubkeyIdentifier signingSubkeyIdentifier, PGPSignature detachedSignature) {
this.detachedSignatures.put(signingSubkeyIdentifier, detachedSignature);
return this;
}
public Builder setFileName(@Nonnull String fileName) {
this.fileName = fileName;
return this;
}
public Builder setModificationDate(@Nonnull Date modificationDate) {
this.modificationDate = modificationDate;
return this;
}
public Builder setFileEncoding(StreamEncoding fileEncoding) {
this.encoding = fileEncoding;
return this;
}
public EncryptionResult build() {
if (encryptionAlgorithm == null) {
throw new IllegalStateException("Encryption algorithm not set.");
}
if (compressionAlgorithm == null) {
throw new IllegalStateException("Compression algorithm not set.");
}
return new EncryptionResult(encryptionAlgorithm, compressionAlgorithm, detachedSignatures, recipients,
fileName, modificationDate, encoding);
}
}
}

View File

@ -1,328 +0,0 @@
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.encryption_signing;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.annotation.Nonnull;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.BCPGOutputStream;
import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder;
import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator;
import org.pgpainless.algorithm.CompressionAlgorithm;
import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.algorithm.StreamEncoding;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.util.ArmorUtils;
import org.pgpainless.util.ArmoredOutputStreamFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* OutputStream that produces an OpenPGP message. The message can be encrypted, signed, or both,
* depending on its configuration.
*
* This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream.
* @see <a href="https://github.com/neuhalje/bouncy-gpg/blob/master/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStream.java">Source</a>
*/
public final class EncryptionStream extends OutputStream {
private static final Logger LOGGER = LoggerFactory.getLogger(EncryptionStream.class);
private final ProducerOptions options;
private final EncryptionResult.Builder resultBuilder = EncryptionResult.builder();
private boolean closed = false;
// 1 << 8 causes wrong partial body length encoding
// 1 << 9 fixes this.
// see https://github.com/pgpainless/pgpainless/issues/160
private static final int BUFFER_SIZE = 1 << 9;
OutputStream outermostStream;
OutputStream signatureLayerStream;
private ArmoredOutputStream armorOutputStream = null;
private OutputStream publicKeyEncryptedStream = null;
private PGPCompressedDataGenerator compressedDataGenerator;
private BCPGOutputStream basicCompressionStream;
private PGPLiteralDataGenerator literalDataGenerator;
private OutputStream literalDataStream;
EncryptionStream(@Nonnull OutputStream targetOutputStream,
@Nonnull ProducerOptions options)
throws IOException, PGPException {
this.options = options;
outermostStream = targetOutputStream;
prepareArmor();
prepareEncryption();
prepareCompression();
prepareOnePassSignatures();
prepareLiteralDataProcessing();
prepareSigningStream();
prepareInputEncoding();
}
private void prepareArmor() {
if (!options.isAsciiArmor()) {
LOGGER.debug("Output will be unarmored");
return;
}
// ArmoredOutputStream better be buffered
outermostStream = new BufferedOutputStream(outermostStream);
LOGGER.debug("Wrap encryption output in ASCII armor");
armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream, options);
if (options.hasComment()) {
String[] commentLines = options.getComment().split("\n");
for (String commentLine : commentLines) {
if (!commentLine.trim().isEmpty()) {
ArmorUtils.addCommentHeader(armorOutputStream, commentLine.trim());
}
}
}
if (options.hasVersion()) {
String version = options.getVersion().trim();
if (!version.isEmpty()) {
ArmorUtils.setVersionHeader(armorOutputStream, version);
}
}
outermostStream = armorOutputStream;
}
private void prepareEncryption() throws IOException, PGPException {
EncryptionOptions encryptionOptions = options.getEncryptionOptions();
if (encryptionOptions == null || encryptionOptions.getEncryptionMethods().isEmpty()) {
// No encryption options/methods -> no encryption
resultBuilder.setEncryptionAlgorithm(SymmetricKeyAlgorithm.NULL);
return;
}
SymmetricKeyAlgorithm encryptionAlgorithm = EncryptionBuilder.negotiateSymmetricEncryptionAlgorithm(encryptionOptions);
resultBuilder.setEncryptionAlgorithm(encryptionAlgorithm);
LOGGER.debug("Encrypt message using {}", encryptionAlgorithm);
PGPDataEncryptorBuilder dataEncryptorBuilder =
ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(encryptionAlgorithm);
dataEncryptorBuilder.setWithIntegrityPacket(true);
PGPEncryptedDataGenerator encryptedDataGenerator =
new PGPEncryptedDataGenerator(dataEncryptorBuilder);
for (PGPKeyEncryptionMethodGenerator encryptionMethod : encryptionOptions.getEncryptionMethods()) {
encryptedDataGenerator.addMethod(encryptionMethod);
}
for (SubkeyIdentifier recipientSubkeyIdentifier : encryptionOptions.getEncryptionKeyIdentifiers()) {
resultBuilder.addRecipient(recipientSubkeyIdentifier);
}
publicKeyEncryptedStream = encryptedDataGenerator.open(outermostStream, new byte[BUFFER_SIZE]);
outermostStream = publicKeyEncryptedStream;
}
private void prepareCompression() throws IOException {
CompressionAlgorithm compressionAlgorithm = EncryptionBuilder.negotiateCompressionAlgorithm(options);
resultBuilder.setCompressionAlgorithm(compressionAlgorithm);
compressedDataGenerator = new PGPCompressedDataGenerator(
compressionAlgorithm.getAlgorithmId());
if (compressionAlgorithm == CompressionAlgorithm.UNCOMPRESSED) {
return;
}
LOGGER.debug("Compress using {}", compressionAlgorithm);
basicCompressionStream = new BCPGOutputStream(compressedDataGenerator.open(outermostStream));
outermostStream = basicCompressionStream;
}
private void prepareOnePassSignatures() throws IOException, PGPException {
signatureLayerStream = outermostStream;
SigningOptions signingOptions = options.getSigningOptions();
if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) {
// No singing options/methods -> no signing
return;
}
int sigIndex = 0;
for (SubkeyIdentifier identifier : signingOptions.getSigningMethods().keySet()) {
sigIndex++;
SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(identifier);
if (!signingMethod.isDetached()) {
PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator();
// The last sig is not nested, all others are
boolean nested = sigIndex != signingOptions.getSigningMethods().size();
signatureGenerator.generateOnePassVersion(nested).encode(outermostStream);
}
}
}
private void prepareLiteralDataProcessing() throws IOException {
if (options.isCleartextSigned()) {
int[] algorithmIds = collectHashAlgorithmsForCleartextSigning();
armorOutputStream.beginClearText(algorithmIds);
return;
}
literalDataGenerator = new PGPLiteralDataGenerator();
literalDataStream = literalDataGenerator.open(outermostStream, options.getEncoding().getCode(),
options.getFileName(), options.getModificationDate(), new byte[BUFFER_SIZE]);
outermostStream = literalDataStream;
resultBuilder.setFileName(options.getFileName())
.setModificationDate(options.getModificationDate())
.setFileEncoding(options.getEncoding());
}
public void prepareSigningStream() {
outermostStream = new SignatureGenerationStream(outermostStream, options.getSigningOptions());
}
public void prepareInputEncoding() {
// By buffering here, we drastically improve performance
// Reason is that CRLFGeneratorStream only implements write(int), so we need BufferedOutputStream to
// "convert" to write(buf) calls again
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outermostStream);
CRLFGeneratorStream crlfGeneratorStream = new CRLFGeneratorStream(bufferedOutputStream,
options.isApplyCRLFEncoding() ? StreamEncoding.UTF8 : StreamEncoding.BINARY);
outermostStream = crlfGeneratorStream;
}
private int[] collectHashAlgorithmsForCleartextSigning() {
SigningOptions signOpts = options.getSigningOptions();
Set<HashAlgorithm> hashAlgorithms = new HashSet<>();
if (signOpts != null) {
for (SigningOptions.SigningMethod method : signOpts.getSigningMethods().values()) {
hashAlgorithms.add(method.getHashAlgorithm());
}
}
int[] algorithmIds = new int[hashAlgorithms.size()];
Iterator<HashAlgorithm> iterator = hashAlgorithms.iterator();
for (int i = 0; i < algorithmIds.length; i++) {
algorithmIds[i] = iterator.next().getAlgorithmId();
}
return algorithmIds;
}
@Override
public void write(int data) throws IOException {
outermostStream.write(data);
}
@Override
public void write(@Nonnull byte[] buffer) throws IOException {
write(buffer, 0, buffer.length);
}
@Override
public void write(@Nonnull byte[] buffer, int off, int len) throws IOException {
outermostStream.write(buffer, 0, len);
}
@Override
public void flush() throws IOException {
outermostStream.flush();
}
@Override
public void close() throws IOException {
if (closed) {
return;
}
outermostStream.close();
// Literal Data
if (literalDataStream != null) {
literalDataStream.flush();
literalDataStream.close();
}
if (literalDataGenerator != null) {
literalDataGenerator.close();
}
if (options.isCleartextSigned()) {
// Add linebreak between body and signatures
// TODO: We should only add this line if required.
// I.e. if the message already ends with \n, don't add another linebreak.
armorOutputStream.write('\r');
armorOutputStream.write('\n');
armorOutputStream.endClearText();
}
try {
writeSignatures();
} catch (PGPException e) {
throw new IOException("Exception while writing signatures.", e);
}
// Compressed Data
compressedDataGenerator.close();
// Public Key Encryption
if (publicKeyEncryptedStream != null) {
publicKeyEncryptedStream.flush();
publicKeyEncryptedStream.close();
}
// Armor
if (armorOutputStream != null) {
armorOutputStream.flush();
armorOutputStream.close();
}
closed = true;
}
private void writeSignatures() throws PGPException, IOException {
SigningOptions signingOptions = options.getSigningOptions();
if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) {
return;
}
// One-Pass-Signatures are bracketed. That means we have to append the signatures in reverse order
// compared to the one-pass-signature packets.
List<SubkeyIdentifier> signingKeys = new ArrayList<>(signingOptions.getSigningMethods().keySet());
for (int i = signingKeys.size() - 1; i >= 0; i--) {
SubkeyIdentifier signingKey = signingKeys.get(i);
SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey);
PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator();
PGPSignature signature = signatureGenerator.generate();
if (signingMethod.isDetached()) {
resultBuilder.addDetachedSignature(signingKey, signature);
}
if (!signingMethod.isDetached() || options.isCleartextSigned()) {
signature.encode(signatureLayerStream);
}
}
}
public EncryptionResult getResult() {
if (!closed) {
throw new IllegalStateException("EncryptionStream must be closed before accessing the Result.");
}
return resultBuilder.build();
}
public boolean isClosed() {
return closed;
}
}

View File

@ -1,83 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.encryption_signing;
import java.io.OutputStream;
import java.security.MessageDigest;
import javax.annotation.Nonnull;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.Signer;
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
abstract class PGPHashContextContentSignerBuilder implements PGPContentSignerBuilder {
// Copied from BC, required since BCs class is package visible only
static class SignerOutputStream
extends OutputStream {
private Signer sig;
SignerOutputStream(Signer sig) {
this.sig = sig;
}
public void write(@Nonnull byte[] bytes, int off, int len) {
sig.update(bytes, off, len);
}
public void write(@Nonnull byte[] bytes) {
sig.update(bytes, 0, bytes.length);
}
public void write(int b) {
sig.update((byte) b);
}
}
static class ExistingMessageDigest implements Digest {
private final MessageDigest digest;
ExistingMessageDigest(MessageDigest messageDigest) {
this.digest = messageDigest;
}
@Override
public void update(byte in) {
digest.update(in);
}
@Override
public void update(byte[] in, int inOff, int len) {
digest.update(in, inOff, len);
}
@Override
public int doFinal(byte[] out, int outOff) {
byte[] hash = digest.digest();
System.arraycopy(hash, 0, out, outOff, hash.length);
return getDigestSize();
}
@Override
public void reset() {
// Nope!
// We cannot reset, since BCs signer classes are resetting in their init() methods, which would also reset
// the messageDigest, losing its state. This would shatter our intention.
}
@Override
public String getAlgorithmName() {
return digest.getAlgorithm();
}
@Override
public int getDigestSize() {
return digest.getDigestLength();
}
}
}

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