mirror of
https://github.com/pgpainless/pgpainless.git
synced 2025-09-09 02:09:38 +02:00
Compare commits
260 commits
2ed16e8f52
...
82b7bf6c31
Author | SHA1 | Date | |
---|---|---|---|
82b7bf6c31 | |||
6ca9b6f4ed | |||
833484450c | |||
2b155b4ec0 | |||
3fd8b82c9b | |||
15d50bb4af | |||
a49df00a9e | |||
98c48232f5 | |||
9617b35703 | |||
aa1f99fe39 | |||
72ec1b1e06 | |||
7313c5e5a9 | |||
c054cb9705 | |||
0a639e1c2a | |||
d789d3e0c4 | |||
21439854e3 | |||
026be063f8 | |||
fd85f8e567 | |||
24887e2521 | |||
df136adfab | |||
8f24bcfb26 | |||
76820b8cd5 | |||
0027a3ed24 | |||
e45b551ab3 | |||
a575f46867 | |||
65e2de8186 | |||
46367aff93 | |||
18a49d0afd | |||
45a79a0e65 | |||
5b39aea421 | |||
4e5eff6113 | |||
946d8aace0 | |||
bfd67abab7 | |||
aa4ffbaba5 | |||
7b32da722f | |||
c914a43853 | |||
4405c579a1 | |||
4462abce9f | |||
4d8179edc1 | |||
f786de4c54 | |||
eaeb0e1ab2 | |||
ed92f321dd | |||
f97591a509 | |||
8c7e9e1b54 | |||
f3b5664d95 | |||
82db3a9ea6 | |||
06d0b90ff6 | |||
88d9fae2fc | |||
2714c9770b | |||
e44e97844c | |||
48ba9dbe98 | |||
ab34413fa8 | |||
a76128cf79 | |||
65f341f687 | |||
a0ef949bb4 | |||
21246138aa | |||
01c112770a | |||
7c22d32a11 | |||
3e867be780 | |||
2d0e4b4fc0 | |||
1b6601cc19 | |||
02d72c2691 | |||
244113bc2f | |||
3bc07f045c | |||
76efbf2e45 | |||
f7dd72dd79 | |||
6e8982df59 | |||
ab6ab04bcb | |||
dc2fe5d65a | |||
05ea7bd94f | |||
c2f7a8b2fd | |||
333addf262 | |||
cc4928ab22 | |||
0266d14594 | |||
94febc33df | |||
3cef99d256 | |||
48f000f6f4 | |||
bdd5a9e26e | |||
9343e1e0f2 | |||
1dd666d32b | |||
b7dedbd619 | |||
d540febc7f | |||
168c884f27 | |||
148af79794 | |||
85856567dd | |||
4797ce34c3 | |||
68be1ffc5f | |||
9f2371932e | |||
24cef79831 | |||
3080e8bdd3 | |||
2d1c2d2737 | |||
1b19634415 | |||
c7c3d5b3ab | |||
f3257d9405 | |||
b8f41b6212 | |||
96fa3af08c | |||
ff62a39dc8 | |||
187416bbe1 | |||
d1861e51cd | |||
654756c919 | |||
2d6675ec06 | |||
7281ce530a | |||
8aaa042087 | |||
bab5a4b0bf | |||
a8a09b7db7 | |||
e2d8db6796 | |||
bd24db9cc6 | |||
9f35be1b0e | |||
bb64188473 | |||
54d83daee5 | |||
cad89b9bde | |||
c22a2e4fcf | |||
2dea73c584 | |||
47ec445ef7 | |||
ca22446f1c | |||
41251296ce | |||
a37f6dfce9 | |||
69b0b2d371 | |||
1e67447efd | |||
5f3e1b4da3 | |||
53b44e2817 | |||
c8694840d8 | |||
c7ce79a5af | |||
e2832249cb | |||
2c1d89a249 | |||
cb7c61cf10 | |||
053eb2c830 | |||
7db10432fe | |||
e2d79e00cc | |||
793ee40290 | |||
3b9858f9ef | |||
c88d1573d7 | |||
364bebed14 | |||
0fbf7fac04 | |||
8c58ca620d | |||
a8cbd36a52 | |||
4a7e690806 | |||
312a00e5d4 | |||
57b6795513 | |||
acbb93066e | |||
9a7aeae9fa | |||
bab448eb6d | |||
221d329254 | |||
4c180bbd59 | |||
63d1f855de | |||
e61c3007c0 | |||
c8880619f9 | |||
2d42457ce4 | |||
b24d0ef99c | |||
2ae9c94841 | |||
a00a90c175 | |||
3a28b33355 | |||
eefc622f63 | |||
665db5ceb6 | |||
b828e5477c | |||
053f6cf362 | |||
8a48cc40f7 | |||
2200cb7372 | |||
57540d8028 | |||
2a71a98bba | |||
74c821c1e8 | |||
bca4ddcb6f | |||
04160fbe27 | |||
429186c5e1 | |||
b181efee00 | |||
7a5ece0907 | |||
77890cc933 | |||
93ee037ef0 | |||
12fd807f75 | |||
7e345a0e33 | |||
f74932c4d0 | |||
8a9b5aa567 | |||
0e48e94a91 | |||
1967483984 | |||
62f3a35c02 | |||
d6d52cd544 | |||
1e7a357b68 | |||
0ff347b836 | |||
e284fca0f8 | |||
33ee03ee35 | |||
6cfa87201b | |||
a95ebce07b | |||
6c68285a95 | |||
97e6591f0a | |||
16a2e77776 | |||
aace92214a | |||
d92ae054d9 | |||
18cdf6bbc7 | |||
3abc2a4e39 | |||
a25ba5943e | |||
34633cfeac | |||
42c262a99f | |||
321053d66e | |||
fc87d985b6 | |||
f9c2ade2d0 | |||
8b5d9af522 | |||
d34cb2db61 | |||
5de1e6a56d | |||
67af718db9 | |||
69fc590d26 | |||
44d90c600f | |||
9812d4d78c | |||
996984cbb5 | |||
63bdff58bf | |||
ac0c37925a | |||
07d2311b0e | |||
0109624020 | |||
714b5bd9c9 | |||
f70792f92d | |||
446b8eaaca | |||
22a1f54a9b | |||
e53e4f5f3c | |||
c00a9709de | |||
3030f2af2b | |||
1379942c07 | |||
0fc9ee716e | |||
b61ba46d24 | |||
88df92fd1f | |||
975548fc76 | |||
2a2595a757 | |||
58a96b5776 | |||
0583a826d1 | |||
fac87c371a | |||
23cb47365e | |||
0ea19d3b9a | |||
9e9ccc8624 | |||
df1d74962b | |||
c0b6ea8f96 | |||
3e8dd78e74 | |||
a54382a78e | |||
0b4f1a0f01 | |||
8c557ad945 | |||
0c7055455b | |||
0b165ee273 | |||
217a25bd62 | |||
53053cf3fc | |||
dd4a989606 | |||
66a2b7e0fc | |||
ead93345e4 | |||
7991af06d4 | |||
69f802d442 | |||
b488b70050 | |||
41a1d0d596 | |||
1738fb1d7d | |||
5938ea9cff | |||
c9a7accec8 | |||
70cb9df8a9 | |||
4ecc590d8f | |||
f9d217c0b1 | |||
2b9c6e58ed | |||
b571dd177e | |||
0fceb4db2d | |||
da9c610d14 | |||
c6dbc029d7 | |||
2a43d5704b | |||
31e6f2e73a | |||
edea8121ce | |||
1acda0e970 | |||
87f3d28567 | |||
37042467f4 |
376 changed files with 11858 additions and 10130 deletions
3
.github/ISSUE_TEMPLATE/cli-application.md
vendored
3
.github/ISSUE_TEMPLATE/cli-application.md
vendored
|
@ -7,6 +7,9 @@ assignees: ''
|
|||
|
||||
---
|
||||
|
||||
**Preliminary**
|
||||
[ ] This bug was found using AI assistance.
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
|
|
3
.github/ISSUE_TEMPLATE/library.md
vendored
3
.github/ISSUE_TEMPLATE/library.md
vendored
|
@ -7,6 +7,9 @@ assignees: ''
|
|||
|
||||
---
|
||||
|
||||
**Preliminary**
|
||||
[ ] This bug was found using AI assistance.
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
|
|
22
BUILD.md
Normal file
22
BUILD.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2025 Paul Schaub <info@pgpainless.org>
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
# Build PGPainless
|
||||
|
||||
There are a number of different artifacts that can be built from the PGPainless source code:
|
||||
|
||||
## `pgpainless-cli/build/libs/pgpainless-cli-X.Y.Z-all.jar`
|
||||
|
||||
This is a fat jar, built using the Shadow plugin.
|
||||
It bundles all necessary dependencies required by the CLI application at runtime.
|
||||
This artifact will be produced by the `gradle shadowJar` task, which is run as part of the `gradle assemble` task.
|
||||
|
||||
## `pgpainless-cli/build/native/nativeCompile/pgpainless-cli`
|
||||
|
||||
This is a native image, that can be built using GraalVM which compared to the executable jar file above
|
||||
offers greatly improved performance by skipping the JVM startup overhead.
|
||||
|
||||
To build this image, you need to run `gradle nativeCompile` using a GraalVM-enabled Java SDK.
|
44
README.md
44
README.md
|
@ -24,7 +24,7 @@ PGPainless aims to make using OpenPGP in Java projects as simple as possible.
|
|||
It does so by introducing an intuitive Builder structure, which allows easy
|
||||
setup of encryption/decryption operations, as well as straight forward key generation.
|
||||
|
||||
PGPainless is based around the Bouncy Castle java library and can be used on Android down to API level 10.
|
||||
PGPainless is based around the Bouncy Castle java library and can be used on Android.
|
||||
It can be configured to either use the Java Cryptographic Engine (JCE), or Bouncy Castles lightweight reimplementation.
|
||||
|
||||
While signature verification in Bouncy Castle is limited to signature correctness, PGPainless goes much further.
|
||||
|
@ -32,7 +32,7 @@ It also checks if signing subkeys are properly bound to their primary key, if ke
|
|||
if keys are allowed to create signatures in the first place.
|
||||
|
||||
These rigorous checks make PGPainless stand out from other Java-based OpenPGP libraries and are the reason why
|
||||
PGPainless currently [*scores first place* on Sequoia-PGPs Interoperability Test-Suite](https://tests.sequoia-pgp.org).
|
||||
PGPainless currently scores above average on Sequoia-PGPs [Interoperability Test-Suite](https://tests.sequoia-pgp.org).
|
||||
|
||||
> At FlowCrypt we are using PGPainless in our Kotlin code bases on Android and on server side.
|
||||
> The ergonomics of legacy PGP tooling on Java is not very good, and PGPainless improves it greatly.
|
||||
|
@ -65,24 +65,23 @@ Reading keys from ASCII armored strings or from binary files is easy:
|
|||
|
||||
```java
|
||||
String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"...
|
||||
PGPSecretKeyRing secretKey = PGPainless.readKeyRing()
|
||||
.secretKeyRing(key);
|
||||
OpenPGPKey secretKey = PGPainless.getInstance().readKey()
|
||||
.parseKey(key);
|
||||
```
|
||||
|
||||
Similarly, keys can quickly be exported::
|
||||
|
||||
```java
|
||||
PGPSecretKeyRing secretKey = ...;
|
||||
String armored = PGPainless.asciiArmor(secretKey);
|
||||
ByteArrayOutputStream binary = new ByteArrayOutputStream();
|
||||
secretKey.encode(binary);
|
||||
OpenPGPKey secretKey = ...;
|
||||
String armored = secretKey.toAsciiArmoredString();
|
||||
byte[] binary = secretKey.getEncoded();
|
||||
```
|
||||
|
||||
Extract a public key certificate from a secret key:
|
||||
|
||||
```java
|
||||
PGPSecretKeyRing secretKey = ...;
|
||||
PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey);
|
||||
OpenPGPKey secretKey = ...;
|
||||
OpenPGPCertificate certificate = secretKey.toCertificate();
|
||||
```
|
||||
|
||||
### Easily Generate Keys
|
||||
|
@ -90,16 +89,17 @@ PGPainless comes with a simple to use `KeyRingBuilder` class that helps you to q
|
|||
There are some predefined key archetypes, but it is possible to fully customize key generation to your needs.
|
||||
|
||||
```java
|
||||
PGPainless api = PGPainless.getInstance();
|
||||
// RSA key without additional subkeys
|
||||
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing()
|
||||
OpenPGPKey secretKeys = api.generateKey()
|
||||
.simpleRsaKeyRing("Juliet <juliet@montague.lit>", RsaLength._4096);
|
||||
|
||||
// EdDSA primary key with EdDSA signing- and XDH encryption subkeys
|
||||
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing()
|
||||
OpenPGPKey secretKeys = api.generateKey()
|
||||
.modernKeyRing("Romeo <romeo@montague.lit>", "I defy you, stars!");
|
||||
|
||||
// Customized key
|
||||
PGPSecretKeyRing keyRing = PGPainless.buildKeyRing()
|
||||
OpenPGPKey keyRing = api.buildKey()
|
||||
.setPrimaryKey(KeySpec.getBuilder(
|
||||
RSA.withLength(RsaLength._8192),
|
||||
KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER))
|
||||
|
@ -124,24 +124,26 @@ algorithms accordingly.
|
|||
Still it allows you to manually specify which algorithms to use of course.
|
||||
|
||||
```java
|
||||
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
|
||||
PGPainless api = PGPainless.getInstance();
|
||||
EncryptionStream encryptionStream = api.generateMessage()
|
||||
.onOutputStream(outputStream)
|
||||
.withOptions(
|
||||
ProducerOptions.signAndEncrypt(
|
||||
new EncryptionOptions()
|
||||
EncryptionOptions.get(api)
|
||||
.addRecipient(aliceKey)
|
||||
.addRecipient(bobsKey)
|
||||
// optionally encrypt to a passphrase
|
||||
.addMessagePassphrase(Passphrase.fromPassword("password123"))
|
||||
// optionally override symmetric encryption algorithm
|
||||
.overrideEncryptionAlgorithm(SymmetricKeyAlgorithm.AES_192),
|
||||
new SigningOptions()
|
||||
SigningOptions.get(api)
|
||||
// Sign in-line (using one-pass-signature packet)
|
||||
.addInlineSignature(secretKeyDecryptor, aliceSecKey, signatureType)
|
||||
// Sign using a detached signature
|
||||
.addDetachedSignature(secretKeyDecryptor, aliceSecKey, signatureType)
|
||||
// optionally override hash algorithm
|
||||
.overrideHashAlgorithm(HashAlgorithm.SHA256)
|
||||
.overrideHashAlgorithm(HashAlgorithm.SHA256),
|
||||
api
|
||||
).setAsciiArmor(true) // Ascii armor or not
|
||||
);
|
||||
|
||||
|
@ -161,9 +163,9 @@ Furthermore, PGPainless will reject signatures made using weak algorithms like S
|
|||
This behaviour can be modified though using the `Policy` class.
|
||||
|
||||
```java
|
||||
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
|
||||
DecryptionStream decryptionStream = PGPainless.getInstance().processMessage()
|
||||
.onInputStream(encryptedInputStream)
|
||||
.withOptions(new ConsumerOptions()
|
||||
.withOptions(ConsumerOptions.get(api)
|
||||
.addDecryptionKey(bobSecKeys, secretKeyProtector)
|
||||
.addVerificationCert(alicePubKeys)
|
||||
);
|
||||
|
@ -215,10 +217,10 @@ which contains the bug you are fixing. That way we can update older revisions of
|
|||
Please follow the [code of conduct](CODE_OF_CONDUCT.md) if you want to be part of the project.
|
||||
|
||||
## Acknowledgements
|
||||
Development on PGPainless is generously sponsored by [FlowCrypt.com](https://flowcrypt.com). Thank you very very very much!
|
||||
In the past, development on PGPainless has been generously sponsored by [FlowCrypt.com](https://flowcrypt.com). Thank you very very very much!
|
||||
[](https://flowcrypt.com)
|
||||
|
||||
Parts of PGPainless development ([project page](https://nlnet.nl/project/PGPainless/)) will be funded by [NGI Assure](https://nlnet.nl/assure/) through [NLNet](https://nlnet.nl).
|
||||
Parts of PGPainless development ([project page](https://nlnet.nl/project/PGPainless/)) has been funded by [NGI Assure](https://nlnet.nl/assure/) through [NLNet](https://nlnet.nl).
|
||||
NGI Assure is made possible with financial support from the [European Commission](https://ec.europa.eu/)'s [Next Generation Internet](https://ngi.eu/) programme, under the aegis of [DG Communications Networks, Content and Technology](https://ec.europa.eu/info/departments/communications-networks-content-and-technology_en).
|
||||
[](https://nlnet.nl/assure/)
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ buildscript {
|
|||
}
|
||||
|
||||
plugins {
|
||||
id 'org.jetbrains.kotlin.jvm' version "1.8.10"
|
||||
id 'org.jetbrains.kotlin.jvm' version "1.9.21"
|
||||
id 'com.diffplug.spotless' version '6.22.0' apply false
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,6 @@ allprojects {
|
|||
// without this we would generate an empty pgpainless.jar for the project root
|
||||
// https://stackoverflow.com/a/25445035
|
||||
jar {
|
||||
reproducibleFileOrder = true
|
||||
onlyIf { !sourceSets.main.allSource.files.isEmpty() }
|
||||
}
|
||||
|
||||
|
|
|
@ -35,4 +35,5 @@ Contents
|
|||
quickstart.md
|
||||
pgpainless-cli/usage.md
|
||||
sop.md
|
||||
pgpainless-core/indepth.rst
|
||||
pgpainless-core/indepth.rst
|
||||
pgpainless-core/migration_2.0.md
|
137
docs/source/pgpainless-core/migration_2.0.md
Normal file
137
docs/source/pgpainless-core/migration_2.0.md
Normal file
|
@ -0,0 +1,137 @@
|
|||
# Migration Guide PGPainless 2.0
|
||||
|
||||
PGPainless 2.0 makes use of Bouncy Castles new High-Level API.
|
||||
As a consequence, the use of certain "mid-level" classes, such as `PGPPublicKeyRing`, `PGPSecretKeyRing` is now
|
||||
discouraged in favor of their high-level counterparts, e.g. `OpenPGPCertificate`, `OpenPGPKey`.
|
||||
|
||||
|
||||
## Terminology Changes
|
||||
|
||||
Bouncy Castles high-level API uses OpenPGP terminology as described in the book [OpenPGP for application developers](https://openpgp.dev/book/).
|
||||
Therefore, some terms used in the mid-level API are no longer used.
|
||||
|
||||
| Old Term | New Term | Description |
|
||||
|------------------------|----------------------------|------------------------------------------------------|
|
||||
| key ring | OpenPGP certificate or key | |
|
||||
| master key | primary key | |
|
||||
| public key ring | (OpenPGP) certificate | |
|
||||
| secret key ring | (OpenPGP) key | |
|
||||
| subkey | component key | A component key is either a primary key, or a subkey |
|
||||
| primary key identifier | certificate identifier | |
|
||||
| subkey identifier | component key identifier | |
|
||||
|
||||
|
||||
## API
|
||||
|
||||
PGPainless 2.0 switches away from the Singleton pattern.
|
||||
|
||||
The API entrypoints for PGPainless 1.X were static methods of the `PGPainless` class.
|
||||
Configuration was done by modifying singletons, e.g. `Policy`.
|
||||
|
||||
With PGPainless 2.X, the recommended way to use the API is to create individual instances of the `PGPainless` class,
|
||||
which provide non-static methods for different OpenPGP operations.
|
||||
That way, you can have multiple API instances with different, per-instance configurations.
|
||||
|
||||
|
||||
## Key Material
|
||||
|
||||
The use of `PGPPublicKeyRing` objects is now discouraged in favor of `OpenPGPCertificate`.
|
||||
Appropriately, `OpenPGPKey` replaces `PGPSecretKeyRing`. `OpenPGPKey` extends the `OpenPGPCertificate` class, but also
|
||||
contains secret key material.
|
||||
|
||||
An `OpenPGPCertificate` consists of `OpenPGPCertificateComponent`s such as `OpenPGPComponentKey`s and
|
||||
`OpenPGPIdentityComponent`s, which are bound to the certificate with `OpenPGPComponentSignature`s.
|
||||
`OpenPGPIdentityComponent`s are either `OpenPGPUserId`s or `OpenPGPUserAttribute`s (the latter being more or less
|
||||
deprecated).
|
||||
Components of an OpenPGP certificate, which contain key material (public keys, secret keys, subkeys...) are represented
|
||||
by the `OpenPGPComponentKey` class, from which `OpenPGPPrimaryKey`, `OpenPGPSubkey` and `OpenPGPSecretKey` inherit.
|
||||
|
||||
As stated above, `OpenPGPCertificateComponent`s are bound to the certificate using `OpenPGPSignature`s,
|
||||
which Bouncy Castle arranges into `OpenPGPSignatureChains` internally.
|
||||
This chain structure is evaluated to determine the status of a certificate component at a given time, as well as
|
||||
its applicable properties (algorithm preferences, features, key flags...)
|
||||
|
||||
In places, where you cannot switch to using `OpenPGPCertificate`, you can access the underlying `PGPPublicKeyRing`
|
||||
by calling `certificate.getPGPPublicKeyRing()`.
|
||||
Analog, you can access the underlying `PGPSecretKeyRing` of an `OpenPGPKey` via `key.getPGPSecretKeyRing()`.
|
||||
|
||||
|
||||
### Key Versions
|
||||
|
||||
PGPainless 1.X primarily supported OpenPGP keys of version 4.
|
||||
The 2.X release introduces support for OpenPGP v6 as well, which makes it necessary to specify the desired key version
|
||||
e.g. when generating keys.
|
||||
|
||||
This can be done by passing an `OpenPGPKeyVersion` enum.
|
||||
|
||||
|
||||
## `KeyIdentifier`
|
||||
|
||||
OpenPGP has evolved over time and with it the way to identify individual keys.
|
||||
Old protocol versions rely on 64-bit key-ids, which are nowadays deprecated, as 64-bits are not exactly
|
||||
collision-resistant.
|
||||
For some time already, the use of fingerprints is therefore encouraged as a replacement.
|
||||
However, key-ids were not everywhere at once in the protocol, so many artifacts still contain elements with
|
||||
key-ids in them.
|
||||
An example for this are public-key encrypted session-key packets, which in version 1 still only contain the recipients
|
||||
key-id.
|
||||
In signatures, both key-ids and fingerprints are present.
|
||||
|
||||
To solve this inconsistency, Bouncy Castle introduced the `KeyIdentifier` type as an abstraction of both key-ids
|
||||
and fingerprints.
|
||||
Now most methods that take some sort of identifier, be it fingerprint or key-id, now also accept a `KeyIdentifier`
|
||||
object.
|
||||
|
||||
Consequently, `KeyIdentifier` is now also the preferred way to reference keys in PGPainless and many places where
|
||||
previously a key-id or fingerprint was expected, now also accept `KeyIdentifier` objects.
|
||||
In places, where you need to access a 64-bit key-id, you can call `keyIdentifier.getKeyId()`.
|
||||
|
||||
## `SecretKeyRingProtector`
|
||||
|
||||
When an OpenPGP v6 key is encrypted, the public key parts are incorporated as authenticated data into the encryption
|
||||
process. Therefore, when instantiating a `PBESecretKeyEncryptor`, the public key needs to be passed in.
|
||||
As a consequence, the API of `SecretKeyRingProtector` changed and now a `PGPPublicKey` needs to be passed in,
|
||||
instead of merely a key-id or `KeyIdentifier`.
|
||||
|
||||
## Differences between BCs high-level API and PGPainless
|
||||
|
||||
With Bouncy Castle now introducing its own high-level API, you might ask, what differences there are between
|
||||
high-level PGPainless classes and their new Bouncy Castle counterparts.
|
||||
|
||||
### `KeyRingInfo` vs. `OpenPGPCertificate`/`OpenPGPKey`
|
||||
|
||||
PGPainless' `KeyRingInfo` class fulfils a similar task as the new `OpenPGPCertificate`/`OpenPGPKey` classes,
|
||||
namely evaluating OpenPGP key material, checking self signatures, exposing certain properties like
|
||||
subkeys, algorithm preferences etc. in a way accessible for the user, all with respect to a given reference time.
|
||||
|
||||
However, `KeyRingInfo` historically gets instantiated *per reference time*, while`OpenPGPCertificate`/`OpenPGPKey`
|
||||
is instantiated only *once* and expects you to pass in the reference time each time you are using a
|
||||
property getter, lazily evaluating applicable signatures as needed.
|
||||
Under the hood, the Bouncy Castle classes now cache expensive signature verification results for later use.
|
||||
Consequently, `KeyRingInfo` now wraps `OpenPGPCertificate`/`OpenPGPKey`, forwarding method calls while passing along
|
||||
the chosen reference time and mapping basic data types to PGPainless' high-level types / enums.
|
||||
|
||||
|
||||
## Type Replacements
|
||||
|
||||
| Old | New | Comment |
|
||||
|------------------------------|------------------------------|---------------------------------------------------------------------|
|
||||
| `PGPPublicKeyRing` | `OpenPGPCertificate` | Self-Signatures are automagically evaluated |
|
||||
| `PGPSecretKeyRing` | `OpenPGPKey` | Same as `OpenPGPCertificate`, but also contains secret key material |
|
||||
| `PGPPublicKey` (primary key) | `OpenPGPPrimaryKey` | Primary keys provide getters to access bound user identities |
|
||||
| `PGPPublicKey` (subkey) | `OpenPGPComponentKey` | - |
|
||||
| `PGPSecretKey` (primary key) | `OpenPGPSecretKey` | - |
|
||||
| `PGPSecretKey` (subkey) | `OpenPGPSecretKey` | - |
|
||||
| `PGPPrivateKey` | `OpenPGPPrivateKey` | - |
|
||||
| `Long` (Key-ID) | `KeyIdentifier` | - |
|
||||
| `byte[]` (Key Fingerprint) | `KeyIdentifier` | - |
|
||||
| `MissingPublicKeyCallback` | `OpenPGPCertificateProvider` | - |
|
||||
| (detached) `PGPSignature` | `OpenPGPDocumentSignature` | - |
|
||||
|
||||
|
||||
## Algorithm Support
|
||||
|
||||
The use of ElGamal as public key algorithm is now deprecated. Consequently, it is no longer possible to generate
|
||||
ElGamal keys.
|
||||
|
||||
RFC9580 introduced new key types `Ed25519`, `Ed448`, `X25519`, `X448`.
|
|
@ -11,12 +11,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate;
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.pgpainless.PGPainless;
|
||||
import org.pgpainless.key.info.KeyRingInfo;
|
||||
|
@ -25,24 +22,26 @@ import sop.exception.SOPGPException;
|
|||
|
||||
public class ExtractCertCmdTest extends CLITest {
|
||||
|
||||
private final PGPainless api = PGPainless.getInstance();
|
||||
|
||||
public ExtractCertCmdTest() {
|
||||
super(LoggerFactory.getLogger(ExtractCertCmdTest.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractCert()
|
||||
throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException {
|
||||
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing()
|
||||
throws IOException {
|
||||
OpenPGPKey key = api.generateKey()
|
||||
.simpleEcKeyRing("Juliet Capulet <juliet@capulet.lit>");
|
||||
|
||||
pipeBytesToStdin(secretKeys.getEncoded());
|
||||
pipeBytesToStdin(key.getEncoded());
|
||||
ByteArrayOutputStream out = pipeStdoutToStream();
|
||||
assertSuccess(executeCommand("extract-cert", "--armor"));
|
||||
|
||||
assertTrue(out.toString().startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"));
|
||||
|
||||
PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(out.toByteArray());
|
||||
KeyRingInfo info = PGPainless.inspectKeyRing(publicKeys);
|
||||
OpenPGPCertificate certificate = api.readKey().parseCertificate(out.toByteArray());
|
||||
KeyRingInfo info = api.inspect(certificate);
|
||||
assertFalse(info.isSecretKey());
|
||||
assertTrue(info.isUserIdValid("Juliet Capulet <juliet@capulet.lit>"));
|
||||
}
|
||||
|
|
|
@ -11,13 +11,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate;
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.pgpainless.PGPainless;
|
||||
import org.pgpainless.algorithm.KeyFlag;
|
||||
|
@ -25,6 +21,8 @@ import org.pgpainless.key.generation.KeySpec;
|
|||
import org.pgpainless.key.generation.type.KeyType;
|
||||
import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve;
|
||||
import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec;
|
||||
import org.pgpainless.sop.EncryptImpl;
|
||||
import org.pgpainless.sop.GenerateKeyImpl;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sop.exception.SOPGPException;
|
||||
|
||||
|
@ -138,7 +136,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@Disabled("Disabled, since we now read certificates from secret keys")
|
||||
// @Disabled("Disabled, since we now read certificates from secret keys")
|
||||
public void testEncryptingForKeyFails() throws IOException {
|
||||
File notACert = writeFile("key.asc", KEY);
|
||||
|
||||
|
@ -298,14 +296,14 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptWithIncapableCert() throws PGPException,
|
||||
InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
|
||||
PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing()
|
||||
public void testEncryptWithIncapableCert() throws IOException {
|
||||
PGPainless api = PGPainless.getInstance();
|
||||
OpenPGPKey key = api.buildKey()
|
||||
.addUserId("No Crypt <no@crypt.key>")
|
||||
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519),
|
||||
KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA))
|
||||
.build();
|
||||
PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys);
|
||||
OpenPGPCertificate cert = key.toCertificate();
|
||||
File certFile = writeFile("cert.pgp", cert.getEncoded());
|
||||
|
||||
pipeStringToStdin("Hello, World!\n");
|
||||
|
@ -318,15 +316,16 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
|
|||
|
||||
@Test
|
||||
public void testSignWithIncapableKey()
|
||||
throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
|
||||
PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing()
|
||||
throws IOException {
|
||||
PGPainless api = PGPainless.getInstance();
|
||||
OpenPGPKey key = api.buildKey()
|
||||
.addUserId("Cannot Sign <cannot@sign.key>")
|
||||
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER))
|
||||
.addSubkey(KeySpec.getBuilder(
|
||||
KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE))
|
||||
.build();
|
||||
File keyFile = writeFile("key.pgp", secretKeys.getEncoded());
|
||||
File certFile = writeFile("cert.pgp", PGPainless.extractCertificate(secretKeys).getEncoded());
|
||||
File keyFile = writeFile("key.pgp", key.getEncoded());
|
||||
File certFile = writeFile("cert.pgp", key.toCertificate().getEncoded());
|
||||
|
||||
pipeStringToStdin("Hello, World!\n");
|
||||
ByteArrayOutputStream out = pipeStdoutToStream();
|
||||
|
@ -650,7 +649,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
|
|||
// Generate key
|
||||
File passwordFile = writeFile("password", "sw0rdf1sh");
|
||||
File keyFile = pipeStdoutToFile("key.asc");
|
||||
assertSuccess(executeCommand("generate-key", "--profile=rfc4880", "--with-key-password", passwordFile.getAbsolutePath(), "Alice <alice@example.org>"));
|
||||
assertSuccess(executeCommand("generate-key", "--profile=" + GenerateKeyImpl.RFC4880_RSA4096_PROFILE.getName(), "--with-key-password", passwordFile.getAbsolutePath(), "Alice <alice@example.org>"));
|
||||
|
||||
File certFile = pipeStdoutToFile("cert.asc");
|
||||
pipeFileToStdin(keyFile);
|
||||
|
@ -662,7 +661,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest {
|
|||
// Encrypt
|
||||
File ciphertextFile = pipeStdoutToFile("msg.asc");
|
||||
pipeFileToStdin(plaintextFile);
|
||||
assertSuccess(executeCommand("encrypt", "--profile=rfc4880", certFile.getAbsolutePath()));
|
||||
assertSuccess(executeCommand("encrypt", "--profile=" + EncryptImpl.RFC4880_PROFILE.getName(), certFile.getAbsolutePath()));
|
||||
|
||||
ByteArrayOutputStream decrypted = pipeStdoutToStream();
|
||||
pipeFileToStdin(ciphertextFile);
|
||||
|
|
|
@ -15,8 +15,8 @@ import java.nio.charset.StandardCharsets;
|
|||
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.pgpainless.PGPainless;
|
||||
import org.pgpainless.algorithm.CompressionAlgorithm;
|
||||
|
@ -350,12 +350,13 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest {
|
|||
|
||||
@Test
|
||||
public void createMalformedMessage() throws IOException, PGPException {
|
||||
PGPainless api = PGPainless.getInstance();
|
||||
String msg = "Hello, World!\n";
|
||||
PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY_2);
|
||||
OpenPGPKey key = api.readKey().parseKey(KEY_2);
|
||||
ByteArrayOutputStream ciphertext = new ByteArrayOutputStream();
|
||||
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
|
||||
EncryptionStream encryptionStream = api.generateMessage()
|
||||
.onOutputStream(ciphertext)
|
||||
.withOptions(ProducerOptions.sign(SigningOptions.get()
|
||||
.withOptions(ProducerOptions.sign(SigningOptions.get(api)
|
||||
.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key)
|
||||
).overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)
|
||||
.setAsciiArmor(false));
|
||||
|
|
|
@ -11,12 +11,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.ParseException;
|
||||
import java.util.Date;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -199,12 +196,13 @@ public class RoundTripSignVerifyCmdTest extends CLITest {
|
|||
|
||||
@Test
|
||||
public void testSignWithIncapableKey()
|
||||
throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
|
||||
throws IOException {
|
||||
PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing()
|
||||
.addUserId("Cannot Sign <cannot@sign.key>")
|
||||
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER))
|
||||
.addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE))
|
||||
.build();
|
||||
.build()
|
||||
.getPGPSecretKeyRing();
|
||||
File keyFile = writeFile("key.pgp", secretKeys.getEncoded());
|
||||
|
||||
pipeStringToStdin("Hello, World!\n");
|
||||
|
@ -252,7 +250,7 @@ public class RoundTripSignVerifyCmdTest extends CLITest {
|
|||
String verification = verificationsOut.toString();
|
||||
String[] split = verification.split(" ");
|
||||
OpenPgpV4Fingerprint primaryKeyFingerprint = new OpenPgpV4Fingerprint(cert);
|
||||
OpenPgpV4Fingerprint signingKeyFingerprint = new OpenPgpV4Fingerprint(info.getSigningSubkeys().get(0));
|
||||
OpenPgpV4Fingerprint signingKeyFingerprint = new OpenPgpV4Fingerprint(info.getSigningSubkeys().get(0).getPGPPublicKey());
|
||||
assertEquals(signingKeyFingerprint.toString(), split[1].trim(), verification);
|
||||
assertEquals(primaryKeyFingerprint.toString(), split[2].trim());
|
||||
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.gnupg;
|
||||
|
||||
import org.bouncycastle.bcpg.S2K;
|
||||
|
||||
public enum GnuPGDummyExtension {
|
||||
|
||||
/**
|
||||
* Do not store the secret part at all.
|
||||
*/
|
||||
NO_PRIVATE_KEY(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY),
|
||||
|
||||
/**
|
||||
* A stub to access smartcards.
|
||||
*/
|
||||
DIVERT_TO_CARD(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD),
|
||||
;
|
||||
|
||||
private final int id;
|
||||
|
||||
GnuPGDummyExtension(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
|
@ -1,210 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.gnupg;
|
||||
|
||||
import org.bouncycastle.bcpg.PublicKeyPacket;
|
||||
import org.bouncycastle.bcpg.S2K;
|
||||
import org.bouncycastle.bcpg.SecretKeyPacket;
|
||||
import org.bouncycastle.bcpg.SecretSubkeyPacket;
|
||||
import org.bouncycastle.openpgp.PGPSecretKey;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.pgpainless.key.SubkeyIdentifier;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This class can be used to remove private keys from secret software-keys by replacing them with
|
||||
* stub secret keys in the style of GnuPGs proprietary extensions.
|
||||
*
|
||||
* @see <a href="https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;hb=HEAD#l1489">
|
||||
* GnuPGs doc/DETAILS - GNU extensions to the S2K algorithm</a>
|
||||
*/
|
||||
public final class GnuPGDummyKeyUtil {
|
||||
|
||||
private GnuPGDummyKeyUtil() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key-ids of all keys which appear to be stored on a hardware token / smartcard by GnuPG.
|
||||
* Note, that this functionality is based on GnuPGs proprietary S2K extensions, which are not strictly required
|
||||
* for dealing with hardware-backed keys.
|
||||
*
|
||||
* @param secretKeys secret keys
|
||||
* @return set of keys with S2K type GNU_DUMMY_S2K and protection mode DIVERT_TO_CARD
|
||||
*/
|
||||
public static Set<SubkeyIdentifier> getIdsOfKeysWithGnuPGS2KDivertedToCard(@Nonnull PGPSecretKeyRing secretKeys) {
|
||||
Set<SubkeyIdentifier> hardwareBackedKeys = new HashSet<>();
|
||||
for (PGPSecretKey secretKey : secretKeys) {
|
||||
S2K s2K = secretKey.getS2K();
|
||||
if (s2K == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int type = s2K.getType();
|
||||
int mode = s2K.getProtectionMode();
|
||||
// TODO: Is GNU_DUMMY_S2K appropriate?
|
||||
if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) {
|
||||
SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID());
|
||||
hardwareBackedKeys.add(hardwareBackedKey);
|
||||
}
|
||||
}
|
||||
return hardwareBackedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the given {@link PGPSecretKeyRing}.
|
||||
*
|
||||
* @param secretKeys secret keys
|
||||
* @return builder
|
||||
*/
|
||||
public static Builder modify(@Nonnull PGPSecretKeyRing secretKeys) {
|
||||
return new Builder(secretKeys);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private final PGPSecretKeyRing keys;
|
||||
|
||||
private Builder(@Nonnull PGPSecretKeyRing keys) {
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with
|
||||
* GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#NO_PRIVATE_KEY}.
|
||||
*
|
||||
* @param filter filter to select keys for removal
|
||||
* @return modified key ring
|
||||
*/
|
||||
public PGPSecretKeyRing removePrivateKeys(@Nonnull KeyFilter filter) {
|
||||
return replacePrivateKeys(GnuPGDummyExtension.NO_PRIVATE_KEY, null, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with
|
||||
* GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}.
|
||||
* This method will set the serial number of the card to 0x00000000000000000000000000000000.
|
||||
* NOTE: This method does not actually move any keys to a card.
|
||||
*
|
||||
* @param filter filter to select keys for removal
|
||||
* @return modified key ring
|
||||
*/
|
||||
public PGPSecretKeyRing divertPrivateKeysToCard(@Nonnull KeyFilter filter) {
|
||||
return divertPrivateKeysToCard(filter, new byte[16]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with
|
||||
* GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}.
|
||||
* This method will include the card serial number into the encoded dummy key.
|
||||
* NOTE: This method does not actually move any keys to a card.
|
||||
*
|
||||
* @param filter filter to select keys for removal
|
||||
* @param cardSerialNumber serial number of the card (at most 16 bytes long)
|
||||
* @return modified key ring
|
||||
*/
|
||||
public PGPSecretKeyRing divertPrivateKeysToCard(@Nonnull KeyFilter filter, @Nullable byte[] cardSerialNumber) {
|
||||
if (cardSerialNumber != null && cardSerialNumber.length > 16) {
|
||||
throw new IllegalArgumentException("Card serial number length cannot exceed 16 bytes.");
|
||||
}
|
||||
return replacePrivateKeys(GnuPGDummyExtension.DIVERT_TO_CARD, cardSerialNumber, filter);
|
||||
}
|
||||
|
||||
private PGPSecretKeyRing replacePrivateKeys(@Nonnull GnuPGDummyExtension extension,
|
||||
@Nullable byte[] serial,
|
||||
@Nonnull KeyFilter filter) {
|
||||
byte[] encodedSerial = serial != null ? encodeSerial(serial) : null;
|
||||
S2K s2k = extensionToS2K(extension);
|
||||
|
||||
List<PGPSecretKey> secretKeyList = new ArrayList<>();
|
||||
for (PGPSecretKey secretKey : keys) {
|
||||
if (!filter.filter(secretKey.getKeyID())) {
|
||||
// No conversion, do not modify subkey
|
||||
secretKeyList.add(secretKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
PublicKeyPacket publicKeyPacket = secretKey.getPublicKey().getPublicKeyPacket();
|
||||
if (secretKey.isMasterKey()) {
|
||||
SecretKeyPacket keyPacket = new SecretKeyPacket(publicKeyPacket,
|
||||
0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial);
|
||||
PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey());
|
||||
secretKeyList.add(onCard);
|
||||
} else {
|
||||
SecretSubkeyPacket keyPacket = new SecretSubkeyPacket(publicKeyPacket,
|
||||
0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial);
|
||||
PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey());
|
||||
secretKeyList.add(onCard);
|
||||
}
|
||||
}
|
||||
|
||||
return new PGPSecretKeyRing(secretKeyList);
|
||||
}
|
||||
|
||||
private byte[] encodeSerial(@Nonnull byte[] serial) {
|
||||
byte[] encoded = new byte[serial.length + 1];
|
||||
encoded[0] = (byte) (serial.length & 0xff);
|
||||
System.arraycopy(serial, 0, encoded, 1, serial.length);
|
||||
return encoded;
|
||||
}
|
||||
|
||||
private S2K extensionToS2K(@Nonnull GnuPGDummyExtension extension) {
|
||||
return S2K.gnuDummyS2K(extension == GnuPGDummyExtension.DIVERT_TO_CARD ?
|
||||
S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter for selecting keys.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface KeyFilter {
|
||||
|
||||
/**
|
||||
* Return true, if the given key should be selected, false otherwise.
|
||||
*
|
||||
* @param keyId id of the key
|
||||
* @return select
|
||||
*/
|
||||
boolean filter(long keyId);
|
||||
|
||||
/**
|
||||
* Select any key.
|
||||
*
|
||||
* @return filter
|
||||
*/
|
||||
static KeyFilter any() {
|
||||
return keyId -> true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select only the given keyId.
|
||||
*
|
||||
* @param onlyKeyId only acceptable key id
|
||||
* @return filter
|
||||
*/
|
||||
static KeyFilter only(long onlyKeyId) {
|
||||
return keyId -> keyId == onlyKeyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select all keyIds which are contained in the given set of ids.
|
||||
*
|
||||
* @param ids set of acceptable keyIds
|
||||
* @return filter
|
||||
*/
|
||||
static KeyFilter selected(Collection<Long> ids) {
|
||||
// noinspection Convert2MethodRef
|
||||
return keyId -> ids.contains(keyId);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Utility classes related to creating keys with GNU DUMMY S2K values.
|
||||
*/
|
||||
package org.gnupg;
|
|
@ -1,417 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.decryption_verification;
|
||||
|
||||
import static org.bouncycastle.bcpg.PacketTags.COMPRESSED_DATA;
|
||||
import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_1;
|
||||
import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_2;
|
||||
import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_3;
|
||||
import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_4;
|
||||
import static org.bouncycastle.bcpg.PacketTags.LITERAL_DATA;
|
||||
import static org.bouncycastle.bcpg.PacketTags.MARKER;
|
||||
import static org.bouncycastle.bcpg.PacketTags.MOD_DETECTION_CODE;
|
||||
import static org.bouncycastle.bcpg.PacketTags.ONE_PASS_SIGNATURE;
|
||||
import static org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY;
|
||||
import static org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY_ENC_SESSION;
|
||||
import static org.bouncycastle.bcpg.PacketTags.PUBLIC_SUBKEY;
|
||||
import static org.bouncycastle.bcpg.PacketTags.RESERVED;
|
||||
import static org.bouncycastle.bcpg.PacketTags.SECRET_KEY;
|
||||
import static org.bouncycastle.bcpg.PacketTags.SECRET_SUBKEY;
|
||||
import static org.bouncycastle.bcpg.PacketTags.SIGNATURE;
|
||||
import static org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC;
|
||||
import static org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC_SESSION;
|
||||
import static org.bouncycastle.bcpg.PacketTags.SYM_ENC_INTEGRITY_PRO;
|
||||
import static org.bouncycastle.bcpg.PacketTags.TRUST;
|
||||
import static org.bouncycastle.bcpg.PacketTags.USER_ATTRIBUTE;
|
||||
import static org.bouncycastle.bcpg.PacketTags.USER_ID;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import org.bouncycastle.bcpg.BCPGInputStream;
|
||||
import org.bouncycastle.openpgp.PGPCompressedData;
|
||||
import org.bouncycastle.openpgp.PGPEncryptedData;
|
||||
import org.bouncycastle.openpgp.PGPLiteralData;
|
||||
import org.bouncycastle.openpgp.PGPOnePassSignature;
|
||||
import org.pgpainless.algorithm.CompressionAlgorithm;
|
||||
import org.pgpainless.algorithm.HashAlgorithm;
|
||||
import org.pgpainless.algorithm.PublicKeyAlgorithm;
|
||||
import org.pgpainless.algorithm.SignatureType;
|
||||
import org.pgpainless.algorithm.StreamEncoding;
|
||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
|
||||
|
||||
/**
|
||||
* InputStream used to determine the nature of potential OpenPGP data.
|
||||
*/
|
||||
public class OpenPgpInputStream extends BufferedInputStream {
|
||||
|
||||
@SuppressWarnings("CharsetObjectCanBeUsed")
|
||||
private static final byte[] ARMOR_HEADER = "-----BEGIN PGP ".getBytes(Charset.forName("UTF8"));
|
||||
|
||||
// Buffer beginning bytes of the data
|
||||
public static final int MAX_BUFFER_SIZE = 8192 * 2;
|
||||
|
||||
private final byte[] buffer;
|
||||
private final int bufferLen;
|
||||
|
||||
private boolean containsArmorHeader;
|
||||
private boolean containsOpenPgpPackets;
|
||||
private boolean isLikelyOpenPgpMessage;
|
||||
|
||||
public OpenPgpInputStream(InputStream in, boolean check) throws IOException {
|
||||
super(in, MAX_BUFFER_SIZE);
|
||||
|
||||
mark(MAX_BUFFER_SIZE);
|
||||
buffer = new byte[MAX_BUFFER_SIZE];
|
||||
bufferLen = read(buffer);
|
||||
reset();
|
||||
|
||||
if (check) {
|
||||
inspectBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
public OpenPgpInputStream(InputStream in) throws IOException {
|
||||
this(in, true);
|
||||
}
|
||||
|
||||
private void inspectBuffer() throws IOException {
|
||||
if (checkForAsciiArmor()) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkForBinaryOpenPgp();
|
||||
}
|
||||
|
||||
private boolean checkForAsciiArmor() {
|
||||
if (startsWithIgnoringWhitespace(buffer, ARMOR_HEADER, bufferLen)) {
|
||||
containsArmorHeader = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is still brittle.
|
||||
* Basically we try to parse OpenPGP packets from the buffer.
|
||||
* If we run into exceptions, then we know that the data is non-OpenPGP'ish.
|
||||
*
|
||||
* This breaks down though if we read plausible garbage where the data accidentally makes sense,
|
||||
* or valid, yet incomplete packets (remember, we are still only working on a portion of the data).
|
||||
*/
|
||||
private void checkForBinaryOpenPgp() throws IOException {
|
||||
if (bufferLen == -1) {
|
||||
// Empty data
|
||||
return;
|
||||
}
|
||||
|
||||
ByteArrayInputStream bufferIn = new ByteArrayInputStream(buffer, 0, bufferLen);
|
||||
nonExhaustiveParseAndCheckPlausibility(bufferIn);
|
||||
}
|
||||
|
||||
private void nonExhaustiveParseAndCheckPlausibility(ByteArrayInputStream bufferIn) throws IOException {
|
||||
// Read the packet header
|
||||
int hdr = bufferIn.read();
|
||||
if (hdr < 0 || (hdr & 0x80) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean newPacket = (hdr & 0x40) != 0;
|
||||
int tag = 0;
|
||||
int bodyLen = 0;
|
||||
boolean partial = false;
|
||||
|
||||
// Determine the packet length
|
||||
if (newPacket) {
|
||||
tag = hdr & 0x3f;
|
||||
|
||||
int l = bufferIn.read();
|
||||
if (l < 192) {
|
||||
bodyLen = l;
|
||||
} else if (l <= 223) {
|
||||
int b = bufferIn.read();
|
||||
bodyLen = ((l - 192) << 8) + (b) + 192;
|
||||
} else if (l == 255) {
|
||||
bodyLen = (bufferIn.read() << 24) | (bufferIn.read() << 16) | (bufferIn.read() << 8) | bufferIn.read();
|
||||
} else {
|
||||
partial = true;
|
||||
bodyLen = 1 << (l & 0x1f);
|
||||
}
|
||||
} else {
|
||||
int lengthType = hdr & 0x3;
|
||||
tag = (hdr & 0x3f) >> 2;
|
||||
switch (lengthType) {
|
||||
case 0:
|
||||
bodyLen = bufferIn.read();
|
||||
break;
|
||||
case 1:
|
||||
bodyLen = (bufferIn.read() << 8) | bufferIn.read();
|
||||
break;
|
||||
case 2:
|
||||
bodyLen = (bufferIn.read() << 24) | (bufferIn.read() << 16) | (bufferIn.read() << 8) | bufferIn.read();
|
||||
break;
|
||||
case 3:
|
||||
partial = true;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Negative body length -> garbage
|
||||
if (bodyLen < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to unexhaustively parse the first packet bit by bit and check for plausibility
|
||||
BCPGInputStream bcpgIn = new BCPGInputStream(bufferIn);
|
||||
switch (tag) {
|
||||
case RESERVED:
|
||||
// How to handle this? Probably discard as garbage...
|
||||
return;
|
||||
|
||||
case PUBLIC_KEY_ENC_SESSION:
|
||||
int pkeskVersion = bcpgIn.read();
|
||||
if (pkeskVersion <= 0 || pkeskVersion > 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip Key-ID
|
||||
for (int i = 0; i < 8; i++) {
|
||||
bcpgIn.read();
|
||||
}
|
||||
|
||||
int pkeskAlg = bcpgIn.read();
|
||||
if (PublicKeyAlgorithm.fromId(pkeskAlg) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
containsOpenPgpPackets = true;
|
||||
isLikelyOpenPgpMessage = true;
|
||||
break;
|
||||
|
||||
case SIGNATURE:
|
||||
int sigVersion = bcpgIn.read();
|
||||
int sigType;
|
||||
if (sigVersion == 2 || sigVersion == 3) {
|
||||
int l = bcpgIn.read();
|
||||
sigType = bcpgIn.read();
|
||||
} else if (sigVersion == 4 || sigVersion == 5) {
|
||||
sigType = bcpgIn.read();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SignatureType.requireFromCode(sigType);
|
||||
} catch (NoSuchElementException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
containsOpenPgpPackets = true;
|
||||
break;
|
||||
|
||||
case SYMMETRIC_KEY_ENC_SESSION:
|
||||
int skeskVersion = bcpgIn.read();
|
||||
if (skeskVersion == 4) {
|
||||
int skeskAlg = bcpgIn.read();
|
||||
if (SymmetricKeyAlgorithm.fromId(skeskAlg) == null) {
|
||||
return;
|
||||
}
|
||||
// TODO: Parse S2K?
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
containsOpenPgpPackets = true;
|
||||
isLikelyOpenPgpMessage = true;
|
||||
break;
|
||||
|
||||
case ONE_PASS_SIGNATURE:
|
||||
int opsVersion = bcpgIn.read();
|
||||
if (opsVersion == 3) {
|
||||
int opsSigType = bcpgIn.read();
|
||||
try {
|
||||
SignatureType.requireFromCode(opsSigType);
|
||||
} catch (NoSuchElementException e) {
|
||||
return;
|
||||
}
|
||||
int opsHashAlg = bcpgIn.read();
|
||||
if (HashAlgorithm.fromId(opsHashAlg) == null) {
|
||||
return;
|
||||
}
|
||||
int opsKeyAlg = bcpgIn.read();
|
||||
if (PublicKeyAlgorithm.fromId(opsKeyAlg) == null) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
containsOpenPgpPackets = true;
|
||||
isLikelyOpenPgpMessage = true;
|
||||
break;
|
||||
|
||||
case SECRET_KEY:
|
||||
case PUBLIC_KEY:
|
||||
case SECRET_SUBKEY:
|
||||
case PUBLIC_SUBKEY:
|
||||
int keyVersion = bcpgIn.read();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
// Creation time
|
||||
bcpgIn.read();
|
||||
}
|
||||
if (keyVersion == 3) {
|
||||
long validDays = (in.read() << 8) | in.read();
|
||||
if (validDays < 0) {
|
||||
return;
|
||||
}
|
||||
} else if (keyVersion == 4) {
|
||||
|
||||
} else if (keyVersion == 5) {
|
||||
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
int keyAlg = bcpgIn.read();
|
||||
if (PublicKeyAlgorithm.fromId(keyAlg) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
containsOpenPgpPackets = true;
|
||||
break;
|
||||
|
||||
case COMPRESSED_DATA:
|
||||
int compAlg = bcpgIn.read();
|
||||
if (CompressionAlgorithm.fromId(compAlg) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
containsOpenPgpPackets = true;
|
||||
isLikelyOpenPgpMessage = true;
|
||||
break;
|
||||
|
||||
case SYMMETRIC_KEY_ENC:
|
||||
// No data to compare :(
|
||||
containsOpenPgpPackets = true;
|
||||
// While this is a valid OpenPGP message, enabling the line below would lead to too many false positives
|
||||
// isLikelyOpenPgpMessage = true;
|
||||
break;
|
||||
|
||||
case MARKER:
|
||||
byte[] marker = new byte[3];
|
||||
bcpgIn.readFully(marker);
|
||||
if (marker[0] != 0x50 || marker[1] != 0x47 || marker[2] != 0x50) {
|
||||
return;
|
||||
}
|
||||
|
||||
containsOpenPgpPackets = true;
|
||||
break;
|
||||
|
||||
case LITERAL_DATA:
|
||||
int format = bcpgIn.read();
|
||||
if (StreamEncoding.fromCode(format) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
containsOpenPgpPackets = true;
|
||||
isLikelyOpenPgpMessage = true;
|
||||
break;
|
||||
|
||||
case TRUST:
|
||||
case USER_ID:
|
||||
case USER_ATTRIBUTE:
|
||||
// Not much to compare
|
||||
containsOpenPgpPackets = true;
|
||||
break;
|
||||
|
||||
case SYM_ENC_INTEGRITY_PRO:
|
||||
int seipVersion = bcpgIn.read();
|
||||
if (seipVersion != 1) {
|
||||
return;
|
||||
}
|
||||
isLikelyOpenPgpMessage = true;
|
||||
containsOpenPgpPackets = true;
|
||||
break;
|
||||
|
||||
case MOD_DETECTION_CODE:
|
||||
byte[] digest = new byte[20];
|
||||
bcpgIn.readFully(digest);
|
||||
|
||||
containsOpenPgpPackets = true;
|
||||
break;
|
||||
|
||||
case EXPERIMENTAL_1:
|
||||
case EXPERIMENTAL_2:
|
||||
case EXPERIMENTAL_3:
|
||||
case EXPERIMENTAL_4:
|
||||
return;
|
||||
default:
|
||||
containsOpenPgpPackets = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean startsWithIgnoringWhitespace(byte[] bytes, byte[] subsequence, int bufferLen) {
|
||||
if (bufferLen == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < bufferLen; i++) {
|
||||
// Working on bytes is not trivial with unicode data, but its good enough here
|
||||
if (Character.isWhitespace(bytes[i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((i + subsequence.length) > bytes.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int j = 0; j < subsequence.length; j++) {
|
||||
if (bytes[i + j] != subsequence[j]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isAsciiArmored() {
|
||||
return containsArmorHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true, if the data is possibly binary OpenPGP.
|
||||
* The criterion for this are less strict than for {@link #isLikelyOpenPgpMessage()},
|
||||
* as it also accepts other OpenPGP packets at the beginning of the data stream.
|
||||
*
|
||||
* Use with caution.
|
||||
*
|
||||
* @return true if data appears to be binary OpenPGP data
|
||||
*/
|
||||
public boolean isBinaryOpenPgp() {
|
||||
return containsOpenPgpPackets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true, if the underlying data is very likely (more than 99,9%) an OpenPGP message.
|
||||
* OpenPGP Message means here that it starts with either an {@link PGPEncryptedData},
|
||||
* {@link PGPCompressedData}, {@link PGPOnePassSignature} or {@link PGPLiteralData} packet.
|
||||
* The plausability of these data packets is checked as far as possible.
|
||||
*
|
||||
* @return true if likely OpenPGP message
|
||||
*/
|
||||
public boolean isLikelyOpenPgpMessage() {
|
||||
return isLikelyOpenPgpMessage;
|
||||
}
|
||||
|
||||
public boolean isNonOpenPgp() {
|
||||
return !isAsciiArmored() && !isBinaryOpenPgp();
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Classes used to decryption and verification of OpenPGP encrypted / signed data.
|
||||
*/
|
||||
package org.pgpainless.decryption_verification;
|
|
@ -1,132 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.pgpainless.algorithm.PublicKeyAlgorithm;
|
||||
import org.pgpainless.key.OpenPgpFingerprint;
|
||||
import org.pgpainless.util.DateUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Date;
|
||||
|
||||
public abstract class KeyException extends RuntimeException {
|
||||
|
||||
private final OpenPgpFingerprint fingerprint;
|
||||
|
||||
protected KeyException(@Nonnull String message, @Nonnull OpenPgpFingerprint fingerprint) {
|
||||
super(message);
|
||||
this.fingerprint = fingerprint;
|
||||
}
|
||||
|
||||
protected KeyException(@Nonnull String message, @Nonnull OpenPgpFingerprint fingerprint, @Nonnull Throwable underlying) {
|
||||
super(message, underlying);
|
||||
this.fingerprint = fingerprint;
|
||||
}
|
||||
|
||||
public OpenPgpFingerprint getFingerprint() {
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
public static class ExpiredKeyException extends KeyException {
|
||||
|
||||
public ExpiredKeyException(@Nonnull OpenPgpFingerprint fingerprint, @Nonnull Date expirationDate) {
|
||||
super("Key " + fingerprint + " is expired. Expiration date: " + DateUtil.formatUTCDate(expirationDate), fingerprint);
|
||||
}
|
||||
}
|
||||
|
||||
public static class RevokedKeyException extends KeyException {
|
||||
|
||||
public RevokedKeyException(@Nonnull OpenPgpFingerprint fingerprint) {
|
||||
super("Key " + fingerprint + " appears to be revoked.", fingerprint);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnacceptableEncryptionKeyException extends KeyException {
|
||||
|
||||
public UnacceptableEncryptionKeyException(@Nonnull OpenPgpFingerprint fingerprint) {
|
||||
super("Key " + fingerprint + " has no acceptable encryption key.", fingerprint);
|
||||
}
|
||||
|
||||
public UnacceptableEncryptionKeyException(@Nonnull PublicKeyAlgorithmPolicyException reason) {
|
||||
super("Key " + reason.getFingerprint() + " has no acceptable encryption key.", reason.getFingerprint(), reason);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnacceptableSigningKeyException extends KeyException {
|
||||
|
||||
public UnacceptableSigningKeyException(@Nonnull OpenPgpFingerprint fingerprint) {
|
||||
super("Key " + fingerprint + " has no acceptable signing key.", fingerprint);
|
||||
}
|
||||
|
||||
public UnacceptableSigningKeyException(@Nonnull PublicKeyAlgorithmPolicyException reason) {
|
||||
super("Key " + reason.getFingerprint() + " has no acceptable signing key.", reason.getFingerprint(), reason);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnacceptableThirdPartyCertificationKeyException extends KeyException {
|
||||
|
||||
public UnacceptableThirdPartyCertificationKeyException(@Nonnull OpenPgpFingerprint fingerprint) {
|
||||
super("Key " + fingerprint + " has no acceptable certification key.", fingerprint);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnacceptableSelfSignatureException extends KeyException {
|
||||
|
||||
public UnacceptableSelfSignatureException(@Nonnull OpenPgpFingerprint fingerprint) {
|
||||
super("Key " + fingerprint + " does not have a valid/acceptable signature to derive an expiration date from.", fingerprint);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MissingSecretKeyException extends KeyException {
|
||||
|
||||
private final long missingSecretKeyId;
|
||||
|
||||
public MissingSecretKeyException(@Nonnull OpenPgpFingerprint fingerprint, long keyId) {
|
||||
super("Key " + fingerprint + " does not contain a secret key for public key " + Long.toHexString(keyId), fingerprint);
|
||||
this.missingSecretKeyId = keyId;
|
||||
}
|
||||
|
||||
public long getMissingSecretKeyId() {
|
||||
return missingSecretKeyId;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PublicKeyAlgorithmPolicyException extends KeyException {
|
||||
|
||||
private final long violatingSubkeyId;
|
||||
|
||||
public PublicKeyAlgorithmPolicyException(@Nonnull OpenPgpFingerprint fingerprint, long keyId, @Nonnull PublicKeyAlgorithm algorithm, int bitSize) {
|
||||
super("Subkey " + Long.toHexString(keyId) + " of key " + fingerprint + " is violating the Public Key Algorithm Policy:\n" +
|
||||
algorithm + " of size " + bitSize + " is not acceptable.", fingerprint);
|
||||
this.violatingSubkeyId = keyId;
|
||||
}
|
||||
|
||||
public long getViolatingSubkeyId() {
|
||||
return violatingSubkeyId;
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnboundUserIdException extends KeyException {
|
||||
|
||||
public UnboundUserIdException(@Nonnull OpenPgpFingerprint fingerprint, @Nonnull String userId,
|
||||
@Nullable PGPSignature userIdSignature, @Nullable PGPSignature userIdRevocation) {
|
||||
super(errorMessage(fingerprint, userId, userIdSignature, userIdRevocation), fingerprint);
|
||||
}
|
||||
|
||||
private static String errorMessage(@Nonnull OpenPgpFingerprint fingerprint, @Nonnull String userId,
|
||||
@Nullable PGPSignature userIdSignature, @Nullable PGPSignature userIdRevocation) {
|
||||
String errorMessage = "UserID '" + userId + "' is not valid for key " + fingerprint + ": ";
|
||||
if (userIdSignature == null) {
|
||||
return errorMessage + "Missing binding signature.";
|
||||
}
|
||||
if (userIdRevocation != null) {
|
||||
return errorMessage + "UserID is revoked.";
|
||||
}
|
||||
return errorMessage + "Unacceptable binding signature.";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception;
|
||||
|
||||
/**
|
||||
* This exception gets thrown, when the integrity of an OpenPGP key is broken.
|
||||
* That could happen on accident, or during an active attack, so take this exception seriously.
|
||||
*/
|
||||
public class KeyIntegrityException extends AssertionError {
|
||||
|
||||
public KeyIntegrityException() {
|
||||
super("Key Integrity Exception");
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception;
|
||||
|
||||
import org.pgpainless.decryption_verification.syntax_check.InputSymbol;
|
||||
import org.pgpainless.decryption_verification.syntax_check.StackSymbol;
|
||||
import org.pgpainless.decryption_verification.syntax_check.State;
|
||||
|
||||
/**
|
||||
* Exception that gets thrown if the OpenPGP message is malformed.
|
||||
* Malformed messages are messages which do not follow the grammar specified in the RFC.
|
||||
*
|
||||
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-11.3">RFC4880 §11.3. OpenPGP Messages</a>
|
||||
*/
|
||||
public class MalformedOpenPgpMessageException extends RuntimeException {
|
||||
|
||||
public MalformedOpenPgpMessageException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public MalformedOpenPgpMessageException(State state, InputSymbol input, StackSymbol stackItem) {
|
||||
this("There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack.");
|
||||
}
|
||||
|
||||
public MalformedOpenPgpMessageException(String s, MalformedOpenPgpMessageException e) {
|
||||
super(s, e);
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
|
||||
public class MessageNotIntegrityProtectedException extends PGPException {
|
||||
|
||||
public MessageNotIntegrityProtectedException() {
|
||||
super("Message is encrypted using a 'Symmetrically Encrypted Data' (SED) packet, which enables certain types of attacks. " +
|
||||
"A 'Symmetrically Encrypted Integrity Protected' (SEIP) packet should be used instead.");
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
|
||||
/**
|
||||
* Exception that is thrown when decryption fails due to a missing decryption key or decryption passphrase.
|
||||
* This can happen when the user does not provide the right set of keys / the right password when decrypting
|
||||
* a message.
|
||||
*/
|
||||
public class MissingDecryptionMethodException extends PGPException {
|
||||
|
||||
public MissingDecryptionMethodException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.pgpainless.key.SubkeyIdentifier;
|
||||
|
||||
public class MissingPassphraseException extends PGPException {
|
||||
|
||||
private final Set<SubkeyIdentifier> keyIds;
|
||||
|
||||
public MissingPassphraseException(Set<SubkeyIdentifier> keyIds) {
|
||||
super("Missing passphrase encountered for keys " + Arrays.toString(keyIds.toArray()));
|
||||
this.keyIds = Collections.unmodifiableSet(keyIds);
|
||||
}
|
||||
|
||||
public Set<SubkeyIdentifier> getKeyIds() {
|
||||
return keyIds;
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Exception that gets thrown when the verification of a modification detection code failed.
|
||||
*/
|
||||
public class ModificationDetectionException extends IOException {
|
||||
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.pgpainless.algorithm.SignatureType;
|
||||
|
||||
public class SignatureValidationException extends PGPException {
|
||||
|
||||
public SignatureValidationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SignatureValidationException(String message, Exception underlying) {
|
||||
super(message, underlying);
|
||||
}
|
||||
|
||||
public SignatureValidationException(String message, Map<PGPSignature, Exception> rejections) {
|
||||
super(message + ": " + exceptionMapToString(rejections));
|
||||
}
|
||||
|
||||
private static String exceptionMapToString(Map<PGPSignature, Exception> rejections) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(rejections.size()).append(" rejected signatures:\n");
|
||||
for (PGPSignature signature : rejections.keySet()) {
|
||||
String typeString;
|
||||
SignatureType type = SignatureType.fromCode(signature.getSignatureType());
|
||||
if (type == null) {
|
||||
typeString = "0x" + Long.toHexString(signature.getSignatureType());
|
||||
} else {
|
||||
typeString = type.toString();
|
||||
}
|
||||
sb.append(typeString).append(' ')
|
||||
.append(signature.getCreationTime()).append(": ")
|
||||
.append(rejections.get(signature).getMessage()).append('\n');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
|
||||
/**
|
||||
* Exception that gets thrown if unacceptable algorithms are encountered.
|
||||
*/
|
||||
public class UnacceptableAlgorithmException extends PGPException {
|
||||
|
||||
public UnacceptableAlgorithmException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
|
||||
public class WrongConsumingMethodException extends PGPException {
|
||||
|
||||
public WrongConsumingMethodException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
|
||||
public class WrongPassphraseException extends PGPException {
|
||||
|
||||
public WrongPassphraseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public WrongPassphraseException(long keyId, PGPException cause) {
|
||||
this("Wrong passphrase provided for key " + Long.toHexString(keyId), cause);
|
||||
}
|
||||
|
||||
public WrongPassphraseException(String message, PGPException cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Exceptions.
|
||||
*/
|
||||
package org.pgpainless.exception;
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Classes related to OpenPGP keys.
|
||||
*/
|
||||
package org.pgpainless.key;
|
|
@ -1,113 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.key.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.pgpainless.algorithm.HashAlgorithm;
|
||||
import org.pgpainless.algorithm.SignatureType;
|
||||
|
||||
public final class OpenPgpKeyAttributeUtil {
|
||||
|
||||
private OpenPgpKeyAttributeUtil() {
|
||||
|
||||
}
|
||||
|
||||
public static List<HashAlgorithm> getPreferredHashAlgorithms(PGPPublicKey publicKey) {
|
||||
List<HashAlgorithm> hashAlgorithms = new ArrayList<>();
|
||||
Iterator<?> keySignatures = publicKey.getSignatures();
|
||||
while (keySignatures.hasNext()) {
|
||||
PGPSignature signature = (PGPSignature) keySignatures.next();
|
||||
|
||||
if (signature.getKeyID() != publicKey.getKeyID()) {
|
||||
// Signature from a foreign key. Skip.
|
||||
continue;
|
||||
}
|
||||
|
||||
SignatureType signatureType = SignatureType.fromCode(signature.getSignatureType());
|
||||
if (signatureType == null) {
|
||||
// unknown signature type
|
||||
continue;
|
||||
}
|
||||
if (signatureType == SignatureType.POSITIVE_CERTIFICATION
|
||||
|| signatureType == SignatureType.GENERIC_CERTIFICATION) {
|
||||
int[] hashAlgos = signature.getHashedSubPackets().getPreferredHashAlgorithms();
|
||||
if (hashAlgos == null) {
|
||||
continue;
|
||||
}
|
||||
for (int h : hashAlgos) {
|
||||
HashAlgorithm algorithm = HashAlgorithm.fromId(h);
|
||||
if (algorithm != null) {
|
||||
hashAlgorithms.add(algorithm);
|
||||
}
|
||||
}
|
||||
// Exit the loop after the first key signature with hash algorithms.
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hashAlgorithms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the hash algorithm that was used in the latest self signature.
|
||||
*
|
||||
* @param publicKey public key
|
||||
* @return list of hash algorithm
|
||||
*/
|
||||
public static List<HashAlgorithm> guessPreferredHashAlgorithms(PGPPublicKey publicKey) {
|
||||
HashAlgorithm hashAlgorithm = null;
|
||||
Date lastCreationDate = null;
|
||||
|
||||
Iterator<?> keySignatures = publicKey.getSignatures();
|
||||
while (keySignatures.hasNext()) {
|
||||
PGPSignature signature = (PGPSignature) keySignatures.next();
|
||||
if (signature.getKeyID() != publicKey.getKeyID()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SignatureType signatureType = SignatureType.fromCode(signature.getSignatureType());
|
||||
if (signatureType == null || signatureType != SignatureType.POSITIVE_CERTIFICATION
|
||||
&& signatureType != SignatureType.GENERIC_CERTIFICATION) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Date creationDate = signature.getCreationTime();
|
||||
if (lastCreationDate == null || lastCreationDate.before(creationDate)) {
|
||||
lastCreationDate = creationDate;
|
||||
hashAlgorithm = HashAlgorithm.fromId(signature.getHashAlgorithm());
|
||||
}
|
||||
}
|
||||
|
||||
if (hashAlgorithm == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Collections.singletonList(hashAlgorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to extract hash algorithm preferences from self signatures.
|
||||
* If no self-signature containing hash algorithm preferences is found,
|
||||
* try to derive a hash algorithm preference by inspecting the hash algorithm used by existing
|
||||
* self-signatures.
|
||||
*
|
||||
* @param publicKey key
|
||||
* @return hash algorithm preferences (might be empty!)
|
||||
*/
|
||||
public static Set<HashAlgorithm> getOrGuessPreferredHashAlgorithms(PGPPublicKey publicKey) {
|
||||
List<HashAlgorithm> preferredHashAlgorithms = OpenPgpKeyAttributeUtil.getPreferredHashAlgorithms(publicKey);
|
||||
if (preferredHashAlgorithms.isEmpty()) {
|
||||
preferredHashAlgorithms = OpenPgpKeyAttributeUtil.guessPreferredHashAlgorithms(publicKey);
|
||||
}
|
||||
return new LinkedHashSet<>(preferredHashAlgorithms);
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Utility functions to deal with OpenPGP keys.
|
||||
*/
|
||||
package org.pgpainless.key.util;
|
|
@ -1,10 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* PGPainless - Use OpenPGP Painlessly!
|
||||
*
|
||||
* @see <a href="http://pgpainless.org">org.pgpainless.core.org</a>
|
||||
*/
|
||||
package org.pgpainless;
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Utility classes.
|
||||
*/
|
||||
package org.pgpainless.util;
|
|
@ -1,49 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.util.selection.keyring;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.pgpainless.util.MultiMap;
|
||||
|
||||
/**
|
||||
* Filter for selecting public / secret key rings based on identifiers (e.g. user-ids).
|
||||
*
|
||||
* @param <R> Type of {@link org.bouncycastle.openpgp.PGPKeyRing} ({@link org.bouncycastle.openpgp.PGPSecretKeyRing}
|
||||
* or {@link org.bouncycastle.openpgp.PGPPublicKeyRing}).
|
||||
* @param <C> Type of key ring collection (e.g. {@link org.bouncycastle.openpgp.PGPSecretKeyRingCollection}
|
||||
* or {@link org.bouncycastle.openpgp.PGPPublicKeyRingCollection}).
|
||||
* @param <O> Type of key identifier
|
||||
*/
|
||||
public interface KeyRingSelectionStrategy<R, C, O> {
|
||||
|
||||
/**
|
||||
* Return true, if the filter accepts the given <pre>keyRing</pre> based on the given <pre>identifier</pre>.
|
||||
*
|
||||
* @param identifier identifier
|
||||
* @param keyRing key ring
|
||||
* @return acceptance
|
||||
*/
|
||||
boolean accept(O identifier, R keyRing);
|
||||
|
||||
/**
|
||||
* Iterate of the given <pre>keyRingCollection</pre> and return a {@link Set} of all acceptable
|
||||
* keyRings in the collection, based on the given <pre>identifier</pre>.
|
||||
*
|
||||
* @param identifier identifier
|
||||
* @param keyRingCollection collection
|
||||
* @return set of acceptable key rings
|
||||
*/
|
||||
Set<R> selectKeyRingsFromCollection(O identifier, C keyRingCollection);
|
||||
|
||||
/**
|
||||
* Iterate over all keyRings in the given {@link MultiMap} of keyRingCollections and return a new {@link MultiMap}
|
||||
* which for every identifier (key of the map) contains all acceptable keyRings based on that identifier.
|
||||
*
|
||||
* @param keyRingCollections MultiMap of identifiers and keyRingCollections.
|
||||
* @return MultiMap of identifiers and acceptable keyRings.
|
||||
*/
|
||||
MultiMap<O, R> selectKeyRingsFromCollections(MultiMap<O, C> keyRingCollections);
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.util.selection.keyring;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
||||
import org.pgpainless.util.MultiMap;
|
||||
|
||||
/**
|
||||
* Abstract {@link KeyRingSelectionStrategy} for {@link PGPPublicKeyRing PGPPublicKeyRings}.
|
||||
*
|
||||
* @param <O> Type of identifier
|
||||
*/
|
||||
public abstract class PublicKeyRingSelectionStrategy<O> implements KeyRingSelectionStrategy<PGPPublicKeyRing, PGPPublicKeyRingCollection, O> {
|
||||
|
||||
@Override
|
||||
public Set<PGPPublicKeyRing> selectKeyRingsFromCollection(@Nonnull O identifier, @Nonnull PGPPublicKeyRingCollection keyRingCollection) {
|
||||
Set<PGPPublicKeyRing> accepted = new HashSet<>();
|
||||
for (Iterator<PGPPublicKeyRing> i = keyRingCollection.getKeyRings(); i.hasNext(); ) {
|
||||
PGPPublicKeyRing ring = i.next();
|
||||
if (accept(identifier, ring)) accepted.add(ring);
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiMap<O, PGPPublicKeyRing> selectKeyRingsFromCollections(@Nonnull MultiMap<O, PGPPublicKeyRingCollection> keyRingCollections) {
|
||||
MultiMap<O, PGPPublicKeyRing> keyRings = new MultiMap<>();
|
||||
for (Map.Entry<O, Set<PGPPublicKeyRingCollection>> entry : keyRingCollections.entrySet()) {
|
||||
for (PGPPublicKeyRingCollection collection : entry.getValue()) {
|
||||
keyRings.plus(entry.getKey(), selectKeyRingsFromCollection(entry.getKey(), collection));
|
||||
}
|
||||
}
|
||||
return keyRings;
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.util.selection.keyring;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
||||
import org.pgpainless.util.MultiMap;
|
||||
|
||||
/**
|
||||
* Abstract {@link KeyRingSelectionStrategy} for {@link PGPSecretKeyRing PGPSecretKeyRings}.
|
||||
*
|
||||
* @param <O> Type of identifier
|
||||
*/
|
||||
public abstract class SecretKeyRingSelectionStrategy<O> implements KeyRingSelectionStrategy<PGPSecretKeyRing, PGPSecretKeyRingCollection, O> {
|
||||
@Override
|
||||
public Set<PGPSecretKeyRing> selectKeyRingsFromCollection(O identifier, @Nonnull PGPSecretKeyRingCollection keyRingCollection) {
|
||||
Set<PGPSecretKeyRing> accepted = new HashSet<>();
|
||||
for (Iterator<PGPSecretKeyRing> i = keyRingCollection.getKeyRings(); i.hasNext(); ) {
|
||||
PGPSecretKeyRing ring = i.next();
|
||||
if (accept(identifier, ring)) accepted.add(ring);
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiMap<O, PGPSecretKeyRing> selectKeyRingsFromCollections(@Nonnull MultiMap<O, PGPSecretKeyRingCollection> keyRingCollections) {
|
||||
MultiMap<O, PGPSecretKeyRing> keyRings = new MultiMap<>();
|
||||
for (Map.Entry<O, Set<PGPSecretKeyRingCollection>> entry : keyRingCollections.entrySet()) {
|
||||
for (PGPSecretKeyRingCollection collection : entry.getValue()) {
|
||||
keyRings.plus(entry.getKey(), selectKeyRingsFromCollection(entry.getKey(), collection));
|
||||
}
|
||||
}
|
||||
return keyRings;
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.util.selection.keyring.impl;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.pgpainless.key.util.KeyRingUtils;
|
||||
import org.pgpainless.util.selection.keyring.PublicKeyRingSelectionStrategy;
|
||||
import org.pgpainless.util.selection.keyring.SecretKeyRingSelectionStrategy;
|
||||
|
||||
/**
|
||||
* Implementations of {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which select key rings
|
||||
* based on the exact user-id.
|
||||
*/
|
||||
public final class ExactUserId {
|
||||
|
||||
private ExactUserId() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link PublicKeyRingSelectionStrategy} which accepts {@link PGPPublicKeyRing PGPPublicKeyRings} if those
|
||||
* have a user-id which exactly matches the given <pre>identifier</pre>.
|
||||
*/
|
||||
public static class PubRingSelectionStrategy extends PublicKeyRingSelectionStrategy<String> {
|
||||
|
||||
@Override
|
||||
public boolean accept(String identifier, PGPPublicKeyRing keyRing) {
|
||||
List<String> userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keyRing.getPublicKey());
|
||||
for (String userId : userIds) {
|
||||
if (userId.equals(identifier)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SecretKeyRingSelectionStrategy} which accepts {@link PGPSecretKeyRing PGPSecretKeyRings} if those
|
||||
* have a user-id which exactly matches the given <pre>identifier</pre>.
|
||||
*/
|
||||
public static class SecRingSelectionStrategy extends SecretKeyRingSelectionStrategy<String> {
|
||||
|
||||
@Override
|
||||
public boolean accept(String identifier, PGPSecretKeyRing keyRing) {
|
||||
List<String> userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keyRing.getPublicKey());
|
||||
for (String userId : userIds) {
|
||||
if (userId.equals(identifier)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.util.selection.keyring.impl;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.pgpainless.util.selection.keyring.PublicKeyRingSelectionStrategy;
|
||||
import org.pgpainless.util.selection.keyring.SecretKeyRingSelectionStrategy;
|
||||
import org.pgpainless.util.MultiMap;
|
||||
|
||||
/**
|
||||
* Implementations of {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accept PGP KeyRings
|
||||
* based on a whitelist of acceptable keyIds.
|
||||
*/
|
||||
public final class Whitelist {
|
||||
|
||||
private Whitelist() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accepts
|
||||
* {@link PGPPublicKeyRing PGPPublicKeyRings} if the <pre>whitelist</pre> contains their primary key id.
|
||||
*
|
||||
* If the whitelist contains 123L for "alice@pgpainless.org", the key with primary key id 123L is
|
||||
* acceptable for "alice@pgpainless.org".
|
||||
*
|
||||
* @param <O> Type of identifier for {@link org.bouncycastle.openpgp.PGPPublicKeyRingCollection PGPPublicKeyRingCollections}.
|
||||
*/
|
||||
public static class PubRingSelectionStrategy<O> extends PublicKeyRingSelectionStrategy<O> {
|
||||
|
||||
private final MultiMap<O, Long> whitelist;
|
||||
|
||||
public PubRingSelectionStrategy(MultiMap<O, Long> whitelist) {
|
||||
this.whitelist = whitelist;
|
||||
}
|
||||
|
||||
public PubRingSelectionStrategy(Map<O, Set<Long>> whitelist) {
|
||||
this(new MultiMap<>(whitelist));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(O identifier, PGPPublicKeyRing keyRing) {
|
||||
Set<Long> whitelistedKeyIds = whitelist.get(identifier);
|
||||
|
||||
if (whitelistedKeyIds == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return whitelistedKeyIds.contains(keyRing.getPublicKey().getKeyID());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accepts
|
||||
* {@link PGPSecretKeyRing PGPSecretKeyRings} if the <pre>whitelist</pre> contains their primary key id.
|
||||
*
|
||||
* If the whitelist contains 123L for "alice@pgpainless.org", the key with primary key id 123L is
|
||||
* acceptable for "alice@pgpainless.org".
|
||||
*
|
||||
* @param <O> Type of identifier for {@link org.bouncycastle.openpgp.PGPSecretKeyRingCollection PGPSecretKeyRingCollections}.
|
||||
*/
|
||||
public static class SecRingSelectionStrategy<O> extends SecretKeyRingSelectionStrategy<O> {
|
||||
|
||||
private final MultiMap<O, Long> whitelist;
|
||||
|
||||
public SecRingSelectionStrategy(MultiMap<O, Long> whitelist) {
|
||||
this.whitelist = whitelist;
|
||||
}
|
||||
|
||||
public SecRingSelectionStrategy(Map<O, Set<Long>> whitelist) {
|
||||
this(new MultiMap<>(whitelist));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(O identifier, PGPSecretKeyRing keyRing) {
|
||||
Set<Long> whitelistedKeyIds = whitelist.get(identifier);
|
||||
|
||||
if (whitelistedKeyIds == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return whitelistedKeyIds.contains(keyRing.getPublicKey().getKeyID());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.util.selection.keyring.impl;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.pgpainless.util.selection.keyring.PublicKeyRingSelectionStrategy;
|
||||
import org.pgpainless.util.selection.keyring.SecretKeyRingSelectionStrategy;
|
||||
|
||||
/**
|
||||
* Implementations of {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accept all keyRings.
|
||||
*/
|
||||
public final class Wildcard {
|
||||
|
||||
private Wildcard() {
|
||||
|
||||
}
|
||||
|
||||
public static class PubRingSelectionStrategy<O> extends PublicKeyRingSelectionStrategy<O> {
|
||||
|
||||
@Override
|
||||
public boolean accept(O identifier, PGPPublicKeyRing keyRing) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SecRingSelectionStrategy<O> extends SecretKeyRingSelectionStrategy<O> {
|
||||
|
||||
@Override
|
||||
public boolean accept(O identifier, PGPSecretKeyRing keyRing) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.util.selection.keyring.impl;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
|
||||
/**
|
||||
* Implementations of {@link org.pgpainless.util.selection.keyring.KeyRingSelectionStrategy} which accept KeyRings
|
||||
* containing a given XMPP address of the format "xmpp:alice@pgpainless.org".
|
||||
*/
|
||||
public final class XMPP {
|
||||
|
||||
private XMPP() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link org.pgpainless.util.selection.keyring.PublicKeyRingSelectionStrategy} which accepts a given
|
||||
* {@link PGPPublicKeyRing} if its primary key has a user-id that matches the given <pre>jid</pre>.
|
||||
*
|
||||
* The argument <pre>jid</pre> can either contain the prefix "xmpp:", or not, the result will be the same.
|
||||
*/
|
||||
public static class PubRingSelectionStrategy extends ExactUserId.PubRingSelectionStrategy {
|
||||
|
||||
@Override
|
||||
public boolean accept(String jid, PGPPublicKeyRing keyRing) {
|
||||
if (!jid.matches("^xmpp:.+$")) {
|
||||
jid = "xmpp:" + jid;
|
||||
}
|
||||
return super.accept(jid, keyRing);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link org.pgpainless.util.selection.keyring.SecretKeyRingSelectionStrategy} which accepts a given
|
||||
* {@link PGPSecretKeyRing} if its primary key has a user-id that matches the given <pre>jid</pre>.
|
||||
*
|
||||
* The argument <pre>jid</pre> can either contain the prefix "xmpp:", or not, the result will be the same.
|
||||
*/
|
||||
public static class SecRingSelectionStrategy extends ExactUserId.SecRingSelectionStrategy {
|
||||
|
||||
@Override
|
||||
public boolean accept(String jid, PGPSecretKeyRing keyRing) {
|
||||
if (!jid.matches("^xmpp:.+$")) {
|
||||
jid = "xmpp:" + jid;
|
||||
}
|
||||
return super.accept(jid, keyRing);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Implementations of Key Ring Selection Strategies.
|
||||
*/
|
||||
package org.pgpainless.util.selection.keyring.impl;
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Different Key Ring Selection Strategies.
|
||||
*/
|
||||
package org.pgpainless.util.selection.keyring;
|
|
@ -14,7 +14,6 @@ import java.util.*
|
|||
* Since '0' is a special date value in the OpenPGP specification (e.g. '0' means no expiration for
|
||||
* expiration dates), this method will return 'null' if seconds is 0.
|
||||
*
|
||||
* @param date date
|
||||
* @param seconds number of seconds to be added
|
||||
* @return date plus seconds or null if seconds is '0'
|
||||
*/
|
||||
|
@ -25,9 +24,19 @@ fun Date.plusSeconds(seconds: Long): Date? {
|
|||
return if (seconds == 0L) null else Date(this.time + 1000 * seconds)
|
||||
}
|
||||
|
||||
/** This date in seconds since epoch. */
|
||||
val Date.asSeconds: Long
|
||||
get() = time / 1000
|
||||
|
||||
/**
|
||||
* Return the number of seconds that would need to be added to this date in order to reach the later
|
||||
* date. Note: This method requires the [later] date to be indeed greater or equal to this date,
|
||||
* otherwise an [IllegalArgumentException] is thrown.
|
||||
*
|
||||
* @param later later date
|
||||
* @return difference between this and [later] in seconds
|
||||
* @throws IllegalArgumentException if later is not greater or equal to this date
|
||||
*/
|
||||
fun Date.secondsTill(later: Date): Long {
|
||||
require(this <= later) { "Timestamp MUST be before the later timestamp." }
|
||||
return later.asSeconds - this.asSeconds
|
||||
|
|
|
@ -9,7 +9,7 @@ fun Long.openPgpKeyId(): String {
|
|||
return String.format("%016X", this).uppercase()
|
||||
}
|
||||
|
||||
/** Parse a Long form a 16 digit hex encoded OpenPgp key-ID. */
|
||||
/** Parse a Long from a 16 digit hex encoded OpenPgp key-ID. */
|
||||
fun Long.Companion.fromOpenPgpKeyId(hexKeyId: String): Long {
|
||||
require("^[0-9A-Fa-f]{16}$".toRegex().matches(hexKeyId)) {
|
||||
"Provided long key-id does not match expected format. " +
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.gnupg
|
||||
|
||||
import org.bouncycastle.bcpg.S2K
|
||||
|
||||
enum class GnuPGDummyExtension(val id: Int) {
|
||||
|
||||
/** Do not store the secret part at all. */
|
||||
NO_PRIVATE_KEY(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY),
|
||||
|
||||
/** A stub to access smartcards. */
|
||||
DIVERT_TO_CARD(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD)
|
||||
}
|
206
pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt
Normal file
206
pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt
Normal file
|
@ -0,0 +1,206 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.gnupg
|
||||
|
||||
import kotlin.experimental.and
|
||||
import org.bouncycastle.bcpg.KeyIdentifier
|
||||
import org.bouncycastle.bcpg.S2K
|
||||
import org.bouncycastle.bcpg.SecretKeyPacket
|
||||
import org.bouncycastle.bcpg.SecretSubkeyPacket
|
||||
import org.bouncycastle.openpgp.PGPSecretKey
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey
|
||||
import org.gnupg.GnuPGDummyKeyUtil.KeyFilter
|
||||
import org.pgpainless.key.SubkeyIdentifier
|
||||
|
||||
/**
|
||||
* This class can be used to remove private keys from secret software-keys by replacing them with
|
||||
* stub secret keys in the style of GnuPGs proprietary extensions.
|
||||
*
|
||||
* @see
|
||||
* [GnuPGs doc/DETAILS - GNU extensions to the S2K algorithm](https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;hb=HEAD#l1489)
|
||||
*/
|
||||
class GnuPGDummyKeyUtil private constructor() {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Return the key-ids of all keys which appear to be stored on a hardware token / smartcard
|
||||
* by GnuPG. Note, that this functionality is based on GnuPGs proprietary S2K extensions,
|
||||
* which are not strictly required for dealing with hardware-backed keys.
|
||||
*
|
||||
* @param secretKeys secret keys
|
||||
* @return set of keys with S2K type [S2K.GNU_DUMMY_S2K] and protection mode
|
||||
* [GnuPGDummyExtension.DIVERT_TO_CARD]
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getIdsOfKeysWithGnuPGS2KDivertedToCard(
|
||||
secretKeys: PGPSecretKeyRing
|
||||
): Set<SubkeyIdentifier> =
|
||||
secretKeys
|
||||
.filter {
|
||||
it.s2K?.type == S2K.GNU_DUMMY_S2K &&
|
||||
it.s2K?.protectionMode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD
|
||||
}
|
||||
.map { SubkeyIdentifier(secretKeys, it.keyIdentifier) }
|
||||
.toSet()
|
||||
|
||||
@JvmStatic fun modify(key: OpenPGPKey): Builder = modify(key.pgpSecretKeyRing)
|
||||
|
||||
/**
|
||||
* Modify the given [PGPSecretKeyRing].
|
||||
*
|
||||
* @param secretKeys secret keys
|
||||
* @return builder
|
||||
*/
|
||||
@JvmStatic fun modify(secretKeys: PGPSecretKeyRing) = Builder(secretKeys)
|
||||
}
|
||||
|
||||
class Builder(private val keys: PGPSecretKeyRing) {
|
||||
|
||||
/**
|
||||
* Remove all private keys that match the given [KeyFilter] from the key ring and replace
|
||||
* them with GNU_DUMMY keys with S2K protection mode [GnuPGDummyExtension.NO_PRIVATE_KEY].
|
||||
*
|
||||
* @param filter filter to select keys for removal
|
||||
* @return modified key ring
|
||||
*/
|
||||
fun removePrivateKeys(filter: KeyFilter): PGPSecretKeyRing {
|
||||
return replacePrivateKeys(GnuPGDummyExtension.NO_PRIVATE_KEY, null, filter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all private keys that match the given [KeyFilter] from the key ring and replace
|
||||
* them with GNU_DUMMY keys with S2K protection mode [GnuPGDummyExtension.DIVERT_TO_CARD].
|
||||
* This method will set the serial number of the card to 0x00000000000000000000000000000000.
|
||||
* NOTE: This method does not actually move any keys to a card.
|
||||
*
|
||||
* @param filter filter to select keys for removal
|
||||
* @return modified key ring
|
||||
*/
|
||||
fun divertPrivateKeysToCard(filter: KeyFilter): PGPSecretKeyRing {
|
||||
return divertPrivateKeysToCard(filter, ByteArray(16))
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all private keys that match the given [KeyFilter] from the key ring and replace
|
||||
* them with GNU_DUMMY keys with S2K protection mode [GnuPGDummyExtension.DIVERT_TO_CARD].
|
||||
* This method will include the card serial number into the encoded dummy key. NOTE: This
|
||||
* method does not actually move any keys to a card.
|
||||
*
|
||||
* @param filter filter to select keys for removal
|
||||
* @param cardSerialNumber serial number of the card (at most 16 bytes long)
|
||||
* @return modified key ring
|
||||
*/
|
||||
fun divertPrivateKeysToCard(
|
||||
filter: KeyFilter,
|
||||
cardSerialNumber: ByteArray?
|
||||
): PGPSecretKeyRing {
|
||||
require(cardSerialNumber == null || cardSerialNumber.size <= 16) {
|
||||
"Card serial number length cannot exceed 16 bytes."
|
||||
}
|
||||
return replacePrivateKeys(GnuPGDummyExtension.DIVERT_TO_CARD, cardSerialNumber, filter)
|
||||
}
|
||||
|
||||
private fun replacePrivateKeys(
|
||||
extension: GnuPGDummyExtension,
|
||||
serial: ByteArray?,
|
||||
filter: KeyFilter
|
||||
): PGPSecretKeyRing {
|
||||
val encodedSerial: ByteArray? = serial?.let { encodeSerial(it) }
|
||||
val s2k: S2K = extensionToS2K(extension)
|
||||
|
||||
return PGPSecretKeyRing(
|
||||
keys
|
||||
.map {
|
||||
if (!filter.filter(it.keyIdentifier)) {
|
||||
// Leave non-filtered key intact
|
||||
it
|
||||
} else {
|
||||
val publicKeyPacket = it.publicKey.publicKeyPacket
|
||||
// Convert key packet
|
||||
val keyPacket: SecretKeyPacket =
|
||||
if (it.isMasterKey) {
|
||||
SecretKeyPacket(
|
||||
publicKeyPacket,
|
||||
0,
|
||||
SecretKeyPacket.USAGE_SHA1,
|
||||
s2k,
|
||||
null,
|
||||
encodedSerial)
|
||||
} else {
|
||||
SecretSubkeyPacket(
|
||||
publicKeyPacket,
|
||||
0,
|
||||
SecretKeyPacket.USAGE_SHA1,
|
||||
s2k,
|
||||
null,
|
||||
encodedSerial)
|
||||
}
|
||||
PGPSecretKey(keyPacket, it.publicKey)
|
||||
}
|
||||
}
|
||||
.toList())
|
||||
}
|
||||
|
||||
private fun encodeSerial(serial: ByteArray): ByteArray {
|
||||
val encoded = ByteArray(serial.size + 1)
|
||||
encoded[0] = serial.size.toByte().and(0xff.toByte())
|
||||
System.arraycopy(serial, 0, encoded, 1, serial.size)
|
||||
return encoded
|
||||
}
|
||||
|
||||
private fun extensionToS2K(extension: GnuPGDummyExtension): S2K {
|
||||
return S2K.gnuDummyS2K(
|
||||
if (extension == GnuPGDummyExtension.DIVERT_TO_CARD)
|
||||
S2K.GNUDummyParams.divertToCard()
|
||||
else S2K.GNUDummyParams.noPrivateKey())
|
||||
}
|
||||
}
|
||||
|
||||
/** Filter for selecting keys. */
|
||||
fun interface KeyFilter {
|
||||
fun filter(keyIdentifier: KeyIdentifier): Boolean
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Select any key.
|
||||
*
|
||||
* @return filter
|
||||
*/
|
||||
@JvmStatic fun any(): KeyFilter = KeyFilter { true }
|
||||
|
||||
/**
|
||||
* Select only the given keyId.
|
||||
*
|
||||
* @param onlyKeyId only acceptable key id
|
||||
* @return filter
|
||||
*/
|
||||
@JvmStatic
|
||||
@Deprecated("Use only(KeyIdentifier) instead.")
|
||||
fun only(onlyKeyId: Long) = only(KeyIdentifier(onlyKeyId))
|
||||
|
||||
/**
|
||||
* Select only the given keyIdentifier.
|
||||
*
|
||||
* @param onlyKeyIdentifier only acceptable key identifier
|
||||
* @return filter
|
||||
*/
|
||||
@JvmStatic
|
||||
fun only(onlyKeyIdentifier: KeyIdentifier) = KeyFilter {
|
||||
it.matchesExplicit(onlyKeyIdentifier)
|
||||
}
|
||||
|
||||
/**
|
||||
* Select all keyIds which are contained in the given set of ids.
|
||||
*
|
||||
* @param ids set of acceptable keyIds
|
||||
* @return filter
|
||||
*/
|
||||
@JvmStatic fun selected(ids: Collection<KeyIdentifier>) = KeyFilter { ids.contains(it) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,12 +4,27 @@
|
|||
|
||||
package org.pgpainless
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream
|
||||
import org.bouncycastle.bcpg.BCPGOutputStream
|
||||
import org.bouncycastle.bcpg.PacketFormat
|
||||
import org.bouncycastle.openpgp.PGPKeyRing
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||
import org.bouncycastle.openpgp.PGPSignature
|
||||
import org.bouncycastle.openpgp.api.OpenPGPApi
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate
|
||||
import org.bouncycastle.openpgp.api.OpenPGPImplementation
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKeyGenerator
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKeyReader
|
||||
import org.bouncycastle.openpgp.api.OpenPGPSignature
|
||||
import org.bouncycastle.openpgp.api.bc.BcOpenPGPApi
|
||||
import org.pgpainless.algorithm.OpenPGPKeyVersion
|
||||
import org.pgpainless.bouncycastle.PolicyAdapter
|
||||
import org.pgpainless.bouncycastle.extensions.setAlgorithmSuite
|
||||
import org.pgpainless.decryption_verification.DecryptionBuilder
|
||||
import org.pgpainless.encryption_signing.EncryptionBuilder
|
||||
import org.pgpainless.key.certification.CertifyCertificate
|
||||
|
@ -18,34 +33,270 @@ import org.pgpainless.key.generation.KeyRingTemplates
|
|||
import org.pgpainless.key.info.KeyRingInfo
|
||||
import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor
|
||||
import org.pgpainless.key.parsing.KeyRingReader
|
||||
import org.pgpainless.key.util.KeyRingUtils
|
||||
import org.pgpainless.policy.Policy
|
||||
import org.pgpainless.util.ArmorUtils
|
||||
|
||||
class PGPainless private constructor() {
|
||||
/**
|
||||
* Main entry point to the PGPainless OpenPGP API. Historically, this class was used through static
|
||||
* factory methods only, and configuration was done using the Singleton pattern. However, now it is
|
||||
* recommended to instantiate the API and apply configuration on a per-instance manner. The benefit
|
||||
* of this being that you can have multiple differently configured instances at the same time.
|
||||
*
|
||||
* @param implementation OpenPGP Implementation - either BCs lightweight
|
||||
* [org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation] or JCAs
|
||||
* [org.bouncycastle.openpgp.api.jcajce.JcaOpenPGPImplementation].
|
||||
* @param algorithmPolicy policy, deciding acceptable algorithms
|
||||
*/
|
||||
class PGPainless(
|
||||
val implementation: OpenPGPImplementation = OpenPGPImplementation.getInstance(),
|
||||
val algorithmPolicy: Policy = Policy()
|
||||
) {
|
||||
|
||||
constructor(
|
||||
algorithmPolicy: Policy
|
||||
) : this(OpenPGPImplementation.getInstance(), algorithmPolicy)
|
||||
|
||||
private val api: OpenPGPApi
|
||||
|
||||
init {
|
||||
implementation.setPolicy(
|
||||
PolicyAdapter(algorithmPolicy)) // adapt PGPainless' Policy to BCs OpenPGPPolicy
|
||||
api = BcOpenPGPApi(implementation)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun toAsciiArmor(
|
||||
certOrKey: OpenPGPCertificate,
|
||||
packetFormat: PacketFormat = PacketFormat.ROUNDTRIP
|
||||
): String {
|
||||
val armorBuilder = ArmoredOutputStream.builder().clearHeaders()
|
||||
ArmorUtils.keyToHeader(certOrKey.primaryKey.pgpPublicKey)
|
||||
.getOrDefault(ArmorUtils.HEADER_COMMENT, setOf())
|
||||
.forEach { armorBuilder.addComment(it) }
|
||||
return certOrKey.toAsciiArmoredString(packetFormat, armorBuilder)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun toAsciiArmor(
|
||||
signature: OpenPGPSignature,
|
||||
packetFormat: PacketFormat = PacketFormat.ROUNDTRIP
|
||||
): String {
|
||||
val armorBuilder = ArmoredOutputStream.builder().clearHeaders()
|
||||
armorBuilder.addComment(signature.keyIdentifier.toPrettyPrint())
|
||||
return signature.toAsciiArmoredString(packetFormat, armorBuilder)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun toAsciiArmor(
|
||||
signature: PGPSignature,
|
||||
packetFormat: PacketFormat = PacketFormat.ROUNDTRIP
|
||||
): String {
|
||||
val armorBuilder = ArmoredOutputStream.builder().clearHeaders()
|
||||
OpenPGPSignature.getMostExpressiveIdentifier(signature.keyIdentifiers)?.let {
|
||||
armorBuilder.addComment(it.toPrettyPrint())
|
||||
}
|
||||
val bOut = ByteArrayOutputStream()
|
||||
val aOut = armorBuilder.build(bOut)
|
||||
val pOut = BCPGOutputStream(aOut, packetFormat)
|
||||
signature.encode(pOut)
|
||||
pOut.close()
|
||||
aOut.close()
|
||||
return bOut.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new [OpenPGPKey] from predefined templates.
|
||||
*
|
||||
* @param version [OpenPGPKeyVersion], defaults to [OpenPGPKeyVersion.v4]
|
||||
* @param creationTime of the key, defaults to now
|
||||
* @return [KeyRingTemplates] api
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun generateKey(
|
||||
version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4,
|
||||
creationTime: Date = Date()
|
||||
): KeyRingTemplates = KeyRingTemplates(version, creationTime, this)
|
||||
|
||||
/**
|
||||
* Build a fresh, custom [OpenPGPKey] using PGPainless' API.
|
||||
*
|
||||
* @param version [OpenPGPKeyVersion], defaults to [OpenPGPKeyVersion.v4]
|
||||
* @return [KeyRingBuilder] api
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun buildKey(version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4): KeyRingBuilder =
|
||||
KeyRingBuilder(version, this)
|
||||
|
||||
/**
|
||||
* Build a fresh, custom [OpenPGPKey] using BCs new API.
|
||||
*
|
||||
* @param version [OpenPGPKeyVersion], defaults to [OpenPGPKeyVersion.v4]
|
||||
* @param creationTime creation time of the key, defaults to now
|
||||
* @return [OpenPGPKeyGenerator] api
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun _buildKey(
|
||||
version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4,
|
||||
creationTime: Date = Date()
|
||||
): OpenPGPKeyGenerator =
|
||||
OpenPGPKeyGenerator(
|
||||
implementation,
|
||||
version.numeric,
|
||||
algorithmPolicy.keyProtectionSettings.aead,
|
||||
creationTime)
|
||||
.setAlgorithmSuite(algorithmPolicy.keyGenerationAlgorithmSuite)
|
||||
|
||||
/**
|
||||
* Inspect an [OpenPGPKey] or [OpenPGPCertificate], gaining convenient access to its properties.
|
||||
*
|
||||
* @param keyOrCertificate [OpenPGPKey] or [OpenPGPCertificate]
|
||||
* @param referenceTime reference time for evaluation
|
||||
* @return [KeyRingInfo] wrapper
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun inspect(keyOrCertificate: OpenPGPCertificate, referenceTime: Date = Date()): KeyRingInfo =
|
||||
KeyRingInfo(keyOrCertificate, this, referenceTime)
|
||||
|
||||
/**
|
||||
* Modify an [OpenPGPKey], adding new components and signatures. This API can be used to add new
|
||||
* subkeys, user-ids or user-attributes to the key, extend or alter its expiration time, revoke
|
||||
* individual components of the entire certificate, etc.
|
||||
*
|
||||
* @param key key to modify
|
||||
* @param referenceTime timestamp for modifications
|
||||
* @return [SecretKeyRingEditor] api
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun modify(key: OpenPGPKey, referenceTime: Date = Date()): SecretKeyRingEditor =
|
||||
SecretKeyRingEditor(key, this, referenceTime)
|
||||
|
||||
/**
|
||||
* Parse [OpenPGPKey]/[OpenPGPCertificate] material from binary or ASCII armored encoding.
|
||||
*
|
||||
* @return [OpenPGPKeyReader] api
|
||||
*/
|
||||
fun readKey(): OpenPGPKeyReader = api.readKeyOrCertificate()
|
||||
|
||||
/**
|
||||
* Convert a [PGPSecretKeyRing] into an [OpenPGPKey].
|
||||
*
|
||||
* @param secretKeyRing mid-level API [PGPSecretKeyRing] object
|
||||
* @return high-level API [OpenPGPKey] object
|
||||
*/
|
||||
fun toKey(secretKeyRing: PGPSecretKeyRing): OpenPGPKey =
|
||||
OpenPGPKey(secretKeyRing, implementation)
|
||||
|
||||
/**
|
||||
* Convert a [PGPPublicKeyRing] into an [OpenPGPCertificate].
|
||||
*
|
||||
* @param certificate mid-level API [PGPSecretKeyRing] object
|
||||
* @return high-level API [OpenPGPCertificate] object
|
||||
*/
|
||||
fun toCertificate(certificate: PGPPublicKeyRing): OpenPGPCertificate =
|
||||
OpenPGPCertificate(certificate, implementation)
|
||||
|
||||
/**
|
||||
* Depending on the type, convert either a [PGPSecretKeyRing] into an [OpenPGPKey] or a
|
||||
* [PGPPublicKeyRing] into an [OpenPGPCertificate].
|
||||
*
|
||||
* @param keyOrCertificate [PGPKeyRing], either [PGPSecretKeyRing] or [PGPPublicKeyRing]
|
||||
* @return depending on the type of [keyOrCertificate], either an [OpenPGPKey] or
|
||||
* [OpenPGPCertificate]
|
||||
*/
|
||||
fun toKeyOrCertificate(keyOrCertificate: PGPKeyRing): OpenPGPCertificate =
|
||||
when (keyOrCertificate) {
|
||||
is PGPSecretKeyRing -> toKey(keyOrCertificate)
|
||||
is PGPPublicKeyRing -> toCertificate(keyOrCertificate)
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unexpected PGPKeyRing subclass: ${keyOrCertificate.javaClass.name}")
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two copies of an [OpenPGPCertificate] into a single copy. This method can be used to
|
||||
* import new third-party signatures into a certificate.
|
||||
*
|
||||
* @param originalCopy local copy of the certificate
|
||||
* @param updatedCopy copy of the same certificate, potentially carrying new signatures and
|
||||
* components
|
||||
* @return merged [OpenPGPCertificate]
|
||||
*/
|
||||
fun mergeCertificate(
|
||||
originalCopy: OpenPGPCertificate,
|
||||
updatedCopy: OpenPGPCertificate
|
||||
): OpenPGPCertificate {
|
||||
return OpenPGPCertificate.join(originalCopy, updatedCopy)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an encrypted and/or signed OpenPGP message.
|
||||
*
|
||||
* @return [EncryptionBuilder] api
|
||||
*/
|
||||
fun generateMessage(): EncryptionBuilder = EncryptionBuilder(this)
|
||||
|
||||
/**
|
||||
* Process an OpenPGP message. This method attempts decryption of encrypted messages and
|
||||
* performs signature verification.
|
||||
*
|
||||
* @return [DecryptionBuilder] api
|
||||
*/
|
||||
fun processMessage(): DecryptionBuilder = DecryptionBuilder(this)
|
||||
|
||||
/**
|
||||
* Create certification signatures on third-party [OpenPGPCertificates][OpenPGPCertificate].
|
||||
*
|
||||
* @return [CertifyCertificate] api
|
||||
*/
|
||||
fun generateCertification(): CertifyCertificate = CertifyCertificate(this)
|
||||
|
||||
companion object {
|
||||
|
||||
@Volatile private var instance: PGPainless? = null
|
||||
|
||||
@JvmStatic
|
||||
fun getInstance(): PGPainless =
|
||||
instance ?: synchronized(this) { instance ?: PGPainless().also { instance = it } }
|
||||
|
||||
@JvmStatic
|
||||
fun setInstance(api: PGPainless) {
|
||||
instance = api
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a fresh OpenPGP key ring from predefined templates.
|
||||
* Generate a fresh [OpenPGPKey] from predefined templates.
|
||||
*
|
||||
* @return templates
|
||||
*/
|
||||
@JvmStatic fun generateKeyRing() = KeyRingTemplates()
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
@Deprecated(
|
||||
"Call .generateKey() on an instance of PGPainless instead.",
|
||||
replaceWith = ReplaceWith("generateKey(version)"))
|
||||
fun generateKeyRing(version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4): KeyRingTemplates =
|
||||
getInstance().generateKey(version)
|
||||
|
||||
/**
|
||||
* Build a custom OpenPGP key ring.
|
||||
*
|
||||
* @return builder
|
||||
*/
|
||||
@JvmStatic fun buildKeyRing() = KeyRingBuilder()
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
@Deprecated(
|
||||
"Call buildKey() on an instance of PGPainless instead.",
|
||||
replaceWith = ReplaceWith("buildKey(version)"))
|
||||
fun buildKeyRing(version: OpenPGPKeyVersion = OpenPGPKeyVersion.v4): KeyRingBuilder =
|
||||
getInstance().buildKey(version)
|
||||
|
||||
/**
|
||||
* Read an existing OpenPGP key ring.
|
||||
*
|
||||
* @return builder
|
||||
*/
|
||||
@JvmStatic fun readKeyRing() = KeyRingReader()
|
||||
@JvmStatic
|
||||
@Deprecated("Use readKey() instead.", replaceWith = ReplaceWith("readKey()"))
|
||||
fun readKeyRing(): KeyRingReader = KeyRingReader()
|
||||
|
||||
/**
|
||||
* Extract a public key certificate from a secret key.
|
||||
|
@ -54,8 +305,9 @@ class PGPainless private constructor() {
|
|||
* @return public key certificate
|
||||
*/
|
||||
@JvmStatic
|
||||
fun extractCertificate(secretKey: PGPSecretKeyRing) =
|
||||
KeyRingUtils.publicKeyRingFrom(secretKey)
|
||||
@Deprecated("Use .toKey() and then .toCertificate() instead.")
|
||||
fun extractCertificate(secretKey: PGPSecretKeyRing): PGPPublicKeyRing =
|
||||
secretKey.toCertificate()
|
||||
|
||||
/**
|
||||
* Merge two copies of the same certificate (e.g. an old copy, and one retrieved from a key
|
||||
|
@ -67,8 +319,12 @@ class PGPainless private constructor() {
|
|||
* @throws PGPException in case of an error
|
||||
*/
|
||||
@JvmStatic
|
||||
fun mergeCertificate(originalCopy: PGPPublicKeyRing, updatedCopy: PGPPublicKeyRing) =
|
||||
PGPPublicKeyRing.join(originalCopy, updatedCopy)
|
||||
@Deprecated(
|
||||
"Use mergeCertificate() instead.", replaceWith = ReplaceWith("mergeCertificate()"))
|
||||
fun mergeCertificate(
|
||||
originalCopy: PGPPublicKeyRing,
|
||||
updatedCopy: PGPPublicKeyRing
|
||||
): PGPPublicKeyRing = PGPPublicKeyRing.join(originalCopy, updatedCopy)
|
||||
|
||||
/**
|
||||
* Wrap a key or certificate in ASCII armor.
|
||||
|
@ -78,9 +334,14 @@ class PGPainless private constructor() {
|
|||
* @throws IOException in case of an error during the armoring process
|
||||
*/
|
||||
@JvmStatic
|
||||
fun asciiArmor(key: PGPKeyRing) =
|
||||
if (key is PGPSecretKeyRing) ArmorUtils.toAsciiArmoredString(key)
|
||||
else ArmorUtils.toAsciiArmoredString(key as PGPPublicKeyRing)
|
||||
fun asciiArmor(key: PGPKeyRing): String =
|
||||
getInstance().toAsciiArmor(getInstance().toKeyOrCertificate(key))
|
||||
|
||||
@JvmStatic
|
||||
@Deprecated(
|
||||
"Call getInstance().toAsciiArmor(cert) instead.",
|
||||
replaceWith = ReplaceWith("getInstance().toAsciiArmor(cert)"))
|
||||
fun asciiArmor(cert: OpenPGPCertificate): String = getInstance().toAsciiArmor(cert)
|
||||
|
||||
/**
|
||||
* Wrap a key of certificate in ASCII armor and write the result into the given
|
||||
|
@ -105,7 +366,10 @@ class PGPainless private constructor() {
|
|||
* @throws IOException in case of an error during the armoring process
|
||||
*/
|
||||
@JvmStatic
|
||||
fun asciiArmor(signature: PGPSignature) = ArmorUtils.toAsciiArmoredString(signature)
|
||||
@Deprecated(
|
||||
"Call toAsciiArmor(signature) on an instance of PGPainless instead.",
|
||||
replaceWith = ReplaceWith("getInstance().toAsciiArmor(signature)"))
|
||||
fun asciiArmor(signature: PGPSignature): String = getInstance().toAsciiArmor(signature)
|
||||
|
||||
/**
|
||||
* Create an [EncryptionBuilder], which can be used to encrypt and/or sign data using
|
||||
|
@ -113,7 +377,11 @@ class PGPainless private constructor() {
|
|||
*
|
||||
* @return builder
|
||||
*/
|
||||
@JvmStatic fun encryptAndOrSign() = EncryptionBuilder()
|
||||
@Deprecated(
|
||||
"Call generateMessage() on an instance of PGPainless instead.",
|
||||
replaceWith = ReplaceWith("generateMessage()"))
|
||||
@JvmStatic
|
||||
fun encryptAndOrSign(): EncryptionBuilder = getInstance().generateMessage()
|
||||
|
||||
/**
|
||||
* Create a [DecryptionBuilder], which can be used to decrypt and/or verify data using
|
||||
|
@ -121,7 +389,11 @@ class PGPainless private constructor() {
|
|||
*
|
||||
* @return builder
|
||||
*/
|
||||
@JvmStatic fun decryptAndOrVerify() = DecryptionBuilder()
|
||||
@Deprecated(
|
||||
"Call processMessage() on an instance of PGPainless instead.",
|
||||
replaceWith = ReplaceWith("processMessage()"))
|
||||
@JvmStatic
|
||||
fun decryptAndOrVerify(): DecryptionBuilder = getInstance().processMessage()
|
||||
|
||||
/**
|
||||
* Make changes to a secret key at the given reference time. This method can be used to
|
||||
|
@ -137,35 +409,56 @@ class PGPainless private constructor() {
|
|||
*/
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun modifyKeyRing(secretKey: PGPSecretKeyRing, referenceTime: Date = Date()) =
|
||||
SecretKeyRingEditor(secretKey, referenceTime)
|
||||
fun modifyKeyRing(
|
||||
secretKey: PGPSecretKeyRing,
|
||||
referenceTime: Date = Date()
|
||||
): SecretKeyRingEditor = getInstance().modify(getInstance().toKey(secretKey), referenceTime)
|
||||
|
||||
/**
|
||||
* Quickly access information about a [org.bouncycastle.openpgp.PGPPublicKeyRing] /
|
||||
* [PGPSecretKeyRing]. This method can be used to determine expiration dates, key flags and
|
||||
* other information about a key at a specific time.
|
||||
*
|
||||
* @param keyRing key ring
|
||||
* @param key key ring
|
||||
* @param referenceTime date of inspection
|
||||
* @return access object
|
||||
*/
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun inspectKeyRing(key: PGPKeyRing, referenceTime: Date = Date()) =
|
||||
KeyRingInfo(key, referenceTime)
|
||||
@Deprecated(
|
||||
"Use inspect(key) on an instance of PGPainless instead.",
|
||||
replaceWith = ReplaceWith("inspect(key)"))
|
||||
fun inspectKeyRing(key: PGPKeyRing, referenceTime: Date = Date()): KeyRingInfo =
|
||||
getInstance().inspect(getInstance().toKeyOrCertificate(key), referenceTime)
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
@Deprecated(
|
||||
"Use inspect(key) on an instance of PGPainless instead.",
|
||||
replaceWith = ReplaceWith("inspect(key)"))
|
||||
fun inspectKeyRing(key: OpenPGPCertificate, referenceTime: Date = Date()): KeyRingInfo =
|
||||
getInstance().inspect(key, referenceTime)
|
||||
|
||||
/**
|
||||
* Access, and make changes to PGPainless policy on acceptable/default algorithms etc.
|
||||
*
|
||||
* @return policy
|
||||
*/
|
||||
@JvmStatic fun getPolicy() = Policy.getInstance()
|
||||
@Deprecated(
|
||||
"Use PGPainless.getInstance().getAlgorithmPolicy() instead.",
|
||||
replaceWith = ReplaceWith("getInstance().algorithmPolicy"))
|
||||
@JvmStatic
|
||||
fun getPolicy(): Policy = getInstance().algorithmPolicy
|
||||
|
||||
/**
|
||||
* Create different kinds of signatures on other keys.
|
||||
*
|
||||
* @return builder
|
||||
*/
|
||||
@JvmStatic fun certify() = CertifyCertificate()
|
||||
@Deprecated(
|
||||
"Call .generateCertification() on an instance of PGPainless instead.",
|
||||
replaceWith = ReplaceWith("generateCertification()"))
|
||||
@JvmStatic
|
||||
fun certify(): CertifyCertificate = getInstance().generateCertification()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,35 +4,75 @@
|
|||
|
||||
package org.pgpainless.algorithm
|
||||
|
||||
import org.bouncycastle.openpgp.api.MessageEncryptionMechanism
|
||||
|
||||
/**
|
||||
* AEAD Algorithm.
|
||||
*
|
||||
* @param algorithmId numeric algorithm id
|
||||
* @param ivLength length of the initialization vector
|
||||
* @param tagLength length of the tag
|
||||
* @see
|
||||
* [RFC9580 - AEAD Algorithms](https://www.rfc-editor.org/rfc/rfc9580.html#name-aead-algorithms)
|
||||
*/
|
||||
enum class AEADAlgorithm(val algorithmId: Int, val ivLength: Int, val tagLength: Int) {
|
||||
|
||||
/**
|
||||
* Encrypt-then-Authenticate-then-Translate mode.
|
||||
* https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-eax-mode
|
||||
*
|
||||
* @see [RFC9580 - EAX Mode](https://www.rfc-editor.org/rfc/rfc9580.html#name-eax-mode)
|
||||
*/
|
||||
EAX(1, 16, 16),
|
||||
|
||||
/**
|
||||
* Offset-Codebook mode. OCB is mandatory to implement in crypto-refresh. Favored by GnuPG. Is
|
||||
* not yet FIPS compliant, but supported by most implementations and therefore favorable.
|
||||
* https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-ocb-mode
|
||||
*
|
||||
* @see [RFC9580 - OCB Mode](https://www.rfc-editor.org/rfc/rfc9580.html#name-ocb-mode)
|
||||
*/
|
||||
OCB(2, 15, 16),
|
||||
|
||||
/**
|
||||
* Galois/Counter-Mode. GCM is controversial. Some say it is hard to get right. Some
|
||||
* implementations like GnuPG therefore avoid it. May be necessary to achieve FIPS compliance.
|
||||
* https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-gcm-mode
|
||||
*
|
||||
* @see [RFC9580 - GCM Mode](https://www.rfc-editor.org/rfc/rfc9580.html#name-gcm-mode)
|
||||
*/
|
||||
GCM(3, 12, 16),
|
||||
;
|
||||
|
||||
/**
|
||||
* Return a [MessageEncryptionMechanism] instance representing AEAD using this algorithm and the
|
||||
* given [SymmetricKeyAlgorithm].
|
||||
*
|
||||
* @param ciphermode symmetric key algorithm
|
||||
* @return MessageEncryptionMechanism representing aead(this, ciphermode)
|
||||
*/
|
||||
fun toMechanism(ciphermode: SymmetricKeyAlgorithm): MessageEncryptionMechanism =
|
||||
MessageEncryptionMechanism.aead(ciphermode.algorithmId, this.algorithmId)
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Parse an [AEADAlgorithm] from an algorithm id. If no matching [AEADAlgorithm] is known,
|
||||
* return `null`.
|
||||
*
|
||||
* @param id algorithm id
|
||||
* @return aeadAlgorithm or null
|
||||
*/
|
||||
@JvmStatic
|
||||
fun fromId(id: Int): AEADAlgorithm? {
|
||||
return values().firstOrNull { algorithm -> algorithm.algorithmId == id }
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an [AEADAlgorithm] from an algorithm id. If no matching [AEADAlgorithm] is known,
|
||||
* throw a [NoSuchElementException].
|
||||
*
|
||||
* @param id algorithm id
|
||||
* @return aeadAlgorithm
|
||||
* @throws NoSuchElementException for unknown algorithm ids
|
||||
*/
|
||||
@JvmStatic
|
||||
fun requireFromId(id: Int): AEADAlgorithm {
|
||||
return fromId(id) ?: throw NoSuchElementException("No AEADAlgorithm found for id $id")
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.algorithm
|
||||
|
||||
import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites.Combination
|
||||
|
||||
data class AEADCipherMode(val aeadAlgorithm: AEADAlgorithm, val ciphermode: SymmetricKeyAlgorithm) {
|
||||
constructor(
|
||||
combination: Combination
|
||||
) : this(
|
||||
AEADAlgorithm.requireFromId(combination.aeadAlgorithm),
|
||||
SymmetricKeyAlgorithm.requireFromId(combination.symmetricAlgorithm))
|
||||
}
|
|
@ -4,18 +4,91 @@
|
|||
|
||||
package org.pgpainless.algorithm
|
||||
|
||||
class AlgorithmSuite(
|
||||
symmetricKeyAlgorithms: List<SymmetricKeyAlgorithm>,
|
||||
hashAlgorithms: List<HashAlgorithm>,
|
||||
compressionAlgorithms: List<CompressionAlgorithm>
|
||||
class AlgorithmSuite
|
||||
private constructor(
|
||||
val symmetricKeyAlgorithms: Set<SymmetricKeyAlgorithm>?,
|
||||
val hashAlgorithms: Set<HashAlgorithm>?,
|
||||
val compressionAlgorithms: Set<CompressionAlgorithm>?,
|
||||
val aeadAlgorithms: Set<AEADCipherMode>?,
|
||||
val features: Set<Feature>?
|
||||
) {
|
||||
|
||||
val symmetricKeyAlgorithms: Set<SymmetricKeyAlgorithm> = symmetricKeyAlgorithms.toSet()
|
||||
val hashAlgorithms: Set<HashAlgorithm> = hashAlgorithms.toSet()
|
||||
val compressionAlgorithms: Set<CompressionAlgorithm> = compressionAlgorithms.toSet()
|
||||
constructor(
|
||||
symmetricKeyAlgorithms: List<SymmetricKeyAlgorithm>?,
|
||||
hashAlgorithms: List<HashAlgorithm>?,
|
||||
compressionAlgorithms: List<CompressionAlgorithm>?,
|
||||
aeadAlgorithms: List<AEADCipherMode>?,
|
||||
features: List<Feature>?
|
||||
) : this(
|
||||
symmetricKeyAlgorithms?.toSet(),
|
||||
hashAlgorithms?.toSet(),
|
||||
compressionAlgorithms?.toSet(),
|
||||
aeadAlgorithms?.toSet(),
|
||||
features?.toSet())
|
||||
|
||||
fun modify(): Builder = Builder(this)
|
||||
|
||||
class Builder(suite: AlgorithmSuite? = null) {
|
||||
private var symmetricKeyAlgorithms: Set<SymmetricKeyAlgorithm>? =
|
||||
suite?.symmetricKeyAlgorithms
|
||||
private var hashAlgorithms: Set<HashAlgorithm>? = suite?.hashAlgorithms
|
||||
private var compressionAlgorithms: Set<CompressionAlgorithm>? = suite?.compressionAlgorithms
|
||||
private var aeadAlgorithms: Set<AEADCipherMode>? = suite?.aeadAlgorithms
|
||||
private var features: Set<Feature>? = suite?.features
|
||||
|
||||
fun overrideSymmetricKeyAlgorithms(
|
||||
vararg symmetricKeyAlgorithms: SymmetricKeyAlgorithm
|
||||
): Builder = overrideSymmetricKeyAlgorithms(symmetricKeyAlgorithms.toSet())
|
||||
|
||||
fun overrideSymmetricKeyAlgorithms(
|
||||
symmetricKeyAlgorithms: Collection<SymmetricKeyAlgorithm>?
|
||||
): Builder = apply { this.symmetricKeyAlgorithms = symmetricKeyAlgorithms?.toSet() }
|
||||
|
||||
fun overrideHashAlgorithms(vararg hashAlgorithms: HashAlgorithm): Builder =
|
||||
overrideHashAlgorithms(hashAlgorithms.toSet())
|
||||
|
||||
fun overrideHashAlgorithms(hashAlgorithms: Collection<HashAlgorithm>?): Builder = apply {
|
||||
this.hashAlgorithms = hashAlgorithms?.toSet()
|
||||
}
|
||||
|
||||
fun overrideCompressionAlgorithms(
|
||||
vararg compressionAlgorithms: CompressionAlgorithm
|
||||
): Builder = overrideCompressionAlgorithms(compressionAlgorithms.toSet())
|
||||
|
||||
fun overrideCompressionAlgorithms(
|
||||
compressionAlgorithms: Collection<CompressionAlgorithm>?
|
||||
): Builder = apply { this.compressionAlgorithms = compressionAlgorithms?.toSet() }
|
||||
|
||||
fun overrideAeadAlgorithms(vararg aeadAlgorithms: AEADCipherMode): Builder =
|
||||
overrideAeadAlgorithms(aeadAlgorithms.toSet())
|
||||
|
||||
fun overrideAeadAlgorithms(aeadAlgorithms: Collection<AEADCipherMode>?): Builder = apply {
|
||||
this.aeadAlgorithms = aeadAlgorithms?.toSet()
|
||||
}
|
||||
|
||||
fun overrideFeatures(vararg features: Feature): Builder = overrideFeatures(features.toSet())
|
||||
|
||||
fun overrideFeatures(features: Collection<Feature>?): Builder = apply {
|
||||
this.features = features?.toSet()
|
||||
}
|
||||
|
||||
fun build(): AlgorithmSuite {
|
||||
return AlgorithmSuite(
|
||||
symmetricKeyAlgorithms,
|
||||
hashAlgorithms,
|
||||
compressionAlgorithms,
|
||||
aeadAlgorithms,
|
||||
features)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun emptyBuilder(): Builder {
|
||||
return Builder()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val defaultSymmetricKeyAlgorithms =
|
||||
listOf(
|
||||
|
@ -39,9 +112,26 @@ class AlgorithmSuite(
|
|||
CompressionAlgorithm.ZIP,
|
||||
CompressionAlgorithm.UNCOMPRESSED)
|
||||
|
||||
@JvmStatic
|
||||
val defaultAEADAlgorithmSuites =
|
||||
listOf(
|
||||
AEADCipherMode(AEADAlgorithm.EAX, SymmetricKeyAlgorithm.AES_256),
|
||||
AEADCipherMode(AEADAlgorithm.OCB, SymmetricKeyAlgorithm.AES_256),
|
||||
AEADCipherMode(AEADAlgorithm.GCM, SymmetricKeyAlgorithm.AES_256),
|
||||
AEADCipherMode(AEADAlgorithm.EAX, SymmetricKeyAlgorithm.AES_192),
|
||||
AEADCipherMode(AEADAlgorithm.EAX, SymmetricKeyAlgorithm.AES_192))
|
||||
|
||||
@JvmStatic
|
||||
val defaultFeatures =
|
||||
listOf(Feature.MODIFICATION_DETECTION, Feature.MODIFICATION_DETECTION_2)
|
||||
|
||||
@JvmStatic
|
||||
val defaultAlgorithmSuite =
|
||||
AlgorithmSuite(
|
||||
defaultSymmetricKeyAlgorithms, defaultHashAlgorithms, defaultCompressionAlgorithms)
|
||||
defaultSymmetricKeyAlgorithms,
|
||||
defaultHashAlgorithms,
|
||||
defaultCompressionAlgorithms,
|
||||
defaultAEADAlgorithmSuites,
|
||||
defaultFeatures)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
|
||||
package org.pgpainless.algorithm
|
||||
|
||||
enum class EncryptionPurpose {
|
||||
import org.bouncycastle.bcpg.sig.KeyFlags
|
||||
|
||||
enum class EncryptionPurpose(val code: Int) {
|
||||
/** The stream will encrypt communication that goes over the wire. E.g. EMail, Chat... */
|
||||
COMMUNICATIONS,
|
||||
COMMUNICATIONS(KeyFlags.ENCRYPT_COMMS),
|
||||
/** The stream will encrypt data at rest. E.g. Encrypted backup... */
|
||||
STORAGE,
|
||||
STORAGE(KeyFlags.ENCRYPT_STORAGE),
|
||||
/** The stream will use keys with either flags to encrypt the data. */
|
||||
ANY
|
||||
ANY(KeyFlags.ENCRYPT_COMMS or KeyFlags.ENCRYPT_STORAGE)
|
||||
}
|
||||
|
|
|
@ -11,15 +11,19 @@ package org.pgpainless.algorithm
|
|||
*/
|
||||
enum class HashAlgorithm(val algorithmId: Int, val algorithmName: String) {
|
||||
|
||||
// 0 is reserved
|
||||
@Deprecated("MD5 is deprecated") MD5(1, "MD5"),
|
||||
SHA1(2, "SHA1"),
|
||||
RIPEMD160(3, "RIPEMD160"),
|
||||
// 4 - 7 are reserved
|
||||
SHA256(8, "SHA256"),
|
||||
SHA384(9, "SHA384"),
|
||||
SHA512(10, "SHA512"),
|
||||
SHA224(11, "SHA224"),
|
||||
SHA3_256(12, "SHA3-256"),
|
||||
// 13 is reserved
|
||||
SHA3_512(14, "SHA3-512"),
|
||||
// 100 - 110 are private / experimental
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
@ -57,13 +61,15 @@ enum class HashAlgorithm(val algorithmId: Int, val algorithmName: String) {
|
|||
* for a list of algorithms and names.
|
||||
*
|
||||
* @param name text name
|
||||
* @return enum value
|
||||
* @return enum value or null
|
||||
*/
|
||||
@JvmStatic
|
||||
fun fromName(name: String): HashAlgorithm? {
|
||||
return name.uppercase().let { algoName ->
|
||||
// find value where it.algorithmName == ALGO-NAME
|
||||
values().firstOrNull { it.algorithmName == algoName }
|
||||
?: values().firstOrNull { it.algorithmName == algoName.replace("-", "") }
|
||||
// else, find value where it.algorithmName == ALGONAME
|
||||
?: values().firstOrNull { it.algorithmName == algoName.replace("-", "") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.algorithm
|
||||
|
||||
enum class OpenPGPKeyVersion(val numeric: Int) {
|
||||
// Version 2 packets are identical in format to Version 3 packets, but are generated by
|
||||
// PGP 2.5 or before. V2 packets are deprecated and they MUST NOT be generated.
|
||||
|
||||
/**
|
||||
* Version 3 packets were first generated by PGP 2.6. Version 3 keys are deprecated. They
|
||||
* contain three weaknesses. First, it is relatively easy to construct a version 3 key that has
|
||||
* the same Key ID as any other key because the Key ID is simply the low 64 bits of the public
|
||||
* modulus. Second, because the fingerprint of a version 3 key hashes the key material, but not
|
||||
* its length, there is an increased opportunity for fingerprint collisions. Third, there are
|
||||
* weaknesses in the MD5 hash algorithm that cause developers to prefer other algorithms.
|
||||
*/
|
||||
@Deprecated("V3 keys are deprecated.") v3(3),
|
||||
|
||||
/**
|
||||
* Version 4 packets are used in RFC2440, RFC4880, RFC9580. The version 4 format is widely
|
||||
* supported by various implementations.
|
||||
*
|
||||
* @see [RFC2440](https://www.rfc-editor.org/rfc/rfc2440.html)
|
||||
* @see [RFC4880](https://www.rfc-editor.org/rfc/rfc4880.html)
|
||||
* @see [RFC9580](https://www.rfc-editor.org/rfc/rfc9580.html)
|
||||
*/
|
||||
v4(4),
|
||||
|
||||
/**
|
||||
* "V5"-keys are introduced in the LibrePGP document. These are NOT OpenPGP keys and are
|
||||
* primarily supported by GnuPG and RNP.
|
||||
*
|
||||
* @see [LibrePGP](https://datatracker.ietf.org/doc/draft-koch-librepgp/)
|
||||
*/
|
||||
librePgp(5),
|
||||
|
||||
/**
|
||||
* Version 6 packets are introduced in RFC9580. The version 6 format is similar to the version 4
|
||||
* format except for the addition of a count for the key material. This count helps parsing
|
||||
* Secret Key packets (which are an extension of the Public Key packet format) in the case of an
|
||||
* unknown algorithm. In addition, fingerprints of version 6 keys are calculated differently
|
||||
* from version 4 keys, preventing the KOpenPGP attack.
|
||||
*
|
||||
* @see [RFC9580](https://www.rfc-editor.org/rfc/rfc9580.html)
|
||||
*/
|
||||
v6(6),
|
||||
;
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun from(numeric: Int): OpenPGPKeyVersion {
|
||||
return values().find { numeric == it.numeric }
|
||||
?: throw IllegalArgumentException("Unknown key version $numeric")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,76 +4,105 @@
|
|||
|
||||
package org.pgpainless.algorithm
|
||||
|
||||
import org.bouncycastle.bcpg.PublicKeyUtils
|
||||
|
||||
/**
|
||||
* Enumeration of public key algorithms as defined in RFC4880.
|
||||
* Enumeration of public key algorithms as defined in RFC4880, RFC9580, Persistent Symmetric Keys.
|
||||
*
|
||||
* See [RFC4880: Public-Key Algorithms](https://tools.ietf.org/html/rfc4880#section-9.1)
|
||||
* @see [RFC4880: Public-Key Algorithms](https://tools.ietf.org/html/rfc4880#section-9.1)
|
||||
* @see
|
||||
* [RFC9580: Public-Key Algorithms](https://www.rfc-editor.org/rfc/rfc9580.html#name-public-key-algorithms)
|
||||
* @see
|
||||
* [Persistent Symmetric Keys in OpenPGP](https://www.ietf.org/archive/id/draft-ietf-openpgp-persistent-symmetric-keys-01.html#name-persistent-symmetric-key-al)
|
||||
*/
|
||||
enum class PublicKeyAlgorithm(
|
||||
val algorithmId: Int,
|
||||
val signingCapable: Boolean,
|
||||
val encryptionCapable: Boolean
|
||||
) {
|
||||
enum class PublicKeyAlgorithm(val algorithmId: Int) {
|
||||
|
||||
// RFC4880
|
||||
|
||||
/** RSA capable of encryption and signatures. */
|
||||
RSA_GENERAL(1, true, true),
|
||||
RSA_GENERAL(1),
|
||||
|
||||
/**
|
||||
* RSA with usage encryption.
|
||||
*
|
||||
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.5">Deprecation
|
||||
* notice</a>
|
||||
* @deprecated see [Deprecation notice](https://tools.ietf.org/html/rfc4880#section-13.5)
|
||||
*/
|
||||
@Deprecated("RSA_ENCRYPT is deprecated in favor of RSA_GENERAL", ReplaceWith("RSA_GENERAL"))
|
||||
RSA_ENCRYPT(2, false, true),
|
||||
RSA_ENCRYPT(2),
|
||||
|
||||
/**
|
||||
* RSA with usage of creating signatures.
|
||||
*
|
||||
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.5">Deprecation
|
||||
* notice</a>
|
||||
* @deprecated see [Deprecation notice](https://tools.ietf.org/html/rfc4880#section-13.5)
|
||||
*/
|
||||
@Deprecated("RSA_SIGN is deprecated in favor of RSA_GENERAL", ReplaceWith("RSA_GENERAL"))
|
||||
RSA_SIGN(3, true, false),
|
||||
RSA_SIGN(3),
|
||||
|
||||
/** ElGamal with usage encryption. */
|
||||
ELGAMAL_ENCRYPT(16, false, true),
|
||||
ELGAMAL_ENCRYPT(16),
|
||||
|
||||
/** Digital Signature Algorithm. */
|
||||
DSA(17, true, false),
|
||||
DSA(17),
|
||||
|
||||
/** Elliptic Curve Diffie-Hellman. */
|
||||
ECDH(18, false, true),
|
||||
ECDH(18),
|
||||
|
||||
/** Elliptic Curve Digital Signature Algorithm. */
|
||||
ECDSA(19, true, false),
|
||||
ECDSA(19),
|
||||
|
||||
/**
|
||||
* ElGamal General.
|
||||
*
|
||||
* @deprecated see <a href="https://tools.ietf.org/html/rfc4880#section-13.8">Deprecation
|
||||
* notice</a>
|
||||
* @deprecated see [Deprecation notice](https://tools.ietf.org/html/rfc4880#section-13.8)
|
||||
*/
|
||||
@Deprecated("ElGamal is deprecated") ELGAMAL_GENERAL(20, true, true),
|
||||
@Deprecated("ElGamal is deprecated") ELGAMAL_GENERAL(20),
|
||||
|
||||
/** Diffie-Hellman key exchange algorithm. */
|
||||
DIFFIE_HELLMAN(21, false, true),
|
||||
DIFFIE_HELLMAN(21),
|
||||
|
||||
/** Digital Signature Algorithm based on twisted Edwards Curves. */
|
||||
EDDSA_LEGACY(22, true, false),
|
||||
EDDSA_LEGACY(22),
|
||||
|
||||
// RFC9580
|
||||
|
||||
/** X25519 encryption algorithm. */
|
||||
X25519(25, false, true),
|
||||
X25519(25),
|
||||
|
||||
/** X448 encryption algorithm. */
|
||||
X448(26, false, true),
|
||||
X448(26),
|
||||
|
||||
/** Ed25519 signature algorithm. */
|
||||
ED25519(27, true, false),
|
||||
ED25519(27),
|
||||
|
||||
/** Ed448 signature algorithm. */
|
||||
ED448(28, true, false),
|
||||
;
|
||||
ED448(28),
|
||||
|
||||
// Persistent Symmetric Keys in OpenPGP
|
||||
|
||||
/**
|
||||
* AEAD can be used as a persistent key symmetric encryption algorithm for message encryption.
|
||||
*
|
||||
* @see
|
||||
* [Persistent Symmetric Keys in OpenPGP](https://datatracker.ietf.org/doc/draft-ietf-openpgp-persistent-symmetric-keys/)
|
||||
*/
|
||||
AEAD(128) {
|
||||
override val signingCapable = false
|
||||
override val encryptionCapable = true
|
||||
},
|
||||
|
||||
/**
|
||||
* HMAC can be used as a persistent key symmetric signing algorithm for message signing.
|
||||
*
|
||||
* @see
|
||||
* [Persistent Symmetric Keys in OpenPGP](https://datatracker.ietf.org/doc/draft-ietf-openpgp-persistent-symmetric-keys/)
|
||||
*/
|
||||
HMAC(129) {
|
||||
override val signingCapable = true
|
||||
override val encryptionCapable = false
|
||||
};
|
||||
|
||||
open val signingCapable: Boolean = PublicKeyUtils.isSigningAlgorithm(algorithmId)
|
||||
open val encryptionCapable: Boolean = PublicKeyUtils.isEncryptionAlgorithm(algorithmId)
|
||||
|
||||
fun isSigningCapable(): Boolean = signingCapable
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.algorithm.negotiation
|
||||
|
||||
import org.pgpainless.algorithm.CompressionAlgorithm
|
||||
import org.pgpainless.policy.Policy
|
||||
|
||||
fun interface CompressionAlgorithmNegotiator {
|
||||
|
||||
/**
|
||||
* Negotiate a suitable [CompressionAlgorithm] by taking into consideration the [Policy], a
|
||||
* user-provided [compressionAlgorithmOverride] and the users set of [orderedPreferences].
|
||||
*
|
||||
* @param policy implementations [Policy]
|
||||
* @param compressionAlgorithmOverride user-provided [CompressionAlgorithm] override.
|
||||
* @param orderedPreferences preferred compression algorithms taken from the users certificate
|
||||
* @return negotiated [CompressionAlgorithm]
|
||||
*/
|
||||
fun negotiate(
|
||||
policy: Policy,
|
||||
compressionAlgorithmOverride: CompressionAlgorithm?,
|
||||
orderedPreferences: Set<CompressionAlgorithm>?
|
||||
): CompressionAlgorithm
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Static negotiation of compression algorithms. This implementation discards compression
|
||||
* algorithm preferences and instead either returns the non-null algorithm override,
|
||||
* otherwise the policies default hash algorithm.
|
||||
*
|
||||
* @return delegate implementation
|
||||
*/
|
||||
@JvmStatic
|
||||
fun staticNegotiation(): CompressionAlgorithmNegotiator =
|
||||
CompressionAlgorithmNegotiator { policy, override, _ ->
|
||||
override ?: policy.compressionAlgorithmPolicy.defaultCompressionAlgorithm
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.algorithm.negotiation
|
||||
|
||||
import org.bouncycastle.openpgp.api.MessageEncryptionMechanism
|
||||
import org.pgpainless.algorithm.AEADAlgorithm
|
||||
import org.pgpainless.algorithm.AEADCipherMode
|
||||
import org.pgpainless.algorithm.Feature
|
||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||
import org.pgpainless.policy.Policy
|
||||
|
||||
fun interface EncryptionMechanismNegotiator {
|
||||
|
||||
fun negotiate(
|
||||
policy: Policy,
|
||||
override: MessageEncryptionMechanism?,
|
||||
features: List<Set<Feature>>,
|
||||
aeadAlgorithmPreferences: List<Set<AEADCipherMode>>,
|
||||
symmetricAlgorithmPreferences: List<Set<SymmetricKeyAlgorithm>>
|
||||
): MessageEncryptionMechanism
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun modificationDetectionOrBetter(
|
||||
symmetricKeyAlgorithmNegotiator: SymmetricKeyAlgorithmNegotiator
|
||||
): EncryptionMechanismNegotiator =
|
||||
object : EncryptionMechanismNegotiator {
|
||||
|
||||
override fun negotiate(
|
||||
policy: Policy,
|
||||
override: MessageEncryptionMechanism?,
|
||||
features: List<Set<Feature>>,
|
||||
aeadAlgorithmPreferences: List<Set<AEADCipherMode>>,
|
||||
symmetricAlgorithmPreferences: List<Set<SymmetricKeyAlgorithm>>
|
||||
): MessageEncryptionMechanism {
|
||||
|
||||
// If the user supplied an override, use that
|
||||
if (override != null) {
|
||||
return override
|
||||
}
|
||||
|
||||
// If all support SEIPD2, use SEIPD2
|
||||
if (features.all { it.contains(Feature.MODIFICATION_DETECTION_2) }) {
|
||||
// Find best supported algorithm combination
|
||||
val counted = mutableMapOf<AEADCipherMode, Int>()
|
||||
for (pref in aeadAlgorithmPreferences) {
|
||||
for (mode in pref) {
|
||||
counted[mode] = counted.getOrDefault(mode, 0) + 1
|
||||
}
|
||||
}
|
||||
// filter for supported combinations and find most widely supported
|
||||
val bestSupportedMode: AEADCipherMode =
|
||||
counted
|
||||
.filter {
|
||||
policy.messageEncryptionAlgorithmPolicy.isAcceptable(
|
||||
MessageEncryptionMechanism.aead(
|
||||
it.key.ciphermode.algorithmId,
|
||||
it.key.aeadAlgorithm.algorithmId))
|
||||
}
|
||||
.maxByOrNull { it.value }
|
||||
?.key
|
||||
?: AEADCipherMode(AEADAlgorithm.OCB, SymmetricKeyAlgorithm.AES_128)
|
||||
|
||||
// return best supported mode or symmetric key fallback mechanism
|
||||
return MessageEncryptionMechanism.aead(
|
||||
bestSupportedMode.ciphermode.algorithmId,
|
||||
bestSupportedMode.aeadAlgorithm.algorithmId)
|
||||
} else if (features.all { it.contains(Feature.LIBREPGP_OCB_ENCRYPTED_DATA) }) {
|
||||
return MessageEncryptionMechanism.librePgp(
|
||||
symmetricKeyAlgorithmNegotiator
|
||||
.negotiate(
|
||||
policy.messageEncryptionAlgorithmPolicy
|
||||
.symmetricAlgorithmPolicy,
|
||||
null,
|
||||
symmetricAlgorithmPreferences)
|
||||
.algorithmId)
|
||||
}
|
||||
// If all support SEIPD1, negotiate SEIPD1 using symmetricKeyAlgorithmNegotiator
|
||||
else if (features.all { it.contains(Feature.MODIFICATION_DETECTION) }) {
|
||||
return MessageEncryptionMechanism.integrityProtected(
|
||||
symmetricKeyAlgorithmNegotiator
|
||||
.negotiate(
|
||||
policy.messageEncryptionAlgorithmPolicy
|
||||
.symmetricAlgorithmPolicy,
|
||||
null,
|
||||
symmetricAlgorithmPreferences)
|
||||
.algorithmId)
|
||||
}
|
||||
// Else fall back to fallback mechanism from policy
|
||||
else {
|
||||
return policy.messageEncryptionAlgorithmPolicy.asymmetricFallbackMechanism
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ interface HashAlgorithmNegotiator {
|
|||
* @param orderedPrefs hash algorithm preferences
|
||||
* @return picked algorithms
|
||||
*/
|
||||
fun negotiateHashAlgorithm(orderedPrefs: Set<HashAlgorithm>): HashAlgorithm
|
||||
fun negotiateHashAlgorithm(orderedPrefs: Set<HashAlgorithm>?): HashAlgorithm
|
||||
|
||||
companion object {
|
||||
|
||||
|
@ -62,9 +62,9 @@ interface HashAlgorithmNegotiator {
|
|||
): HashAlgorithmNegotiator {
|
||||
return object : HashAlgorithmNegotiator {
|
||||
override fun negotiateHashAlgorithm(
|
||||
orderedPrefs: Set<HashAlgorithm>
|
||||
orderedPrefs: Set<HashAlgorithm>?
|
||||
): HashAlgorithm {
|
||||
return orderedPrefs.firstOrNull { hashAlgorithmPolicy.isAcceptable(it) }
|
||||
return orderedPrefs?.firstOrNull { hashAlgorithmPolicy.isAcceptable(it) }
|
||||
?: hashAlgorithmPolicy.defaultHashAlgorithm()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
package org.pgpainless.algorithm.negotiation
|
||||
|
||||
import java.lang.IllegalArgumentException
|
||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||
import org.pgpainless.policy.Policy
|
||||
|
||||
|
@ -36,9 +35,8 @@ interface SymmetricKeyAlgorithmNegotiator {
|
|||
override: SymmetricKeyAlgorithm?,
|
||||
keyPreferences: List<Set<SymmetricKeyAlgorithm>>
|
||||
): SymmetricKeyAlgorithm {
|
||||
if (override == SymmetricKeyAlgorithm.NULL) {
|
||||
throw IllegalArgumentException(
|
||||
"Algorithm override cannot be NULL (plaintext).")
|
||||
require(override != SymmetricKeyAlgorithm.NULL) {
|
||||
"Algorithm override cannot be NULL (plaintext)."
|
||||
}
|
||||
|
||||
if (override != null) {
|
||||
|
|
|
@ -5,14 +5,46 @@
|
|||
package org.pgpainless.authentication
|
||||
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate
|
||||
import org.pgpainless.PGPainless
|
||||
|
||||
/**
|
||||
* A certificate authenticity record, indicating, to what degree the certificate is authenticated.
|
||||
*
|
||||
* @param userId identity, was changed to [CharSequence] instead of [String] starting with
|
||||
* PGPainless 2.0.
|
||||
* @param certificate certificate, was changed to [OpenPGPCertificate] instead of
|
||||
* [PGPPublicKeyRing]. Use [pgpPublicKeyRing] if you need to access a [PGPPublicKeyRing].
|
||||
* @param certificationChains map of chains and their trust degrees
|
||||
* @param targetAmount targeted trust amount
|
||||
*/
|
||||
class CertificateAuthenticity(
|
||||
val userId: String,
|
||||
val certificate: PGPPublicKeyRing,
|
||||
val userId: CharSequence,
|
||||
val certificate: OpenPGPCertificate,
|
||||
val certificationChains: Map<CertificationChain, Int>,
|
||||
val targetAmount: Int
|
||||
) {
|
||||
|
||||
/** Legacy constructor accepting a [PGPPublicKeyRing]. */
|
||||
@Deprecated("Pass in an OpenPGPCertificate instead of a PGPPublicKeyRing.")
|
||||
constructor(
|
||||
userId: String,
|
||||
certificate: PGPPublicKeyRing,
|
||||
certificationChains: Map<CertificationChain, Int>,
|
||||
targetAmount: Int
|
||||
) : this(
|
||||
userId,
|
||||
PGPainless.getInstance().toCertificate(certificate),
|
||||
certificationChains,
|
||||
targetAmount)
|
||||
|
||||
/**
|
||||
* Field was introduced to allow backwards compatibility with pre-2.0 API as replacement for
|
||||
* [certificate].
|
||||
*/
|
||||
@Deprecated("Use certificate instead.", replaceWith = ReplaceWith("certificate"))
|
||||
val pgpPublicKeyRing: PGPPublicKeyRing = certificate.pgpPublicKeyRing
|
||||
|
||||
val totalTrustAmount: Int
|
||||
get() = certificationChains.values.sum()
|
||||
|
||||
|
@ -44,5 +76,22 @@ class CertificateAuthenticity(
|
|||
*/
|
||||
class CertificationChain(val trustAmount: Int, val chainLinks: List<ChainLink>) {}
|
||||
|
||||
/** A chain link contains a node in the trust chain. */
|
||||
class ChainLink(val certificate: PGPPublicKeyRing) {}
|
||||
/**
|
||||
* A chain link contains a node in the trust chain.
|
||||
*
|
||||
* @param certificate chain link certificate, was changed from [PGPPublicKeyRing] to
|
||||
* [OpenPGPCertificate] with PGPainless 2.0. Use [pgpPublicKeyRing] if you need to access the
|
||||
* field as [PGPPublicKeyRing].
|
||||
*/
|
||||
class ChainLink(val certificate: OpenPGPCertificate) {
|
||||
constructor(
|
||||
certificate: PGPPublicKeyRing
|
||||
) : this(PGPainless.getInstance().toCertificate(certificate))
|
||||
|
||||
/**
|
||||
* Field was introduced to allow backwards compatibility with pre-2.0 API as replacement for
|
||||
* [certificate].
|
||||
*/
|
||||
@Deprecated("Use certificate instead.", replaceWith = ReplaceWith("certificate"))
|
||||
val pgpPublicKeyRing: PGPPublicKeyRing = certificate.pgpPublicKeyRing
|
||||
}
|
||||
|
|
|
@ -5,14 +5,15 @@
|
|||
package org.pgpainless.authentication
|
||||
|
||||
import java.util.*
|
||||
import org.bouncycastle.bcpg.KeyIdentifier
|
||||
import org.pgpainless.key.OpenPgpFingerprint
|
||||
|
||||
/**
|
||||
* Interface for a CA that can authenticate trust-worthy certificates. Such a CA might be a fixed
|
||||
* list of trustworthy certificates, or a dynamic implementation like the Web-of-Trust.
|
||||
*
|
||||
* @see <a href="https://github.com/pgpainless/pgpainless-wot">PGPainless-WOT</a>
|
||||
* @see <a href="https://sequoia-pgp.gitlab.io/sequoia-wot/">OpenPGP Web of Trust</a>
|
||||
* @see [PGPainless-WOT](https://github.com/pgpainless/pgpainless-wot)
|
||||
* @see [OpenPGP Web of Trust](https://sequoia-pgp.gitlab.io/sequoia-wot/)
|
||||
*/
|
||||
interface CertificateAuthority {
|
||||
|
||||
|
@ -36,7 +37,30 @@ interface CertificateAuthority {
|
|||
email: Boolean,
|
||||
referenceTime: Date,
|
||||
targetAmount: Int
|
||||
): CertificateAuthenticity
|
||||
): CertificateAuthenticity? =
|
||||
authenticateBinding(fingerprint.keyIdentifier, userId, email, referenceTime, targetAmount)
|
||||
|
||||
/**
|
||||
* Determine the authenticity of the binding between the given cert identifier and the userId.
|
||||
* In other words, determine, how much evidence can be gathered, that the certificate with the
|
||||
* given fingerprint really belongs to the user with the given userId.
|
||||
*
|
||||
* @param certIdentifier identifier of the certificate
|
||||
* @param userId userId
|
||||
* @param email if true, the userId will be treated as an email address and all user-IDs
|
||||
* containing the email address will be matched.
|
||||
* @param referenceTime reference time at which the binding shall be evaluated
|
||||
* @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly
|
||||
* authenticated, 60 = partially authenticated...)
|
||||
* @return information about the authenticity of the binding
|
||||
*/
|
||||
fun authenticateBinding(
|
||||
certIdentifier: KeyIdentifier,
|
||||
userId: CharSequence,
|
||||
email: Boolean,
|
||||
referenceTime: Date,
|
||||
targetAmount: Int
|
||||
): CertificateAuthenticity?
|
||||
|
||||
/**
|
||||
* Lookup certificates, which carry a trustworthy binding to the given userId.
|
||||
|
@ -50,7 +74,7 @@ interface CertificateAuthority {
|
|||
* @return list of identified bindings
|
||||
*/
|
||||
fun lookupByUserId(
|
||||
userId: String,
|
||||
userId: CharSequence,
|
||||
email: Boolean,
|
||||
referenceTime: Date,
|
||||
targetAmount: Int
|
||||
|
@ -70,5 +94,22 @@ interface CertificateAuthority {
|
|||
fingerprint: OpenPgpFingerprint,
|
||||
referenceTime: Date,
|
||||
targetAmount: Int
|
||||
): List<CertificateAuthenticity> =
|
||||
identifyByFingerprint(fingerprint.keyIdentifier, referenceTime, targetAmount)
|
||||
|
||||
/**
|
||||
* Identify trustworthy bindings for a certificate. The result is a list of authenticatable
|
||||
* userIds on the certificate.
|
||||
*
|
||||
* @param certIdentifier identifier of the certificate
|
||||
* @param referenceTime reference time for trust calculations
|
||||
* @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly
|
||||
* authenticated, 60 = partially authenticated...)
|
||||
* @return list of identified bindings
|
||||
*/
|
||||
fun identifyByFingerprint(
|
||||
certIdentifier: KeyIdentifier,
|
||||
referenceTime: Date,
|
||||
targetAmount: Int
|
||||
): List<CertificateAuthenticity>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.bouncycastle
|
||||
|
||||
import java.util.Date
|
||||
import org.bouncycastle.openpgp.api.OpenPGPPolicy
|
||||
import org.bouncycastle.openpgp.api.OpenPGPPolicy.OpenPGPNotationRegistry
|
||||
import org.pgpainless.policy.Policy
|
||||
|
||||
/** Adapter class that adapts a PGPainless [Policy] object to Bouncy Castles [OpenPGPPolicy]. */
|
||||
class PolicyAdapter(val policy: Policy) : OpenPGPPolicy {
|
||||
|
||||
/**
|
||||
* Determine, whether the hash algorithm of a document signature is acceptable.
|
||||
*
|
||||
* @param algorithmId hash algorithm ID
|
||||
* @param signatureCreationTime optional signature creation time
|
||||
* @return boolean indicating whether the hash algorithm is acceptable
|
||||
*/
|
||||
override fun isAcceptableDocumentSignatureHashAlgorithm(
|
||||
algorithmId: Int,
|
||||
signatureCreationTime: Date?
|
||||
): Boolean {
|
||||
return if (signatureCreationTime == null)
|
||||
policy.dataSignatureHashAlgorithmPolicy.isAcceptable(algorithmId)
|
||||
else
|
||||
policy.dataSignatureHashAlgorithmPolicy.isAcceptable(algorithmId, signatureCreationTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine, whether the hash algorithm of a revocation signature is acceptable.
|
||||
*
|
||||
* @param algorithmId hash algorithm ID
|
||||
* @param revocationCreationTime optional revocation signature creation time
|
||||
* @return boolean indicating whether the hash algorithm is acceptable
|
||||
*/
|
||||
override fun isAcceptableRevocationSignatureHashAlgorithm(
|
||||
algorithmId: Int,
|
||||
revocationCreationTime: Date?
|
||||
): Boolean {
|
||||
return if (revocationCreationTime == null)
|
||||
policy.revocationSignatureHashAlgorithmPolicy.isAcceptable(algorithmId)
|
||||
else
|
||||
policy.revocationSignatureHashAlgorithmPolicy.isAcceptable(
|
||||
algorithmId, revocationCreationTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine, whether the hash algorithm of a certification signature is acceptable.
|
||||
*
|
||||
* @param algorithmId hash algorithm ID
|
||||
* @param certificationCreationTime optional certification signature creation time
|
||||
* @return boolean indicating whether the hash algorithm is acceptable
|
||||
*/
|
||||
override fun isAcceptableCertificationSignatureHashAlgorithm(
|
||||
algorithmId: Int,
|
||||
certificationCreationTime: Date?
|
||||
): Boolean {
|
||||
return if (certificationCreationTime == null)
|
||||
policy.certificationSignatureHashAlgorithmPolicy.isAcceptable(algorithmId)
|
||||
else
|
||||
policy.certificationSignatureHashAlgorithmPolicy.isAcceptable(
|
||||
algorithmId, certificationCreationTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default hash algorithm for certification signatures. This is used as fallback if
|
||||
* not suitable hash algorithm can be negotiated.
|
||||
*
|
||||
* @return default certification signature hash algorithm
|
||||
*/
|
||||
override fun getDefaultCertificationSignatureHashAlgorithm(): Int {
|
||||
return policy.certificationSignatureHashAlgorithmPolicy.defaultHashAlgorithm.algorithmId
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default hash algorithm for document signatures. This is used as fallback if not
|
||||
* suitable hash algorithm can be negotiated.
|
||||
*
|
||||
* @return default document signature hash algorithm
|
||||
*/
|
||||
override fun getDefaultDocumentSignatureHashAlgorithm(): Int {
|
||||
return policy.dataSignatureHashAlgorithmPolicy.defaultHashAlgorithm.algorithmId
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine, whether the given symmetric encryption algorithm is acceptable.
|
||||
*
|
||||
* @param algorithmId symmetric encryption algorithm ID
|
||||
* @return boolean indicating, whether the encryption algorithm is acceptable
|
||||
*/
|
||||
override fun isAcceptableSymmetricKeyAlgorithm(algorithmId: Int): Boolean {
|
||||
return policy.messageEncryptionAlgorithmPolicy.symmetricAlgorithmPolicy.isAcceptable(
|
||||
algorithmId)
|
||||
}
|
||||
/**
|
||||
* Return the default symmetric encryption algorithm. This algorithm is used as fallback to
|
||||
* encrypt messages if no suitable symmetric encryption algorithm can be negotiated.
|
||||
*
|
||||
* @return default symmetric encryption algorithm
|
||||
*/
|
||||
override fun getDefaultSymmetricKeyAlgorithm(): Int {
|
||||
return policy.messageEncryptionAlgorithmPolicy.symmetricAlgorithmPolicy
|
||||
.defaultSymmetricKeyAlgorithm
|
||||
.algorithmId
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine, whether the [bitStrength] of an asymmetric public key of the given algorithm is
|
||||
* strong enough.
|
||||
*
|
||||
* @param algorithmId public key algorithm ID
|
||||
* @param bitStrength strength of the key in bits
|
||||
* @return boolean indicating whether the bit strength is sufficient
|
||||
*/
|
||||
override fun isAcceptablePublicKeyStrength(algorithmId: Int, bitStrength: Int): Boolean {
|
||||
return policy.publicKeyAlgorithmPolicy.isAcceptable(algorithmId, bitStrength)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt PGPainless' [org.pgpainless.util.NotationRegistry] to Bouncy Castles
|
||||
* [OpenPGPNotationRegistry].
|
||||
*
|
||||
* @return adapted [OpenPGPNotationRegistry]
|
||||
*/
|
||||
override fun getNotationRegistry(): OpenPGPNotationRegistry {
|
||||
return object : OpenPGPNotationRegistry() {
|
||||
|
||||
/** Determine, whether the given [notationName] is known by the registry. */
|
||||
override fun isNotationKnown(notationName: String?): Boolean {
|
||||
return notationName?.let { policy.notationRegistry.isKnownNotation(it) } ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a known notation name to the registry.
|
||||
*
|
||||
* @param notationName notation name
|
||||
*/
|
||||
override fun addKnownNotation(notationName: String?) {
|
||||
notationName?.let { policy.notationRegistry.addKnownNotation(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.bouncycastle.extensions
|
||||
|
||||
import java.io.OutputStream
|
||||
import org.bouncycastle.bcpg.ArmoredOutputStream
|
||||
import org.bouncycastle.bcpg.PacketFormat
|
||||
import org.bouncycastle.openpgp.PGPOnePassSignature
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey
|
||||
import org.pgpainless.algorithm.OpenPGPKeyVersion
|
||||
|
||||
/**
|
||||
* Return the [OpenPGPComponentKey] that issued the given [PGPOnePassSignature].
|
||||
*
|
||||
* @param ops one pass signature
|
||||
*/
|
||||
fun OpenPGPCertificate.getSigningKeyFor(ops: PGPOnePassSignature): OpenPGPComponentKey? =
|
||||
this.getKey(ops.keyIdentifier)
|
||||
|
||||
/** Return the [OpenPGPKeyVersion] of the certificates primary key. */
|
||||
fun OpenPGPCertificate.getKeyVersion(): OpenPGPKeyVersion = primaryKey.getKeyVersion()
|
||||
|
||||
/** Return the [OpenPGPKeyVersion] of the component key. */
|
||||
fun OpenPGPComponentKey.getKeyVersion(): OpenPGPKeyVersion = OpenPGPKeyVersion.from(this.version)
|
||||
|
||||
/**
|
||||
* ASCII-armor-encode the certificate into the given [OutputStream].
|
||||
*
|
||||
* @param outputStream output stream
|
||||
* @param format packet length encoding format, defaults to [PacketFormat.ROUNDTRIP]
|
||||
*/
|
||||
fun OpenPGPCertificate.asciiArmor(
|
||||
outputStream: OutputStream,
|
||||
format: PacketFormat = PacketFormat.ROUNDTRIP
|
||||
) {
|
||||
outputStream.write(toAsciiArmoredString(format).encodeToByteArray())
|
||||
}
|
||||
|
||||
/**
|
||||
* ASCII-armor-encode the certificate into the given [OutputStream].
|
||||
*
|
||||
* @param outputStream output stream
|
||||
* @param format packet length encoding format, defaults to [PacketFormat.ROUNDTRIP]
|
||||
* @param armorBuilder builder for the ASCII armored output stream
|
||||
*/
|
||||
fun OpenPGPCertificate.asciiArmor(
|
||||
outputStream: OutputStream,
|
||||
format: PacketFormat,
|
||||
armorBuilder: ArmoredOutputStream.Builder
|
||||
) {
|
||||
outputStream.write(toAsciiArmoredString(format, armorBuilder).encodeToByteArray())
|
||||
}
|
||||
|
||||
fun OpenPGPCertificate.encode(
|
||||
outputStream: OutputStream,
|
||||
format: PacketFormat = PacketFormat.ROUNDTRIP
|
||||
) {
|
||||
outputStream.write(getEncoded(format))
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.bouncycastle.extensions
|
||||
|
||||
import org.bouncycastle.bcpg.HashAlgorithmTags
|
||||
import org.bouncycastle.openpgp.api.EncryptedDataPacketType
|
||||
import org.bouncycastle.openpgp.api.MessageEncryptionMechanism
|
||||
import org.bouncycastle.openpgp.api.OpenPGPImplementation
|
||||
import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder
|
||||
import org.bouncycastle.openpgp.operator.PGPDigestCalculator
|
||||
|
||||
/**
|
||||
* Return a [PGPDigestCalculator] that is based on [HashAlgorithmTags.SHA1], used for key checksum
|
||||
* calculations.
|
||||
*/
|
||||
fun OpenPGPImplementation.checksumCalculator(): PGPDigestCalculator {
|
||||
return pgpDigestCalculatorProvider().get(HashAlgorithmTags.SHA1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a [PGPDataEncryptorBuilder] for the given [MessageEncryptionMechanism].
|
||||
*
|
||||
* @param mechanism
|
||||
* @return data encryptor builder
|
||||
*/
|
||||
fun OpenPGPImplementation.pgpDataEncryptorBuilder(
|
||||
mechanism: MessageEncryptionMechanism
|
||||
): PGPDataEncryptorBuilder {
|
||||
require(mechanism.isEncrypted) { "Cannot create PGPDataEncryptorBuilder for NULL algorithm." }
|
||||
return pgpDataEncryptorBuilder(mechanism.symmetricKeyAlgorithm).also {
|
||||
when (mechanism.mode!!) {
|
||||
EncryptedDataPacketType.SED -> it.setWithIntegrityPacket(false)
|
||||
EncryptedDataPacketType.SEIPDv1 -> it.setWithIntegrityPacket(true)
|
||||
EncryptedDataPacketType.SEIPDv2 -> {
|
||||
it.setWithAEAD(mechanism.aeadAlgorithm, mechanism.symmetricKeyAlgorithm)
|
||||
it.setUseV6AEAD()
|
||||
}
|
||||
EncryptedDataPacketType.LIBREPGP_OED -> {
|
||||
it.setWithAEAD(mechanism.aeadAlgorithm, mechanism.symmetricKeyAlgorithm)
|
||||
it.setUseV5AEAD()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.bouncycastle.extensions
|
||||
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPPrivateKey
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPSecretKey
|
||||
import org.pgpainless.util.Passphrase
|
||||
|
||||
/**
|
||||
* Return the [OpenPGPSecretKey] that can be used to decrypt the given [PGPPublicKeyEncryptedData].
|
||||
*
|
||||
* @param pkesk public-key encrypted session-key packet
|
||||
* @return secret key or null if no matching secret key was found
|
||||
*/
|
||||
fun OpenPGPKey.getSecretKeyFor(pkesk: PGPPublicKeyEncryptedData): OpenPGPSecretKey? =
|
||||
this.getSecretKey(pkesk.keyIdentifier)
|
||||
|
||||
/**
|
||||
* Unlock the [OpenPGPSecretKey], returning the unlocked [OpenPGPPrivateKey].
|
||||
*
|
||||
* @param passphrase passphrase to unlock the key
|
||||
* @return unlocked [OpenPGPPrivateKey]
|
||||
*/
|
||||
fun OpenPGPSecretKey.unlock(passphrase: Passphrase): OpenPGPPrivateKey =
|
||||
this.unlock(passphrase.getChars())
|
|
@ -0,0 +1,126 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.bouncycastle.extensions
|
||||
|
||||
import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKeyGenerator
|
||||
import org.pgpainless.algorithm.AEADCipherMode
|
||||
import org.pgpainless.algorithm.AlgorithmSuite
|
||||
import org.pgpainless.algorithm.CompressionAlgorithm
|
||||
import org.pgpainless.algorithm.Feature
|
||||
import org.pgpainless.algorithm.HashAlgorithm
|
||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||
|
||||
/**
|
||||
* Apply different algorithm preferences (features, symmetric-key-, hash-, compression- and AEAD
|
||||
* algorithm preferences to the [OpenPGPKeyGenerator] for key generation. The preferences will be
|
||||
* set on preference-signatures on the generated keys.
|
||||
*
|
||||
* @param algorithms algorithm suite
|
||||
* @return this
|
||||
*/
|
||||
fun OpenPGPKeyGenerator.setAlgorithmSuite(algorithms: AlgorithmSuite): OpenPGPKeyGenerator {
|
||||
setDefaultFeatures(true, algorithms.features)
|
||||
setDefaultSymmetricKeyPreferences(true, algorithms.symmetricKeyAlgorithms)
|
||||
setDefaultHashAlgorithmPreferences(true, algorithms.hashAlgorithms)
|
||||
setDefaultCompressionAlgorithmPreferences(true, algorithms.compressionAlgorithms)
|
||||
setDefaultAeadAlgorithmPreferences(false, algorithms.aeadAlgorithms)
|
||||
return this
|
||||
}
|
||||
|
||||
fun OpenPGPKeyGenerator.setDefaultFeatures(
|
||||
critical: Boolean = true,
|
||||
features: Set<Feature>?
|
||||
): OpenPGPKeyGenerator {
|
||||
this.setDefaultFeatures {
|
||||
val b = features?.let { f -> Feature.toBitmask(*f.toTypedArray()) } ?: 0
|
||||
it.apply { setFeature(critical, b) }
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Define [SymmetricKeyAlgorithms][SymmetricKeyAlgorithm] that will be applied as symmetric key
|
||||
* algorithm preferences to preference-signatures on freshly generated keys.
|
||||
*
|
||||
* @param critical whether to mark the preference subpacket as critical
|
||||
* @param symmetricKeyAlgorithms ordered set of preferred symmetric key algorithms
|
||||
* @return this
|
||||
*/
|
||||
fun OpenPGPKeyGenerator.setDefaultSymmetricKeyPreferences(
|
||||
critical: Boolean = true,
|
||||
symmetricKeyAlgorithms: Set<SymmetricKeyAlgorithm>?
|
||||
): OpenPGPKeyGenerator = apply {
|
||||
symmetricKeyAlgorithms?.let { algorithms ->
|
||||
this.setDefaultSymmetricKeyPreferences {
|
||||
val algorithmIds = algorithms.map { a -> a.algorithmId }.toIntArray()
|
||||
it.apply { setPreferredSymmetricAlgorithms(critical, algorithmIds) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define [HashAlgorithms][HashAlgorithm] that will be applied as hash algorithm preferences to
|
||||
* preference-signatures on freshly generated keys.
|
||||
*
|
||||
* @param critical whether to mark the preference subpacket as critical
|
||||
* @param hashAlgorithms ordered set of preferred hash algorithms
|
||||
* @return this
|
||||
*/
|
||||
fun OpenPGPKeyGenerator.setDefaultHashAlgorithmPreferences(
|
||||
critical: Boolean = true,
|
||||
hashAlgorithms: Set<HashAlgorithm>?
|
||||
): OpenPGPKeyGenerator = apply {
|
||||
hashAlgorithms?.let { algorithms ->
|
||||
this.setDefaultHashAlgorithmPreferences {
|
||||
val algorithmIds = algorithms.map { a -> a.algorithmId }.toIntArray()
|
||||
it.apply { setPreferredHashAlgorithms(critical, algorithmIds) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define [CompressionAlgorithms][CompressionAlgorithm] that will be applied as compression
|
||||
* algorithm preferences to preference-signatures on freshly generated keys.
|
||||
*
|
||||
* @param critical whether to mark the preference subpacket as critical
|
||||
* @param compressionAlgorithms ordered set of preferred compression algorithms
|
||||
* @return this
|
||||
*/
|
||||
fun OpenPGPKeyGenerator.setDefaultCompressionAlgorithmPreferences(
|
||||
critical: Boolean = true,
|
||||
compressionAlgorithms: Set<CompressionAlgorithm>?
|
||||
): OpenPGPKeyGenerator = apply {
|
||||
compressionAlgorithms?.let { algorithms ->
|
||||
this.setDefaultCompressionAlgorithmPreferences {
|
||||
val algorithmIds = algorithms.map { a -> a.algorithmId }.toIntArray()
|
||||
it.apply { setPreferredCompressionAlgorithms(critical, algorithmIds) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define [AEADCipherModes][AEADCipherMode] that will be applied as AEAD algorithm preferences to
|
||||
* preference signatures on freshly generated keys.
|
||||
*
|
||||
* @param critical whether to mark the preferences subpacket as critical
|
||||
* @param aeadAlgorithms ordered set of AEAD preferences
|
||||
* @return this
|
||||
*/
|
||||
fun OpenPGPKeyGenerator.setDefaultAeadAlgorithmPreferences(
|
||||
critical: Boolean = false,
|
||||
aeadAlgorithms: Set<AEADCipherMode>?
|
||||
): OpenPGPKeyGenerator = apply {
|
||||
aeadAlgorithms?.let { algorithms ->
|
||||
this.setDefaultAeadAlgorithmPreferences {
|
||||
val builder = PreferredAEADCiphersuites.builder(critical)
|
||||
for (ciphermode: AEADCipherMode in algorithms) {
|
||||
builder.addCombination(
|
||||
ciphermode.ciphermode.algorithmId, ciphermode.aeadAlgorithm.algorithmId)
|
||||
}
|
||||
it.apply { setPreferredAEADCiphersuites(builder) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,19 +4,45 @@
|
|||
|
||||
package org.pgpainless.bouncycastle.extensions
|
||||
|
||||
import openpgp.openPgpKeyId
|
||||
import org.bouncycastle.bcpg.KeyIdentifier
|
||||
import org.bouncycastle.openpgp.PGPKeyRing
|
||||
import org.bouncycastle.openpgp.PGPOnePassSignature
|
||||
import org.bouncycastle.openpgp.PGPPublicKey
|
||||
import org.bouncycastle.openpgp.PGPSignature
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey
|
||||
import org.bouncycastle.openpgp.api.OpenPGPImplementation
|
||||
import org.pgpainless.PGPainless
|
||||
import org.pgpainless.key.OpenPgpFingerprint
|
||||
import org.pgpainless.key.SubkeyIdentifier
|
||||
|
||||
/** Return true, if this [PGPKeyRing] contains the subkey identified by the [SubkeyIdentifier]. */
|
||||
/**
|
||||
* Return true, if this [PGPKeyRing] contains the subkey identified by the [SubkeyIdentifier].
|
||||
*
|
||||
* @param subkeyIdentifier subkey identifier
|
||||
* @return true if the [PGPKeyRing] contains the [SubkeyIdentifier]
|
||||
*/
|
||||
fun PGPKeyRing.matches(subkeyIdentifier: SubkeyIdentifier): Boolean =
|
||||
this.publicKey.keyID == subkeyIdentifier.primaryKeyId &&
|
||||
this.getPublicKey(subkeyIdentifier.subkeyId) != null
|
||||
this.publicKey.keyIdentifier.matchesExplicit(subkeyIdentifier.certificateIdentifier) &&
|
||||
this.getPublicKey(subkeyIdentifier.componentKeyIdentifier) != null
|
||||
|
||||
/**
|
||||
* Return true, if this [PGPKeyRing] contains the given [componentKey].
|
||||
*
|
||||
* @param componentKey component key
|
||||
* @return true if the [PGPKeyRing] contains the [componentKey]
|
||||
*/
|
||||
fun PGPKeyRing.matches(componentKey: OpenPGPComponentKey): Boolean =
|
||||
this.matches(SubkeyIdentifier(componentKey))
|
||||
|
||||
/**
|
||||
* Return true, if the [PGPKeyRing] contains a public key with the given [keyIdentifier].
|
||||
*
|
||||
* @param keyIdentifier KeyIdentifier
|
||||
* @return true if key with the given key-ID is present, false otherwise
|
||||
*/
|
||||
fun PGPKeyRing.hasPublicKey(keyIdentifier: KeyIdentifier): Boolean =
|
||||
this.getPublicKey(keyIdentifier) != null
|
||||
|
||||
/**
|
||||
* Return true, if the [PGPKeyRing] contains a public key with the given key-ID.
|
||||
|
@ -24,7 +50,8 @@ fun PGPKeyRing.matches(subkeyIdentifier: SubkeyIdentifier): Boolean =
|
|||
* @param keyId keyId
|
||||
* @return true if key with the given key-ID is present, false otherwise
|
||||
*/
|
||||
fun PGPKeyRing.hasPublicKey(keyId: Long): Boolean = this.getPublicKey(keyId) != null
|
||||
@Deprecated("Pass in a KeyIdentifier instead.")
|
||||
fun PGPKeyRing.hasPublicKey(keyId: Long): Boolean = hasPublicKey(KeyIdentifier(keyId))
|
||||
|
||||
/**
|
||||
* Return true, if the [PGPKeyRing] contains a public key with the given fingerprint.
|
||||
|
@ -33,7 +60,7 @@ fun PGPKeyRing.hasPublicKey(keyId: Long): Boolean = this.getPublicKey(keyId) !=
|
|||
* @return true if key with the given fingerprint is present, false otherwise
|
||||
*/
|
||||
fun PGPKeyRing.hasPublicKey(fingerprint: OpenPgpFingerprint): Boolean =
|
||||
this.getPublicKey(fingerprint) != null
|
||||
hasPublicKey(fingerprint.keyIdentifier)
|
||||
|
||||
/**
|
||||
* Return the [PGPPublicKey] with the given [OpenPgpFingerprint] or null, if no such key is present.
|
||||
|
@ -42,17 +69,41 @@ fun PGPKeyRing.hasPublicKey(fingerprint: OpenPgpFingerprint): Boolean =
|
|||
* @return public key
|
||||
*/
|
||||
fun PGPKeyRing.getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? =
|
||||
this.getPublicKey(fingerprint.bytes)
|
||||
this.getPublicKey(fingerprint.keyIdentifier)
|
||||
|
||||
fun PGPKeyRing.requirePublicKey(keyId: Long): PGPPublicKey =
|
||||
getPublicKey(keyId)
|
||||
?: throw NoSuchElementException(
|
||||
"OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.")
|
||||
/**
|
||||
* Return the [PGPPublicKey] with the given [keyIdentifier], or throw a [NoSuchElementException] if
|
||||
* no matching public key was found.
|
||||
*
|
||||
* @param keyIdentifier key identifier
|
||||
* @return public key
|
||||
* @throws NoSuchElementException if no matching public key was found
|
||||
*/
|
||||
fun PGPKeyRing.requirePublicKey(keyIdentifier: KeyIdentifier): PGPPublicKey =
|
||||
getPublicKey(keyIdentifier)
|
||||
?: throw NoSuchElementException("OpenPGP key does not contain key with id $keyIdentifier.")
|
||||
|
||||
/**
|
||||
* Return the [PGPPublicKey] with the given key-id, or throw a [NoSuchElementException] if no
|
||||
* matching public key was found.
|
||||
*
|
||||
* @param keyId key id
|
||||
* @return public key
|
||||
* @throws NoSuchElementException if no matching public key was found
|
||||
*/
|
||||
@Deprecated("Pass in a KeyIdentifier instead.")
|
||||
fun PGPKeyRing.requirePublicKey(keyId: Long): PGPPublicKey = requirePublicKey(KeyIdentifier(keyId))
|
||||
|
||||
/**
|
||||
* Return the [PGPPublicKey] with the given [fingerprint], or throw a [NoSuchElementException] if no
|
||||
* matching public key was found.
|
||||
*
|
||||
* @param fingerprint key fingerprint
|
||||
* @return public key
|
||||
* @throws NoSuchElementException if no matching public key was found
|
||||
*/
|
||||
fun PGPKeyRing.requirePublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey =
|
||||
getPublicKey(fingerprint)
|
||||
?: throw NoSuchElementException(
|
||||
"OpenPGP key does not contain key with fingerprint $fingerprint.")
|
||||
requirePublicKey(fingerprint.keyIdentifier)
|
||||
|
||||
/**
|
||||
* Return the [PGPPublicKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. If
|
||||
|
@ -60,11 +111,12 @@ fun PGPKeyRing.requirePublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey =
|
|||
* subpacket to identify the [PGPPublicKey] via its key-ID.
|
||||
*/
|
||||
fun PGPKeyRing.getPublicKeyFor(signature: PGPSignature): PGPPublicKey? =
|
||||
signature.fingerprint?.let { this.getPublicKey(it) } ?: this.getPublicKey(signature.keyID)
|
||||
signature.fingerprint?.let { this.getPublicKey(it.keyIdentifier) }
|
||||
?: this.getPublicKey(signature.keyID)
|
||||
|
||||
/** Return the [PGPPublicKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */
|
||||
fun PGPKeyRing.getPublicKeyFor(onePassSignature: PGPOnePassSignature): PGPPublicKey? =
|
||||
this.getPublicKey(onePassSignature.keyID)
|
||||
this.getPublicKey(onePassSignature.keyIdentifier)
|
||||
|
||||
/** Return the [OpenPgpFingerprint] of this OpenPGP key. */
|
||||
val PGPKeyRing.openPgpFingerprint: OpenPgpFingerprint
|
||||
|
@ -72,3 +124,22 @@ val PGPKeyRing.openPgpFingerprint: OpenPgpFingerprint
|
|||
|
||||
/** Return this OpenPGP key as an ASCII armored String. */
|
||||
fun PGPKeyRing.toAsciiArmor(): String = PGPainless.asciiArmor(this)
|
||||
|
||||
/**
|
||||
* Convert the given [PGPKeyRing] into an [OpenPGPCertificate].
|
||||
*
|
||||
* @return certificate
|
||||
*/
|
||||
@Deprecated("Use toOpenPGPCertificate(implementation) instead.")
|
||||
fun PGPKeyRing.toOpenPGPCertificate(): OpenPGPCertificate =
|
||||
toOpenPGPCertificate(PGPainless.getInstance().implementation)
|
||||
|
||||
/**
|
||||
* Convert the given [PGPKeyRing] into an [OpenPGPCertificate] using the given
|
||||
* [OpenPGPImplementation].
|
||||
*
|
||||
* @param implementation OpenPGP implementation
|
||||
* @return certificate
|
||||
*/
|
||||
fun PGPKeyRing.toOpenPGPCertificate(implementation: OpenPGPImplementation): OpenPGPCertificate =
|
||||
OpenPGPCertificate(this, implementation)
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
package org.pgpainless.bouncycastle.extensions
|
||||
|
||||
import org.bouncycastle.asn1.gnu.GNUObjectIdentifiers
|
||||
import org.bouncycastle.bcpg.ECDHPublicBCPGKey
|
||||
import org.bouncycastle.bcpg.ECDSAPublicBCPGKey
|
||||
import org.bouncycastle.bcpg.EdDSAPublicBCPGKey
|
||||
import org.bouncycastle.internal.asn1.gnu.GNUObjectIdentifiers
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil
|
||||
import org.bouncycastle.openpgp.PGPPublicKey
|
||||
import org.pgpainless.algorithm.PublicKeyAlgorithm
|
||||
|
|
|
@ -4,8 +4,16 @@
|
|||
|
||||
package org.pgpainless.bouncycastle.extensions
|
||||
|
||||
import openpgp.openPgpKeyId
|
||||
import org.bouncycastle.openpgp.*
|
||||
import org.bouncycastle.bcpg.KeyIdentifier
|
||||
import org.bouncycastle.openpgp.PGPOnePassSignature
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing
|
||||
import org.bouncycastle.openpgp.PGPSecretKey
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||
import org.bouncycastle.openpgp.PGPSignature
|
||||
import org.bouncycastle.openpgp.api.OpenPGPImplementation
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey
|
||||
import org.pgpainless.PGPainless
|
||||
import org.pgpainless.key.OpenPgpFingerprint
|
||||
|
||||
/** OpenPGP certificate containing the public keys of this OpenPGP key. */
|
||||
|
@ -18,7 +26,17 @@ val PGPSecretKeyRing.certificate: PGPPublicKeyRing
|
|||
* @param keyId keyId of the secret key
|
||||
* @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise
|
||||
*/
|
||||
fun PGPSecretKeyRing.hasSecretKey(keyId: Long): Boolean = this.getSecretKey(keyId) != null
|
||||
@Deprecated("Pass in a KeyIdentifier instead.")
|
||||
fun PGPSecretKeyRing.hasSecretKey(keyId: Long): Boolean = hasSecretKey(KeyIdentifier(keyId))
|
||||
|
||||
/**
|
||||
* Return true, if the [PGPSecretKeyRing] contains a [PGPSecretKey] with the given [keyIdentifier].
|
||||
*
|
||||
* @param keyIdentifier identifier of the secret key
|
||||
* @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise
|
||||
*/
|
||||
fun PGPSecretKeyRing.hasSecretKey(keyIdentifier: KeyIdentifier): Boolean =
|
||||
this.getSecretKey(keyIdentifier) != null
|
||||
|
||||
/**
|
||||
* Return true, if the [PGPSecretKeyRing] contains a [PGPSecretKey] with the given fingerprint.
|
||||
|
@ -27,7 +45,7 @@ fun PGPSecretKeyRing.hasSecretKey(keyId: Long): Boolean = this.getSecretKey(keyI
|
|||
* @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise
|
||||
*/
|
||||
fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean =
|
||||
this.getSecretKey(fingerprint) != null
|
||||
hasSecretKey(fingerprint.keyIdentifier)
|
||||
|
||||
/**
|
||||
* Return the [PGPSecretKey] with the given [OpenPgpFingerprint].
|
||||
|
@ -36,7 +54,7 @@ fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean =
|
|||
* @return the secret key or null
|
||||
*/
|
||||
fun PGPSecretKeyRing.getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? =
|
||||
this.getSecretKey(fingerprint.bytes)
|
||||
this.getSecretKey(fingerprint.keyIdentifier)
|
||||
|
||||
/**
|
||||
* Return the [PGPSecretKey] with the given key-ID.
|
||||
|
@ -44,10 +62,20 @@ fun PGPSecretKeyRing.getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey
|
|||
* @throws NoSuchElementException if the OpenPGP key doesn't contain a secret key with the given
|
||||
* key-ID
|
||||
*/
|
||||
@Deprecated("Pass in a KeyIdentifier instead.")
|
||||
fun PGPSecretKeyRing.requireSecretKey(keyId: Long): PGPSecretKey =
|
||||
getSecretKey(keyId)
|
||||
requireSecretKey(KeyIdentifier(keyId))
|
||||
|
||||
/**
|
||||
* Return the [PGPSecretKey] with the given [keyIdentifier].
|
||||
*
|
||||
* @throws NoSuchElementException if the OpenPGP key doesn't contain a secret key with the given
|
||||
* keyIdentifier
|
||||
*/
|
||||
fun PGPSecretKeyRing.requireSecretKey(keyIdentifier: KeyIdentifier): PGPSecretKey =
|
||||
getSecretKey(keyIdentifier)
|
||||
?: throw NoSuchElementException(
|
||||
"OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.")
|
||||
"OpenPGP key does not contain key with id ${keyIdentifier}.")
|
||||
|
||||
/**
|
||||
* Return the [PGPSecretKey] with the given fingerprint.
|
||||
|
@ -56,9 +84,7 @@ fun PGPSecretKeyRing.requireSecretKey(keyId: Long): PGPSecretKey =
|
|||
* fingerprint
|
||||
*/
|
||||
fun PGPSecretKeyRing.requireSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey =
|
||||
getSecretKey(fingerprint)
|
||||
?: throw NoSuchElementException(
|
||||
"OpenPGP key does not contain key with fingerprint $fingerprint.")
|
||||
requireSecretKey(fingerprint.keyIdentifier)
|
||||
|
||||
/**
|
||||
* Return the [PGPSecretKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. If
|
||||
|
@ -70,10 +96,32 @@ fun PGPSecretKeyRing.getSecretKeyFor(signature: PGPSignature): PGPSecretKey? =
|
|||
|
||||
/** Return the [PGPSecretKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */
|
||||
fun PGPSecretKeyRing.getSecretKeyFor(onePassSignature: PGPOnePassSignature): PGPSecretKey? =
|
||||
this.getSecretKey(onePassSignature.keyID)
|
||||
this.getSecretKey(onePassSignature.keyIdentifier)
|
||||
|
||||
/**
|
||||
* Return the [PGPSecretKey] that can be used to decrypt the given [PGPPublicKeyEncryptedData]
|
||||
* packet.
|
||||
*
|
||||
* @param pkesk public-key encrypted session-key packet
|
||||
* @return secret-key or null if no matching secret key was found
|
||||
*/
|
||||
fun PGPSecretKeyRing.getSecretKeyFor(pkesk: PGPPublicKeyEncryptedData): PGPSecretKey? =
|
||||
when (pkesk.version) {
|
||||
3 -> this.getSecretKey(pkesk.keyID)
|
||||
else -> throw NotImplementedError("Version 6 PKESKs are not yet supported.")
|
||||
}
|
||||
this.getSecretKey(pkesk.keyIdentifier)
|
||||
|
||||
/**
|
||||
* Convert the [PGPSecretKeyRing] into an [OpenPGPKey].
|
||||
*
|
||||
* @return key
|
||||
*/
|
||||
@Deprecated("Use toOpenPGPKey(implementation) instead.")
|
||||
fun PGPSecretKeyRing.toOpenPGPKey(): OpenPGPKey =
|
||||
toOpenPGPKey(PGPainless.getInstance().implementation)
|
||||
|
||||
/**
|
||||
* Convert the [PGPSecretKeyRing] into an [OpenPGPKey] using the given [OpenPGPImplementation].
|
||||
*
|
||||
* @param implementation openpgp implementation
|
||||
* @return key
|
||||
*/
|
||||
fun PGPSecretKeyRing.toOpenPGPKey(implementation: OpenPGPImplementation): OpenPGPKey =
|
||||
OpenPGPKey(this, implementation)
|
||||
|
|
|
@ -5,13 +5,16 @@
|
|||
package org.pgpainless.bouncycastle.extensions
|
||||
|
||||
import java.util.*
|
||||
import openpgp.formatUTC
|
||||
import openpgp.plusSeconds
|
||||
import org.bouncycastle.bcpg.KeyIdentifier
|
||||
import org.bouncycastle.openpgp.PGPPublicKey
|
||||
import org.bouncycastle.openpgp.PGPSignature
|
||||
import org.pgpainless.algorithm.HashAlgorithm
|
||||
import org.pgpainless.algorithm.PublicKeyAlgorithm
|
||||
import org.pgpainless.algorithm.RevocationState
|
||||
import org.pgpainless.algorithm.SignatureType
|
||||
import org.pgpainless.exception.SignatureValidationException
|
||||
import org.pgpainless.key.OpenPgpFingerprint
|
||||
import org.pgpainless.key.util.RevocationAttributes.Reason
|
||||
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil
|
||||
|
@ -54,25 +57,31 @@ val PGPSignature.issuerKeyId: Long
|
|||
}
|
||||
}
|
||||
|
||||
/** Return true, if the signature was likely issued by a key with the given fingerprint. */
|
||||
fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint): Boolean =
|
||||
this.fingerprint?.let { it.keyId == fingerprint.keyId } ?: (keyID == fingerprint.keyId)
|
||||
|
||||
/**
|
||||
* Return true, if the signature was likely issued by a key with the given fingerprint.
|
||||
*
|
||||
* @param fingerprint fingerprint bytes
|
||||
* @param fingerprint fingerprint of the key
|
||||
* @return true if signature was likely issued by the key
|
||||
*/
|
||||
@Deprecated("Discouraged in favor of method taking an OpenPgpFingerprint.")
|
||||
fun PGPSignature.wasIssuedBy(fingerprint: ByteArray): Boolean =
|
||||
try {
|
||||
wasIssuedBy(OpenPgpFingerprint.parseFromBinary(fingerprint))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Unknown fingerprint length / format
|
||||
false
|
||||
}
|
||||
fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint): Boolean =
|
||||
wasIssuedBy(fingerprint.keyIdentifier)
|
||||
|
||||
fun PGPSignature.wasIssuedBy(key: PGPPublicKey): Boolean = wasIssuedBy(OpenPgpFingerprint.of(key))
|
||||
/**
|
||||
* Return true, if the signature was likely issued by the given key.
|
||||
*
|
||||
* @param key key
|
||||
* @return true if signature was likely issued by the key
|
||||
*/
|
||||
fun PGPSignature.wasIssuedBy(key: PGPPublicKey): Boolean = wasIssuedBy(key.keyIdentifier)
|
||||
|
||||
/**
|
||||
* Return true, if the signature was likely issued by a key with the given identifier.
|
||||
*
|
||||
* @param keyIdentifier key identifier
|
||||
* @return true if signature was likely issued by the key
|
||||
*/
|
||||
fun PGPSignature.wasIssuedBy(keyIdentifier: KeyIdentifier): Boolean =
|
||||
KeyIdentifier.matches(this.keyIdentifiers, keyIdentifier, true)
|
||||
|
||||
/** Return true, if this signature is a hard revocation. */
|
||||
val PGPSignature.isHardRevocation
|
||||
|
@ -90,19 +99,57 @@ val PGPSignature.isHardRevocation
|
|||
else -> false // Not a revocation
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the signatures creation time falls into the period between [notBefore] and
|
||||
* [notAfter].
|
||||
*
|
||||
* @param notBefore lower bound. If null, do not check the lower bound
|
||||
* @param notAfter upper bound. If null, do not check the upper bound
|
||||
*/
|
||||
fun PGPSignature.assertCreatedInBounds(notBefore: Date?, notAfter: Date?) {
|
||||
if (notBefore != null && creationTime < notBefore) {
|
||||
throw SignatureValidationException(
|
||||
"Signature was made before the earliest allowed signature creation time." +
|
||||
" Created: ${creationTime.formatUTC()}," +
|
||||
" earliest allowed: ${notBefore.formatUTC()}")
|
||||
}
|
||||
if (notAfter != null && creationTime > notAfter) {
|
||||
throw SignatureValidationException(
|
||||
"Signature was made after the latest allowed signature creation time." +
|
||||
" Created: ${creationTime.formatUTC()}," +
|
||||
" latest allowed: ${notAfter.formatUTC()}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduce a [RevocationState] from the signature. Non-revocation signatures result in
|
||||
* [RevocationState.notRevoked]. Hard revocations result in [RevocationState.hardRevoked], while
|
||||
* soft revocations return [RevocationState.softRevoked]
|
||||
*
|
||||
* @return revocation state
|
||||
*/
|
||||
fun PGPSignature?.toRevocationState() =
|
||||
if (this == null) RevocationState.notRevoked()
|
||||
else if (isHardRevocation) RevocationState.hardRevoked()
|
||||
else RevocationState.softRevoked(creationTime)
|
||||
|
||||
/** The signatures issuer fingerprint as [OpenPgpFingerprint]. */
|
||||
val PGPSignature.fingerprint: OpenPgpFingerprint?
|
||||
get() = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this)
|
||||
|
||||
/** The signatures [PublicKeyAlgorithm]. */
|
||||
val PGPSignature.publicKeyAlgorithm: PublicKeyAlgorithm
|
||||
get() = PublicKeyAlgorithm.requireFromId(keyAlgorithm)
|
||||
|
||||
/** The signatures [HashAlgorithm]. */
|
||||
val PGPSignature.signatureHashAlgorithm: HashAlgorithm
|
||||
get() = HashAlgorithm.requireFromId(hashAlgorithm)
|
||||
|
||||
/**
|
||||
* Return true if the signature has the given [SignatureType].
|
||||
*
|
||||
* @param type signature type
|
||||
* @return true if the signature type matches the signatures type
|
||||
*/
|
||||
fun PGPSignature.isOfType(type: SignatureType): Boolean =
|
||||
SignatureType.fromCode(signatureType) == type
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.bouncycastle.extensions
|
||||
|
||||
import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites
|
||||
import org.pgpainless.algorithm.AEADCipherMode
|
||||
|
||||
/** Convert the [PreferredAEADCiphersuites] packet into a [Set] of [AEADCipherMode]. */
|
||||
fun PreferredAEADCiphersuites?.toAEADCipherModes(): Set<AEADCipherMode> {
|
||||
return this?.algorithms?.asSequence()?.map { AEADCipherMode(it) }?.toSet() ?: setOf()
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.bouncycastle.extensions
|
||||
|
||||
import org.bouncycastle.bcpg.sig.PreferredAlgorithms
|
||||
import org.pgpainless.algorithm.CompressionAlgorithm
|
||||
import org.pgpainless.algorithm.HashAlgorithm
|
||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||
|
||||
/** Convert the [PreferredAlgorithms] packet into a [Set] of [HashAlgorithm] preferences. */
|
||||
fun PreferredAlgorithms?.toHashAlgorithms(): Set<HashAlgorithm> {
|
||||
return this?.preferences?.asSequence()?.map { HashAlgorithm.requireFromId(it) }?.toSet()
|
||||
?: setOf()
|
||||
}
|
||||
|
||||
/** Convert the [PreferredAlgorithms] packet into a [Set] of [SymmetricKeyAlgorithm] preferences. */
|
||||
fun PreferredAlgorithms?.toSymmetricKeyAlgorithms(): Set<SymmetricKeyAlgorithm> {
|
||||
return this?.preferences?.asSequence()?.map { SymmetricKeyAlgorithm.requireFromId(it) }?.toSet()
|
||||
?: setOf()
|
||||
}
|
||||
|
||||
/** Convert the [PreferredAlgorithms] packet into a [Set] of [CompressionAlgorithm] preferences. */
|
||||
fun PreferredAlgorithms?.toCompressionAlgorithms(): Set<CompressionAlgorithm> {
|
||||
return this?.preferences?.asSequence()?.map { CompressionAlgorithm.requireFromId(it) }?.toSet()
|
||||
?: setOf()
|
||||
}
|
|
@ -46,6 +46,7 @@ class CachingBcPublicKeyDataDecryptorFactory(
|
|||
return decryptorFactory.createDataDecryptor(p0, p1)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun recoverSessionData(
|
||||
keyAlgorithm: Int,
|
||||
secKeyData: Array<out ByteArray>,
|
||||
|
|
|
@ -7,9 +7,14 @@ package org.pgpainless.decryption_verification
|
|||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import org.bouncycastle.bcpg.KeyIdentifier
|
||||
import org.bouncycastle.openpgp.*
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKeyMaterialProvider.OpenPGPCertificateProvider
|
||||
import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature
|
||||
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory
|
||||
import org.pgpainless.bouncycastle.extensions.getPublicKeyFor
|
||||
import org.pgpainless.PGPainless
|
||||
import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy
|
||||
import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy
|
||||
import org.pgpainless.key.SubkeyIdentifier
|
||||
|
@ -19,7 +24,7 @@ import org.pgpainless.util.Passphrase
|
|||
import org.pgpainless.util.SessionKey
|
||||
|
||||
/** Options for decryption and signature verification. */
|
||||
class ConsumerOptions {
|
||||
class ConsumerOptions(private val api: PGPainless) {
|
||||
|
||||
private var ignoreMDCErrors = false
|
||||
var isDisableAsciiArmorCRC = false
|
||||
|
@ -27,17 +32,18 @@ class ConsumerOptions {
|
|||
private var verifyNotBefore: Date? = null
|
||||
private var verifyNotAfter: Date? = Date()
|
||||
|
||||
private val certificates = CertificateSource()
|
||||
private val certificates = CertificateSource(api)
|
||||
private val detachedSignatures = mutableSetOf<PGPSignature>()
|
||||
private var missingCertificateCallback: MissingPublicKeyCallback? = null
|
||||
private var missingCertificateCallback: OpenPGPCertificateProvider? = null
|
||||
|
||||
private var sessionKey: SessionKey? = null
|
||||
private val customDecryptorFactories =
|
||||
mutableMapOf<SubkeyIdentifier, PublicKeyDataDecryptorFactory>()
|
||||
private val decryptionKeys = mutableMapOf<PGPSecretKeyRing, SecretKeyRingProtector>()
|
||||
private val decryptionKeys = mutableMapOf<OpenPGPKey, SecretKeyRingProtector>()
|
||||
private val decryptionPassphrases = mutableSetOf<Passphrase>()
|
||||
private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE
|
||||
private var multiPassStrategy: MultiPassStrategy = InMemoryMultiPassStrategy()
|
||||
private var allowDecryptionWithNonEncryptionKey: Boolean = false
|
||||
|
||||
/**
|
||||
* Consider signatures on the message made before the given timestamp invalid. Null means no
|
||||
|
@ -65,14 +71,26 @@ class ConsumerOptions {
|
|||
|
||||
fun getVerifyNotAfter() = verifyNotAfter
|
||||
|
||||
fun addVerificationCert(verificationCert: OpenPGPCertificate): ConsumerOptions = apply {
|
||||
this.certificates.addCertificate(verificationCert)
|
||||
}
|
||||
|
||||
fun addVerificationCerts(verificationCerts: Collection<OpenPGPCertificate>): ConsumerOptions =
|
||||
apply {
|
||||
for (cert in verificationCerts) {
|
||||
addVerificationCert(cert)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a certificate (public key ring) for signature verification.
|
||||
*
|
||||
* @param verificationCert certificate for signature verification
|
||||
* @return options
|
||||
*/
|
||||
@Deprecated("Pass OpenPGPCertificate instead.")
|
||||
fun addVerificationCert(verificationCert: PGPPublicKeyRing): ConsumerOptions = apply {
|
||||
this.certificates.addCertificate(verificationCert)
|
||||
this.certificates.addCertificate(api.toCertificate(verificationCert))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -81,10 +99,11 @@ class ConsumerOptions {
|
|||
* @param verificationCerts certificates for signature verification
|
||||
* @return options
|
||||
*/
|
||||
@Deprecated("Use of methods taking PGPPublicKeyRingCollections is discouraged.")
|
||||
fun addVerificationCerts(verificationCerts: PGPPublicKeyRingCollection): ConsumerOptions =
|
||||
apply {
|
||||
for (cert in verificationCerts) {
|
||||
addVerificationCert(cert)
|
||||
addVerificationCert(api.toCertificate(cert))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,6 +136,14 @@ class ConsumerOptions {
|
|||
}
|
||||
}
|
||||
|
||||
fun addVerificationOfDetachedSignature(signature: OpenPGPDocumentSignature): ConsumerOptions =
|
||||
apply {
|
||||
if (signature.issuerCertificate != null) {
|
||||
addVerificationCert(signature.issuerCertificate)
|
||||
}
|
||||
addVerificationOfDetachedSignature(signature.signature)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a detached signature for the signature verification process.
|
||||
*
|
||||
|
@ -137,9 +164,10 @@ class ConsumerOptions {
|
|||
* @param callback callback
|
||||
* @return options
|
||||
*/
|
||||
fun setMissingCertificateCallback(callback: MissingPublicKeyCallback): ConsumerOptions = apply {
|
||||
this.missingCertificateCallback = callback
|
||||
}
|
||||
fun setMissingCertificateCallback(callback: OpenPGPCertificateProvider): ConsumerOptions =
|
||||
apply {
|
||||
this.missingCertificateCallback = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt decryption using a session key.
|
||||
|
@ -155,52 +183,45 @@ class ConsumerOptions {
|
|||
|
||||
fun getSessionKey() = sessionKey
|
||||
|
||||
@JvmOverloads
|
||||
fun addDecryptionKey(
|
||||
key: OpenPGPKey,
|
||||
protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()
|
||||
) = apply { decryptionKeys[key] = protector }
|
||||
|
||||
/**
|
||||
* Add a key for message decryption. If the key is encrypted, the [SecretKeyRingProtector] is
|
||||
* used to decrypt it when needed.
|
||||
*
|
||||
* @param key key
|
||||
* @param keyRingProtector protector for the secret key
|
||||
* @param protector protector for the secret key
|
||||
* @return options
|
||||
*/
|
||||
@JvmOverloads
|
||||
@Deprecated("Pass OpenPGPKey instead.")
|
||||
fun addDecryptionKey(
|
||||
key: PGPSecretKeyRing,
|
||||
protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()
|
||||
) = apply { decryptionKeys[key] = protector }
|
||||
) = addDecryptionKey(api.toKey(key), protector)
|
||||
|
||||
/**
|
||||
* Add the keys in the provided key collection for message decryption.
|
||||
*
|
||||
* @param keys key collection
|
||||
* @param keyRingProtector protector for encrypted secret keys
|
||||
* @param protector protector for encrypted secret keys
|
||||
* @return options
|
||||
*/
|
||||
@JvmOverloads
|
||||
@Deprecated("Pass OpenPGPKey instances instead.")
|
||||
fun addDecryptionKeys(
|
||||
keys: PGPSecretKeyRingCollection,
|
||||
protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()
|
||||
) = apply {
|
||||
for (key in keys) {
|
||||
addDecryptionKey(key, protector)
|
||||
addDecryptionKey(api.toKey(key), protector)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a passphrase for message decryption. This passphrase will be used to try to decrypt
|
||||
* messages which were symmetrically encrypted for a passphrase.
|
||||
*
|
||||
* See
|
||||
* [Symmetrically Encrypted Data Packet](https://datatracker.ietf.org/doc/html/rfc4880#section-5.7)
|
||||
*
|
||||
* @param passphrase passphrase
|
||||
* @return options
|
||||
*/
|
||||
@Deprecated(
|
||||
"Deprecated in favor of addMessagePassphrase",
|
||||
ReplaceWith("addMessagePassphrase(passphrase)"))
|
||||
fun addDecryptionPassphrase(passphrase: Passphrase) = addMessagePassphrase(passphrase)
|
||||
|
||||
/**
|
||||
* Add a passphrase for message decryption. This passphrase will be used to try to decrypt
|
||||
* messages which were symmetrically encrypted for a passphrase.
|
||||
|
@ -240,21 +261,21 @@ class ConsumerOptions {
|
|||
*
|
||||
* @return decryption keys
|
||||
*/
|
||||
fun getDecryptionKeys() = decryptionKeys.keys.toSet()
|
||||
fun getDecryptionKeys(): Set<OpenPGPKey> = decryptionKeys.keys.toSet()
|
||||
|
||||
/**
|
||||
* Return the set of available message decryption passphrases.
|
||||
*
|
||||
* @return decryption passphrases
|
||||
*/
|
||||
fun getDecryptionPassphrases() = decryptionPassphrases.toSet()
|
||||
fun getDecryptionPassphrases(): Set<Passphrase> = decryptionPassphrases.toSet()
|
||||
|
||||
/**
|
||||
* Return an object holding available certificates for signature verification.
|
||||
*
|
||||
* @return certificate source
|
||||
*/
|
||||
fun getCertificateSource() = certificates
|
||||
fun getCertificateSource(): CertificateSource = certificates
|
||||
|
||||
/**
|
||||
* Return the callback that gets called when a certificate for signature verification is
|
||||
|
@ -262,7 +283,7 @@ class ConsumerOptions {
|
|||
*
|
||||
* @return missing public key callback
|
||||
*/
|
||||
fun getMissingCertificateCallback() = missingCertificateCallback
|
||||
fun getMissingCertificateCallback(): OpenPGPCertificateProvider? = missingCertificateCallback
|
||||
|
||||
/**
|
||||
* Return the [SecretKeyRingProtector] for the given [PGPSecretKeyRing].
|
||||
|
@ -270,7 +291,7 @@ class ConsumerOptions {
|
|||
* @param decryptionKeyRing secret key
|
||||
* @return protector for that particular secret key
|
||||
*/
|
||||
fun getSecretKeyProtector(decryptionKeyRing: PGPSecretKeyRing): SecretKeyRingProtector? {
|
||||
fun getSecretKeyProtector(decryptionKeyRing: OpenPGPKey): SecretKeyRingProtector? {
|
||||
return decryptionKeys[decryptionKeyRing]
|
||||
}
|
||||
|
||||
|
@ -306,7 +327,15 @@ class ConsumerOptions {
|
|||
this.ignoreMDCErrors = ignoreMDCErrors
|
||||
}
|
||||
|
||||
fun isIgnoreMDCErrors() = ignoreMDCErrors
|
||||
fun isIgnoreMDCErrors(): Boolean = ignoreMDCErrors
|
||||
|
||||
fun setAllowDecryptionWithMissingKeyFlags(): ConsumerOptions = apply {
|
||||
allowDecryptionWithNonEncryptionKey = true
|
||||
}
|
||||
|
||||
fun getAllowDecryptionWithNonEncryptionKey(): Boolean {
|
||||
return allowDecryptionWithNonEncryptionKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Force PGPainless to handle the data provided by the [InputStream] as non-OpenPGP data. This
|
||||
|
@ -322,7 +351,7 @@ class ConsumerOptions {
|
|||
*
|
||||
* @return true if non-OpenPGP data is forced
|
||||
*/
|
||||
fun isForceNonOpenPgpData() = forceNonOpenPgpData
|
||||
fun isForceNonOpenPgpData(): Boolean = forceNonOpenPgpData
|
||||
|
||||
/**
|
||||
* Specify the [MissingKeyPassphraseStrategy]. This strategy defines, how missing passphrases
|
||||
|
@ -377,15 +406,25 @@ class ConsumerOptions {
|
|||
* Source for OpenPGP certificates. When verifying signatures on a message, this object holds
|
||||
* available signer certificates.
|
||||
*/
|
||||
class CertificateSource {
|
||||
private val explicitCertificates: MutableSet<PGPPublicKeyRing> = mutableSetOf()
|
||||
class CertificateSource(private val api: PGPainless) {
|
||||
private val explicitCertificates: MutableSet<OpenPGPCertificate> = mutableSetOf()
|
||||
|
||||
/**
|
||||
* Add a certificate as verification cert explicitly.
|
||||
*
|
||||
* @param certificate certificate
|
||||
*/
|
||||
@Deprecated("Pass in an OpenPGPCertificate instead.")
|
||||
fun addCertificate(certificate: PGPPublicKeyRing) {
|
||||
explicitCertificates.add(api.toCertificate(certificate))
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a certificate as explicitly provided verification cert.
|
||||
*
|
||||
* @param certificate explicit verification cert
|
||||
*/
|
||||
fun addCertificate(certificate: OpenPGPCertificate) {
|
||||
explicitCertificates.add(certificate)
|
||||
}
|
||||
|
||||
|
@ -394,7 +433,7 @@ class ConsumerOptions {
|
|||
*
|
||||
* @return explicitly set verification certs
|
||||
*/
|
||||
fun getExplicitCertificates(): Set<PGPPublicKeyRing> {
|
||||
fun getExplicitCertificates(): Set<OpenPGPCertificate> {
|
||||
return explicitCertificates.toSet()
|
||||
}
|
||||
|
||||
|
@ -406,15 +445,31 @@ class ConsumerOptions {
|
|||
* @param keyId key id
|
||||
* @return certificate
|
||||
*/
|
||||
fun getCertificate(keyId: Long): PGPPublicKeyRing? {
|
||||
return explicitCertificates.firstOrNull { it.getPublicKey(keyId) != null }
|
||||
@Deprecated("Pass in a KeyIdentifier instead.")
|
||||
fun getCertificate(keyId: Long): OpenPGPCertificate? {
|
||||
return getCertificate(KeyIdentifier(keyId))
|
||||
}
|
||||
|
||||
fun getCertificate(signature: PGPSignature): PGPPublicKeyRing? =
|
||||
explicitCertificates.firstOrNull { it.getPublicKeyFor(signature) != null }
|
||||
/**
|
||||
* Return a certificate which contains a component key for the given [identifier]. This
|
||||
* method first checks all explicitly provided verification certs and if no cert is found it
|
||||
* consults the certificate stores.
|
||||
*
|
||||
* @param identifier key identifier
|
||||
* @return certificate or null if no match is found
|
||||
*/
|
||||
fun getCertificate(identifier: KeyIdentifier): OpenPGPCertificate? {
|
||||
return explicitCertificates.firstOrNull { it.getKey(identifier) != null }
|
||||
}
|
||||
|
||||
/** Find a certificate containing the issuer component key for the given [signature]. */
|
||||
fun getCertificate(signature: PGPSignature): OpenPGPCertificate? =
|
||||
explicitCertificates.firstOrNull { it.getSigningKeyFor(signature) != null }
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun get() = ConsumerOptions()
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun get(api: PGPainless = PGPainless.getInstance()) = ConsumerOptions(api)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,22 +5,24 @@
|
|||
package org.pgpainless.decryption_verification
|
||||
|
||||
import java.io.InputStream
|
||||
import org.pgpainless.PGPainless
|
||||
|
||||
/**
|
||||
* Builder class that takes an [InputStream] of ciphertext (or plaintext signed data) and combines
|
||||
* it with a configured [ConsumerOptions] object to form a [DecryptionStream] which can be used to
|
||||
* decrypt an OpenPGP message or verify signatures.
|
||||
*/
|
||||
class DecryptionBuilder : DecryptionBuilderInterface {
|
||||
class DecryptionBuilder(private val api: PGPainless) : DecryptionBuilderInterface {
|
||||
|
||||
override fun onInputStream(inputStream: InputStream): DecryptionBuilderInterface.DecryptWith {
|
||||
return DecryptWithImpl(inputStream)
|
||||
return DecryptWithImpl(inputStream, api)
|
||||
}
|
||||
|
||||
class DecryptWithImpl(val inputStream: InputStream) : DecryptionBuilderInterface.DecryptWith {
|
||||
class DecryptWithImpl(val inputStream: InputStream, val api: PGPainless) :
|
||||
DecryptionBuilderInterface.DecryptWith {
|
||||
|
||||
override fun withOptions(consumerOptions: ConsumerOptions): DecryptionStream {
|
||||
return OpenPgpMessageInputStream.create(inputStream, consumerOptions)
|
||||
return OpenPgpMessageInputStream.create(inputStream, consumerOptions, api)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ package org.pgpainless.decryption_verification
|
|||
|
||||
import kotlin.jvm.Throws
|
||||
import org.bouncycastle.bcpg.AEADEncDataPacket
|
||||
import org.bouncycastle.bcpg.KeyIdentifier
|
||||
import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket
|
||||
import org.bouncycastle.openpgp.PGPException
|
||||
import org.bouncycastle.openpgp.PGPSessionKey
|
||||
|
@ -33,12 +34,36 @@ class HardwareSecurity {
|
|||
* @return decrypted session key
|
||||
* @throws HardwareSecurityException exception
|
||||
*/
|
||||
@Deprecated("Pass in a KeyIdentifier instead of a Long keyId.")
|
||||
@Throws(HardwareSecurityException::class)
|
||||
fun decryptSessionKey(
|
||||
keyId: Long,
|
||||
keyAlgorithm: Int,
|
||||
sessionKeyData: ByteArray,
|
||||
pkeskVersion: Int
|
||||
): ByteArray =
|
||||
decryptSessionKey(KeyIdentifier(keyId), keyAlgorithm, sessionKeyData, pkeskVersion)
|
||||
|
||||
/**
|
||||
* Delegate decryption of a Public-Key-Encrypted-Session-Key (PKESK) to an external API for
|
||||
* dealing with hardware security modules such as smartcards or TPMs.
|
||||
*
|
||||
* If decryption fails for some reason, a subclass of the [HardwareSecurityException] is
|
||||
* thrown.
|
||||
*
|
||||
* @param keyIdentifier identifier of the encryption component key
|
||||
* @param keyAlgorithm algorithm
|
||||
* @param sessionKeyData encrypted session key
|
||||
* @param pkeskVersion version of the Public-Key-Encrypted-Session-Key packet (3 or 6)
|
||||
* @return decrypted session key
|
||||
* @throws HardwareSecurityException exception
|
||||
*/
|
||||
@Throws(HardwareSecurityException::class)
|
||||
fun decryptSessionKey(
|
||||
keyIdentifier: KeyIdentifier,
|
||||
keyAlgorithm: Int,
|
||||
sessionKeyData: ByteArray,
|
||||
pkeskVersion: Int
|
||||
): ByteArray
|
||||
}
|
||||
|
||||
|
@ -77,6 +102,7 @@ class HardwareSecurity {
|
|||
return factory.createDataDecryptor(seipd, sessionKey)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun recoverSessionData(
|
||||
keyAlgorithm: Int,
|
||||
secKeyData: Array<out ByteArray>,
|
||||
|
@ -84,7 +110,7 @@ class HardwareSecurity {
|
|||
): ByteArray {
|
||||
return try {
|
||||
callback.decryptSessionKey(
|
||||
subkeyIdentifier.subkeyId, keyAlgorithm, secKeyData[0], pkeskVersion)
|
||||
subkeyIdentifier.keyIdentifier, keyAlgorithm, secKeyData[0], pkeskVersion)
|
||||
} catch (e: HardwareSecurityException) {
|
||||
throw PGPException("Hardware-backed decryption failed.", e)
|
||||
}
|
||||
|
|
|
@ -9,8 +9,6 @@ import java.io.InputStream
|
|||
import org.bouncycastle.openpgp.PGPEncryptedData
|
||||
import org.bouncycastle.openpgp.PGPException
|
||||
import org.pgpainless.exception.ModificationDetectionException
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class IntegrityProtectedInputStream(
|
||||
private val inputStream: InputStream,
|
||||
|
@ -30,15 +28,9 @@ class IntegrityProtectedInputStream(
|
|||
if (encryptedData.isIntegrityProtected && !options.isIgnoreMDCErrors()) {
|
||||
try {
|
||||
if (!encryptedData.verify()) throw ModificationDetectionException()
|
||||
LOGGER.debug("Integrity Protection check passed.")
|
||||
} catch (e: PGPException) {
|
||||
throw IOException("Data appears to not be integrity protected.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
val LOGGER: Logger = LoggerFactory.getLogger(IntegrityProtectedInputStream::class.java)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,104 +6,103 @@ package org.pgpainless.decryption_verification
|
|||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import org.bouncycastle.bcpg.KeyIdentifier
|
||||
import org.bouncycastle.openpgp.*
|
||||
import org.pgpainless.implementation.ImplementationFactory
|
||||
import org.bouncycastle.openpgp.api.OpenPGPImplementation
|
||||
import org.pgpainless.PGPainless
|
||||
import org.pgpainless.util.ArmorUtils
|
||||
|
||||
/**
|
||||
* Inspect an OpenPGP message to determine IDs of its encryption keys or whether it is passphrase
|
||||
* protected.
|
||||
*/
|
||||
class MessageInspector {
|
||||
class MessageInspector(val api: PGPainless = PGPainless.getInstance()) {
|
||||
|
||||
/**
|
||||
* Info about an OpenPGP message.
|
||||
*
|
||||
* @param keyIds List of recipient key ids for whom the message is encrypted.
|
||||
* @param keyIdentifiers List of recipient [KeyIdentifiers][KeyIdentifier] for whom the message
|
||||
* is encrypted.
|
||||
* @param isPassphraseEncrypted true, if the message is encrypted for a passphrase
|
||||
* @param isSignedOnly true, if the message is not encrypted, but signed using OnePassSignatures
|
||||
*/
|
||||
data class EncryptionInfo(
|
||||
val keyIds: List<Long>,
|
||||
val keyIdentifiers: List<KeyIdentifier>,
|
||||
val isPassphraseEncrypted: Boolean,
|
||||
val isSignedOnly: Boolean
|
||||
) {
|
||||
|
||||
val isEncrypted: Boolean
|
||||
get() = isPassphraseEncrypted || keyIds.isNotEmpty()
|
||||
|
||||
val keyIds: List<Long> = keyIdentifiers.map { it.keyId }
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Parses parts of the provided OpenPGP message in order to determine which keys were used to
|
||||
* encrypt it.
|
||||
*
|
||||
* @param message OpenPGP message
|
||||
* @return encryption info
|
||||
* @throws PGPException in case the message is broken
|
||||
* @throws IOException in case of an IO error
|
||||
*/
|
||||
@Throws(PGPException::class, IOException::class)
|
||||
fun determineEncryptionInfoForMessage(message: String): EncryptionInfo =
|
||||
determineEncryptionInfoForMessage(message.byteInputStream())
|
||||
|
||||
/**
|
||||
* Parses parts of the provided OpenPGP message in order to determine which keys were used
|
||||
* to encrypt it.
|
||||
*
|
||||
* @param message OpenPGP message
|
||||
* @return encryption info
|
||||
* @throws PGPException in case the message is broken
|
||||
* @throws IOException in case of an IO error
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(PGPException::class, IOException::class)
|
||||
fun determineEncryptionInfoForMessage(message: String): EncryptionInfo =
|
||||
determineEncryptionInfoForMessage(message.byteInputStream())
|
||||
/**
|
||||
* Parses parts of the provided OpenPGP message in order to determine which keys were used to
|
||||
* encrypt it. Note: This method does not rewind the passed in Stream, so you might need to take
|
||||
* care of that yourselves.
|
||||
*
|
||||
* @param inputStream openpgp message
|
||||
* @return encryption information
|
||||
* @throws IOException in case of an IO error
|
||||
* @throws PGPException if the message is broken
|
||||
*/
|
||||
@Throws(PGPException::class, IOException::class)
|
||||
fun determineEncryptionInfoForMessage(inputStream: InputStream): EncryptionInfo {
|
||||
return processMessage(ArmorUtils.getDecoderStream(inputStream))
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses parts of the provided OpenPGP message in order to determine which keys were used
|
||||
* to encrypt it. Note: This method does not rewind the passed in Stream, so you might need
|
||||
* to take care of that yourselves.
|
||||
*
|
||||
* @param inputStream openpgp message
|
||||
* @return encryption information
|
||||
* @throws IOException in case of an IO error
|
||||
* @throws PGPException if the message is broken
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(PGPException::class, IOException::class)
|
||||
fun determineEncryptionInfoForMessage(inputStream: InputStream): EncryptionInfo {
|
||||
return processMessage(ArmorUtils.getDecoderStream(inputStream))
|
||||
}
|
||||
@Throws(PGPException::class, IOException::class)
|
||||
private fun processMessage(inputStream: InputStream): EncryptionInfo {
|
||||
var objectFactory = api.implementation.pgpObjectFactory(inputStream)
|
||||
|
||||
@JvmStatic
|
||||
@Throws(PGPException::class, IOException::class)
|
||||
private fun processMessage(inputStream: InputStream): EncryptionInfo {
|
||||
var objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(inputStream)
|
||||
|
||||
var n: Any?
|
||||
while (objectFactory.nextObject().also { n = it } != null) {
|
||||
when (val next = n!!) {
|
||||
is PGPOnePassSignatureList -> {
|
||||
if (!next.isEmpty) {
|
||||
return EncryptionInfo(
|
||||
listOf(), isPassphraseEncrypted = false, isSignedOnly = true)
|
||||
}
|
||||
}
|
||||
is PGPEncryptedDataList -> {
|
||||
var isPassphraseEncrypted = false
|
||||
val keyIds = mutableListOf<Long>()
|
||||
for (encryptedData in next) {
|
||||
if (encryptedData is PGPPublicKeyEncryptedData) {
|
||||
keyIds.add(encryptedData.keyID)
|
||||
} else if (encryptedData is PGPPBEEncryptedData) {
|
||||
isPassphraseEncrypted = true
|
||||
}
|
||||
}
|
||||
// Data is encrypted, we cannot go deeper
|
||||
return EncryptionInfo(keyIds, isPassphraseEncrypted, false)
|
||||
}
|
||||
is PGPCompressedData -> {
|
||||
objectFactory =
|
||||
ImplementationFactory.getInstance()
|
||||
.getPGPObjectFactory(PGPUtil.getDecoderStream(next.dataStream))
|
||||
continue
|
||||
}
|
||||
is PGPLiteralData -> {
|
||||
break
|
||||
var n: Any?
|
||||
while (objectFactory.nextObject().also { n = it } != null) {
|
||||
when (val next = n!!) {
|
||||
is PGPOnePassSignatureList -> {
|
||||
if (!next.isEmpty) {
|
||||
return EncryptionInfo(
|
||||
listOf(), isPassphraseEncrypted = false, isSignedOnly = true)
|
||||
}
|
||||
}
|
||||
is PGPEncryptedDataList -> {
|
||||
var isPassphraseEncrypted = false
|
||||
val keyIdentifiers = mutableListOf<KeyIdentifier>()
|
||||
for (encryptedData in next) {
|
||||
if (encryptedData is PGPPublicKeyEncryptedData) {
|
||||
keyIdentifiers.add(encryptedData.keyIdentifier)
|
||||
} else if (encryptedData is PGPPBEEncryptedData) {
|
||||
isPassphraseEncrypted = true
|
||||
}
|
||||
}
|
||||
// Data is encrypted, we cannot go deeper
|
||||
return EncryptionInfo(keyIdentifiers, isPassphraseEncrypted, false)
|
||||
}
|
||||
is PGPCompressedData -> {
|
||||
objectFactory =
|
||||
OpenPGPImplementation.getInstance()
|
||||
.pgpObjectFactory(PGPUtil.getDecoderStream(next.dataStream))
|
||||
continue
|
||||
}
|
||||
is PGPLiteralData -> {
|
||||
break
|
||||
}
|
||||
}
|
||||
return EncryptionInfo(listOf(), isPassphraseEncrypted = false, isSignedOnly = false)
|
||||
}
|
||||
return EncryptionInfo(listOf(), isPassphraseEncrypted = false, isSignedOnly = false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,11 @@ package org.pgpainless.decryption_verification
|
|||
|
||||
import java.util.*
|
||||
import javax.annotation.Nonnull
|
||||
import org.bouncycastle.bcpg.KeyIdentifier
|
||||
import org.bouncycastle.openpgp.PGPKeyRing
|
||||
import org.bouncycastle.openpgp.PGPLiteralData
|
||||
import org.bouncycastle.openpgp.api.MessageEncryptionMechanism
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate
|
||||
import org.pgpainless.algorithm.CompressionAlgorithm
|
||||
import org.pgpainless.algorithm.StreamEncoding
|
||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||
|
@ -21,35 +24,67 @@ import org.pgpainless.util.SessionKey
|
|||
/** View for extracting metadata about a [Message]. */
|
||||
class MessageMetadata(val message: Message) {
|
||||
|
||||
// ################################################################################################################
|
||||
// ### Encryption
|
||||
// ###
|
||||
// ################################################################################################################
|
||||
// ##########################################################################################################
|
||||
// Encryption
|
||||
// ##########################################################################################################
|
||||
|
||||
/**
|
||||
* The [SymmetricKeyAlgorithm] of the outermost encrypted data packet, or null if message is
|
||||
* unencrypted.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Deprecated in favor of encryptionMechanism",
|
||||
replaceWith = ReplaceWith("encryptionMechanism"))
|
||||
val encryptionAlgorithm: SymmetricKeyAlgorithm?
|
||||
get() = encryptionAlgorithms.let { if (it.hasNext()) it.next() else null }
|
||||
|
||||
/**
|
||||
* The [MessageEncryptionMechanism] of the outermost encrypted data packet, or null if the
|
||||
* message is unencrypted.
|
||||
*/
|
||||
val encryptionMechanism: MessageEncryptionMechanism?
|
||||
get() = encryptionMechanisms.let { if (it.hasNext()) it.next() else null }
|
||||
|
||||
/**
|
||||
* [Iterator] of each [SymmetricKeyAlgorithm] encountered in the message. The first item
|
||||
* returned by the iterator is the algorithm of the outermost encrypted data packet, the next
|
||||
* item that of the next nested encrypted data packet and so on. The iterator might also be
|
||||
* empty, in case of an unencrypted message.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Deprecated in favor of encryptionMechanisms",
|
||||
replaceWith = ReplaceWith("encryptionMechanisms"))
|
||||
val encryptionAlgorithms: Iterator<SymmetricKeyAlgorithm>
|
||||
get() = encryptionLayers.asSequence().map { it.algorithm }.iterator()
|
||||
|
||||
/**
|
||||
* [Iterator] of each [MessageEncryptionMechanism] encountered in the message. The first item
|
||||
* returned by the iterator is the encryption mechanism of the outermost encrypted data packet,
|
||||
* the next item that of the next nested encrypted data packet and so on. The iterator might
|
||||
* also be empty in case of an unencrypted message.
|
||||
*/
|
||||
val encryptionMechanisms: Iterator<MessageEncryptionMechanism>
|
||||
get() = encryptionLayers.asSequence().map { it.mechanism }.iterator()
|
||||
|
||||
/** Return true, if the message is encrypted, false otherwise. */
|
||||
val isEncrypted: Boolean
|
||||
get() =
|
||||
if (encryptionAlgorithm == null) false
|
||||
else encryptionAlgorithm != SymmetricKeyAlgorithm.NULL
|
||||
if (encryptionMechanism == null) false
|
||||
else
|
||||
encryptionMechanism!!.symmetricKeyAlgorithm !=
|
||||
SymmetricKeyAlgorithm.NULL.algorithmId
|
||||
|
||||
fun isEncryptedFor(keys: PGPKeyRing): Boolean {
|
||||
/** Return true, if the message was encrypted for the given [OpenPGPCertificate]. */
|
||||
fun isEncryptedFor(cert: OpenPGPCertificate): Boolean {
|
||||
return encryptionLayers.asSequence().any {
|
||||
it.recipients.any { keyId -> keys.getPublicKey(keyId) != null }
|
||||
it.recipients.any { identifier -> cert.getKey(identifier) != null }
|
||||
}
|
||||
}
|
||||
|
||||
/** Return true, if the message was encrypted for the given [PGPKeyRing]. */
|
||||
fun isEncryptedFor(cert: PGPKeyRing): Boolean {
|
||||
return encryptionLayers.asSequence().any {
|
||||
it.recipients.any { keyId -> cert.getPublicKey(keyId) != null }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,17 +113,25 @@ class MessageMetadata(val message: Message) {
|
|||
get() = encryptionLayers.asSequence().mapNotNull { it.decryptionKey }.firstOrNull()
|
||||
|
||||
/** List containing all recipient keyIDs. */
|
||||
@Deprecated(
|
||||
"Use of key-ids is discouraged in favor of KeyIdentifiers",
|
||||
replaceWith = ReplaceWith("recipientKeyIdentifiers"))
|
||||
val recipientKeyIds: List<Long>
|
||||
get() = recipientKeyIdentifiers.map { it.keyId }.toList()
|
||||
|
||||
/** List containing all recipient [KeyIdentifiers][KeyIdentifier]. */
|
||||
val recipientKeyIdentifiers: List<KeyIdentifier>
|
||||
get() =
|
||||
encryptionLayers
|
||||
.asSequence()
|
||||
.map { it.recipients.toMutableList() }
|
||||
.reduce { all, keyIds ->
|
||||
all.addAll(keyIds)
|
||||
.reduce { all, keyIdentifiers ->
|
||||
all.addAll(keyIdentifiers)
|
||||
all
|
||||
}
|
||||
.toList()
|
||||
|
||||
/** [Iterator] of all [EncryptedData] layers of the message. */
|
||||
val encryptionLayers: Iterator<EncryptedData>
|
||||
get() =
|
||||
object : LayerIterator<EncryptedData>(message) {
|
||||
|
@ -97,10 +140,9 @@ class MessageMetadata(val message: Message) {
|
|||
override fun getProperty(last: Layer) = last as EncryptedData
|
||||
}
|
||||
|
||||
// ################################################################################################################
|
||||
// ### Compression
|
||||
// ###
|
||||
// ################################################################################################################
|
||||
// ##########################################################################################################
|
||||
// Compression
|
||||
// ##########################################################################################################
|
||||
|
||||
/**
|
||||
* [CompressionAlgorithm] of the outermost compressed data packet, or null, if the message does
|
||||
|
@ -118,6 +160,7 @@ class MessageMetadata(val message: Message) {
|
|||
val compressionAlgorithms: Iterator<CompressionAlgorithm>
|
||||
get() = compressionLayers.asSequence().map { it.algorithm }.iterator()
|
||||
|
||||
/** [Iterator] of all [CompressedData] layers of the message. */
|
||||
val compressionLayers: Iterator<CompressedData>
|
||||
get() =
|
||||
object : LayerIterator<CompressedData>(message) {
|
||||
|
@ -126,10 +169,9 @@ class MessageMetadata(val message: Message) {
|
|||
override fun getProperty(last: Layer) = last as CompressedData
|
||||
}
|
||||
|
||||
// ################################################################################################################
|
||||
// ### Signatures
|
||||
// ###
|
||||
// ################################################################################################################
|
||||
// ##########################################################################################################
|
||||
// Signatures
|
||||
// ##########################################################################################################
|
||||
|
||||
val isUsingCleartextSignatureFramework: Boolean
|
||||
get() = message.cleartextSigned
|
||||
|
@ -253,7 +295,8 @@ class MessageMetadata(val message: Message) {
|
|||
email,
|
||||
it.signature.creationTime,
|
||||
targetAmount)
|
||||
.authenticated
|
||||
?.authenticated
|
||||
?: false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -270,6 +313,9 @@ class MessageMetadata(val message: Message) {
|
|||
fun isVerifiedSignedBy(keys: PGPKeyRing) =
|
||||
verifiedSignatures.any { keys.matches(it.signingKey) }
|
||||
|
||||
fun isVerifiedSignedBy(cert: OpenPGPCertificate) =
|
||||
verifiedSignatures.any { cert.pgpKeyRing.matches(it.signingKey) }
|
||||
|
||||
fun isVerifiedDetachedSignedBy(fingerprint: OpenPgpFingerprint) =
|
||||
verifiedDetachedSignatures.any { it.signingKey.matches(fingerprint) }
|
||||
|
||||
|
@ -282,18 +328,16 @@ class MessageMetadata(val message: Message) {
|
|||
fun isVerifiedInlineSignedBy(keys: PGPKeyRing) =
|
||||
verifiedInlineSignatures.any { keys.matches(it.signingKey) }
|
||||
|
||||
// ################################################################################################################
|
||||
// ### Literal Data
|
||||
// ###
|
||||
// ################################################################################################################
|
||||
// ##########################################################################################################
|
||||
// Literal Data
|
||||
// ##########################################################################################################
|
||||
|
||||
/**
|
||||
* Value of the literal data packet's filename field. This value can be used to store a
|
||||
* decrypted file under its original filename, but since this field is not necessarily part of
|
||||
* the signed data of a message, usage of this field is discouraged.
|
||||
*
|
||||
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-5.9">RFC4880 §5.9. Literal Data
|
||||
* Packet</a>
|
||||
* @see [RFC4880 §5.9. Literal Data Packet](https://www.rfc-editor.org/rfc/rfc4880#section-5.9)
|
||||
*/
|
||||
val filename: String? = findLiteralData()?.fileName
|
||||
|
||||
|
@ -310,8 +354,7 @@ class MessageMetadata(val message: Message) {
|
|||
* the modification date of a decrypted file, but since this field is not necessarily part of
|
||||
* the signed data, its use is discouraged.
|
||||
*
|
||||
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-5.9">RFC4880 §5.9. Literal Data
|
||||
* Packet</a>
|
||||
* @see [RFC4880 §5.9. Literal Data Packet](https://www.rfc-editor.org/rfc/rfc4880#section-5.9)
|
||||
*/
|
||||
val modificationDate: Date? = findLiteralData()?.modificationDate
|
||||
|
||||
|
@ -320,8 +363,7 @@ class MessageMetadata(val message: Message) {
|
|||
* binary data, ...) the data has. Since this field is not necessarily part of the signed data
|
||||
* of a message, its usage is discouraged.
|
||||
*
|
||||
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-5.9">RFC4880 §5.9. Literal Data
|
||||
* Packet</a>
|
||||
* @see [RFC4880 §5.9. Literal Data Packet](https://www.rfc-editor.org/rfc/rfc4880#section-5.9)
|
||||
*/
|
||||
val literalDataEncoding: StreamEncoding? = findLiteralData()?.format
|
||||
|
||||
|
@ -349,10 +391,9 @@ class MessageMetadata(val message: Message) {
|
|||
return nested as LiteralData
|
||||
}
|
||||
|
||||
// ################################################################################################################
|
||||
// ### Message Structure
|
||||
// ###
|
||||
// ################################################################################################################
|
||||
// ##########################################################################################################
|
||||
// Message Structure
|
||||
// ##########################################################################################################
|
||||
|
||||
interface Packet
|
||||
|
||||
|
@ -415,8 +456,8 @@ class MessageMetadata(val message: Message) {
|
|||
* Outermost OpenPGP Message structure.
|
||||
*
|
||||
* @param cleartextSigned whether the message is using the Cleartext Signature Framework
|
||||
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-7">RFC4880 §7. Cleartext
|
||||
* Signature Framework</a>
|
||||
* @see
|
||||
* [RFC4880 §7. Cleartext Signature Framework](https://www.rfc-editor.org/rfc/rfc4880#section-7)
|
||||
*/
|
||||
class Message(var cleartextSigned: Boolean = false) : Layer(0) {
|
||||
fun setCleartextSigned() = apply { cleartextSigned = true }
|
||||
|
@ -455,18 +496,24 @@ class MessageMetadata(val message: Message) {
|
|||
/**
|
||||
* Encrypted Data.
|
||||
*
|
||||
* @param algorithm symmetric key algorithm used to encrypt the packet.
|
||||
* @param mechanism mechanism used to encrypt the packet.
|
||||
* @param depth nesting depth at which this packet was encountered.
|
||||
*/
|
||||
class EncryptedData(val algorithm: SymmetricKeyAlgorithm, depth: Int) : Layer(depth), Nested {
|
||||
class EncryptedData(val mechanism: MessageEncryptionMechanism, depth: Int) :
|
||||
Layer(depth), Nested {
|
||||
|
||||
/** [SessionKey] used to decrypt the packet. */
|
||||
var sessionKey: SessionKey? = null
|
||||
|
||||
/** List of all recipient key ids to which the packet was encrypted for. */
|
||||
val recipients: List<Long> = mutableListOf()
|
||||
val recipients: List<KeyIdentifier> = mutableListOf()
|
||||
|
||||
fun addRecipients(keyIds: List<Long>) = apply { (recipients as MutableList).addAll(keyIds) }
|
||||
val algorithm: SymmetricKeyAlgorithm =
|
||||
SymmetricKeyAlgorithm.requireFromId(mechanism.symmetricKeyAlgorithm)
|
||||
|
||||
fun addRecipients(keyIds: List<KeyIdentifier>) = apply {
|
||||
(recipients as MutableList).addAll(keyIds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifier of the subkey that was used to decrypt the packet (in case of a public key
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.decryption_verification
|
||||
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing
|
||||
|
||||
fun interface MissingPublicKeyCallback {
|
||||
|
||||
/**
|
||||
* This method gets called if we encounter a signature made by a key which was not provided for
|
||||
* signature verification. If you cannot provide the requested key, it is safe to return null
|
||||
* here. PGPainless will then continue verification with the next signature.
|
||||
*
|
||||
* Note: The key-id might belong to a subkey, so be aware that when looking up the
|
||||
* [PGPPublicKeyRing], you may not only search for the key-id on the key rings primary key!
|
||||
*
|
||||
* It would be super cool to provide the OpenPgp fingerprint here, but unfortunately
|
||||
* one-pass-signatures only contain the key id.
|
||||
*
|
||||
* @param keyId ID of the missing signing (sub)key
|
||||
* @return keyring containing the key or null
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.4">RFC</a>
|
||||
*/
|
||||
fun onMissingPublicKeyEncountered(keyId: Long): PGPPublicKeyRing?
|
||||
}
|
|
@ -0,0 +1,321 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.decryption_verification
|
||||
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import org.bouncycastle.bcpg.AEADEncDataPacket
|
||||
import org.bouncycastle.bcpg.BCPGInputStream
|
||||
import org.bouncycastle.bcpg.CompressedDataPacket
|
||||
import org.bouncycastle.bcpg.LiteralDataPacket
|
||||
import org.bouncycastle.bcpg.MarkerPacket
|
||||
import org.bouncycastle.bcpg.OnePassSignaturePacket
|
||||
import org.bouncycastle.bcpg.PacketFormat
|
||||
import org.bouncycastle.bcpg.PacketTags.AEAD_ENC_DATA
|
||||
import org.bouncycastle.bcpg.PacketTags.COMPRESSED_DATA
|
||||
import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_1
|
||||
import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_2
|
||||
import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_3
|
||||
import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_4
|
||||
import org.bouncycastle.bcpg.PacketTags.LITERAL_DATA
|
||||
import org.bouncycastle.bcpg.PacketTags.MARKER
|
||||
import org.bouncycastle.bcpg.PacketTags.MOD_DETECTION_CODE
|
||||
import org.bouncycastle.bcpg.PacketTags.ONE_PASS_SIGNATURE
|
||||
import org.bouncycastle.bcpg.PacketTags.PADDING
|
||||
import org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY
|
||||
import org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY_ENC_SESSION
|
||||
import org.bouncycastle.bcpg.PacketTags.PUBLIC_SUBKEY
|
||||
import org.bouncycastle.bcpg.PacketTags.RESERVED
|
||||
import org.bouncycastle.bcpg.PacketTags.SECRET_KEY
|
||||
import org.bouncycastle.bcpg.PacketTags.SECRET_SUBKEY
|
||||
import org.bouncycastle.bcpg.PacketTags.SIGNATURE
|
||||
import org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC
|
||||
import org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC_SESSION
|
||||
import org.bouncycastle.bcpg.PacketTags.SYM_ENC_INTEGRITY_PRO
|
||||
import org.bouncycastle.bcpg.PacketTags.TRUST
|
||||
import org.bouncycastle.bcpg.PacketTags.USER_ATTRIBUTE
|
||||
import org.bouncycastle.bcpg.PacketTags.USER_ID
|
||||
import org.bouncycastle.bcpg.PublicKeyEncSessionPacket
|
||||
import org.bouncycastle.bcpg.PublicKeyPacket
|
||||
import org.bouncycastle.bcpg.SecretKeyPacket
|
||||
import org.bouncycastle.bcpg.SignaturePacket
|
||||
import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket
|
||||
import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket
|
||||
import org.bouncycastle.util.Arrays
|
||||
import org.pgpainless.algorithm.AEADAlgorithm
|
||||
import org.pgpainless.algorithm.CompressionAlgorithm
|
||||
import org.pgpainless.algorithm.HashAlgorithm
|
||||
import org.pgpainless.algorithm.PublicKeyAlgorithm
|
||||
import org.pgpainless.algorithm.SignatureType
|
||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||
|
||||
/**
|
||||
* InputStream used to determine the nature of potential OpenPGP data.
|
||||
*
|
||||
* @param input underlying input stream
|
||||
* @param check whether to perform the costly checking inside the constructor
|
||||
*/
|
||||
class OpenPGPAnimalSnifferInputStream(input: InputStream, check: Boolean) :
|
||||
BufferedInputStream(input) {
|
||||
|
||||
private val buffer: ByteArray
|
||||
private val bufferLen: Int
|
||||
|
||||
private var containsArmorHeader: Boolean = false
|
||||
private var containsOpenPgpPackets: Boolean = false
|
||||
private var resemblesMessage: Boolean = false
|
||||
|
||||
init {
|
||||
mark(MAX_BUFFER_SIZE)
|
||||
buffer = ByteArray(MAX_BUFFER_SIZE)
|
||||
bufferLen = read(buffer)
|
||||
reset()
|
||||
|
||||
if (check) {
|
||||
inspectBuffer()
|
||||
}
|
||||
}
|
||||
|
||||
constructor(input: InputStream) : this(input, true)
|
||||
|
||||
/** Return true, if the underlying data is ASCII armored. */
|
||||
val isAsciiArmored: Boolean
|
||||
get() = containsArmorHeader
|
||||
|
||||
/**
|
||||
* Return true, if the data is possibly binary OpenPGP. The criterion for this are less strict
|
||||
* than for [resemblesMessage], as it also accepts other OpenPGP packets at the beginning of the
|
||||
* data stream.
|
||||
*
|
||||
* <p>
|
||||
* Use with caution.
|
||||
*
|
||||
* @return true if data appears to be binary OpenPGP data
|
||||
*/
|
||||
val isBinaryOpenPgp: Boolean
|
||||
get() = containsOpenPgpPackets
|
||||
|
||||
/**
|
||||
* Returns true, if the underlying data is very likely (more than 99,9%) an OpenPGP message.
|
||||
* OpenPGP Message means here that it starts with either a [PGPEncryptedData],
|
||||
* [PGPCompressedData], [PGPOnePassSignature] or [PGPLiteralData] packet. The plausibility of
|
||||
* these data packets is checked as far as possible.
|
||||
*
|
||||
* @return true if likely OpenPGP message
|
||||
*/
|
||||
val isLikelyOpenPgpMessage: Boolean
|
||||
get() = resemblesMessage
|
||||
|
||||
/** Return true, if the underlying data is non-OpenPGP data. */
|
||||
val isNonOpenPgp: Boolean
|
||||
get() = !isAsciiArmored && !isBinaryOpenPgp
|
||||
|
||||
/** Costly perform a plausibility check of the first encountered OpenPGP packet. */
|
||||
fun inspectBuffer() {
|
||||
if (checkForAsciiArmor()) {
|
||||
return
|
||||
}
|
||||
|
||||
checkForBinaryOpenPgp()
|
||||
}
|
||||
|
||||
private fun checkForAsciiArmor(): Boolean {
|
||||
if (startsWithIgnoringWhitespace(buffer, ARMOR_HEADER, bufferLen)) {
|
||||
containsArmorHeader = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is still brittle. Basically we try to parse OpenPGP packets from the buffer. If
|
||||
* we run into exceptions, then we know that the data is non-OpenPGP'ish.
|
||||
*
|
||||
* <p>
|
||||
* This breaks down though if we read plausible garbage where the data accidentally makes sense,
|
||||
* or valid, yet incomplete packets (remember, we are still only working on a portion of the
|
||||
* data).
|
||||
*/
|
||||
private fun checkForBinaryOpenPgp() {
|
||||
if (bufferLen == -1) {
|
||||
// empty data
|
||||
return
|
||||
}
|
||||
|
||||
val bufferIn = ByteArrayInputStream(buffer, 0, bufferLen)
|
||||
val pIn = BCPGInputStream(bufferIn)
|
||||
try {
|
||||
nonExhaustiveParseAndCheckPlausibility(pIn)
|
||||
} catch (e: Exception) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private fun nonExhaustiveParseAndCheckPlausibility(packetIn: BCPGInputStream) {
|
||||
val packet = packetIn.readPacket()
|
||||
when (packet.packetTag) {
|
||||
PUBLIC_KEY_ENC_SESSION -> {
|
||||
packet as PublicKeyEncSessionPacket
|
||||
if (PublicKeyAlgorithm.fromId(packet.algorithm) == null) {
|
||||
return
|
||||
}
|
||||
}
|
||||
SIGNATURE -> {
|
||||
packet as SignaturePacket
|
||||
if (SignatureType.fromCode(packet.signatureType) == null) {
|
||||
return
|
||||
}
|
||||
if (PublicKeyAlgorithm.fromId(packet.keyAlgorithm) == null) {
|
||||
return
|
||||
}
|
||||
if (HashAlgorithm.fromId(packet.hashAlgorithm) == null) {
|
||||
return
|
||||
}
|
||||
}
|
||||
ONE_PASS_SIGNATURE -> {
|
||||
packet as OnePassSignaturePacket
|
||||
if (SignatureType.fromCode(packet.signatureType) == null) {
|
||||
return
|
||||
}
|
||||
if (PublicKeyAlgorithm.fromId(packet.keyAlgorithm) == null) {
|
||||
return
|
||||
}
|
||||
if (HashAlgorithm.fromId(packet.hashAlgorithm) == null) {
|
||||
return
|
||||
}
|
||||
}
|
||||
SYMMETRIC_KEY_ENC_SESSION -> {
|
||||
packet as SymmetricKeyEncSessionPacket
|
||||
if (SymmetricKeyAlgorithm.fromId(packet.encAlgorithm) == null) {
|
||||
return
|
||||
}
|
||||
}
|
||||
SECRET_KEY -> {
|
||||
packet as SecretKeyPacket
|
||||
val publicKey = packet.publicKeyPacket
|
||||
if (PublicKeyAlgorithm.fromId(publicKey.algorithm) == null) {
|
||||
return
|
||||
}
|
||||
if (publicKey.version !in 3..6) {
|
||||
return
|
||||
}
|
||||
}
|
||||
PUBLIC_KEY -> {
|
||||
packet as PublicKeyPacket
|
||||
if (PublicKeyAlgorithm.fromId(packet.algorithm) == null) {
|
||||
return
|
||||
}
|
||||
if (packet.version !in 3..6) {
|
||||
return
|
||||
}
|
||||
}
|
||||
COMPRESSED_DATA -> {
|
||||
packet as CompressedDataPacket
|
||||
if (CompressionAlgorithm.fromId(packet.algorithm) == null) {
|
||||
return
|
||||
}
|
||||
}
|
||||
SYMMETRIC_KEY_ENC -> {
|
||||
// Not much we can check here
|
||||
}
|
||||
MARKER -> {
|
||||
packet as MarkerPacket
|
||||
if (!Arrays.areEqual(
|
||||
packet.getEncoded(PacketFormat.CURRENT),
|
||||
byteArrayOf(0xca.toByte(), 0x03, 0x50, 0x47, 0x50),
|
||||
)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
LITERAL_DATA -> {
|
||||
packet as LiteralDataPacket
|
||||
if (packet.format.toChar() !in charArrayOf('b', 'u', 't', 'l', '1', 'm')) {
|
||||
return
|
||||
}
|
||||
}
|
||||
SYM_ENC_INTEGRITY_PRO -> {
|
||||
packet as SymmetricEncIntegrityPacket
|
||||
if (packet.version !in
|
||||
intArrayOf(
|
||||
SymmetricEncIntegrityPacket.VERSION_1,
|
||||
SymmetricEncIntegrityPacket.VERSION_2)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (packet.version == SymmetricEncIntegrityPacket.VERSION_2) {
|
||||
if (SymmetricKeyAlgorithm.fromId(packet.cipherAlgorithm) == null) {
|
||||
return
|
||||
}
|
||||
if (AEADAlgorithm.fromId(packet.aeadAlgorithm) == null) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
AEAD_ENC_DATA -> {
|
||||
packet as AEADEncDataPacket
|
||||
if (SymmetricKeyAlgorithm.fromId(packet.algorithm.toInt()) == null) {
|
||||
return
|
||||
}
|
||||
}
|
||||
RESERVED, // this Packet Type ID MUST NOT be used
|
||||
PUBLIC_SUBKEY, // Never found at the start of a stream
|
||||
SECRET_SUBKEY, // Never found at the start of a stream
|
||||
TRUST, // Never found at the start of a stream
|
||||
MOD_DETECTION_CODE, // At the end of SED data - Never found at the start of a stream
|
||||
USER_ID, // Never found at the start of a stream
|
||||
USER_ATTRIBUTE, // Never found at the start of a stream
|
||||
PADDING, // At the end of messages (optionally padded message) or certificates
|
||||
EXPERIMENTAL_1, // experimental
|
||||
EXPERIMENTAL_2, // experimental
|
||||
EXPERIMENTAL_3, // experimental
|
||||
EXPERIMENTAL_4 -> { // experimental
|
||||
containsOpenPgpPackets = true
|
||||
resemblesMessage = false
|
||||
return
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
|
||||
containsOpenPgpPackets = true
|
||||
if (packet.packetTag != SYMMETRIC_KEY_ENC) {
|
||||
resemblesMessage = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun startsWithIgnoringWhitespace(
|
||||
bytes: ByteArray,
|
||||
subSequence: CharSequence,
|
||||
bufferLen: Int
|
||||
): Boolean {
|
||||
if (bufferLen == -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (i in 0 until bufferLen) {
|
||||
// Working on bytes is not trivial with unicode data, but its good enough here
|
||||
if (Character.isWhitespace(bytes[i].toInt())) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ((i + subSequence.length) > bytes.size) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (j in subSequence.indices) {
|
||||
if (bytes[i + j].toInt().toChar() != subSequence[j]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ARMOR_HEADER = "-----BEGIN PGP "
|
||||
const val MAX_BUFFER_SIZE = 8192 * 2
|
||||
}
|
||||
}
|
|
@ -11,22 +11,32 @@ import java.io.OutputStream
|
|||
import java.util.zip.Inflater
|
||||
import java.util.zip.InflaterInputStream
|
||||
import openpgp.openPgpKeyId
|
||||
import org.bouncycastle.bcpg.AEADEncDataPacket
|
||||
import org.bouncycastle.bcpg.BCPGInputStream
|
||||
import org.bouncycastle.bcpg.CompressionAlgorithmTags
|
||||
import org.bouncycastle.bcpg.KeyIdentifier
|
||||
import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket
|
||||
import org.bouncycastle.bcpg.UnsupportedPacketVersionException
|
||||
import org.bouncycastle.openpgp.PGPCompressedData
|
||||
import org.bouncycastle.openpgp.PGPEncryptedData
|
||||
import org.bouncycastle.openpgp.PGPEncryptedDataList
|
||||
import org.bouncycastle.openpgp.PGPException
|
||||
import org.bouncycastle.openpgp.PGPKeyPair
|
||||
import org.bouncycastle.openpgp.PGPOnePassSignature
|
||||
import org.bouncycastle.openpgp.PGPPBEEncryptedData
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey
|
||||
import org.bouncycastle.openpgp.PGPPublicKey
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing
|
||||
import org.bouncycastle.openpgp.PGPSecretKey
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||
import org.bouncycastle.openpgp.PGPSessionKey
|
||||
import org.bouncycastle.openpgp.PGPSignature
|
||||
import org.bouncycastle.openpgp.PGPSignatureException
|
||||
import org.bouncycastle.openpgp.api.EncryptedDataPacketType
|
||||
import org.bouncycastle.openpgp.api.MessageEncryptionMechanism
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPPrivateKey
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPSecretKey
|
||||
import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature
|
||||
import org.bouncycastle.openpgp.api.exception.MalformedOpenPGPSignatureException
|
||||
import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory
|
||||
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory
|
||||
import org.bouncycastle.util.io.TeeInputStream
|
||||
|
@ -35,10 +45,10 @@ import org.pgpainless.algorithm.CompressionAlgorithm
|
|||
import org.pgpainless.algorithm.OpenPgpPacket
|
||||
import org.pgpainless.algorithm.StreamEncoding
|
||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||
import org.pgpainless.bouncycastle.extensions.getPublicKeyFor
|
||||
import org.pgpainless.bouncycastle.extensions.assertCreatedInBounds
|
||||
import org.pgpainless.bouncycastle.extensions.getSecretKeyFor
|
||||
import org.pgpainless.bouncycastle.extensions.getSigningKeyFor
|
||||
import org.pgpainless.bouncycastle.extensions.issuerKeyId
|
||||
import org.pgpainless.bouncycastle.extensions.unlock
|
||||
import org.pgpainless.decryption_verification.MessageMetadata.CompressedData
|
||||
import org.pgpainless.decryption_verification.MessageMetadata.EncryptedData
|
||||
import org.pgpainless.decryption_verification.MessageMetadata.Layer
|
||||
|
@ -55,14 +65,10 @@ import org.pgpainless.exception.MissingDecryptionMethodException
|
|||
import org.pgpainless.exception.MissingPassphraseException
|
||||
import org.pgpainless.exception.SignatureValidationException
|
||||
import org.pgpainless.exception.UnacceptableAlgorithmException
|
||||
import org.pgpainless.implementation.ImplementationFactory
|
||||
import org.pgpainless.exception.WrongPassphraseException
|
||||
import org.pgpainless.key.SubkeyIdentifier
|
||||
import org.pgpainless.key.util.KeyRingUtils
|
||||
import org.pgpainless.policy.Policy
|
||||
import org.pgpainless.signature.consumer.CertificateValidator
|
||||
import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey
|
||||
import org.pgpainless.signature.consumer.OnePassSignatureCheck
|
||||
import org.pgpainless.signature.consumer.SignatureCheck
|
||||
import org.pgpainless.signature.consumer.SignatureValidator
|
||||
import org.pgpainless.util.ArmoredInputStreamFactory
|
||||
import org.pgpainless.util.SessionKey
|
||||
import org.slf4j.LoggerFactory
|
||||
|
@ -72,10 +78,10 @@ class OpenPgpMessageInputStream(
|
|||
inputStream: InputStream,
|
||||
private val options: ConsumerOptions,
|
||||
private val layerMetadata: Layer,
|
||||
private val policy: Policy
|
||||
private val api: PGPainless
|
||||
) : DecryptionStream() {
|
||||
|
||||
private val signatures: Signatures = Signatures(options)
|
||||
private val signatures: Signatures = Signatures(options, api)
|
||||
private var packetInputStream: TeeBCPGInputStream? = null
|
||||
private var nestedInputStream: InputStream? = null
|
||||
private val syntaxVerifier = PDA()
|
||||
|
@ -129,8 +135,8 @@ class OpenPgpMessageInputStream(
|
|||
inputStream: InputStream,
|
||||
options: ConsumerOptions,
|
||||
metadata: Layer,
|
||||
policy: Policy
|
||||
) : this(Type.standard, inputStream, options, metadata, policy)
|
||||
api: PGPainless
|
||||
) : this(Type.standard, inputStream, options, metadata, api)
|
||||
|
||||
private fun consumePackets() {
|
||||
val pIn = packetInputStream ?: return
|
||||
|
@ -176,7 +182,7 @@ class OpenPgpMessageInputStream(
|
|||
}
|
||||
OpenPgpPacket.PADDING -> {
|
||||
LOGGER.debug("Skipping Padding Packet")
|
||||
pIn.readPacket()
|
||||
pIn.readPadding()
|
||||
}
|
||||
OpenPgpPacket.SK,
|
||||
OpenPgpPacket.PK,
|
||||
|
@ -186,10 +192,6 @@ class OpenPgpMessageInputStream(
|
|||
OpenPgpPacket.UID,
|
||||
OpenPgpPacket.UATTR ->
|
||||
throw MalformedOpenPgpMessageException("Illegal Packet in Stream: $packet")
|
||||
OpenPgpPacket.PADDING -> {
|
||||
LOGGER.debug("Padding packet")
|
||||
pIn.readPadding()
|
||||
}
|
||||
OpenPgpPacket.EXP_1,
|
||||
OpenPgpPacket.EXP_2,
|
||||
OpenPgpPacket.EXP_3,
|
||||
|
@ -230,7 +232,7 @@ class OpenPgpMessageInputStream(
|
|||
LOGGER.debug(
|
||||
"Compressed Data Packet (${compressionLayer.algorithm}) at depth ${layerMetadata.depth} encountered.")
|
||||
nestedInputStream =
|
||||
OpenPgpMessageInputStream(decompress(compressedData), options, compressionLayer, policy)
|
||||
OpenPgpMessageInputStream(decompress(compressedData), options, compressionLayer, api)
|
||||
}
|
||||
|
||||
private fun decompress(compressedData: PGPCompressedData): InputStream {
|
||||
|
@ -311,7 +313,7 @@ class OpenPgpMessageInputStream(
|
|||
signatures
|
||||
.leaveNesting() // TODO: Only leave nesting if all OPSs of the nesting layer are
|
||||
// dealt with
|
||||
signatures.addCorrespondingOnePassSignature(signature, layerMetadata, policy)
|
||||
signatures.addCorrespondingOnePassSignature(signature, layerMetadata)
|
||||
} else {
|
||||
LOGGER.debug(
|
||||
"Prepended Signature Packet by key ${keyId.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.")
|
||||
|
@ -320,20 +322,38 @@ class OpenPgpMessageInputStream(
|
|||
}
|
||||
|
||||
private fun processEncryptedData(): Boolean {
|
||||
LOGGER.debug(
|
||||
"Symmetrically Encrypted Data Packet at depth ${layerMetadata.depth} encountered.")
|
||||
// TODO: Replace by dedicated encryption packet type input symbols
|
||||
syntaxVerifier.next(InputSymbol.ENCRYPTED_DATA)
|
||||
|
||||
val encDataList = packetInputStream!!.readEncryptedDataList()
|
||||
if (!encDataList.isIntegrityProtected && !encDataList.get(0).isAEAD) {
|
||||
LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected.")
|
||||
if (!options.isIgnoreMDCErrors()) {
|
||||
throw MessageNotIntegrityProtectedException()
|
||||
val esks = ESKsAndData(encDataList)
|
||||
|
||||
when (EncryptedDataPacketType.of(encDataList)!!) {
|
||||
EncryptedDataPacketType.SEIPDv2 ->
|
||||
LOGGER.debug(
|
||||
"Symmetrically Encrypted Integrity Protected Data Packet version 2 at depth " +
|
||||
"${layerMetadata.depth} encountered.")
|
||||
EncryptedDataPacketType.SEIPDv1 ->
|
||||
LOGGER.debug(
|
||||
"Symmetrically Encrypted Integrity Protected Data Packet version 1 at depth " +
|
||||
"${layerMetadata.depth} encountered.")
|
||||
EncryptedDataPacketType.LIBREPGP_OED ->
|
||||
LOGGER.debug(
|
||||
"LibrePGP OCB-Encrypted Data Packet at depth " +
|
||||
"${layerMetadata.depth} encountered.")
|
||||
EncryptedDataPacketType.SED -> {
|
||||
LOGGER.debug(
|
||||
"(Deprecated) Symmetrically Encrypted Data Packet at depth " +
|
||||
"${layerMetadata.depth} encountered.")
|
||||
LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected.")
|
||||
if (!options.isIgnoreMDCErrors()) {
|
||||
throw MessageNotIntegrityProtectedException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val esks = SortedESKs(encDataList)
|
||||
LOGGER.debug(
|
||||
"Symmetrically Encrypted Integrity-Protected Data has ${esks.skesks.size} SKESK(s) and" +
|
||||
"Encrypted Data has ${esks.skesks.size} SKESK(s) and" +
|
||||
" ${esks.pkesks.size + esks.anonPkesks.size} PKESK(s) from which ${esks.anonPkesks.size} PKESK(s)" +
|
||||
" have an anonymous recipient.")
|
||||
|
||||
|
@ -343,7 +363,7 @@ class OpenPgpMessageInputStream(
|
|||
esks.pkesks
|
||||
.filter {
|
||||
// find matching PKESK
|
||||
it.keyID == key.subkeyId
|
||||
it.keyIdentifier == key.keyIdentifier
|
||||
}
|
||||
.forEach {
|
||||
// attempt decryption
|
||||
|
@ -359,9 +379,9 @@ class OpenPgpMessageInputStream(
|
|||
LOGGER.debug("Attempt decryption with provided session key.")
|
||||
throwIfUnacceptable(sk.algorithm)
|
||||
|
||||
val decryptorFactory =
|
||||
ImplementationFactory.getInstance().getSessionKeyDataDecryptorFactory(sk)
|
||||
val layer = EncryptedData(sk.algorithm, layerMetadata.depth + 1)
|
||||
val pgpSk = PGPSessionKey(sk.algorithm.algorithmId, sk.key)
|
||||
val decryptorFactory = api.implementation.sessionKeyDataDecryptorFactory(pgpSk)
|
||||
val layer = esks.toEncryptedData(sk, layerMetadata.depth + 1)
|
||||
val skEncData = encDataList.extractSessionKeyEncryptedData()
|
||||
try {
|
||||
val decrypted = skEncData.getDataStream(decryptorFactory)
|
||||
|
@ -369,7 +389,7 @@ class OpenPgpMessageInputStream(
|
|||
val integrityProtected =
|
||||
IntegrityProtectedInputStream(decrypted, skEncData, options)
|
||||
nestedInputStream =
|
||||
OpenPgpMessageInputStream(integrityProtected, options, layer, policy)
|
||||
OpenPgpMessageInputStream(integrityProtected, options, layer, api)
|
||||
LOGGER.debug("Successfully decrypted data using provided session key")
|
||||
return true
|
||||
} catch (e: PGPException) {
|
||||
|
@ -392,7 +412,7 @@ class OpenPgpMessageInputStream(
|
|||
}
|
||||
|
||||
val decryptorFactory =
|
||||
ImplementationFactory.getInstance().getPBEDataDecryptorFactory(passphrase)
|
||||
api.implementation.pbeDataDecryptorFactory(passphrase.getChars())
|
||||
if (decryptSKESKAndStream(esks, skesk, decryptorFactory)) {
|
||||
return true
|
||||
}
|
||||
|
@ -400,30 +420,45 @@ class OpenPgpMessageInputStream(
|
|||
}
|
||||
|
||||
val postponedDueToMissingPassphrase =
|
||||
mutableListOf<Pair<PGPSecretKey, PGPPublicKeyEncryptedData>>()
|
||||
mutableListOf<Pair<OpenPGPSecretKey, PGPPublicKeyEncryptedData>>()
|
||||
|
||||
// try (known) secret keys
|
||||
esks.pkesks.forEach { pkesk ->
|
||||
LOGGER.debug("Encountered PKESK for recipient ${pkesk.keyID.openPgpKeyId()}")
|
||||
LOGGER.debug("Encountered PKESK for recipient ${pkesk.keyIdentifier}")
|
||||
val decryptionKeyCandidates = getDecryptionKeys(pkesk)
|
||||
for (decryptionKeys in decryptionKeyCandidates) {
|
||||
val secretKey = decryptionKeys.getSecretKeyFor(pkesk)!!
|
||||
val decryptionKeyId = SubkeyIdentifier(decryptionKeys, secretKey.keyID)
|
||||
if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) {
|
||||
if (!secretKey.isEncryptionKey &&
|
||||
!options.getAllowDecryptionWithNonEncryptionKey()) {
|
||||
LOGGER.debug(
|
||||
"Message is encrypted for ${secretKey.keyIdentifier}, but the key is not encryption capable.")
|
||||
continue
|
||||
}
|
||||
if (hasUnsupportedS2KSpecifier(secretKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
LOGGER.debug("Attempt decryption using secret key $decryptionKeyId")
|
||||
LOGGER.debug("Attempt decryption using secret key ${decryptionKeys.keyIdentifier}")
|
||||
val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue
|
||||
if (!protector.hasPassphraseFor(secretKey.keyID)) {
|
||||
if (!protector.hasPassphraseFor(secretKey.keyIdentifier)) {
|
||||
LOGGER.debug(
|
||||
"Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.")
|
||||
"Missing passphrase for key ${decryptionKeys.keyIdentifier}. Postponing decryption until all other keys were tried.")
|
||||
postponedDueToMissingPassphrase.add(secretKey to pkesk)
|
||||
continue
|
||||
}
|
||||
|
||||
val privateKey = secretKey.unlock(protector)
|
||||
if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) {
|
||||
val privateKey =
|
||||
try {
|
||||
unlockSecretKey(secretKey, protector)
|
||||
} catch (e: PGPException) {
|
||||
throw WrongPassphraseException(secretKey.keyIdentifier, e)
|
||||
}
|
||||
if (decryptWithPrivateKey(
|
||||
esks,
|
||||
privateKey.keyPair,
|
||||
SubkeyIdentifier(
|
||||
secretKey.openPGPKey.pgpSecretKeyRing, secretKey.keyIdentifier),
|
||||
pkesk)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -431,24 +466,24 @@ class OpenPgpMessageInputStream(
|
|||
|
||||
// try anonymous secret keys
|
||||
for (pkesk in esks.anonPkesks) {
|
||||
for ((decryptionKeys, secretKey) in findPotentialDecryptionKeys(pkesk)) {
|
||||
val decryptionKeyId = SubkeyIdentifier(decryptionKeys, secretKey.keyID)
|
||||
if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) {
|
||||
for (decryptionKey in findPotentialDecryptionKeys(pkesk)) {
|
||||
if (hasUnsupportedS2KSpecifier(decryptionKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
LOGGER.debug("Attempt decryption of anonymous PKESK with key $decryptionKeyId.")
|
||||
val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue
|
||||
LOGGER.debug("Attempt decryption of anonymous PKESK with key $decryptionKey.")
|
||||
val protector = options.getSecretKeyProtector(decryptionKey.openPGPKey) ?: continue
|
||||
|
||||
if (!protector.hasPassphraseFor(secretKey.keyID)) {
|
||||
if (!protector.hasPassphraseFor(decryptionKey.keyIdentifier)) {
|
||||
LOGGER.debug(
|
||||
"Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.")
|
||||
postponedDueToMissingPassphrase.add(secretKey to pkesk)
|
||||
"Missing passphrase for key ${decryptionKey.keyIdentifier}. Postponing decryption until all other keys were tried.")
|
||||
postponedDueToMissingPassphrase.add(decryptionKey to pkesk)
|
||||
continue
|
||||
}
|
||||
|
||||
val privateKey = secretKey.unlock(protector)
|
||||
if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) {
|
||||
val privateKey = decryptionKey.unlock(protector)
|
||||
if (decryptWithPrivateKey(
|
||||
esks, privateKey.keyPair, SubkeyIdentifier(decryptionKey), pkesk)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -458,23 +493,28 @@ class OpenPgpMessageInputStream(
|
|||
MissingKeyPassphraseStrategy.THROW_EXCEPTION) {
|
||||
// Non-interactive mode: Throw an exception with all locked decryption keys
|
||||
postponedDueToMissingPassphrase
|
||||
.map { SubkeyIdentifier(getDecryptionKey(it.first.keyID)!!, it.first.keyID) }
|
||||
.map { SubkeyIdentifier(it.first) }
|
||||
.also { if (it.isNotEmpty()) throw MissingPassphraseException(it.toSet()) }
|
||||
} else if (options.getMissingKeyPassphraseStrategy() ==
|
||||
MissingKeyPassphraseStrategy.INTERACTIVE) {
|
||||
for ((secretKey, pkesk) in postponedDueToMissingPassphrase) {
|
||||
val keyId = secretKey.keyID
|
||||
val keyId = secretKey.keyIdentifier
|
||||
val decryptionKeys = getDecryptionKey(pkesk)!!
|
||||
val decryptionKeyId = SubkeyIdentifier(decryptionKeys, keyId)
|
||||
if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) {
|
||||
val decryptionKeyId = SubkeyIdentifier(decryptionKeys.pgpSecretKeyRing, keyId)
|
||||
if (hasUnsupportedS2KSpecifier(secretKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
LOGGER.debug(
|
||||
"Attempt decryption with key $decryptionKeyId while interactively requesting its passphrase.")
|
||||
val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue
|
||||
val privateKey = secretKey.unlock(protector)
|
||||
if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) {
|
||||
val privateKey: OpenPGPPrivateKey =
|
||||
try {
|
||||
unlockSecretKey(secretKey, protector)
|
||||
} catch (e: PGPException) {
|
||||
throw WrongPassphraseException(secretKey.keyIdentifier, e)
|
||||
}
|
||||
if (decryptWithPrivateKey(esks, privateKey.keyPair, decryptionKeyId, pkesk)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -488,25 +528,22 @@ class OpenPgpMessageInputStream(
|
|||
}
|
||||
|
||||
private fun decryptWithPrivateKey(
|
||||
esks: SortedESKs,
|
||||
privateKey: PGPPrivateKey,
|
||||
esks: ESKsAndData,
|
||||
privateKey: PGPKeyPair,
|
||||
decryptionKeyId: SubkeyIdentifier,
|
||||
pkesk: PGPPublicKeyEncryptedData
|
||||
): Boolean {
|
||||
val decryptorFactory =
|
||||
ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey)
|
||||
api.implementation.publicKeyDataDecryptorFactory(privateKey.privateKey)
|
||||
return decryptPKESKAndStream(esks, decryptionKeyId, decryptorFactory, pkesk)
|
||||
}
|
||||
|
||||
private fun hasUnsupportedS2KSpecifier(
|
||||
secretKey: PGPSecretKey,
|
||||
decryptionKeyId: SubkeyIdentifier
|
||||
): Boolean {
|
||||
val s2k = secretKey.s2K
|
||||
private fun hasUnsupportedS2KSpecifier(secretKey: OpenPGPSecretKey): Boolean {
|
||||
val s2k = secretKey.pgpSecretKey.s2K
|
||||
if (s2k != null) {
|
||||
if (s2k.type in 100..110) {
|
||||
LOGGER.debug(
|
||||
"Skipping PKESK because key $decryptionKeyId has unsupported private S2K specifier ${s2k.type}")
|
||||
"Skipping PKESK because key ${secretKey.keyIdentifier} has unsupported private S2K specifier ${s2k.type}")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -514,7 +551,7 @@ class OpenPgpMessageInputStream(
|
|||
}
|
||||
|
||||
private fun decryptSKESKAndStream(
|
||||
esks: SortedESKs,
|
||||
esks: ESKsAndData,
|
||||
skesk: PGPPBEEncryptedData,
|
||||
decryptorFactory: PBEDataDecryptorFactory
|
||||
): Boolean {
|
||||
|
@ -522,13 +559,13 @@ class OpenPgpMessageInputStream(
|
|||
val decrypted = skesk.getDataStream(decryptorFactory)
|
||||
val sessionKey = SessionKey(skesk.getSessionKey(decryptorFactory))
|
||||
throwIfUnacceptable(sessionKey.algorithm)
|
||||
val encryptedData = EncryptedData(sessionKey.algorithm, layerMetadata.depth + 1)
|
||||
val encryptedData = esks.toEncryptedData(sessionKey, layerMetadata.depth + 1)
|
||||
encryptedData.sessionKey = sessionKey
|
||||
encryptedData.addRecipients(esks.pkesks.map { it.keyID })
|
||||
encryptedData.addRecipients(esks.pkesks.map { it.keyIdentifier })
|
||||
LOGGER.debug("Successfully decrypted data with passphrase")
|
||||
val integrityProtected = IntegrityProtectedInputStream(decrypted, skesk, options)
|
||||
nestedInputStream =
|
||||
OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy)
|
||||
OpenPgpMessageInputStream(integrityProtected, options, encryptedData, api)
|
||||
return true
|
||||
} catch (e: UnacceptableAlgorithmException) {
|
||||
throw e
|
||||
|
@ -540,7 +577,7 @@ class OpenPgpMessageInputStream(
|
|||
}
|
||||
|
||||
private fun decryptPKESKAndStream(
|
||||
esks: SortedESKs,
|
||||
esks: ESKsAndData,
|
||||
decryptionKeyId: SubkeyIdentifier,
|
||||
decryptorFactory: PublicKeyDataDecryptorFactory,
|
||||
pkesk: PGPPublicKeyEncryptedData
|
||||
|
@ -550,18 +587,14 @@ class OpenPgpMessageInputStream(
|
|||
val sessionKey = SessionKey(pkesk.getSessionKey(decryptorFactory))
|
||||
throwIfUnacceptable(sessionKey.algorithm)
|
||||
|
||||
val encryptedData =
|
||||
EncryptedData(
|
||||
SymmetricKeyAlgorithm.requireFromId(
|
||||
pkesk.getSymmetricAlgorithm(decryptorFactory)),
|
||||
layerMetadata.depth + 1)
|
||||
val encryptedData = esks.toEncryptedData(sessionKey, layerMetadata.depth)
|
||||
encryptedData.decryptionKey = decryptionKeyId
|
||||
encryptedData.sessionKey = sessionKey
|
||||
encryptedData.addRecipients(esks.pkesks.plus(esks.anonPkesks).map { it.keyID })
|
||||
encryptedData.addRecipients(esks.pkesks.plus(esks.anonPkesks).map { it.keyIdentifier })
|
||||
LOGGER.debug("Successfully decrypted data with key $decryptionKeyId")
|
||||
val integrityProtected = IntegrityProtectedInputStream(decrypted, pkesk, options)
|
||||
nestedInputStream =
|
||||
OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy)
|
||||
OpenPgpMessageInputStream(integrityProtected, options, encryptedData, api)
|
||||
return true
|
||||
} catch (e: UnacceptableAlgorithmException) {
|
||||
throw e
|
||||
|
@ -599,7 +632,7 @@ class OpenPgpMessageInputStream(
|
|||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
signatures.finish(layerMetadata, policy)
|
||||
signatures.finish(layerMetadata)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
@ -626,7 +659,7 @@ class OpenPgpMessageInputStream(
|
|||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
signatures.finish(layerMetadata, policy)
|
||||
signatures.finish(layerMetadata)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
@ -672,44 +705,33 @@ class OpenPgpMessageInputStream(
|
|||
return MessageMetadata((layerMetadata as Message))
|
||||
}
|
||||
|
||||
private fun getDecryptionKey(keyId: Long): PGPSecretKeyRing? =
|
||||
private fun getDecryptionKey(pkesk: PGPPublicKeyEncryptedData): OpenPGPKey? =
|
||||
options.getDecryptionKeys().firstOrNull {
|
||||
it.any { k -> k.keyID == keyId }
|
||||
.and(PGPainless.inspectKeyRing(it).decryptionSubkeys.any { k -> k.keyID == keyId })
|
||||
}
|
||||
|
||||
private fun getDecryptionKey(pkesk: PGPPublicKeyEncryptedData): PGPSecretKeyRing? =
|
||||
options.getDecryptionKeys().firstOrNull {
|
||||
it.getSecretKeyFor(pkesk) != null &&
|
||||
PGPainless.inspectKeyRing(it).decryptionSubkeys.any { subkey ->
|
||||
when (pkesk.version) {
|
||||
3 -> pkesk.keyID == subkey.keyID
|
||||
else -> throw NotImplementedError("Version 6 PKESK not yet supported.")
|
||||
}
|
||||
it.pgpSecretKeyRing.getSecretKeyFor(pkesk) != null &&
|
||||
api.inspect(it).decryptionSubkeys.any { subkey ->
|
||||
pkesk.keyIdentifier.matchesExplicit(subkey.keyIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List<PGPSecretKeyRing> =
|
||||
private fun getDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List<OpenPGPKey> =
|
||||
options.getDecryptionKeys().filter {
|
||||
it.getSecretKeyFor(pkesk) != null &&
|
||||
PGPainless.inspectKeyRing(it).decryptionSubkeys.any { subkey ->
|
||||
when (pkesk.version) {
|
||||
3 -> pkesk.keyID == subkey.keyID
|
||||
else -> throw NotImplementedError("Version 6 PKESK not yet supported.")
|
||||
}
|
||||
it.pgpSecretKeyRing.getSecretKeyFor(pkesk) != null &&
|
||||
api.inspect(it).decryptionSubkeys.any { subkey ->
|
||||
pkesk.keyIdentifier.matchesExplicit(subkey.keyIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findPotentialDecryptionKeys(
|
||||
pkesk: PGPPublicKeyEncryptedData
|
||||
): List<Pair<PGPSecretKeyRing, PGPSecretKey>> {
|
||||
): List<OpenPGPSecretKey> {
|
||||
val algorithm = pkesk.algorithm
|
||||
val candidates = mutableListOf<Pair<PGPSecretKeyRing, PGPSecretKey>>()
|
||||
val candidates = mutableListOf<OpenPGPSecretKey>()
|
||||
options.getDecryptionKeys().forEach {
|
||||
val info = PGPainless.inspectKeyRing(it)
|
||||
val info = api.inspect(it)
|
||||
for (key in info.decryptionSubkeys) {
|
||||
if (key.algorithm == algorithm && info.isSecretKeyAvailable(key.keyID)) {
|
||||
candidates.add(it to it.getSecretKey(key.keyID))
|
||||
if (key.pgpPublicKey.algorithm == algorithm &&
|
||||
info.isSecretKeyAvailable(key.keyIdentifier)) {
|
||||
candidates.add(it.getSecretKey(key.keyIdentifier))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -717,7 +739,8 @@ class OpenPgpMessageInputStream(
|
|||
}
|
||||
|
||||
private fun isAcceptable(algorithm: SymmetricKeyAlgorithm): Boolean =
|
||||
policy.symmetricKeyDecryptionAlgorithmPolicy.isAcceptable(algorithm)
|
||||
api.algorithmPolicy.messageDecryptionAlgorithmPolicy.symmetricAlgorithmPolicy.isAcceptable(
|
||||
algorithm)
|
||||
|
||||
private fun throwIfUnacceptable(algorithm: SymmetricKeyAlgorithm) {
|
||||
if (!isAcceptable(algorithm)) {
|
||||
|
@ -726,7 +749,32 @@ class OpenPgpMessageInputStream(
|
|||
}
|
||||
}
|
||||
|
||||
private class SortedESKs(esks: PGPEncryptedDataList) {
|
||||
private class ESKsAndData(private val esks: PGPEncryptedDataList) {
|
||||
fun toEncryptedData(sk: SessionKey, depth: Int): EncryptedData {
|
||||
return when (EncryptedDataPacketType.of(esks)!!) {
|
||||
EncryptedDataPacketType.SED ->
|
||||
EncryptedData(
|
||||
MessageEncryptionMechanism.legacyEncryptedNonIntegrityProtected(
|
||||
sk.algorithm.algorithmId),
|
||||
depth)
|
||||
EncryptedDataPacketType.SEIPDv1 ->
|
||||
EncryptedData(
|
||||
MessageEncryptionMechanism.integrityProtected(sk.algorithm.algorithmId),
|
||||
depth)
|
||||
EncryptedDataPacketType.SEIPDv2 -> {
|
||||
val seipd2 = esks.encryptedData as SymmetricEncIntegrityPacket
|
||||
EncryptedData(
|
||||
MessageEncryptionMechanism.aead(
|
||||
seipd2.cipherAlgorithm, seipd2.aeadAlgorithm),
|
||||
depth)
|
||||
}
|
||||
EncryptedDataPacketType.LIBREPGP_OED -> {
|
||||
val oed = esks.encryptedData as AEADEncDataPacket
|
||||
EncryptedData(MessageEncryptionMechanism.librePgp(oed.algorithm.toInt()), depth)
|
||||
}
|
||||
}.also { it.sessionKey = sk }
|
||||
}
|
||||
|
||||
val skesks: List<PGPPBEEncryptedData>
|
||||
val pkesks: List<PGPPublicKeyEncryptedData>
|
||||
val anonPkesks: List<PGPPublicKeyEncryptedData>
|
||||
|
@ -735,14 +783,15 @@ class OpenPgpMessageInputStream(
|
|||
skesks = mutableListOf()
|
||||
pkesks = mutableListOf()
|
||||
anonPkesks = mutableListOf()
|
||||
|
||||
for (esk in esks) {
|
||||
if (esk is PGPPBEEncryptedData) {
|
||||
skesks.add(esk)
|
||||
} else if (esk is PGPPublicKeyEncryptedData) {
|
||||
if (esk.keyID != 0L) {
|
||||
pkesks.add(esk)
|
||||
} else {
|
||||
if (esk.keyIdentifier.isWildcard) {
|
||||
anonPkesks.add(esk)
|
||||
} else {
|
||||
pkesks.add(esk)
|
||||
}
|
||||
} else {
|
||||
throw IllegalArgumentException("Unknown ESK class type ${esk.javaClass}")
|
||||
|
@ -754,9 +803,9 @@ class OpenPgpMessageInputStream(
|
|||
get() = skesks.plus(pkesks).plus(anonPkesks)
|
||||
}
|
||||
|
||||
private class Signatures(val options: ConsumerOptions) : OutputStream() {
|
||||
val detachedSignatures = mutableListOf<SignatureCheck>()
|
||||
val prependedSignatures = mutableListOf<SignatureCheck>()
|
||||
private class Signatures(val options: ConsumerOptions, val api: PGPainless) : OutputStream() {
|
||||
val detachedSignatures = mutableListOf<OpenPGPDocumentSignature>()
|
||||
val prependedSignatures = mutableListOf<OpenPGPDocumentSignature>()
|
||||
val onePassSignatures = mutableListOf<OnePassSignatureCheck>()
|
||||
val opsUpdateStack = ArrayDeque<MutableList<OnePassSignatureCheck>>()
|
||||
var literalOPS = mutableListOf<OnePassSignatureCheck>()
|
||||
|
@ -775,47 +824,49 @@ class OpenPgpMessageInputStream(
|
|||
fun addDetachedSignature(signature: PGPSignature) {
|
||||
val check = initializeSignature(signature)
|
||||
val keyId = signature.issuerKeyId
|
||||
if (check != null) {
|
||||
if (check.issuer != null) {
|
||||
detachedSignatures.add(check)
|
||||
} else {
|
||||
LOGGER.debug(
|
||||
"No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.")
|
||||
detachedSignaturesWithMissingCert.add(
|
||||
SignatureVerification.Failure(
|
||||
signature, null, SignatureValidationException("Missing verification key.")))
|
||||
check, SignatureValidationException("Missing verification key.")))
|
||||
}
|
||||
}
|
||||
|
||||
fun addPrependedSignature(signature: PGPSignature) {
|
||||
val check = initializeSignature(signature)
|
||||
val keyId = signature.issuerKeyId
|
||||
if (check != null) {
|
||||
if (check.issuer != null) {
|
||||
prependedSignatures.add(check)
|
||||
} else {
|
||||
LOGGER.debug(
|
||||
"No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.")
|
||||
prependedSignaturesWithMissingCert.add(
|
||||
SignatureVerification.Failure(
|
||||
signature, null, SignatureValidationException("Missing verification key")))
|
||||
check, SignatureValidationException("Missing verification key")))
|
||||
}
|
||||
}
|
||||
|
||||
fun initializeSignature(signature: PGPSignature): SignatureCheck? {
|
||||
val certificate = findCertificate(signature) ?: return null
|
||||
val publicKey = certificate.getPublicKeyFor(signature) ?: return null
|
||||
val verifierKey = SubkeyIdentifier(certificate, publicKey.keyID)
|
||||
initialize(signature, publicKey)
|
||||
return SignatureCheck(signature, certificate, verifierKey)
|
||||
fun initializeSignature(signature: PGPSignature): OpenPGPDocumentSignature {
|
||||
val certificate =
|
||||
findCertificate(signature) ?: return OpenPGPDocumentSignature(signature, null)
|
||||
val publicKey =
|
||||
certificate.getSigningKeyFor(signature)
|
||||
?: return OpenPGPDocumentSignature(signature, null)
|
||||
initialize(signature, publicKey.pgpPublicKey)
|
||||
return OpenPGPDocumentSignature(signature, publicKey)
|
||||
}
|
||||
|
||||
fun addOnePassSignature(signature: PGPOnePassSignature) {
|
||||
val certificate = findCertificate(signature)
|
||||
|
||||
if (certificate != null) {
|
||||
val publicKey = certificate.getPublicKeyFor(signature)
|
||||
val publicKey = certificate.getSigningKeyFor(signature)
|
||||
if (publicKey != null) {
|
||||
val ops = OnePassSignatureCheck(signature, certificate)
|
||||
initialize(signature, publicKey)
|
||||
initialize(signature, publicKey.pgpPublicKey)
|
||||
onePassSignatures.add(ops)
|
||||
literalOPS.add(ops)
|
||||
}
|
||||
|
@ -825,15 +876,11 @@ class OpenPgpMessageInputStream(
|
|||
}
|
||||
}
|
||||
|
||||
fun addCorrespondingOnePassSignature(
|
||||
signature: PGPSignature,
|
||||
layer: Layer,
|
||||
policy: Policy
|
||||
) {
|
||||
fun addCorrespondingOnePassSignature(signature: PGPSignature, layer: Layer) {
|
||||
var found = false
|
||||
val keyId = signature.issuerKeyId
|
||||
for ((i, check) in onePassSignatures.withIndex().reversed()) {
|
||||
if (check.onePassSignature.keyID != keyId) {
|
||||
for (check in onePassSignatures.reversed()) {
|
||||
if (!KeyIdentifier.matches(
|
||||
signature.keyIdentifiers, check.onePassSignature.keyIdentifier, true)) {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
|
@ -841,34 +888,42 @@ class OpenPgpMessageInputStream(
|
|||
if (check.signature != null) {
|
||||
continue
|
||||
}
|
||||
|
||||
check.signature = signature
|
||||
val verification =
|
||||
SignatureVerification(
|
||||
signature,
|
||||
SubkeyIdentifier(check.verificationKeys, check.onePassSignature.keyID))
|
||||
|
||||
val documentSignature =
|
||||
OpenPGPDocumentSignature(
|
||||
signature, check.verificationKeys.getSigningKeyFor(signature))
|
||||
val verification = SignatureVerification(documentSignature)
|
||||
|
||||
try {
|
||||
SignatureValidator.signatureWasCreatedInBounds(
|
||||
options.getVerifyNotBefore(), options.getVerifyNotAfter())
|
||||
.verify(signature)
|
||||
CertificateValidator.validateCertificateAndVerifyOnePassSignature(check, policy)
|
||||
LOGGER.debug("Acceptable signature by key ${verification.signingKey}")
|
||||
layer.addVerifiedOnePassSignature(verification)
|
||||
signature.assertCreatedInBounds(
|
||||
options.getVerifyNotBefore(), options.getVerifyNotAfter())
|
||||
if (documentSignature.verify(check.onePassSignature) &&
|
||||
documentSignature.isValid(api.implementation.policy())) {
|
||||
layer.addVerifiedOnePassSignature(verification)
|
||||
} else {
|
||||
throw SignatureValidationException("Incorrect OnePassSignature.")
|
||||
}
|
||||
} catch (e: MalformedOpenPGPSignatureException) {
|
||||
throw SignatureValidationException("Malformed OnePassSignature.", e)
|
||||
} catch (e: SignatureValidationException) {
|
||||
LOGGER.debug("Rejected signature by key ${verification.signingKey}", e)
|
||||
layer.addRejectedOnePassSignature(
|
||||
SignatureVerification.Failure(verification, e))
|
||||
} catch (e: PGPSignatureException) {
|
||||
layer.addRejectedOnePassSignature(
|
||||
SignatureVerification.Failure(
|
||||
verification, SignatureValidationException(e.message, e)))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
LOGGER.debug(
|
||||
"No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.")
|
||||
"No suitable certificate for verification of signature by key ${signature.issuerKeyId.openPgpKeyId()} found.")
|
||||
inbandSignaturesWithMissingCert.add(
|
||||
SignatureVerification.Failure(
|
||||
signature, null, SignatureValidationException("Missing verification key.")))
|
||||
OpenPGPDocumentSignature(signature, null),
|
||||
SignatureValidationException("Missing verification key.")))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -884,7 +939,7 @@ class OpenPgpMessageInputStream(
|
|||
opsUpdateStack.removeFirst()
|
||||
}
|
||||
|
||||
private fun findCertificate(signature: PGPSignature): PGPPublicKeyRing? {
|
||||
private fun findCertificate(signature: PGPSignature): OpenPGPCertificate? {
|
||||
val cert = options.getCertificateSource().getCertificate(signature)
|
||||
if (cert != null) {
|
||||
return cert
|
||||
|
@ -893,21 +948,19 @@ class OpenPgpMessageInputStream(
|
|||
if (options.getMissingCertificateCallback() != null) {
|
||||
return options
|
||||
.getMissingCertificateCallback()!!
|
||||
.onMissingPublicKeyEncountered(signature.keyID)
|
||||
.provide(signature.keyIdentifiers.first())
|
||||
}
|
||||
return null // TODO: Missing cert for sig
|
||||
}
|
||||
|
||||
private fun findCertificate(signature: PGPOnePassSignature): PGPPublicKeyRing? {
|
||||
val cert = options.getCertificateSource().getCertificate(signature.keyID)
|
||||
private fun findCertificate(signature: PGPOnePassSignature): OpenPGPCertificate? {
|
||||
val cert = options.getCertificateSource().getCertificate(signature.keyIdentifier)
|
||||
if (cert != null) {
|
||||
return cert
|
||||
}
|
||||
|
||||
if (options.getMissingCertificateCallback() != null) {
|
||||
return options
|
||||
.getMissingCertificateCallback()!!
|
||||
.onMissingPublicKeyEncountered(signature.keyID)
|
||||
return options.getMissingCertificateCallback()!!.provide(signature.keyIdentifier)
|
||||
}
|
||||
return null // TODO: Missing cert for sig
|
||||
}
|
||||
|
@ -956,40 +1009,39 @@ class OpenPgpMessageInputStream(
|
|||
}
|
||||
}
|
||||
|
||||
fun finish(layer: Layer, policy: Policy) {
|
||||
fun finish(layer: Layer) {
|
||||
for (detached in detachedSignatures) {
|
||||
val verification =
|
||||
SignatureVerification(detached.signature, detached.signingKeyIdentifier)
|
||||
val verification = SignatureVerification(detached)
|
||||
try {
|
||||
SignatureValidator.signatureWasCreatedInBounds(
|
||||
options.getVerifyNotBefore(), options.getVerifyNotAfter())
|
||||
.verify(detached.signature)
|
||||
CertificateValidator.validateCertificateAndVerifyInitializedSignature(
|
||||
detached.signature,
|
||||
KeyRingUtils.publicKeys(detached.signingKeyRing),
|
||||
policy)
|
||||
LOGGER.debug("Acceptable signature by key ${verification.signingKey}")
|
||||
layer.addVerifiedDetachedSignature(verification)
|
||||
detached.signature.assertCreatedInBounds(
|
||||
options.getVerifyNotBefore(), options.getVerifyNotAfter())
|
||||
if (!detached.verify()) {
|
||||
throw SignatureValidationException("Incorrect detached signature.")
|
||||
} else if (!detached.isValid(api.implementation.policy())) {
|
||||
throw SignatureValidationException("Detached signature is not valid.")
|
||||
} else {
|
||||
layer.addVerifiedDetachedSignature(verification)
|
||||
}
|
||||
} catch (e: MalformedOpenPGPSignatureException) {
|
||||
throw SignatureValidationException("Malformed detached signature.", e)
|
||||
} catch (e: SignatureValidationException) {
|
||||
LOGGER.debug("Rejected signature by key ${verification.signingKey}", e)
|
||||
layer.addRejectedDetachedSignature(
|
||||
SignatureVerification.Failure(verification, e))
|
||||
}
|
||||
}
|
||||
|
||||
for (prepended in prependedSignatures) {
|
||||
val verification =
|
||||
SignatureVerification(prepended.signature, prepended.signingKeyIdentifier)
|
||||
val verification = SignatureVerification(prepended)
|
||||
try {
|
||||
SignatureValidator.signatureWasCreatedInBounds(
|
||||
options.getVerifyNotBefore(), options.getVerifyNotAfter())
|
||||
.verify(prepended.signature)
|
||||
CertificateValidator.validateCertificateAndVerifyInitializedSignature(
|
||||
prepended.signature,
|
||||
KeyRingUtils.publicKeys(prepended.signingKeyRing),
|
||||
policy)
|
||||
LOGGER.debug("Acceptable signature by key ${verification.signingKey}")
|
||||
layer.addVerifiedPrependedSignature(verification)
|
||||
prepended.signature.assertCreatedInBounds(
|
||||
options.getVerifyNotBefore(), options.getVerifyNotAfter())
|
||||
if (prepended.verify() && prepended.isValid(api.implementation.policy())) {
|
||||
layer.addVerifiedPrependedSignature(verification)
|
||||
} else {
|
||||
throw SignatureValidationException("Incorrect prepended signature.")
|
||||
}
|
||||
} catch (e: MalformedOpenPGPSignatureException) {
|
||||
throw SignatureValidationException("Malformed prepended signature.", e)
|
||||
} catch (e: SignatureValidationException) {
|
||||
LOGGER.debug("Rejected signature by key ${verification.signingKey}", e)
|
||||
layer.addRejectedPrependedSignature(
|
||||
|
@ -1029,27 +1081,21 @@ class OpenPgpMessageInputStream(
|
|||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
private fun initialize(signature: PGPSignature, publicKey: PGPPublicKey) {
|
||||
val verifierProvider =
|
||||
ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider
|
||||
try {
|
||||
signature.init(verifierProvider, publicKey)
|
||||
} catch (e: PGPException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
private fun initialize(signature: PGPSignature, publicKey: PGPPublicKey) {
|
||||
val verifierProvider = api.implementation.pgpContentVerifierBuilderProvider()
|
||||
try {
|
||||
signature.init(verifierProvider, publicKey)
|
||||
} catch (e: PGPException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun initialize(ops: PGPOnePassSignature, publicKey: PGPPublicKey) {
|
||||
val verifierProvider =
|
||||
ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider
|
||||
try {
|
||||
ops.init(verifierProvider, publicKey)
|
||||
} catch (e: PGPException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
private fun initialize(ops: PGPOnePassSignature, publicKey: PGPPublicKey) {
|
||||
val verifierProvider = api.implementation.pgpContentVerifierBuilderProvider()
|
||||
try {
|
||||
ops.init(verifierProvider, publicKey)
|
||||
} catch (e: PGPException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1059,32 +1105,27 @@ class OpenPgpMessageInputStream(
|
|||
private val LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream::class.java)
|
||||
|
||||
@JvmStatic
|
||||
fun create(inputStream: InputStream, options: ConsumerOptions) =
|
||||
create(inputStream, options, PGPainless.getPolicy())
|
||||
|
||||
@JvmStatic
|
||||
fun create(inputStream: InputStream, options: ConsumerOptions, policy: Policy) =
|
||||
create(inputStream, options, Message(), policy)
|
||||
fun create(inputStream: InputStream, options: ConsumerOptions, api: PGPainless) =
|
||||
create(inputStream, options, Message(), api)
|
||||
|
||||
@JvmStatic
|
||||
internal fun create(
|
||||
inputStream: InputStream,
|
||||
options: ConsumerOptions,
|
||||
metadata: Layer,
|
||||
policy: Policy
|
||||
api: PGPainless
|
||||
): OpenPgpMessageInputStream {
|
||||
val openPgpIn = OpenPgpInputStream(inputStream)
|
||||
val openPgpIn = OpenPGPAnimalSnifferInputStream(inputStream)
|
||||
openPgpIn.reset()
|
||||
|
||||
if (openPgpIn.isNonOpenPgp || options.isForceNonOpenPgpData()) {
|
||||
return OpenPgpMessageInputStream(
|
||||
Type.non_openpgp, openPgpIn, options, metadata, policy)
|
||||
Type.non_openpgp, openPgpIn, options, metadata, api)
|
||||
}
|
||||
|
||||
if (openPgpIn.isBinaryOpenPgp) {
|
||||
// Simply consume OpenPGP message
|
||||
return OpenPgpMessageInputStream(
|
||||
Type.standard, openPgpIn, options, metadata, policy)
|
||||
return OpenPgpMessageInputStream(Type.standard, openPgpIn, options, metadata, api)
|
||||
}
|
||||
|
||||
return if (openPgpIn.isAsciiArmored) {
|
||||
|
@ -1092,10 +1133,10 @@ class OpenPgpMessageInputStream(
|
|||
if (armorIn.isClearText) {
|
||||
(metadata as Message).setCleartextSigned()
|
||||
OpenPgpMessageInputStream(
|
||||
Type.cleartext_signed, armorIn, options, metadata, policy)
|
||||
Type.cleartext_signed, armorIn, options, metadata, api)
|
||||
} else {
|
||||
// Simply consume dearmored OpenPGP message
|
||||
OpenPgpMessageInputStream(Type.standard, armorIn, options, metadata, policy)
|
||||
OpenPgpMessageInputStream(Type.standard, armorIn, options, metadata, api)
|
||||
}
|
||||
} else {
|
||||
throw AssertionError("Cannot deduce type of data.")
|
||||
|
|
|
@ -5,22 +5,23 @@
|
|||
package org.pgpainless.decryption_verification
|
||||
|
||||
import org.bouncycastle.openpgp.PGPSignature
|
||||
import org.pgpainless.decryption_verification.SignatureVerification.Failure
|
||||
import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature
|
||||
import org.pgpainless.exception.SignatureValidationException
|
||||
import org.pgpainless.key.SubkeyIdentifier
|
||||
import org.pgpainless.signature.SignatureUtils
|
||||
|
||||
/**
|
||||
* Tuple of a signature and an identifier of its corresponding verification key. Semantic meaning of
|
||||
* the signature verification (success, failure) is merely given by context. E.g.
|
||||
* [MessageMetadata.getVerifiedInlineSignatures] contains verified verifications, while the class
|
||||
* [Failure] contains failed verifications.
|
||||
* An evaluated document signature.
|
||||
*
|
||||
* @param signature PGPSignature object
|
||||
* @param signingKey [SubkeyIdentifier] of the (sub-) key that is used for signature verification.
|
||||
* Note, that this might be null, e.g. in case of a [Failure] due to missing verification key.
|
||||
* @param documentSignature OpenPGPDocumentSignature object
|
||||
*/
|
||||
data class SignatureVerification(val signature: PGPSignature, val signingKey: SubkeyIdentifier) {
|
||||
data class SignatureVerification(val documentSignature: OpenPGPDocumentSignature) {
|
||||
|
||||
/** Underlying [PGPSignature]. */
|
||||
val signature: PGPSignature = documentSignature.signature
|
||||
|
||||
/** [SubkeyIdentifier] of the component key that created the signature. */
|
||||
val signingKey: SubkeyIdentifier = SubkeyIdentifier(documentSignature.issuer)
|
||||
|
||||
override fun toString(): String {
|
||||
return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)};" +
|
||||
|
@ -31,20 +32,27 @@ data class SignatureVerification(val signature: PGPSignature, val signingKey: Su
|
|||
* Tuple object of a [SignatureVerification] and the corresponding
|
||||
* [SignatureValidationException] that caused the verification to fail.
|
||||
*
|
||||
* @param signatureVerification verification (tuple of [PGPSignature] and corresponding
|
||||
* [SubkeyIdentifier])
|
||||
* @param documentSignature signature that could not be verified
|
||||
* @param validationException exception that caused the verification to fail
|
||||
*/
|
||||
data class Failure(
|
||||
val signature: PGPSignature,
|
||||
val signingKey: SubkeyIdentifier?,
|
||||
val documentSignature: OpenPGPDocumentSignature,
|
||||
val validationException: SignatureValidationException
|
||||
) {
|
||||
|
||||
/** Underlying [PGPSignature]. */
|
||||
val signature: PGPSignature = documentSignature.signature
|
||||
|
||||
/**
|
||||
* [SubkeyIdentifier] of the component key that created the signature. Note: In case of a
|
||||
* missing verification key, this might be null.
|
||||
*/
|
||||
val signingKey: SubkeyIdentifier? = documentSignature.issuer?.let { SubkeyIdentifier(it) }
|
||||
|
||||
constructor(
|
||||
verification: SignatureVerification,
|
||||
validationException: SignatureValidationException
|
||||
) : this(verification.signature, verification.signingKey, validationException)
|
||||
) : this(verification.documentSignature, validationException)
|
||||
|
||||
override fun toString(): String {
|
||||
return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)}; Key: ${signingKey?.toString() ?: "null"}; Failure: ${validationException.message}"
|
||||
|
|
|
@ -8,9 +8,9 @@ import java.io.*
|
|||
import kotlin.jvm.Throws
|
||||
import org.bouncycastle.bcpg.ArmoredInputStream
|
||||
import org.bouncycastle.openpgp.PGPSignatureList
|
||||
import org.bouncycastle.openpgp.api.OpenPGPImplementation
|
||||
import org.bouncycastle.util.Strings
|
||||
import org.pgpainless.exception.WrongConsumingMethodException
|
||||
import org.pgpainless.implementation.ImplementationFactory
|
||||
import org.pgpainless.util.ArmoredInputStreamFactory
|
||||
|
||||
/**
|
||||
|
@ -72,7 +72,7 @@ class ClearsignedMessageUtil {
|
|||
}
|
||||
}
|
||||
|
||||
val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(input)
|
||||
val objectFactory = OpenPGPImplementation.getInstance().pgpObjectFactory(input)
|
||||
val next = objectFactory.nextObject() ?: PGPSignatureList(arrayOf())
|
||||
return next as PGPSignatureList
|
||||
}
|
||||
|
|
|
@ -6,14 +6,13 @@ package org.pgpainless.encryption_signing
|
|||
|
||||
import java.security.MessageDigest
|
||||
import org.bouncycastle.openpgp.PGPException
|
||||
import org.bouncycastle.openpgp.PGPPrivateKey
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||
import org.bouncycastle.openpgp.PGPSignature
|
||||
import org.bouncycastle.openpgp.PGPSignatureGenerator
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey
|
||||
import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature
|
||||
import org.pgpainless.PGPainless
|
||||
import org.pgpainless.algorithm.SignatureType
|
||||
import org.pgpainless.bouncycastle.extensions.unlock
|
||||
import org.pgpainless.key.protection.SecretKeyRingProtector
|
||||
import org.pgpainless.key.protection.UnlockSecretKey
|
||||
|
||||
class BcHashContextSigner {
|
||||
|
||||
|
@ -22,14 +21,17 @@ class BcHashContextSigner {
|
|||
fun signHashContext(
|
||||
hashContext: MessageDigest,
|
||||
signatureType: SignatureType,
|
||||
secretKey: PGPSecretKeyRing,
|
||||
secretKey: OpenPGPKey,
|
||||
protector: SecretKeyRingProtector
|
||||
): PGPSignature {
|
||||
val info = PGPainless.inspectKeyRing(secretKey)
|
||||
): OpenPGPDocumentSignature {
|
||||
val info = PGPainless.getInstance().inspect(secretKey)
|
||||
return info.signingSubkeys
|
||||
.mapNotNull { info.getSecretKey(it.keyID) }
|
||||
.mapNotNull { info.getSecretKey(it.keyIdentifier) }
|
||||
.firstOrNull()
|
||||
?.let { signHashContext(hashContext, signatureType, it.unlock(protector)) }
|
||||
?.let {
|
||||
signHashContext(
|
||||
hashContext, signatureType, UnlockSecretKey.unlockSecretKey(it, protector))
|
||||
}
|
||||
?: throw PGPException("Key does not contain suitable signing subkey.")
|
||||
}
|
||||
|
||||
|
@ -45,11 +47,13 @@ class BcHashContextSigner {
|
|||
internal fun signHashContext(
|
||||
hashContext: MessageDigest,
|
||||
signatureType: SignatureType,
|
||||
privateKey: PGPPrivateKey
|
||||
): PGPSignature {
|
||||
return PGPSignatureGenerator(BcPGPHashContextContentSignerBuilder(hashContext))
|
||||
.apply { init(signatureType.code, privateKey) }
|
||||
privateKey: OpenPGPKey.OpenPGPPrivateKey
|
||||
): OpenPGPDocumentSignature {
|
||||
return PGPSignatureGenerator(
|
||||
BcPGPHashContextContentSignerBuilder(hashContext), privateKey.keyPair.publicKey)
|
||||
.apply { init(signatureType.code, privateKey.keyPair.privateKey) }
|
||||
.generate()
|
||||
.let { OpenPGPDocumentSignature(it, privateKey.publicKey) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,69 +5,25 @@
|
|||
package org.pgpainless.encryption_signing
|
||||
|
||||
import java.io.OutputStream
|
||||
import org.pgpainless.PGPainless.Companion.getPolicy
|
||||
import org.pgpainless.algorithm.CompressionAlgorithm
|
||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||
import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity
|
||||
import org.pgpainless.PGPainless
|
||||
import org.pgpainless.util.NullOutputStream
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class EncryptionBuilder : EncryptionBuilderInterface {
|
||||
class EncryptionBuilder(private val api: PGPainless) : EncryptionBuilderInterface {
|
||||
override fun onOutputStream(
|
||||
outputStream: OutputStream
|
||||
): EncryptionBuilderInterface.WithOptions {
|
||||
return WithOptionsImpl(outputStream)
|
||||
return WithOptionsImpl(outputStream, api)
|
||||
}
|
||||
|
||||
override fun discardOutput(): EncryptionBuilderInterface.WithOptions {
|
||||
return onOutputStream(NullOutputStream())
|
||||
}
|
||||
|
||||
class WithOptionsImpl(val outputStream: OutputStream) : EncryptionBuilderInterface.WithOptions {
|
||||
class WithOptionsImpl(val outputStream: OutputStream, private val api: PGPainless) :
|
||||
EncryptionBuilderInterface.WithOptions {
|
||||
|
||||
override fun withOptions(options: ProducerOptions): EncryptionStream {
|
||||
return EncryptionStream(outputStream, options)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic val LOGGER: Logger = LoggerFactory.getLogger(EncryptionBuilder::class.java)
|
||||
|
||||
/**
|
||||
* Negotiate the [SymmetricKeyAlgorithm] used for message encryption.
|
||||
*
|
||||
* @param encryptionOptions encryption options
|
||||
* @return negotiated symmetric key algorithm
|
||||
*/
|
||||
@JvmStatic
|
||||
fun negotiateSymmetricEncryptionAlgorithm(
|
||||
encryptionOptions: EncryptionOptions
|
||||
): SymmetricKeyAlgorithm {
|
||||
val preferences =
|
||||
encryptionOptions.keyViews.values
|
||||
.map { it.preferredSymmetricKeyAlgorithms }
|
||||
.toList()
|
||||
val algorithm =
|
||||
byPopularity()
|
||||
.negotiate(
|
||||
getPolicy().symmetricKeyEncryptionAlgorithmPolicy,
|
||||
encryptionOptions.encryptionAlgorithmOverride,
|
||||
preferences)
|
||||
LOGGER.debug(
|
||||
"Negotiation resulted in {} being the symmetric encryption algorithm of choice.",
|
||||
algorithm)
|
||||
return algorithm
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun negotiateCompressionAlgorithm(producerOptions: ProducerOptions): CompressionAlgorithm {
|
||||
val compressionAlgorithmOverride = producerOptions.compressionAlgorithmOverride
|
||||
return compressionAlgorithmOverride
|
||||
?: getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm()
|
||||
|
||||
// TODO: Negotiation
|
||||
return EncryptionStream(outputStream, options, api)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,55 +5,69 @@
|
|||
package org.pgpainless.encryption_signing
|
||||
|
||||
import java.util.*
|
||||
import org.bouncycastle.openpgp.PGPPublicKey
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing
|
||||
import org.bouncycastle.openpgp.api.MessageEncryptionMechanism
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey
|
||||
import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator
|
||||
import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator
|
||||
import org.pgpainless.PGPainless
|
||||
import org.pgpainless.algorithm.EncryptionPurpose
|
||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||
import org.pgpainless.algorithm.negotiation.EncryptionMechanismNegotiator
|
||||
import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity
|
||||
import org.pgpainless.authentication.CertificateAuthority
|
||||
import org.pgpainless.encryption_signing.EncryptionOptions.EncryptionKeySelector
|
||||
import org.pgpainless.exception.KeyException
|
||||
import org.pgpainless.exception.KeyException.*
|
||||
import org.pgpainless.implementation.ImplementationFactory
|
||||
import org.pgpainless.key.OpenPgpFingerprint
|
||||
import org.pgpainless.exception.KeyException.ExpiredKeyException
|
||||
import org.pgpainless.exception.KeyException.UnacceptableEncryptionKeyException
|
||||
import org.pgpainless.exception.KeyException.UnacceptableSelfSignatureException
|
||||
import org.pgpainless.key.SubkeyIdentifier
|
||||
import org.pgpainless.key.info.KeyAccessor
|
||||
import org.pgpainless.key.info.KeyRingInfo
|
||||
import org.pgpainless.util.Passphrase
|
||||
|
||||
class EncryptionOptions(private val purpose: EncryptionPurpose) {
|
||||
class EncryptionOptions(private val purpose: EncryptionPurpose, private val api: PGPainless) {
|
||||
|
||||
var encryptionMechanismNegotiator: EncryptionMechanismNegotiator =
|
||||
EncryptionMechanismNegotiator.modificationDetectionOrBetter(byPopularity())
|
||||
private val _encryptionMethods: MutableSet<PGPKeyEncryptionMethodGenerator> = mutableSetOf()
|
||||
private val _encryptionKeyIdentifiers: MutableSet<SubkeyIdentifier> = mutableSetOf()
|
||||
private val keysAndAccessors: MutableMap<OpenPGPComponentKey, KeyAccessor> = mutableMapOf()
|
||||
private val _keyRingInfo: MutableMap<SubkeyIdentifier, KeyRingInfo> = mutableMapOf()
|
||||
private val _keyViews: MutableMap<SubkeyIdentifier, KeyAccessor> = mutableMapOf()
|
||||
private val encryptionKeySelector: EncryptionKeySelector = encryptToAllCapableSubkeys()
|
||||
|
||||
private var allowEncryptionWithMissingKeyFlags = false
|
||||
private var evaluationDate = Date()
|
||||
private var _encryptionAlgorithmOverride: SymmetricKeyAlgorithm? = null
|
||||
private var _encryptionMechanismOverride: MessageEncryptionMechanism? = null
|
||||
|
||||
val encryptionMethods
|
||||
get() = _encryptionMethods.toSet()
|
||||
|
||||
val encryptionKeyIdentifiers
|
||||
get() = _encryptionKeyIdentifiers.toSet()
|
||||
get() = keysAndAccessors.keys.map { SubkeyIdentifier(it) }
|
||||
|
||||
val keyRingInfo
|
||||
get() = _keyRingInfo.toMap()
|
||||
|
||||
val keyViews
|
||||
get() = _keyViews.toMap()
|
||||
val encryptionKeys
|
||||
get() = keysAndAccessors.keys.toSet()
|
||||
|
||||
@Deprecated(
|
||||
"Deprecated in favor of encryptionMechanismOverride",
|
||||
replaceWith = ReplaceWith("encryptionMechanismOverride"))
|
||||
// TODO: Remove in 2.1
|
||||
val encryptionAlgorithmOverride
|
||||
get() = _encryptionAlgorithmOverride
|
||||
get() =
|
||||
_encryptionMechanismOverride?.let {
|
||||
SymmetricKeyAlgorithm.requireFromId(it.symmetricKeyAlgorithm)
|
||||
}
|
||||
|
||||
constructor() : this(EncryptionPurpose.ANY)
|
||||
val encryptionMechanismOverride
|
||||
get() = _encryptionMechanismOverride
|
||||
|
||||
constructor(api: PGPainless) : this(EncryptionPurpose.ANY, api)
|
||||
|
||||
/**
|
||||
* Factory method to create an [EncryptionOptions] object which will encrypt for keys which
|
||||
* carry the flag [org.pgpainless.algorithm.KeyFlag.ENCRYPT_COMMS].
|
||||
* Set the evaluation date for certificate evaluation.
|
||||
*
|
||||
* @return encryption options
|
||||
* @param evaluationDate reference time
|
||||
* @return this
|
||||
*/
|
||||
fun setEvaluationDate(evaluationDate: Date) = apply { this.evaluationDate = evaluationDate }
|
||||
|
||||
|
@ -89,11 +103,14 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
|
|||
|
||||
/**
|
||||
* Add all key rings in the provided [Iterable] (e.g.
|
||||
* [org.bouncycastle.openpgp.PGPPublicKeyRingCollection]) as recipients.
|
||||
* [org.bouncycastle.openpgp.PGPPublicKeyRingCollection]) as recipients. Note: This method is
|
||||
* deprecated. Instead, repeatedly call [addRecipient], passing in individual
|
||||
* [OpenPGPCertificate] instances.
|
||||
*
|
||||
* @param keys keys
|
||||
* @return this
|
||||
*/
|
||||
@Deprecated("Repeatedly pass OpenPGPCertificate instances instead.")
|
||||
fun addRecipients(keys: Iterable<PGPPublicKeyRing>) = apply {
|
||||
keys.toList().let {
|
||||
require(it.isNotEmpty()) { "Set of recipient keys cannot be empty." }
|
||||
|
@ -104,12 +121,15 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
|
|||
/**
|
||||
* Add all key rings in the provided [Iterable] (e.g.
|
||||
* [org.bouncycastle.openpgp.PGPPublicKeyRingCollection]) as recipients. Per key ring, the
|
||||
* selector is applied to select one or more encryption subkeys.
|
||||
* selector is applied to select one or more encryption subkeys. Note: This method is
|
||||
* deprecated. Instead, repeatedly call [addRecipient], passing in individual
|
||||
* [OpenPGPCertificate] instances.
|
||||
*
|
||||
* @param keys keys
|
||||
* @param selector encryption key selector
|
||||
* @return this
|
||||
*/
|
||||
@Deprecated("Repeatedly pass OpenPGPCertificate instances instead.")
|
||||
fun addRecipients(keys: Iterable<PGPPublicKeyRing>, selector: EncryptionKeySelector) = apply {
|
||||
keys.toList().let {
|
||||
require(it.isNotEmpty()) { "Set of recipient keys cannot be empty." }
|
||||
|
@ -117,71 +137,175 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the message to the recipients [OpenPGPCertificate].
|
||||
*
|
||||
* @param cert recipient certificate
|
||||
* @return this
|
||||
*/
|
||||
fun addRecipient(cert: OpenPGPCertificate) = addRecipient(cert, encryptionKeySelector)
|
||||
|
||||
/**
|
||||
* Add a recipient by providing a key.
|
||||
*
|
||||
* @param key key ring
|
||||
* @return this
|
||||
*/
|
||||
@Deprecated(
|
||||
"Pass in OpenPGPCertificate instead.",
|
||||
replaceWith =
|
||||
ReplaceWith("addRecipient(key.toOpenPGPCertificate(), encryptionKeySelector)"))
|
||||
fun addRecipient(key: PGPPublicKeyRing) = addRecipient(key, encryptionKeySelector)
|
||||
|
||||
/**
|
||||
* Encrypt the message for the given recipients [OpenPGPCertificate], sourcing algorithm
|
||||
* preferences by inspecting the binding signature on the passed [userId].
|
||||
*
|
||||
* @param cert recipient certificate
|
||||
* @param userId recipient user-id
|
||||
* @return this
|
||||
*/
|
||||
fun addRecipient(cert: OpenPGPCertificate, userId: CharSequence) =
|
||||
addRecipient(cert, userId, encryptionKeySelector)
|
||||
|
||||
/**
|
||||
* Add a recipient by providing a key and recipient user-id. The user-id is used to determine
|
||||
* the recipients preferences (algorithms etc.).
|
||||
* the recipients preferences (algorithms etc.). Note: This method is deprecated. Replace the
|
||||
* [PGPPublicKeyRing] instance with an [OpenPGPCertificate].
|
||||
*
|
||||
* @param key key ring
|
||||
* @param userId user id
|
||||
* @return this
|
||||
*/
|
||||
@Deprecated(
|
||||
"Pass in OpenPGPCertificate instead.",
|
||||
replaceWith = ReplaceWith("addRecipient(key.toOpenPGPCertificate(), userId)"))
|
||||
fun addRecipient(key: PGPPublicKeyRing, userId: CharSequence) =
|
||||
addRecipient(key, userId, encryptionKeySelector)
|
||||
|
||||
/**
|
||||
* Encrypt the message for the given recipients [OpenPGPCertificate], sourcing algorithm
|
||||
* preferences by inspecting the binding signature on the given [userId] and filtering the
|
||||
* recipient subkeys through the given [EncryptionKeySelector].
|
||||
*
|
||||
* @param cert recipient certificate
|
||||
* @param userId user-id for sourcing algorithm preferences
|
||||
* @param encryptionKeySelector decides which subkeys to encrypt for
|
||||
* @return this
|
||||
*/
|
||||
fun addRecipient(
|
||||
key: PGPPublicKeyRing,
|
||||
cert: OpenPGPCertificate,
|
||||
userId: CharSequence,
|
||||
encryptionKeySelector: EncryptionKeySelector
|
||||
) = apply {
|
||||
val info = KeyRingInfo(key, evaluationDate)
|
||||
val info = api.inspect(cert, evaluationDate)
|
||||
val subkeys =
|
||||
encryptionKeySelector.selectEncryptionSubkeys(
|
||||
info.getEncryptionSubkeys(userId, purpose))
|
||||
if (subkeys.isEmpty()) {
|
||||
throw KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key))
|
||||
throw UnacceptableEncryptionKeyException(cert)
|
||||
}
|
||||
|
||||
for (subkey in subkeys) {
|
||||
val keyId = SubkeyIdentifier(key, subkey.keyID)
|
||||
val keyId = SubkeyIdentifier(subkey)
|
||||
_keyRingInfo[keyId] = info
|
||||
_keyViews[keyId] = KeyAccessor.ViaUserId(info, keyId, userId.toString())
|
||||
addRecipientKey(key, subkey, false)
|
||||
val accessor = KeyAccessor.ViaUserId(subkey, cert.getUserId(userId.toString()))
|
||||
addRecipientKey(subkey, accessor, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun addRecipient(key: PGPPublicKeyRing, encryptionKeySelector: EncryptionKeySelector) = apply {
|
||||
addAsRecipient(key, encryptionKeySelector, false)
|
||||
}
|
||||
/**
|
||||
* Encrypt the message for the given recipients public key, sourcing algorithm preferences by
|
||||
* inspecting the binding signature on the given [userId] and filtering the recipient subkeys
|
||||
* through the given [EncryptionKeySelector].
|
||||
*
|
||||
* @param key recipient public key
|
||||
* @param userId user-id for sourcing algorithm preferences
|
||||
* @param encryptionKeySelector decides which subkeys to encrypt for
|
||||
* @return this
|
||||
*/
|
||||
@Deprecated(
|
||||
"Pass in OpenPGPCertificate instead.",
|
||||
replaceWith =
|
||||
ReplaceWith("addRecipient(key.toOpenPGPCertificate(), userId, encryptionKeySelector)"))
|
||||
fun addRecipient(
|
||||
key: PGPPublicKeyRing,
|
||||
userId: CharSequence,
|
||||
encryptionKeySelector: EncryptionKeySelector
|
||||
) = addRecipient(api.toCertificate(key), userId, encryptionKeySelector)
|
||||
|
||||
/**
|
||||
* Encrypt the message for the given recipients [OpenPGPCertificate], filtering encryption
|
||||
* subkeys through the given [EncryptionKeySelector].
|
||||
*
|
||||
* @param cert recipient certificate
|
||||
* @param encryptionKeySelector decides, which subkeys to encrypt for
|
||||
* @return this
|
||||
*/
|
||||
fun addRecipient(cert: OpenPGPCertificate, encryptionKeySelector: EncryptionKeySelector) =
|
||||
addAsRecipient(cert, encryptionKeySelector, false)
|
||||
|
||||
/**
|
||||
* Encrypt the message for the given recipients public key, filtering encryption subkeys through
|
||||
* the given [EncryptionKeySelector].
|
||||
*
|
||||
* @param key recipient public key
|
||||
* @param encryptionKeySelector decides, which subkeys to encrypt for
|
||||
* @return this
|
||||
*/
|
||||
@Deprecated(
|
||||
"Pass in OpenPGPCertificate instead.",
|
||||
replaceWith =
|
||||
ReplaceWith("addRecipient(key.toOpenPGPCertificate(), encryptionKeySelector)"))
|
||||
fun addRecipient(key: PGPPublicKeyRing, encryptionKeySelector: EncryptionKeySelector) =
|
||||
addRecipient(api.toCertificate(key), encryptionKeySelector)
|
||||
|
||||
/**
|
||||
* Encrypt the message for the recipients [OpenPGPCertificate], keeping the recipient anonymous
|
||||
* by setting a wildcard key-id / fingerprint.
|
||||
*
|
||||
* @param cert recipient certificate
|
||||
* @param selector decides, which subkeys to encrypt for
|
||||
* @return this
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun addHiddenRecipient(
|
||||
cert: OpenPGPCertificate,
|
||||
selector: EncryptionKeySelector = encryptionKeySelector
|
||||
) = addAsRecipient(cert, selector, true)
|
||||
|
||||
/**
|
||||
* Encrypt the message for the recipients public key, keeping the recipient anonymous by setting
|
||||
* a wildcard key-id / fingerprint.
|
||||
*
|
||||
* @param key recipient public key
|
||||
* @param selector decides, which subkeys to encrypt for
|
||||
* @return this
|
||||
*/
|
||||
@JvmOverloads
|
||||
@Deprecated(
|
||||
"Pass in an OpenPGPCertificate instead.",
|
||||
replaceWith = ReplaceWith("addHiddenRecipient(key.toOpenPGPCertificate(), selector)"))
|
||||
fun addHiddenRecipient(
|
||||
key: PGPPublicKeyRing,
|
||||
selector: EncryptionKeySelector = encryptionKeySelector
|
||||
) = apply { addAsRecipient(key, selector, true) }
|
||||
) = addHiddenRecipient(api.toCertificate(key), selector)
|
||||
|
||||
private fun addAsRecipient(
|
||||
key: PGPPublicKeyRing,
|
||||
cert: OpenPGPCertificate,
|
||||
selector: EncryptionKeySelector,
|
||||
wildcardKeyId: Boolean
|
||||
) = apply {
|
||||
val info = KeyRingInfo(key, evaluationDate)
|
||||
val info = api.inspect(cert, evaluationDate)
|
||||
val primaryKeyExpiration =
|
||||
try {
|
||||
info.primaryKeyExpirationDate
|
||||
} catch (e: NoSuchElementException) {
|
||||
throw UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key))
|
||||
throw UnacceptableSelfSignatureException(cert)
|
||||
}
|
||||
|
||||
if (primaryKeyExpiration != null && primaryKeyExpiration < evaluationDate) {
|
||||
throw ExpiredKeyException(OpenPgpFingerprint.of(key), primaryKeyExpiration)
|
||||
throw ExpiredKeyException(cert, primaryKeyExpiration)
|
||||
}
|
||||
|
||||
var encryptionSubkeys = selector.selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose))
|
||||
|
@ -193,31 +317,31 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
|
|||
if (encryptionSubkeys.isEmpty() && allowEncryptionWithMissingKeyFlags) {
|
||||
encryptionSubkeys =
|
||||
info.validSubkeys
|
||||
.filter { it.isEncryptionKey }
|
||||
.filter { info.getKeyFlagsOf(it.keyID).isEmpty() }
|
||||
.filter { it.pgpPublicKey.isEncryptionKey }
|
||||
.filter { info.getKeyFlagsOf(it.keyIdentifier).isEmpty() }
|
||||
}
|
||||
|
||||
if (encryptionSubkeys.isEmpty()) {
|
||||
throw UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key))
|
||||
throw UnacceptableEncryptionKeyException(cert)
|
||||
}
|
||||
|
||||
for (subkey in encryptionSubkeys) {
|
||||
val keyId = SubkeyIdentifier(key, subkey.keyID)
|
||||
val keyId = SubkeyIdentifier(subkey)
|
||||
_keyRingInfo[keyId] = info
|
||||
_keyViews[keyId] = KeyAccessor.ViaKeyId(info, keyId)
|
||||
addRecipientKey(key, subkey, wildcardKeyId)
|
||||
val accessor = KeyAccessor.ViaKeyIdentifier(subkey)
|
||||
addRecipientKey(subkey, accessor, wildcardKeyId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addRecipientKey(
|
||||
certificate: PGPPublicKeyRing,
|
||||
key: PGPPublicKey,
|
||||
wildcardKeyId: Boolean
|
||||
key: OpenPGPComponentKey,
|
||||
accessor: KeyAccessor,
|
||||
wildcardRecipient: Boolean
|
||||
) {
|
||||
_encryptionKeyIdentifiers.add(SubkeyIdentifier(certificate, key.keyID))
|
||||
keysAndAccessors[key] = accessor
|
||||
addEncryptionMethod(
|
||||
ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(key).also {
|
||||
it.setUseWildcardKeyID(wildcardKeyId)
|
||||
api.implementation.publicKeyKeyEncryptionMethodGenerator(key.pgpPublicKey).also {
|
||||
it.setUseWildcardRecipient(wildcardRecipient)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -241,7 +365,7 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
|
|||
fun addMessagePassphrase(passphrase: Passphrase) = apply {
|
||||
require(!passphrase.isEmpty) { "Passphrase MUST NOT be empty." }
|
||||
addEncryptionMethod(
|
||||
ImplementationFactory.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase))
|
||||
api.implementation.pbeKeyEncryptionMethodGenerator(passphrase.getChars()))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -270,11 +394,27 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
|
|||
* @param encryptionAlgorithm encryption algorithm override
|
||||
* @return this
|
||||
*/
|
||||
@Deprecated(
|
||||
"Deprecated in favor of overrideEncryptionMechanism",
|
||||
replaceWith =
|
||||
ReplaceWith(
|
||||
"overrideEncryptionMechanism(MessageEncryptionMechanism.integrityProtected(encryptionAlgorithm.algorithmId))"))
|
||||
// TODO: Remove in 2.1
|
||||
fun overrideEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply {
|
||||
require(encryptionAlgorithm != SymmetricKeyAlgorithm.NULL) {
|
||||
"Encryption algorithm override cannot be NULL."
|
||||
}
|
||||
_encryptionAlgorithmOverride = encryptionAlgorithm
|
||||
overrideEncryptionMechanism(
|
||||
MessageEncryptionMechanism.integrityProtected(encryptionAlgorithm.algorithmId))
|
||||
}
|
||||
|
||||
fun overrideEncryptionMechanism(encryptionMechanism: MessageEncryptionMechanism) = apply {
|
||||
require(
|
||||
api.algorithmPolicy.messageEncryptionAlgorithmPolicy.isAcceptable(
|
||||
encryptionMechanism)) {
|
||||
"Provided symmetric encryption algorithm is not acceptable."
|
||||
}
|
||||
_encryptionMechanismOverride = encryptionMechanism
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -291,16 +431,50 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
|
|||
|
||||
fun hasEncryptionMethod() = _encryptionMethods.isNotEmpty()
|
||||
|
||||
fun usesOnlyPasswordBasedEncryption() =
|
||||
_encryptionMethods.all { it is PBEKeyEncryptionMethodGenerator }
|
||||
|
||||
internal fun negotiateEncryptionMechanism(): MessageEncryptionMechanism {
|
||||
if (encryptionMechanismOverride != null) {
|
||||
return encryptionMechanismOverride!!
|
||||
}
|
||||
|
||||
val features = keysAndAccessors.values.map { it.features }.toList()
|
||||
val aeadAlgorithms = keysAndAccessors.values.map { it.preferredAEADCipherSuites }.toList()
|
||||
val symmetricKeyAlgorithms =
|
||||
keysAndAccessors.values.map { it.preferredSymmetricKeyAlgorithms }.toList()
|
||||
|
||||
val mechanism =
|
||||
encryptionMechanismNegotiator.negotiate(
|
||||
api.algorithmPolicy,
|
||||
encryptionMechanismOverride,
|
||||
features,
|
||||
aeadAlgorithms,
|
||||
symmetricKeyAlgorithms)
|
||||
|
||||
return mechanism
|
||||
}
|
||||
|
||||
fun interface EncryptionKeySelector {
|
||||
fun selectEncryptionSubkeys(encryptionCapableKeys: List<PGPPublicKey>): List<PGPPublicKey>
|
||||
fun selectEncryptionSubkeys(
|
||||
encryptionCapableKeys: List<OpenPGPComponentKey>
|
||||
): List<OpenPGPComponentKey>
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun get() = EncryptionOptions()
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun get(api: PGPainless = PGPainless.getInstance()) = EncryptionOptions(api)
|
||||
|
||||
@JvmStatic fun encryptCommunications() = EncryptionOptions(EncryptionPurpose.COMMUNICATIONS)
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun encryptCommunications(api: PGPainless = PGPainless.getInstance()) =
|
||||
EncryptionOptions(EncryptionPurpose.COMMUNICATIONS, api)
|
||||
|
||||
@JvmStatic fun encryptDataAtRest() = EncryptionOptions(EncryptionPurpose.STORAGE)
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun encryptDataAtRest(api: PGPainless = PGPainless.getInstance()) =
|
||||
EncryptionOptions(EncryptionPurpose.STORAGE, api)
|
||||
|
||||
/**
|
||||
* Only encrypt to the first valid encryption capable subkey we stumble upon.
|
||||
|
|
|
@ -8,23 +8,45 @@ import java.util.*
|
|||
import org.bouncycastle.openpgp.PGPLiteralData
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing
|
||||
import org.bouncycastle.openpgp.PGPSignature
|
||||
import org.bouncycastle.openpgp.api.MessageEncryptionMechanism
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate
|
||||
import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature
|
||||
import org.pgpainless.algorithm.CompressionAlgorithm
|
||||
import org.pgpainless.algorithm.StreamEncoding
|
||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||
import org.pgpainless.bouncycastle.extensions.matches
|
||||
import org.pgpainless.key.SubkeyIdentifier
|
||||
import org.pgpainless.util.MultiMap
|
||||
import org.pgpainless.util.SessionKey
|
||||
|
||||
data class EncryptionResult(
|
||||
val encryptionAlgorithm: SymmetricKeyAlgorithm,
|
||||
val encryptionMechanism: MessageEncryptionMechanism,
|
||||
val sessionKey: SessionKey?,
|
||||
val compressionAlgorithm: CompressionAlgorithm,
|
||||
val detachedSignatures: MultiMap<SubkeyIdentifier, PGPSignature>,
|
||||
val detachedDocumentSignatures: OpenPGPSignatureSet<OpenPGPDocumentSignature>,
|
||||
val recipients: Set<SubkeyIdentifier>,
|
||||
val fileName: String,
|
||||
val modificationDate: Date,
|
||||
val fileEncoding: StreamEncoding
|
||||
) {
|
||||
|
||||
@Deprecated(
|
||||
"Use encryptionMechanism instead.", replaceWith = ReplaceWith("encryptionMechanism"))
|
||||
val encryptionAlgorithm: SymmetricKeyAlgorithm?
|
||||
get() = SymmetricKeyAlgorithm.fromId(encryptionMechanism.symmetricKeyAlgorithm)
|
||||
|
||||
@Deprecated(
|
||||
"Use detachedSignatures instead", replaceWith = ReplaceWith("detachedDocumentSignatures"))
|
||||
// TODO: Remove in 2.1
|
||||
val detachedSignatures: MultiMap<SubkeyIdentifier, PGPSignature>
|
||||
get() {
|
||||
val map = MultiMap<SubkeyIdentifier, PGPSignature>()
|
||||
detachedDocumentSignatures.signatures
|
||||
.map { SubkeyIdentifier(it.issuer) to it.signature }
|
||||
.forEach { map.put(it.first, it.second) }
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true, if the message is marked as for-your-eyes-only. This is typically done by
|
||||
* setting the filename "_CONSOLE".
|
||||
|
@ -34,6 +56,9 @@ data class EncryptionResult(
|
|||
val isForYourEyesOnly: Boolean
|
||||
get() = PGPLiteralData.CONSOLE == fileName
|
||||
|
||||
fun isEncryptedFor(certificate: OpenPGPCertificate) =
|
||||
recipients.any { certificate.getKey(it.keyIdentifier) != null }
|
||||
|
||||
/**
|
||||
* Returns true, if the message was encrypted for at least one subkey of the given certificate.
|
||||
*
|
||||
|
@ -52,17 +77,19 @@ data class EncryptionResult(
|
|||
}
|
||||
|
||||
class Builder {
|
||||
var _encryptionAlgorithm: SymmetricKeyAlgorithm? = null
|
||||
var _encryptionMechanism: MessageEncryptionMechanism =
|
||||
MessageEncryptionMechanism.unencrypted()
|
||||
var _compressionAlgorithm: CompressionAlgorithm? = null
|
||||
|
||||
val detachedSignatures: MultiMap<SubkeyIdentifier, PGPSignature> = MultiMap()
|
||||
val detachedSignatures: MutableList<OpenPGPDocumentSignature> = mutableListOf()
|
||||
val recipients: Set<SubkeyIdentifier> = mutableSetOf()
|
||||
private var _fileName = ""
|
||||
private var _modificationDate = Date(0)
|
||||
private var _encoding = StreamEncoding.BINARY
|
||||
private var _sessionKey: SessionKey? = null
|
||||
|
||||
fun setEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply {
|
||||
_encryptionAlgorithm = encryptionAlgorithm
|
||||
fun setEncryptionMechanism(mechanism: MessageEncryptionMechanism): Builder = apply {
|
||||
_encryptionMechanism = mechanism
|
||||
}
|
||||
|
||||
fun setCompressionAlgorithm(compressionAlgorithm: CompressionAlgorithm) = apply {
|
||||
|
@ -81,19 +108,20 @@ data class EncryptionResult(
|
|||
(recipients as MutableSet).add(recipient)
|
||||
}
|
||||
|
||||
fun addDetachedSignature(
|
||||
signingSubkeyIdentifier: SubkeyIdentifier,
|
||||
detachedSignature: PGPSignature
|
||||
) = apply { detachedSignatures.put(signingSubkeyIdentifier, detachedSignature) }
|
||||
fun setSessionKey(sessionKey: SessionKey) = apply { _sessionKey = sessionKey }
|
||||
|
||||
fun addDetachedSignature(signature: OpenPGPDocumentSignature): Builder = apply {
|
||||
detachedSignatures.add(signature)
|
||||
}
|
||||
|
||||
fun build(): EncryptionResult {
|
||||
checkNotNull(_encryptionAlgorithm) { "Encryption algorithm not set." }
|
||||
checkNotNull(_compressionAlgorithm) { "Compression algorithm not set." }
|
||||
|
||||
return EncryptionResult(
|
||||
_encryptionAlgorithm!!,
|
||||
_encryptionMechanism,
|
||||
_sessionKey,
|
||||
_compressionAlgorithm!!,
|
||||
detachedSignatures,
|
||||
OpenPGPSignatureSet(detachedSignatures),
|
||||
recipients,
|
||||
_fileName,
|
||||
_modificationDate,
|
||||
|
|
|
@ -13,12 +13,14 @@ import org.bouncycastle.openpgp.PGPCompressedDataGenerator
|
|||
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator
|
||||
import org.bouncycastle.openpgp.PGPException
|
||||
import org.bouncycastle.openpgp.PGPLiteralDataGenerator
|
||||
import org.bouncycastle.openpgp.api.MessageEncryptionMechanism
|
||||
import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature
|
||||
import org.pgpainless.PGPainless
|
||||
import org.pgpainless.algorithm.CompressionAlgorithm
|
||||
import org.pgpainless.algorithm.StreamEncoding
|
||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||
import org.pgpainless.implementation.ImplementationFactory
|
||||
import org.pgpainless.bouncycastle.extensions.pgpDataEncryptorBuilder
|
||||
import org.pgpainless.util.ArmoredOutputStreamFactory
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.pgpainless.util.SessionKey
|
||||
|
||||
// 1 << 8 causes wrong partial body length encoding
|
||||
// 1 << 9 fixes this.
|
||||
|
@ -29,14 +31,15 @@ const val BUFFER_SIZE = 1 shl 9
|
|||
* OutputStream that produces an OpenPGP message. The message can be encrypted, signed, or both,
|
||||
* depending on its configuration.
|
||||
*
|
||||
* This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream.
|
||||
* This class was originally based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream.
|
||||
*
|
||||
* @see <a
|
||||
* href="https://github.com/neuhalje/bouncy-gpg/blob/master/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStream.java">Source</a>
|
||||
* @see
|
||||
* [PGPEncryptingStream](https://github.com/neuhalje/bouncy-gpg/blob/master/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStream.java)
|
||||
*/
|
||||
class EncryptionStream(
|
||||
private var outermostStream: OutputStream,
|
||||
private val options: ProducerOptions,
|
||||
private val api: PGPainless
|
||||
) : OutputStream() {
|
||||
|
||||
private val resultBuilder: EncryptionResult.Builder = EncryptionResult.builder()
|
||||
|
@ -62,12 +65,10 @@ class EncryptionStream(
|
|||
|
||||
private fun prepareArmor() {
|
||||
if (!options.isAsciiArmor) {
|
||||
LOGGER.debug("Output will be unarmored.")
|
||||
return
|
||||
}
|
||||
|
||||
outermostStream = BufferedOutputStream(outermostStream)
|
||||
LOGGER.debug("Wrap encryption output in ASCII armor.")
|
||||
armorOutputStream =
|
||||
ArmoredOutputStreamFactory.get(outermostStream, options).also { outermostStream = it }
|
||||
}
|
||||
|
@ -75,45 +76,43 @@ class EncryptionStream(
|
|||
@Throws(IOException::class, PGPException::class)
|
||||
private fun prepareEncryption() {
|
||||
if (options.encryptionOptions == null) {
|
||||
// No encryption options -> no encryption
|
||||
resultBuilder.setEncryptionAlgorithm(SymmetricKeyAlgorithm.NULL)
|
||||
return
|
||||
}
|
||||
require(options.encryptionOptions.encryptionMethods.isNotEmpty()) {
|
||||
"If EncryptionOptions are provided, at least one encryption method MUST be provided as well."
|
||||
}
|
||||
|
||||
EncryptionBuilder.negotiateSymmetricEncryptionAlgorithm(options.encryptionOptions).let {
|
||||
resultBuilder.setEncryptionAlgorithm(it)
|
||||
LOGGER.debug("Encrypt message using symmetric algorithm $it.")
|
||||
val encryptedDataGenerator =
|
||||
PGPEncryptedDataGenerator(
|
||||
ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(it).apply {
|
||||
setWithIntegrityPacket(true)
|
||||
})
|
||||
options.encryptionOptions.encryptionMethods.forEach { m ->
|
||||
encryptedDataGenerator.addMethod(m)
|
||||
}
|
||||
options.encryptionOptions.encryptionKeyIdentifiers.forEach { r ->
|
||||
resultBuilder.addRecipient(r)
|
||||
}
|
||||
val mechanism: MessageEncryptionMechanism =
|
||||
options.encryptionOptions.negotiateEncryptionMechanism()
|
||||
resultBuilder.setEncryptionMechanism(mechanism)
|
||||
val encryptedDataGenerator =
|
||||
PGPEncryptedDataGenerator(api.implementation.pgpDataEncryptorBuilder(mechanism))
|
||||
|
||||
publicKeyEncryptedStream =
|
||||
encryptedDataGenerator.open(outermostStream, ByteArray(BUFFER_SIZE)).also { stream
|
||||
->
|
||||
outermostStream = stream
|
||||
}
|
||||
options.encryptionOptions.encryptionMethods.forEach { m ->
|
||||
encryptedDataGenerator.addMethod(m)
|
||||
}
|
||||
options.encryptionOptions.encryptionKeyIdentifiers.forEach { r ->
|
||||
resultBuilder.addRecipient(r)
|
||||
}
|
||||
encryptedDataGenerator.setSessionKeyExtractionCallback { pgpSessionKey ->
|
||||
if (pgpSessionKey != null) {
|
||||
resultBuilder.setSessionKey(SessionKey(pgpSessionKey))
|
||||
}
|
||||
}
|
||||
|
||||
publicKeyEncryptedStream =
|
||||
encryptedDataGenerator.open(outermostStream, ByteArray(BUFFER_SIZE)).also { stream ->
|
||||
outermostStream = stream
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun prepareCompression() {
|
||||
EncryptionBuilder.negotiateCompressionAlgorithm(options).let {
|
||||
options.negotiateCompressionAlgorithm(api.algorithmPolicy).let {
|
||||
resultBuilder.setCompressionAlgorithm(it)
|
||||
compressedDataGenerator = PGPCompressedDataGenerator(it.algorithmId)
|
||||
if (it == CompressionAlgorithm.UNCOMPRESSED) return
|
||||
|
||||
LOGGER.debug("Compress using $it.")
|
||||
basicCompressionStream =
|
||||
BCPGOutputStream(compressedDataGenerator!!.open(outermostStream)).also { stream ->
|
||||
outermostStream = stream
|
||||
|
@ -249,8 +248,9 @@ class EncryptionStream(
|
|||
|
||||
options.signingOptions.signingMethods.entries.reversed().forEach { (key, method) ->
|
||||
method.signatureGenerator.generate().let { sig ->
|
||||
val documentSignature = OpenPGPDocumentSignature(sig, key.publicKey)
|
||||
if (method.isDetached) {
|
||||
resultBuilder.addDetachedSignature(key, sig)
|
||||
resultBuilder.addDetachedSignature(documentSignature)
|
||||
}
|
||||
if (!method.isDetached || options.isCleartextSigned) {
|
||||
sig.encode(signatureLayerStream)
|
||||
|
@ -266,8 +266,4 @@ class EncryptionStream(
|
|||
|
||||
val isClosed
|
||||
get() = closed
|
||||
|
||||
companion object {
|
||||
@JvmStatic private val LOGGER = LoggerFactory.getLogger(EncryptionStream::class.java)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.encryption_signing
|
||||
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate
|
||||
import org.bouncycastle.openpgp.api.OpenPGPSignature
|
||||
|
||||
class OpenPGPSignatureSet<S : OpenPGPSignature>(val signatures: List<S>) : Iterable<S> {
|
||||
|
||||
fun getSignaturesBy(cert: OpenPGPCertificate): List<S> =
|
||||
signatures.filter { sig -> sig.signature.keyIdentifiers.any { cert.getKey(it) != null } }
|
||||
|
||||
fun getSignaturesBy(componentKey: OpenPGPCertificate.OpenPGPComponentKey): List<S> =
|
||||
signatures.filter { sig ->
|
||||
sig.signature.keyIdentifiers.any { componentKey.keyIdentifier.matchesExplicit(it) }
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<S> {
|
||||
return signatures.iterator()
|
||||
}
|
||||
}
|
|
@ -6,16 +6,17 @@ package org.pgpainless.encryption_signing
|
|||
|
||||
import java.util.*
|
||||
import org.bouncycastle.openpgp.PGPLiteralData
|
||||
import org.pgpainless.PGPainless
|
||||
import org.pgpainless.algorithm.CompressionAlgorithm
|
||||
import org.pgpainless.algorithm.StreamEncoding
|
||||
import org.pgpainless.algorithm.negotiation.CompressionAlgorithmNegotiator
|
||||
import org.pgpainless.policy.Policy
|
||||
|
||||
class ProducerOptions
|
||||
private constructor(
|
||||
class ProducerOptions(
|
||||
val encryptionOptions: EncryptionOptions?,
|
||||
val signingOptions: SigningOptions?
|
||||
) {
|
||||
|
||||
var compressionAlgorithmNegotiator: CompressionAlgorithmNegotiator =
|
||||
CompressionAlgorithmNegotiator.staticNegotiation()
|
||||
private var _fileName: String = ""
|
||||
private var _modificationDate: Date = PGPLiteralData.NOW
|
||||
private var encodingField: StreamEncoding = StreamEncoding.BINARY
|
||||
|
@ -24,8 +25,8 @@ private constructor(
|
|||
private var _hideArmorHeaders = false
|
||||
var isDisableAsciiArmorCRC = false
|
||||
|
||||
private var _compressionAlgorithmOverride: CompressionAlgorithm =
|
||||
PGPainless.getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm
|
||||
private var _compressionAlgorithmOverride: CompressionAlgorithm? = null
|
||||
|
||||
private var asciiArmor = true
|
||||
private var _comment: String? = null
|
||||
private var _version: String? = null
|
||||
|
@ -104,6 +105,13 @@ private constructor(
|
|||
*/
|
||||
fun hasVersion() = version != null
|
||||
|
||||
/**
|
||||
* Configure the resulting OpenPGP message to make use of the Cleartext Signature Framework
|
||||
* (CSF). A CSF message MUST be signed using detached signatures only and MUST NOT be encrypted.
|
||||
*
|
||||
* @see
|
||||
* [RFC9580: OpenPGP - Cleartext Signature Framework](https://www.rfc-editor.org/rfc/rfc9580.html#name-cleartext-signature-framewo)
|
||||
*/
|
||||
fun setCleartextSigned() = apply {
|
||||
require(signingOptions != null) {
|
||||
"Signing Options cannot be null if cleartext signing is enabled."
|
||||
|
@ -174,8 +182,8 @@ private constructor(
|
|||
*
|
||||
* @param encoding encoding
|
||||
* @return this
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.9">RFC4880 §5.9.
|
||||
* Literal Data Packet</a>
|
||||
* @see
|
||||
* [RFC4880 §5.9. Literal Data Packet](https://datatracker.ietf.org/doc/html/rfc4880#section-5.9)
|
||||
* @deprecated options other than the default value of [StreamEncoding.BINARY] are discouraged.
|
||||
*/
|
||||
@Deprecated("Options other than BINARY are discouraged.")
|
||||
|
@ -212,7 +220,7 @@ private constructor(
|
|||
_compressionAlgorithmOverride = compressionAlgorithm
|
||||
}
|
||||
|
||||
val compressionAlgorithmOverride: CompressionAlgorithm
|
||||
val compressionAlgorithmOverride: CompressionAlgorithm?
|
||||
get() = _compressionAlgorithmOverride
|
||||
|
||||
val isHideArmorHeaders: Boolean
|
||||
|
@ -230,6 +238,11 @@ private constructor(
|
|||
_hideArmorHeaders = hideArmorHeaders
|
||||
}
|
||||
|
||||
internal fun negotiateCompressionAlgorithm(policy: Policy): CompressionAlgorithm {
|
||||
return compressionAlgorithmNegotiator.negotiate(
|
||||
policy, compressionAlgorithmOverride, setOf())
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Sign and encrypt some data.
|
||||
|
@ -239,8 +252,10 @@ private constructor(
|
|||
* @return builder
|
||||
*/
|
||||
@JvmStatic
|
||||
fun signAndEncrypt(encryptionOptions: EncryptionOptions, signingOptions: SigningOptions) =
|
||||
ProducerOptions(encryptionOptions, signingOptions)
|
||||
fun signAndEncrypt(
|
||||
encryptionOptions: EncryptionOptions,
|
||||
signingOptions: SigningOptions
|
||||
): ProducerOptions = ProducerOptions(encryptionOptions, signingOptions)
|
||||
|
||||
/**
|
||||
* Sign some data without encryption.
|
||||
|
@ -248,7 +263,9 @@ private constructor(
|
|||
* @param signingOptions signing options
|
||||
* @return builder
|
||||
*/
|
||||
@JvmStatic fun sign(signingOptions: SigningOptions) = ProducerOptions(null, signingOptions)
|
||||
@JvmStatic
|
||||
fun sign(signingOptions: SigningOptions): ProducerOptions =
|
||||
ProducerOptions(null, signingOptions)
|
||||
|
||||
/**
|
||||
* Encrypt some data without signing.
|
||||
|
@ -257,13 +274,14 @@ private constructor(
|
|||
* @return builder
|
||||
*/
|
||||
@JvmStatic
|
||||
fun encrypt(encryptionOptions: EncryptionOptions) = ProducerOptions(encryptionOptions, null)
|
||||
fun encrypt(encryptionOptions: EncryptionOptions): ProducerOptions =
|
||||
ProducerOptions(encryptionOptions, null)
|
||||
|
||||
/**
|
||||
* Only wrap the data in an OpenPGP packet. No encryption or signing will be applied.
|
||||
*
|
||||
* @return builder
|
||||
*/
|
||||
@JvmStatic fun noEncryptionNoSigning() = ProducerOptions(null, null)
|
||||
@JvmStatic fun noEncryptionNoSigning(): ProducerOptions = ProducerOptions(null, null)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,28 +5,32 @@
|
|||
package org.pgpainless.encryption_signing
|
||||
|
||||
import java.util.*
|
||||
import org.bouncycastle.bcpg.KeyIdentifier
|
||||
import org.bouncycastle.openpgp.*
|
||||
import org.pgpainless.PGPainless.Companion.getPolicy
|
||||
import org.pgpainless.PGPainless.Companion.inspectKeyRing
|
||||
import org.bouncycastle.openpgp.api.OpenPGPImplementation
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPPrivateKey
|
||||
import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPSecretKey
|
||||
import org.pgpainless.PGPainless
|
||||
import org.pgpainless.algorithm.DocumentSignatureType
|
||||
import org.pgpainless.algorithm.HashAlgorithm
|
||||
import org.pgpainless.algorithm.PublicKeyAlgorithm.Companion.requireFromId
|
||||
import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator
|
||||
import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator.Companion.negotiateSignatureHashAlgorithm
|
||||
import org.pgpainless.bouncycastle.extensions.unlock
|
||||
import org.pgpainless.exception.KeyException
|
||||
import org.pgpainless.exception.KeyException.*
|
||||
import org.pgpainless.implementation.ImplementationFactory
|
||||
import org.pgpainless.key.OpenPgpFingerprint.Companion.of
|
||||
import org.pgpainless.key.SubkeyIdentifier
|
||||
import org.pgpainless.key.protection.SecretKeyRingProtector
|
||||
import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey
|
||||
import org.pgpainless.policy.Policy
|
||||
import org.pgpainless.signature.subpackets.BaseSignatureSubpackets.Callback
|
||||
import org.pgpainless.signature.subpackets.SignatureSubpackets
|
||||
import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper
|
||||
|
||||
class SigningOptions {
|
||||
|
||||
val signingMethods: Map<SubkeyIdentifier, SigningMethod> = mutableMapOf()
|
||||
class SigningOptions(private val api: PGPainless) {
|
||||
var hashAlgorithmNegotiator: HashAlgorithmNegotiator =
|
||||
negotiateSignatureHashAlgorithm(api.algorithmPolicy)
|
||||
val signingMethods: Map<OpenPGPPrivateKey, SigningMethod> = mutableMapOf()
|
||||
private var _hashAlgorithmOverride: HashAlgorithm? = null
|
||||
private var _evaluationDate: Date = Date()
|
||||
|
||||
|
@ -46,6 +50,7 @@ class SigningOptions {
|
|||
_hashAlgorithmOverride = hashAlgorithmOverride
|
||||
}
|
||||
|
||||
/** Evaluation date for signing keys. */
|
||||
val evaluationDate: Date
|
||||
get() = _evaluationDate
|
||||
|
||||
|
@ -61,17 +66,34 @@ class SigningOptions {
|
|||
* Sign the message using an inline signature made by the provided signing key.
|
||||
*
|
||||
* @param signingKeyProtector protector to unlock the signing key
|
||||
* @param signingKey key ring containing the signing key
|
||||
* @param signingKey OpenPGPKey containing the signing (sub-)key.
|
||||
* @return this
|
||||
* @throws KeyException if something is wrong with the key
|
||||
* @throws PGPException if the key cannot be unlocked or a signing method cannot be created
|
||||
*/
|
||||
@Throws(KeyException::class, PGPException::class)
|
||||
fun addSignature(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKey: OpenPGPKey
|
||||
): SigningOptions = apply {
|
||||
addInlineSignature(
|
||||
signingKeyProtector, signingKey, null, DocumentSignatureType.BINARY_DOCUMENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the message using an inline signature made by the provided signing key.
|
||||
*
|
||||
* @param signingKeyProtector protector to unlock the signing key
|
||||
* @param signingKey key ring containing the signing key
|
||||
* @return this
|
||||
* @throws KeyException if something is wrong with the key
|
||||
* @throws PGPException if the key cannot be unlocked or a signing method cannot be created
|
||||
*/
|
||||
@Deprecated("Pass an OpenPGPKey instead.")
|
||||
@Throws(KeyException::class, PGPException::class)
|
||||
// TODO: Remove in 2.1
|
||||
fun addSignature(signingKeyProtector: SecretKeyRingProtector, signingKey: PGPSecretKeyRing) =
|
||||
apply {
|
||||
addInlineSignature(
|
||||
signingKeyProtector, signingKey, null, DocumentSignatureType.BINARY_DOCUMENT)
|
||||
}
|
||||
addSignature(signingKeyProtector, api.toKey(signingKey))
|
||||
|
||||
/**
|
||||
* Add inline signatures with all secret key rings in the provided secret key ring collection.
|
||||
|
@ -85,6 +107,8 @@ class SigningOptions {
|
|||
* created
|
||||
*/
|
||||
@Throws(KeyException::class, PGPException::class)
|
||||
@Deprecated("Repeatedly call addInlineSignature(), passing an OpenPGPKey instead.")
|
||||
// TODO: Remove in 2.1
|
||||
fun addInlineSignatures(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKeys: Iterable<PGPSecretKeyRing>,
|
||||
|
@ -93,6 +117,23 @@ class SigningOptions {
|
|||
signingKeys.forEach { addInlineSignature(signingKeyProtector, it, null, signatureType) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Add inline signatures with the provided [signingKey].
|
||||
*
|
||||
* @param signingKeyProtector decryptor to unlock the signing secret keys
|
||||
* @param signingKey OpenPGP key
|
||||
* @param signatureType type of signature (binary, canonical text)
|
||||
* @return this
|
||||
* @throws KeyException if something is wrong with any of the keys
|
||||
* @throws PGPException if any of the keys cannot be unlocked or a signing method cannot be
|
||||
* created
|
||||
*/
|
||||
fun addInlineSignature(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKey: OpenPGPKey,
|
||||
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT
|
||||
): SigningOptions = addInlineSignature(signingKeyProtector, signingKey, null, signatureType)
|
||||
|
||||
/**
|
||||
* Add an inline-signature. Inline signatures are being embedded into the message itself and can
|
||||
* be processed in one pass, thanks to the use of one-pass-signature packets.
|
||||
|
@ -105,11 +146,13 @@ class SigningOptions {
|
|||
* @throws PGPException if the key cannot be unlocked or the signing method cannot be created
|
||||
*/
|
||||
@Throws(KeyException::class, PGPException::class)
|
||||
@Deprecated("Pass an OpenPGPKey instead.")
|
||||
// TODO: Remove in 2.1
|
||||
fun addInlineSignature(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKey: PGPSecretKeyRing,
|
||||
signatureType: DocumentSignatureType
|
||||
) = apply { addInlineSignature(signingKeyProtector, signingKey, null, signatureType) }
|
||||
) = addInlineSignature(signingKeyProtector, api.toKey(signingKey), signatureType)
|
||||
|
||||
/**
|
||||
* Add an inline-signature. Inline signatures are being embedded into the message itself and can
|
||||
|
@ -128,16 +171,15 @@ class SigningOptions {
|
|||
* @throws KeyException if the key is invalid
|
||||
* @throws PGPException if the key cannot be unlocked or the signing method cannot be created
|
||||
*/
|
||||
@Throws(KeyException::class, PGPException::class)
|
||||
@JvmOverloads
|
||||
fun addInlineSignature(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKey: PGPSecretKeyRing,
|
||||
signingKey: OpenPGPKey,
|
||||
userId: CharSequence? = null,
|
||||
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
|
||||
subpacketsCallback: Callback? = null
|
||||
) = apply {
|
||||
val keyRingInfo = inspectKeyRing(signingKey, evaluationDate)
|
||||
val keyRingInfo = api.inspect(signingKey, evaluationDate)
|
||||
if (userId != null && !keyRingInfo.isUserIdValid(userId)) {
|
||||
throw UnboundUserIdException(
|
||||
of(signingKey),
|
||||
|
@ -148,23 +190,89 @@ class SigningOptions {
|
|||
|
||||
val signingPubKeys = keyRingInfo.signingSubkeys
|
||||
if (signingPubKeys.isEmpty()) {
|
||||
throw UnacceptableSigningKeyException(of(signingKey))
|
||||
throw UnacceptableSigningKeyException(signingKey)
|
||||
}
|
||||
|
||||
for (signingPubKey in signingPubKeys) {
|
||||
val signingSecKey: PGPSecretKey =
|
||||
signingKey.getSecretKey(signingPubKey.keyID)
|
||||
?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID)
|
||||
val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector)
|
||||
val signingSecKey: OpenPGPSecretKey =
|
||||
signingKey.getSecretKey(signingPubKey)
|
||||
?: throw MissingSecretKeyException(signingPubKey)
|
||||
val signingPrivKey: OpenPGPPrivateKey =
|
||||
unlockSecretKey(signingSecKey, signingKeyProtector)
|
||||
val hashAlgorithms =
|
||||
if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId)
|
||||
else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID)
|
||||
val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy())
|
||||
else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyIdentifier)
|
||||
val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms)
|
||||
addSigningMethod(
|
||||
signingKey, signingSubkey, hashAlgorithm, signatureType, false, subpacketsCallback)
|
||||
signingPrivKey, hashAlgorithm, signatureType, false, subpacketsCallback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an inline-signature. Inline signatures are being embedded into the message itself and can
|
||||
* be processed in one pass, thanks to the use of one-pass-signature packets.
|
||||
*
|
||||
* <p>
|
||||
* This method uses the passed in user-id to select user-specific hash algorithms.
|
||||
*
|
||||
* @param signingKeyProtector decryptor to unlock the signing secret key
|
||||
* @param signingKey signing key
|
||||
* @param userId user-id of the signer
|
||||
* @param signatureType signature type (binary, canonical text)
|
||||
* @param subpacketsCallback callback to modify the hashed and unhashed subpackets of the
|
||||
* signature
|
||||
* @return this
|
||||
* @throws KeyException if the key is invalid
|
||||
* @throws PGPException if the key cannot be unlocked or the signing method cannot be created
|
||||
*/
|
||||
@Deprecated("Pass an OpenPGPKey instead.")
|
||||
@Throws(KeyException::class, PGPException::class)
|
||||
@JvmOverloads
|
||||
// TODO: Remove in 2.1
|
||||
fun addInlineSignature(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKey: PGPSecretKeyRing,
|
||||
userId: CharSequence? = null,
|
||||
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
|
||||
subpacketsCallback: Callback? = null
|
||||
) =
|
||||
addInlineSignature(
|
||||
signingKeyProtector, api.toKey(signingKey), userId, signatureType, subpacketsCallback)
|
||||
|
||||
/**
|
||||
* Create an inline signature using the given signing component key (e.g. a specific subkey).
|
||||
*
|
||||
* @param signingKeyProtector decryptor to unlock the secret key
|
||||
* @param signingKey signing component key
|
||||
* @param signatureType signature type
|
||||
* @param subpacketsCallback callback to modify the signatures subpackets
|
||||
* @return builder
|
||||
* @throws PGPException if the secret key cannot be unlocked or if no signing method can be
|
||||
* created.
|
||||
*/
|
||||
fun addInlineSignature(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKey: OpenPGPSecretKey,
|
||||
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
|
||||
subpacketsCallback: Callback? = null
|
||||
): SigningOptions = apply {
|
||||
val openPGPKey = signingKey.openPGPKey
|
||||
val keyRingInfo = api.inspect(openPGPKey, evaluationDate)
|
||||
val signingPubKeys = keyRingInfo.signingSubkeys
|
||||
if (signingPubKeys.isEmpty()) {
|
||||
throw UnacceptableSigningKeyException(openPGPKey)
|
||||
}
|
||||
|
||||
if (!signingPubKeys.any { it.keyIdentifier.matchesExplicit(signingKey.keyIdentifier) }) {
|
||||
throw MissingSecretKeyException(signingKey)
|
||||
}
|
||||
|
||||
val signingPrivKey = unlockSecretKey(signingKey, signingKeyProtector)
|
||||
val hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingKey.keyIdentifier)
|
||||
val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms)
|
||||
addSigningMethod(signingPrivKey, hashAlgorithm, signatureType, false, subpacketsCallback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an inline signature using the signing key with the given keyId.
|
||||
*
|
||||
|
@ -183,35 +291,23 @@ class SigningOptions {
|
|||
*/
|
||||
@Throws(KeyException::class, PGPException::class)
|
||||
@JvmOverloads
|
||||
@Deprecated("Pass in an OpenPGPSecretKey instead.")
|
||||
// TODO: Remove in 2.1
|
||||
fun addInlineSignature(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKey: PGPSecretKeyRing,
|
||||
keyId: Long,
|
||||
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
|
||||
subpacketsCallback: Callback? = null
|
||||
) = apply {
|
||||
val keyRingInfo = inspectKeyRing(signingKey, evaluationDate)
|
||||
val signingPubKeys = keyRingInfo.signingSubkeys
|
||||
if (signingPubKeys.isEmpty()) {
|
||||
throw UnacceptableSigningKeyException(of(signingKey))
|
||||
}
|
||||
|
||||
for (signingPubKey in signingPubKeys) {
|
||||
if (signingPubKey.keyID != keyId) {
|
||||
continue
|
||||
}
|
||||
|
||||
val signingSecKey =
|
||||
signingKey.getSecretKey(signingPubKey.keyID)
|
||||
?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID)
|
||||
val signingSubkey = signingSecKey.unlock(signingKeyProtector)
|
||||
val hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID)
|
||||
val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy())
|
||||
addSigningMethod(
|
||||
signingKey, signingSubkey, hashAlgorithm, signatureType, false, subpacketsCallback)
|
||||
return this
|
||||
}
|
||||
throw MissingSecretKeyException(of(signingKey), keyId)
|
||||
): SigningOptions {
|
||||
val key = api.toKey(signingKey)
|
||||
val subkeyIdentifier = KeyIdentifier(keyId)
|
||||
return addInlineSignature(
|
||||
signingKeyProtector,
|
||||
key.getSecretKey(subkeyIdentifier)
|
||||
?: throw MissingSecretKeyException(of(signingKey), subkeyIdentifier),
|
||||
signatureType,
|
||||
subpacketsCallback)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -226,6 +322,8 @@ class SigningOptions {
|
|||
* method cannot be created
|
||||
*/
|
||||
@Throws(KeyException::class, PGPException::class)
|
||||
@Deprecated("Repeatedly call addDetachedSignature(), passing an OpenPGPKey instead.")
|
||||
// TODO: Remove in 2.1
|
||||
fun addDetachedSignatures(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKeys: Iterable<PGPSecretKeyRing>,
|
||||
|
@ -234,6 +332,12 @@ class SigningOptions {
|
|||
signingKeys.forEach { addDetachedSignature(signingKeyProtector, it, null, signatureType) }
|
||||
}
|
||||
|
||||
fun addDetachedSignature(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKey: OpenPGPKey,
|
||||
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT
|
||||
): SigningOptions = addDetachedSignature(signingKeyProtector, signingKey, null, signatureType)
|
||||
|
||||
/**
|
||||
* Create a detached signature. Detached signatures are not being added into the PGP message
|
||||
* itself. Instead, they can be distributed separately to the message. Detached signatures are
|
||||
|
@ -247,7 +351,9 @@ class SigningOptions {
|
|||
* @throws PGPException if the key cannot be validated or unlocked, or if no signature method
|
||||
* can be created
|
||||
*/
|
||||
@Deprecated("Pass an OpenPGPKey instead.")
|
||||
@Throws(KeyException::class, PGPException::class)
|
||||
// TODO: Remove in 2.1
|
||||
fun addDetachedSignature(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKey: PGPSecretKeyRing,
|
||||
|
@ -273,15 +379,14 @@ class SigningOptions {
|
|||
* can be created
|
||||
*/
|
||||
@JvmOverloads
|
||||
@Throws(KeyException::class, PGPException::class)
|
||||
fun addDetachedSignature(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKey: PGPSecretKeyRing,
|
||||
userId: String? = null,
|
||||
signingKey: OpenPGPKey,
|
||||
userId: CharSequence? = null,
|
||||
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
|
||||
subpacketCallback: Callback? = null
|
||||
) = apply {
|
||||
val keyRingInfo = inspectKeyRing(signingKey, evaluationDate)
|
||||
): SigningOptions = apply {
|
||||
val keyRingInfo = api.inspect(signingKey, evaluationDate)
|
||||
if (userId != null && !keyRingInfo.isUserIdValid(userId)) {
|
||||
throw UnboundUserIdException(
|
||||
of(signingKey),
|
||||
|
@ -292,23 +397,80 @@ class SigningOptions {
|
|||
|
||||
val signingPubKeys = keyRingInfo.signingSubkeys
|
||||
if (signingPubKeys.isEmpty()) {
|
||||
throw UnacceptableSigningKeyException(of(signingKey))
|
||||
throw UnacceptableSigningKeyException(signingKey)
|
||||
}
|
||||
|
||||
for (signingPubKey in signingPubKeys) {
|
||||
val signingSecKey: PGPSecretKey =
|
||||
signingKey.getSecretKey(signingPubKey.keyID)
|
||||
?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID)
|
||||
val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector)
|
||||
val hashAlgorithms =
|
||||
if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId)
|
||||
else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID)
|
||||
val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy())
|
||||
addSigningMethod(
|
||||
signingKey, signingSubkey, hashAlgorithm, signatureType, true, subpacketCallback)
|
||||
val signingSecKey: OpenPGPSecretKey =
|
||||
signingKey.getSecretKey(signingPubKey.keyIdentifier)
|
||||
?: throw MissingSecretKeyException(signingPubKey)
|
||||
addDetachedSignature(
|
||||
signingKeyProtector, signingSecKey, userId, signatureType, subpacketCallback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a detached signature. Detached signatures are not being added into the PGP message
|
||||
* itself. Instead, they can be distributed separately to the message. Detached signatures are
|
||||
* useful if the data that is being signed shall not be modified (e.g. when signing a file).
|
||||
*
|
||||
* <p>
|
||||
* This method uses the passed in user-id to select user-specific hash algorithms.
|
||||
*
|
||||
* @param signingKeyProtector decryptor to unlock the secret signing key
|
||||
* @param signingKey signing key
|
||||
* @param userId user-id
|
||||
* @param signatureType type of data that is signed (binary, canonical text)
|
||||
* @param subpacketCallback callback to modify hashed and unhashed subpackets of the signature
|
||||
* @return this
|
||||
* @throws KeyException if something is wrong with the key
|
||||
* @throws PGPException if the key cannot be validated or unlocked, or if no signature method
|
||||
* can be created
|
||||
*/
|
||||
@Deprecated("Pass an OpenPGPKey instead.")
|
||||
@JvmOverloads
|
||||
@Throws(KeyException::class, PGPException::class)
|
||||
// TODO: Remove in 2.1
|
||||
fun addDetachedSignature(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKey: PGPSecretKeyRing,
|
||||
userId: String? = null,
|
||||
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
|
||||
subpacketCallback: Callback? = null
|
||||
) =
|
||||
addDetachedSignature(
|
||||
signingKeyProtector, api.toKey(signingKey), userId, signatureType, subpacketCallback)
|
||||
|
||||
/**
|
||||
* Create a detached signature. Detached signatures are not being added into the PGP message
|
||||
* itself. Instead, they can be distributed separately to the message. Detached signatures are
|
||||
* useful if the data that is being signed shall not be modified (e.g. when signing a file).
|
||||
* This method creates a signature using the provided [signingKey], taking into consideration
|
||||
* the preferences found on the binding signature of the given [userId].
|
||||
*
|
||||
* @param signingKeyProtector protector to unlock the signing key
|
||||
* @param signingKey OpenPGP key for signing
|
||||
* @param userId user-id to determine algorithm preferences
|
||||
* @param signatureType document signature type
|
||||
* @param subpacketCallback callback to change the subpackets of the signature
|
||||
* @return this
|
||||
*/
|
||||
fun addDetachedSignature(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKey: OpenPGPSecretKey,
|
||||
userId: CharSequence? = null,
|
||||
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
|
||||
subpacketCallback: Callback? = null
|
||||
): SigningOptions = apply {
|
||||
val keyRingInfo = api.inspect(signingKey.openPGPKey, evaluationDate)
|
||||
val signingPrivKey: OpenPGPPrivateKey = signingKey.unlock(signingKeyProtector)
|
||||
val hashAlgorithms =
|
||||
if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId)
|
||||
else keyRingInfo.getPreferredHashAlgorithms(signingKey.keyIdentifier)
|
||||
val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms)
|
||||
addSigningMethod(signingPrivKey, hashAlgorithm, signatureType, true, subpacketCallback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a detached signature using the signing key with the given keyId.
|
||||
*
|
||||
|
@ -327,63 +489,45 @@ class SigningOptions {
|
|||
*/
|
||||
@Throws(KeyException::class, PGPException::class)
|
||||
@JvmOverloads
|
||||
@Deprecated("Pass an OpenPGPSecretKey instead.")
|
||||
// TODO: Remove in 2.1
|
||||
fun addDetachedSignature(
|
||||
signingKeyProtector: SecretKeyRingProtector,
|
||||
signingKey: PGPSecretKeyRing,
|
||||
keyId: Long,
|
||||
signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT,
|
||||
subpacketsCallback: Callback? = null
|
||||
) = apply {
|
||||
val keyRingInfo = inspectKeyRing(signingKey, evaluationDate)
|
||||
|
||||
val signingPubKeys = keyRingInfo.signingSubkeys
|
||||
if (signingPubKeys.isEmpty()) {
|
||||
throw UnacceptableSigningKeyException(of(signingKey))
|
||||
}
|
||||
|
||||
for (signingPubKey in signingPubKeys) {
|
||||
if (signingPubKey.keyID == keyId) {
|
||||
val signingSecKey: PGPSecretKey =
|
||||
signingKey.getSecretKey(signingPubKey.keyID)
|
||||
?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID)
|
||||
val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector)
|
||||
val hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID)
|
||||
val hashAlgorithm: HashAlgorithm =
|
||||
negotiateHashAlgorithm(hashAlgorithms, getPolicy())
|
||||
addSigningMethod(
|
||||
signingKey,
|
||||
signingSubkey,
|
||||
hashAlgorithm,
|
||||
signatureType,
|
||||
true,
|
||||
subpacketsCallback)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
throw MissingSecretKeyException(of(signingKey), keyId)
|
||||
): SigningOptions {
|
||||
val key = api.toKey(signingKey)
|
||||
val signingKeyIdentifier = KeyIdentifier(keyId)
|
||||
return addDetachedSignature(
|
||||
signingKeyProtector,
|
||||
key.getSecretKey(signingKeyIdentifier)
|
||||
?: throw MissingSecretKeyException(of(key), signingKeyIdentifier),
|
||||
null,
|
||||
signatureType,
|
||||
subpacketsCallback)
|
||||
}
|
||||
|
||||
private fun addSigningMethod(
|
||||
signingKey: PGPSecretKeyRing,
|
||||
signingSubkey: PGPPrivateKey,
|
||||
signingKey: OpenPGPPrivateKey,
|
||||
hashAlgorithm: HashAlgorithm,
|
||||
signatureType: DocumentSignatureType,
|
||||
detached: Boolean,
|
||||
subpacketCallback: Callback? = null
|
||||
) {
|
||||
val signingKeyIdentifier = SubkeyIdentifier(signingKey, signingSubkey.keyID)
|
||||
val signingSecretKey: PGPSecretKey = signingKey.getSecretKey(signingSubkey.keyID)
|
||||
val signingSecretKey: PGPSecretKey = signingKey.secretKey.pgpSecretKey
|
||||
val publicKeyAlgorithm = requireFromId(signingSecretKey.publicKey.algorithm)
|
||||
val bitStrength = signingSecretKey.publicKey.bitStrength
|
||||
if (!getPolicy().publicKeyAlgorithmPolicy.isAcceptable(publicKeyAlgorithm, bitStrength)) {
|
||||
if (!api.algorithmPolicy.publicKeyAlgorithmPolicy.isAcceptable(
|
||||
publicKeyAlgorithm, bitStrength)) {
|
||||
throw UnacceptableSigningKeyException(
|
||||
PublicKeyAlgorithmPolicyException(
|
||||
of(signingKey), signingSecretKey.keyID, publicKeyAlgorithm, bitStrength))
|
||||
signingKey.secretKey, publicKeyAlgorithm, bitStrength))
|
||||
}
|
||||
|
||||
val generator: PGPSignatureGenerator =
|
||||
createSignatureGenerator(signingSubkey, hashAlgorithm, signatureType)
|
||||
createSignatureGenerator(signingKey.keyPair, hashAlgorithm, signatureType)
|
||||
|
||||
// Subpackets
|
||||
val hashedSubpackets =
|
||||
|
@ -399,7 +543,7 @@ class SigningOptions {
|
|||
val signingMethod =
|
||||
if (detached) SigningMethod.detachedSignature(generator, hashAlgorithm)
|
||||
else SigningMethod.inlineSignature(generator, hashAlgorithm)
|
||||
(signingMethods as MutableMap)[signingKeyIdentifier] = signingMethod
|
||||
(signingMethods as MutableMap)[signingKey] = signingMethod
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -414,32 +558,29 @@ class SigningOptions {
|
|||
* @param policy policy
|
||||
* @return selected hash algorithm
|
||||
*/
|
||||
private fun negotiateHashAlgorithm(
|
||||
preferences: Set<HashAlgorithm>,
|
||||
policy: Policy
|
||||
): HashAlgorithm {
|
||||
return _hashAlgorithmOverride
|
||||
?: negotiateSignatureHashAlgorithm(policy).negotiateHashAlgorithm(preferences)
|
||||
private fun negotiateHashAlgorithm(preferences: Set<HashAlgorithm>?): HashAlgorithm {
|
||||
return _hashAlgorithmOverride ?: hashAlgorithmNegotiator.negotiateHashAlgorithm(preferences)
|
||||
}
|
||||
|
||||
@Throws(PGPException::class)
|
||||
private fun createSignatureGenerator(
|
||||
privateKey: PGPPrivateKey,
|
||||
signingKey: PGPKeyPair,
|
||||
hashAlgorithm: HashAlgorithm,
|
||||
signatureType: DocumentSignatureType
|
||||
): PGPSignatureGenerator {
|
||||
return ImplementationFactory.getInstance()
|
||||
.getPGPContentSignerBuilder(
|
||||
privateKey.publicKeyPacket.algorithm, hashAlgorithm.algorithmId)
|
||||
return OpenPGPImplementation.getInstance()
|
||||
.pgpContentSignerBuilder(signingKey.publicKey.algorithm, hashAlgorithm.algorithmId)
|
||||
.let { csb ->
|
||||
PGPSignatureGenerator(csb).also {
|
||||
it.init(signatureType.signatureType.code, privateKey)
|
||||
PGPSignatureGenerator(csb, signingKey.publicKey).also {
|
||||
it.init(signatureType.signatureType.code, signingKey.privateKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun get() = SigningOptions()
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun get(api: PGPainless = PGPainless.getInstance()) = SigningOptions(api)
|
||||
}
|
||||
|
||||
/** A method of signing. */
|
||||
|
|
|
@ -0,0 +1,207 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception
|
||||
|
||||
import java.util.*
|
||||
import javax.annotation.Nonnull
|
||||
import org.bouncycastle.bcpg.KeyIdentifier
|
||||
import org.bouncycastle.openpgp.PGPSignature
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate
|
||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey
|
||||
import org.pgpainless.algorithm.PublicKeyAlgorithm
|
||||
import org.pgpainless.key.OpenPgpFingerprint
|
||||
import org.pgpainless.key.OpenPgpFingerprint.Companion.of
|
||||
import org.pgpainless.util.DateUtil.Companion.formatUTCDate
|
||||
|
||||
abstract class KeyException : RuntimeException {
|
||||
|
||||
val fingerprint: OpenPgpFingerprint
|
||||
|
||||
protected constructor(message: String, fingerprint: OpenPgpFingerprint) : super(message) {
|
||||
this.fingerprint = fingerprint
|
||||
}
|
||||
|
||||
protected constructor(
|
||||
message: String,
|
||||
fingerprint: OpenPgpFingerprint,
|
||||
underlying: Throwable
|
||||
) : super(message, underlying) {
|
||||
this.fingerprint = fingerprint
|
||||
}
|
||||
|
||||
class ExpiredKeyException(fingerprint: OpenPgpFingerprint, expirationDate: Date) :
|
||||
KeyException(
|
||||
"Key $fingerprint is expired. Expiration date: ${formatUTCDate(expirationDate)}",
|
||||
fingerprint,
|
||||
) {
|
||||
|
||||
constructor(cert: OpenPGPCertificate, expirationDate: Date) : this(of(cert), expirationDate)
|
||||
}
|
||||
|
||||
class RevokedKeyException : KeyException {
|
||||
constructor(
|
||||
fingerprint: OpenPgpFingerprint
|
||||
) : super(
|
||||
"Key $fingerprint appears to be revoked.",
|
||||
fingerprint,
|
||||
)
|
||||
|
||||
constructor(
|
||||
componentKey: OpenPGPComponentKey
|
||||
) : super(
|
||||
"Subkey ${componentKey.keyIdentifier} appears to be revoked.",
|
||||
of(componentKey),
|
||||
)
|
||||
|
||||
constructor(
|
||||
cert: OpenPGPCertificate
|
||||
) : super(
|
||||
"Key or certificate ${cert.keyIdentifier} appears to be revoked.",
|
||||
of(cert),
|
||||
)
|
||||
}
|
||||
|
||||
class UnacceptableEncryptionKeyException : KeyException {
|
||||
constructor(cert: OpenPGPCertificate) : this(of(cert))
|
||||
|
||||
constructor(
|
||||
subkey: OpenPGPComponentKey
|
||||
) : super(
|
||||
"Subkey ${subkey.keyIdentifier} is not an acceptable encryption key.",
|
||||
of(subkey),
|
||||
)
|
||||
|
||||
constructor(
|
||||
fingerprint: OpenPgpFingerprint
|
||||
) : super("Key $fingerprint has no acceptable encryption key.", fingerprint)
|
||||
|
||||
constructor(
|
||||
reason: PublicKeyAlgorithmPolicyException
|
||||
) : super(
|
||||
"Key ${reason.fingerprint} has no acceptable encryption key.",
|
||||
reason.fingerprint,
|
||||
reason)
|
||||
}
|
||||
|
||||
class UnacceptableSigningKeyException : KeyException {
|
||||
constructor(cert: OpenPGPCertificate) : this(of(cert))
|
||||
|
||||
constructor(subkey: OpenPGPComponentKey) : this(of(subkey))
|
||||
|
||||
constructor(
|
||||
fingerprint: OpenPgpFingerprint
|
||||
) : super("Key $fingerprint has no acceptable signing key.", fingerprint)
|
||||
|
||||
constructor(
|
||||
reason: KeyException.PublicKeyAlgorithmPolicyException
|
||||
) : super(
|
||||
"Key ${reason.fingerprint} has no acceptable signing key.", reason.fingerprint, reason)
|
||||
}
|
||||
|
||||
class UnacceptableThirdPartyCertificationKeyException(fingerprint: OpenPgpFingerprint) :
|
||||
KeyException("Key $fingerprint has no acceptable certification key.", fingerprint) {}
|
||||
|
||||
class UnacceptableSelfSignatureException : KeyException {
|
||||
constructor(cert: OpenPGPCertificate) : this(of(cert))
|
||||
|
||||
constructor(
|
||||
fingerprint: OpenPgpFingerprint
|
||||
) : super(
|
||||
"Key $fingerprint does not have a valid/acceptable signature to derive an expiration date from.",
|
||||
fingerprint,
|
||||
)
|
||||
}
|
||||
|
||||
class MissingSecretKeyException : KeyException {
|
||||
val missingSecretKeyIdentifier: KeyIdentifier
|
||||
|
||||
constructor(
|
||||
publicKey: OpenPGPComponentKey
|
||||
) : this(
|
||||
of(publicKey.certificate),
|
||||
publicKey.keyIdentifier,
|
||||
)
|
||||
|
||||
constructor(
|
||||
fingerprint: OpenPgpFingerprint,
|
||||
keyIdentifier: KeyIdentifier
|
||||
) : super(
|
||||
"Key $fingerprint does not contain a secret key for public key $keyIdentifier",
|
||||
fingerprint,
|
||||
) {
|
||||
missingSecretKeyIdentifier = keyIdentifier
|
||||
}
|
||||
|
||||
@Deprecated("Pass in a KeyIdentifier instead.")
|
||||
constructor(
|
||||
fingerprint: OpenPgpFingerprint,
|
||||
keyId: Long
|
||||
) : this(fingerprint, KeyIdentifier(keyId))
|
||||
}
|
||||
|
||||
class PublicKeyAlgorithmPolicyException : KeyException {
|
||||
val violatingSubkeyId: KeyIdentifier
|
||||
|
||||
constructor(
|
||||
subkey: OpenPGPComponentKey,
|
||||
algorithm: PublicKeyAlgorithm,
|
||||
bitSize: Int
|
||||
) : super(
|
||||
"""Subkey ${subkey.keyIdentifier} of key ${subkey.certificate.keyIdentifier} is violating the Public Key Algorithm Policy:
|
||||
$algorithm of size $bitSize is not acceptable.""",
|
||||
of(subkey),
|
||||
) {
|
||||
this.violatingSubkeyId = subkey.keyIdentifier
|
||||
}
|
||||
|
||||
constructor(
|
||||
fingerprint: OpenPgpFingerprint,
|
||||
keyId: Long,
|
||||
algorithm: PublicKeyAlgorithm,
|
||||
bitSize: Int
|
||||
) : super(
|
||||
"""Subkey ${java.lang.Long.toHexString(keyId)} of key $fingerprint is violating the Public Key Algorithm Policy:
|
||||
$algorithm of size $bitSize is not acceptable.""",
|
||||
fingerprint,
|
||||
) {
|
||||
this.violatingSubkeyId = KeyIdentifier(keyId)
|
||||
}
|
||||
}
|
||||
|
||||
class UnboundUserIdException(
|
||||
fingerprint: OpenPgpFingerprint,
|
||||
userId: String,
|
||||
userIdSignature: PGPSignature?,
|
||||
userIdRevocation: PGPSignature?
|
||||
) :
|
||||
KeyException(
|
||||
errorMessage(
|
||||
fingerprint,
|
||||
userId,
|
||||
userIdSignature,
|
||||
userIdRevocation,
|
||||
),
|
||||
fingerprint,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private fun errorMessage(
|
||||
@Nonnull fingerprint: OpenPgpFingerprint,
|
||||
@Nonnull userId: String,
|
||||
userIdSignature: PGPSignature?,
|
||||
userIdRevocation: PGPSignature?
|
||||
): String {
|
||||
val errorMessage = "UserID '$userId' is not valid for key $fingerprint: "
|
||||
if (userIdSignature == null) {
|
||||
return errorMessage + "Missing binding signature."
|
||||
}
|
||||
if (userIdRevocation != null) {
|
||||
return errorMessage + "UserID is revoked."
|
||||
}
|
||||
return errorMessage + "Unacceptable binding signature."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception
|
||||
|
||||
import java.lang.AssertionError
|
||||
|
||||
/**
|
||||
* This exception gets thrown, when the integrity of an OpenPGP key is broken. That could happen on
|
||||
* accident, or during an active attack, so take this exception seriously.
|
||||
*/
|
||||
class KeyIntegrityException : AssertionError("Key Integrity Exception")
|
|
@ -0,0 +1,29 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception
|
||||
|
||||
import org.pgpainless.decryption_verification.syntax_check.InputSymbol
|
||||
import org.pgpainless.decryption_verification.syntax_check.StackSymbol
|
||||
import org.pgpainless.decryption_verification.syntax_check.State
|
||||
|
||||
/**
|
||||
* Exception that gets thrown if the OpenPGP message is malformed. Malformed messages are messages
|
||||
* which do not follow the grammar specified in the RFC.
|
||||
*
|
||||
* @see [RFC4880 §11.3. OpenPGP Messages](https://www.rfc-editor.org/rfc/rfc4880#section-11.3)
|
||||
*/
|
||||
class MalformedOpenPgpMessageException : RuntimeException {
|
||||
constructor(message: String) : super(message)
|
||||
|
||||
constructor(message: String, e: MalformedOpenPgpMessageException) : super(message, e)
|
||||
|
||||
constructor(
|
||||
state: State,
|
||||
input: InputSymbol,
|
||||
stackItem: StackSymbol?
|
||||
) : this(
|
||||
"There is no legal transition from state '$state' for input '$input' when '${stackItem ?: "null"}' is on top of the stack.",
|
||||
)
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException
|
||||
|
||||
class MessageNotIntegrityProtectedException :
|
||||
PGPException(
|
||||
"Message is encrypted using a 'Symmetrically Encrypted Data' (SED) packet, which enables certain types of attacks. " +
|
||||
"A 'Symmetrically Encrypted Integrity Protected' (SEIP) packet should be used instead.",
|
||||
)
|
|
@ -0,0 +1,14 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException
|
||||
|
||||
/**
|
||||
* Exception that is thrown when decryption fails due to a missing decryption key or decryption
|
||||
* passphrase. This can happen when the user does not provide the right set of keys / the right
|
||||
* password when decrypting a message.
|
||||
*/
|
||||
class MissingDecryptionMethodException(message: String) : PGPException(message)
|
|
@ -0,0 +1,15 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception
|
||||
|
||||
import java.util.*
|
||||
import org.bouncycastle.openpgp.PGPException
|
||||
import org.pgpainless.key.SubkeyIdentifier
|
||||
|
||||
class MissingPassphraseException(keyIds: Set<SubkeyIdentifier>) :
|
||||
PGPException(
|
||||
"Missing passphrase encountered for keys ${keyIds.toTypedArray().contentToString()}") {
|
||||
val keyIds: Set<SubkeyIdentifier> = Collections.unmodifiableSet(keyIds)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
/** Exception that gets thrown when the verification of a modification detection code failed. */
|
||||
class ModificationDetectionException : IOException()
|
|
@ -0,0 +1,42 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException
|
||||
import org.bouncycastle.openpgp.PGPSignature
|
||||
import org.pgpainless.algorithm.SignatureType
|
||||
|
||||
class SignatureValidationException : PGPException {
|
||||
|
||||
constructor(message: String?) : super(message)
|
||||
|
||||
constructor(message: String?, underlying: Exception) : super(message, underlying)
|
||||
|
||||
constructor(
|
||||
message: String,
|
||||
rejections: Map<PGPSignature, Exception>
|
||||
) : super("$message: ${exceptionMapToString(rejections)}")
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
private fun exceptionMapToString(rejections: Map<PGPSignature, Exception>): String =
|
||||
buildString {
|
||||
append(rejections.size).append(" rejected signatures:\n")
|
||||
for (signature in rejections.keys) {
|
||||
append(sigTypeToString(signature.signatureType))
|
||||
.append(' ')
|
||||
.append(signature.creationTime)
|
||||
.append(": ")
|
||||
.append(rejections[signature]!!.message)
|
||||
.append('\n')
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun sigTypeToString(type: Int): String =
|
||||
SignatureType.fromCode(type)?.toString()
|
||||
?: "0x${java.lang.Long.toHexString(type.toLong())}"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package org.pgpainless.exception
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException
|
||||
|
||||
/** Exception that gets thrown if unacceptable algorithms are encountered. */
|
||||
class UnacceptableAlgorithmException(message: String) : PGPException(message)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue