Compare commits
28 Commits
3293b0edb3
...
4f434dc350
Author | SHA1 | Date |
---|---|---|
Paul Schaub | 4f434dc350 | |
Paul Schaub | dd3ef89a5c | |
Paul Schaub | a6f3a223b1 | |
Paul Schaub | 741d72eadc | |
Paul Schaub | 0b7511a223 | |
Paul Schaub | eeb5986890 | |
Paul Schaub | 32d62c6610 | |
Paul Schaub | 1f9b65e3d2 | |
Paul Schaub | b96f22d0a9 | |
Paul Schaub | 80cf1a7446 | |
Paul Schaub | fe80b1185e | |
Paul Schaub | b393a90da4 | |
Paul Schaub | 8066650584 | |
Paul Schaub | bd1949871a | |
Paul Schaub | 194e4e1458 | |
Paul Schaub | 44be5aa981 | |
Paul Schaub | 3ac273757a | |
Paul Schaub | fa5bdfcd82 | |
Paul Schaub | 89038ebedf | |
Paul Schaub | 337b5d68b6 | |
Paul Schaub | 6a4ea5fbdb | |
Paul Schaub | f62a6a30ff | |
Paul Schaub | 15142b5775 | |
Paul Schaub | fffb4b17a4 | |
Paul Schaub | 77c669d289 | |
Paul Schaub | 8169db4bbd | |
Paul Schaub | 766af27b02 | |
Paul Schaub | 21f8ba8ccf |
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -14,9 +14,12 @@ SPDX-License-Identifier: CC0-1.0
|
||||||
- Rewrote most of the codebase in Kotlin
|
- Rewrote most of the codebase in Kotlin
|
||||||
- Removed `OpenPgpMetadata` (`decryptionStream.getResult()`) in favor of `MessageMetadata` (`decryptionStream.getMetadata()`)
|
- Removed `OpenPgpMetadata` (`decryptionStream.getResult()`) in favor of `MessageMetadata` (`decryptionStream.getMetadata()`)
|
||||||
- `pgpainless-sop`, `pgpainless-cli`
|
- `pgpainless-sop`, `pgpainless-cli`
|
||||||
- Bump `sop-java` to `8.0.1`, implementing [SOP Spec Revision 08](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html)
|
- Bump `sop-java` to `10.0.0`, implementing [SOP Spec Revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html)
|
||||||
- Change API of `sop.encrypt` to return a `ReadyWithResult<EncryptionResult>` to expose the session key
|
- Change API of `sop.encrypt` to return a `ReadyWithResult<EncryptionResult>` to expose the session key
|
||||||
- `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty
|
- `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty
|
||||||
|
- Separate signature verification operations into `SOPV` interface
|
||||||
|
- Add `version --sopv` option
|
||||||
|
- Throw `BadData` error when passing KEYS where CERTS are expected.
|
||||||
- Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice)
|
- Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice)
|
||||||
- Do not choke on unknown signature subpackets (thanks @Jerbell)
|
- Do not choke on unknown signature subpackets (thanks @Jerbell)
|
||||||
- Prevent timing issues resuting in subkey binding signatures predating the subkey (@thanks Jerbell)
|
- Prevent timing issues resuting in subkey binding signatures predating the subkey (@thanks Jerbell)
|
||||||
|
@ -24,6 +27,12 @@ SPDX-License-Identifier: CC0-1.0
|
||||||
- `GNUPG_AEAD_ENCRYPTED_DATA` -> `LIBREPGP_OCB_ENCRYPTED_DATA`
|
- `GNUPG_AEAD_ENCRYPTED_DATA` -> `LIBREPGP_OCB_ENCRYPTED_DATA`
|
||||||
- `GNUPG_VERSION_5_PUBLIC_KEY` -> `LIBREPGP_VERSION_5_PUBLIC_KEY`
|
- `GNUPG_VERSION_5_PUBLIC_KEY` -> `LIBREPGP_VERSION_5_PUBLIC_KEY`
|
||||||
|
|
||||||
|
## 1.6.7
|
||||||
|
- SOP: Fix OOM error when detached-signing large amounts of data (fix #432)
|
||||||
|
- Move `CachingBcPublicKeyDataDecryptorFactory` from `org.bouncycastle` packet to `org.pgpainless.decryption_verification` to avoid package split (partially addresses #428)
|
||||||
|
- Basic support for Java Modules for `pgpainless-core` and `pgpainless-sop`
|
||||||
|
- Added `Automatic-Module-Name` directive to gradle build files
|
||||||
|
|
||||||
## 1.6.6
|
## 1.6.6
|
||||||
- Downgrade `logback-core` and `logback-classic` to `1.2.13` to fix #426
|
- Downgrade `logback-core` and `logback-classic` to `1.2.13` to fix #426
|
||||||
|
|
||||||
|
@ -179,6 +188,9 @@ SPDX-License-Identifier: CC0-1.0
|
||||||
- Make countermeasures against [KOpenPGP](https://kopenpgp.com/) attacks configurable
|
- Make countermeasures against [KOpenPGP](https://kopenpgp.com/) attacks configurable
|
||||||
- Countermeasures are now disabled by default since they are costly and have a specific threat model
|
- Countermeasures are now disabled by default since they are costly and have a specific threat model
|
||||||
- Can be enabled by calling `Policy.setEnableKeyParameterValidation(true)`
|
- Can be enabled by calling `Policy.setEnableKeyParameterValidation(true)`
|
||||||
|
- Add support for parsing RegularExpressions
|
||||||
|
- Add module `hsregex` which uses `tcl-regex-java` implementing Henry Spencers Regular Expression library
|
||||||
|
|
||||||
|
|
||||||
## 1.4.0-rc2
|
## 1.4.0-rc2
|
||||||
- Bump `bcpg-jdk15to18` to `1.72.3`
|
- Bump `bcpg-jdk15to18` to `1.72.3`
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
myst-parser>=0.17
|
myst-parser>=0.17
|
||||||
sphinxcontrib-mermaid>=0.7.1
|
sphinxcontrib-mermaid>=0.7.1
|
||||||
|
sphinx_rtd_theme>=2.0.0
|
||||||
|
|
|
@ -82,23 +82,26 @@ Stateless OpenPGP Protocol
|
||||||
Usage: pgpainless-cli [--stacktrace] [COMMAND]
|
Usage: pgpainless-cli [--stacktrace] [COMMAND]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--stacktrace Print Stacktrace
|
--stacktrace Print stacktrace
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
help Display usage information for the specified subcommand
|
version Display version information about the tool
|
||||||
armor Add ASCII Armor to standard input
|
list-profiles Emit a list of profiles supported by the identified
|
||||||
dearmor Remove ASCII Armor from standard input
|
subcommand
|
||||||
decrypt Decrypt a message from standard input
|
generate-key Generate a secret key
|
||||||
inline-detach Split signatures from a clearsigned message
|
change-key-password Update the password of a key
|
||||||
encrypt Encrypt a message from standard input
|
revoke-key Generate revocation certificates
|
||||||
extract-cert Extract a public key certificate from a secret key from
|
extract-cert Extract a public key certificate from a secret key
|
||||||
standard input
|
sign Create a detached message signature
|
||||||
generate-key Generate a secret key
|
verify Verify a detached signature
|
||||||
sign Create a detached signature on the data from standard input
|
encrypt Encrypt a message from standard input
|
||||||
verify Verify a detached signature over the data from standard input
|
decrypt Decrypt a message
|
||||||
inline-sign Create an inline-signed message from data on standard input
|
inline-detach Split signatures from a clearsigned message
|
||||||
inline-verify Verify inline-signed data from standard input
|
inline-sign Create an inline-signed message
|
||||||
version Display version information about the tool
|
inline-verify Verify an inline-signed message
|
||||||
|
armor Add ASCII Armor to standard input
|
||||||
|
dearmor Remove ASCII Armor from standard input
|
||||||
|
help Display usage information for the specified subcommand
|
||||||
|
|
||||||
Exit Codes:
|
Exit Codes:
|
||||||
0 Successful program execution
|
0 Successful program execution
|
||||||
|
@ -120,6 +123,9 @@ Exit Codes:
|
||||||
71 Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter
|
71 Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter
|
||||||
73 Ambiguous input (a filename matching the designator already exists)
|
73 Ambiguous input (a filename matching the designator already exists)
|
||||||
79 Key is not signing capable
|
79 Key is not signing capable
|
||||||
|
83 Options were supplied that are incompatible with each other
|
||||||
|
89 The requested profile is unsupported, or the indicated subcommand does
|
||||||
|
not accept profiles
|
||||||
```
|
```
|
||||||
|
|
||||||
To get help on a subcommand, e.g. `encrypt`, just call the help subcommand followed by the subcommand you
|
To get help on a subcommand, e.g. `encrypt`, just call the help subcommand followed by the subcommand you
|
||||||
|
|
|
@ -180,14 +180,6 @@ byte[] armoredData = sop.armor()
|
||||||
|
|
||||||
The `data(_)` method can either be called by providing a byte array, or an `InputStream`.
|
The `data(_)` method can either be called by providing a byte array, or an `InputStream`.
|
||||||
|
|
||||||
:::{note}
|
|
||||||
There is a `label(ArmorLabel label)` method, which could theoretically be used to define the label used in the
|
|
||||||
ASCII armor header.
|
|
||||||
However, this method is not (yet?) supported by `pgpainless-sop` and will currently throw an `UnsupportedOption`
|
|
||||||
exception.
|
|
||||||
Instead, the implementation will figure out the data type and set the respective label on its own.
|
|
||||||
:::
|
|
||||||
|
|
||||||
To remove ASCII armor from armored data, simply use the `dearmor()` API:
|
To remove ASCII armor from armored data, simply use the `dearmor()` API:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Evaluate Regular Expressions in OpenPGP Signatures using TCL-Regex
|
||||||
|
|
||||||
|
RFC4880 specifies contains a section about RegularExpression subpackets on signatures.
|
||||||
|
Within this section, the syntax of the RegularExpression subpackets is defined to be the same as Henry Spencer's "almost public domain" regular expression package.
|
||||||
|
|
||||||
|
Since Java's `java.util.regex` syntax is too powerful, this module exists to implement regex evaluation using [tcl-regex](https://github.com/basis-technology-corp/tcl-regex-java)
|
||||||
|
which appears to be a Java port of Henry Spencers regex package.
|
||||||
|
|
||||||
|
To make use of this implementation, simply call
|
||||||
|
```java
|
||||||
|
RegexInterpreterFactory.setInstance(new HSRegexInterpreterFactory());
|
||||||
|
```
|
|
@ -0,0 +1,28 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'java-library'
|
||||||
|
}
|
||||||
|
|
||||||
|
group 'org.pgpainless'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
mavenLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||||
|
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||||
|
|
||||||
|
implementation(project(":pgpainless-core"))
|
||||||
|
|
||||||
|
// Henry Spencers Regular Expression (RegEx packets)
|
||||||
|
implementation 'com.basistech.tclre:tcl-regex:0.14.5'
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.algorithm;
|
||||||
|
|
||||||
|
import com.basistech.tclre.HsrePattern;
|
||||||
|
import com.basistech.tclre.PatternFlags;
|
||||||
|
import com.basistech.tclre.RePattern;
|
||||||
|
import com.basistech.tclre.RegexException;
|
||||||
|
|
||||||
|
public class HSRegexInterpreterFactory extends RegexInterpreterFactory {
|
||||||
|
|
||||||
|
public Regex instantiate(String regex) {
|
||||||
|
return new Regex() {
|
||||||
|
|
||||||
|
private final RePattern pattern;
|
||||||
|
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
pattern = HsrePattern.compile(regex, PatternFlags.ADVANCED);
|
||||||
|
} catch (RegexException e) {
|
||||||
|
throw new IllegalArgumentException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String string) {
|
||||||
|
return pattern.matcher(string).find();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex interpreter implementation based on Henry Spencers Regular Expression library.
|
||||||
|
*
|
||||||
|
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-8">RFC4880 - §8. Regular Expressions</a>
|
||||||
|
*/
|
||||||
|
package org.pgpainless.algorithm;
|
|
@ -0,0 +1,24 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.algorithm;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
public class HSRegexInterpreterFactoryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void dummyRegexTest() {
|
||||||
|
HSRegexInterpreterFactory factory = new HSRegexInterpreterFactory();
|
||||||
|
RegexInterpreterFactory.setInstance(factory);
|
||||||
|
Regex regex = RegexInterpreterFactory.create("Alice|Bob");
|
||||||
|
|
||||||
|
assertTrue(regex.matches("Alice"));
|
||||||
|
assertTrue(regex.matches("Bob"));
|
||||||
|
assertFalse(regex.matches("Charlie"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex interpreter implementation based on Henry Spencers Regular Expression library.
|
||||||
|
*
|
||||||
|
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-8">RFC4880 - §8. Regular Expressions</a>
|
||||||
|
*/
|
||||||
|
package org.pgpainless.algorithm;
|
|
@ -30,16 +30,11 @@
|
||||||
pgpainless\-cli\-armor \- Add ASCII Armor to standard input
|
pgpainless\-cli\-armor \- Add ASCII Armor to standard input
|
||||||
.SH "SYNOPSIS"
|
.SH "SYNOPSIS"
|
||||||
.sp
|
.sp
|
||||||
\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP] [\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP]
|
\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP]
|
||||||
.SH "DESCRIPTION"
|
.SH "DESCRIPTION"
|
||||||
|
|
||||||
.SH "OPTIONS"
|
.SH "OPTIONS"
|
||||||
.sp
|
.sp
|
||||||
\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP
|
|
||||||
.RS 4
|
|
||||||
Label to be used in the header and tail of the armoring
|
|
||||||
.RE
|
|
||||||
.sp
|
|
||||||
\fB\-\-stacktrace\fP
|
\fB\-\-stacktrace\fP
|
||||||
.RS 4
|
.RS 4
|
||||||
Print stacktrace
|
Print stacktrace
|
||||||
|
|
|
@ -31,8 +31,8 @@ pgpainless\-cli\-decrypt \- Decrypt a message
|
||||||
.SH "SYNOPSIS"
|
.SH "SYNOPSIS"
|
||||||
.sp
|
.sp
|
||||||
\fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP]
|
\fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP]
|
||||||
[\fB\-\-verify\-not\-after\fP=\fIDATE\fP] [\fB\-\-verify\-not\-before\fP=\fIDATE\fP]
|
[\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP] [\fB\-\-verify\-not\-after\fP=\fIDATE\fP]
|
||||||
[\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP] [\fB\-\-verify\-with\fP=\fICERT\fP]...
|
[\fB\-\-verify\-not\-before\fP=\fIDATE\fP] [\fB\-\-verify\-with\fP=\fICERT\fP]...
|
||||||
[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]...
|
[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]...
|
||||||
[\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP]... [\fIKEY\fP...]
|
[\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP]... [\fIKEY\fP...]
|
||||||
.SH "DESCRIPTION"
|
.SH "DESCRIPTION"
|
||||||
|
@ -49,7 +49,7 @@ Can be used to learn the session key on successful decryption
|
||||||
Print stacktrace
|
Print stacktrace
|
||||||
.RE
|
.RE
|
||||||
.sp
|
.sp
|
||||||
\fB\-\-verify\-not\-after\fP=\fIDATE\fP
|
\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP, \fB\-\-verify\-not\-after\fP=\fIDATE\fP
|
||||||
.RS 4
|
.RS 4
|
||||||
ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z)
|
ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z)
|
||||||
.sp
|
.sp
|
||||||
|
@ -69,11 +69,6 @@ Reject signatures with a creation date not in range.
|
||||||
Defaults to beginning of time (\(aq\-\(aq).
|
Defaults to beginning of time (\(aq\-\(aq).
|
||||||
.RE
|
.RE
|
||||||
.sp
|
.sp
|
||||||
\fB\-\-verify\-out, \-\-verifications\-out\fP=\fIVERIFICATIONS\fP
|
|
||||||
.RS 4
|
|
||||||
Emits signature verification status to the designated output
|
|
||||||
.RE
|
|
||||||
.sp
|
|
||||||
\fB\-\-verify\-with\fP=\fICERT\fP
|
\fB\-\-verify\-with\fP=\fICERT\fP
|
||||||
.RS 4
|
.RS 4
|
||||||
Certificates for signature verification
|
Certificates for signature verification
|
||||||
|
|
|
@ -31,9 +31,9 @@ pgpainless\-cli\-encrypt \- Encrypt a message from standard input
|
||||||
.SH "SYNOPSIS"
|
.SH "SYNOPSIS"
|
||||||
.sp
|
.sp
|
||||||
\fBpgpainless\-cli encrypt\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP]
|
\fBpgpainless\-cli encrypt\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP]
|
||||||
[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-sign\-with\fP=\fIKEY\fP]...
|
[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP]
|
||||||
[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]...
|
[\fB\-\-sign\-with\fP=\fIKEY\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]...
|
||||||
[\fICERTS\fP...]
|
[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fICERTS\fP...]
|
||||||
.SH "DESCRIPTION"
|
.SH "DESCRIPTION"
|
||||||
|
|
||||||
.SH "OPTIONS"
|
.SH "OPTIONS"
|
||||||
|
@ -53,7 +53,7 @@ ASCII armor the output
|
||||||
Profile identifier to switch between profiles
|
Profile identifier to switch between profiles
|
||||||
.RE
|
.RE
|
||||||
.sp
|
.sp
|
||||||
\fB\-\-sign\-with\fP=\fIKEY\fP
|
\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP, \fB\-\-sign\-with\fP=\fIKEY\fP
|
||||||
.RS 4
|
.RS 4
|
||||||
Sign the output with a private key
|
Sign the output with a private key
|
||||||
.RE
|
.RE
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
pgpainless\-cli\-version \- Display version information about the tool
|
pgpainless\-cli\-version \- Display version information about the tool
|
||||||
.SH "SYNOPSIS"
|
.SH "SYNOPSIS"
|
||||||
.sp
|
.sp
|
||||||
\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\fP]
|
\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\fP | \fB\-\-sopv\fP]
|
||||||
.SH "DESCRIPTION"
|
.SH "DESCRIPTION"
|
||||||
|
|
||||||
.SH "OPTIONS"
|
.SH "OPTIONS"
|
||||||
|
@ -50,7 +50,7 @@ Print an extended version string
|
||||||
Print the latest revision of the SOP specification targeted by the implementation
|
Print the latest revision of the SOP specification targeted by the implementation
|
||||||
.RE
|
.RE
|
||||||
.sp
|
.sp
|
||||||
\fB\-\-stacktrace\fP
|
\fB\-\-sopv\fP, \fB\-\-stacktrace\fP
|
||||||
.RS 4
|
.RS 4
|
||||||
Print stacktrace
|
Print stacktrace
|
||||||
.RE
|
.RE
|
|
@ -13,12 +13,14 @@ do
|
||||||
SRC="${page##*/}"
|
SRC="${page##*/}"
|
||||||
DEST="${SRC/sop/pgpainless-cli}"
|
DEST="${SRC/sop/pgpainless-cli}"
|
||||||
sed \
|
sed \
|
||||||
|
-e 's/sopv/PLACEHOLDERV/g' \
|
||||||
-e 's#.\\" Title: sop#.\\" Title: pgpainless-cli#g' \
|
-e 's#.\\" Title: sop#.\\" Title: pgpainless-cli#g' \
|
||||||
-e 's/Manual: Sop Manual/Manual: PGPainless-CLI Manual/g' \
|
-e 's/Manual: Sop Manual/Manual: PGPainless-CLI Manual/g' \
|
||||||
-e 's/.TH "SOP/.TH "PGPAINLESS\\-CLI/g' \
|
-e 's/.TH "SOP/.TH "PGPAINLESS\\-CLI/g' \
|
||||||
-e 's/"Sop Manual"/"PGPainless\\-CLI Manual"/g' \
|
-e 's/"Sop Manual"/"PGPainless\\-CLI Manual"/g' \
|
||||||
-e 's/\\fBsop/\\fBpgpainless\\-cli/g' \
|
-e 's/\\fBsop/\\fBpgpainless\\-cli/g' \
|
||||||
-e 's/sop/pgpainless\\-cli/g' \
|
-e 's/sop/pgpainless\\-cli/g' \
|
||||||
|
-e 's/PLACEHOLDERV/sopv/g' \
|
||||||
$page > $DEST_DIR/$DEST
|
$page > $DEST_DIR/$DEST
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.pgpainless.PGPainless;
|
import org.pgpainless.PGPainless;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
|
|
||||||
public class ArmorCmdTest extends CLITest {
|
public class ArmorCmdTest extends CLITest {
|
||||||
|
|
||||||
|
@ -89,15 +88,6 @@ public class ArmorCmdTest extends CLITest {
|
||||||
assertTrue(armored.contains("SGVsbG8sIFdvcmxkIQo="));
|
assertTrue(armored.contains("SGVsbG8sIFdvcmxkIQo="));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void labelNotYetSupported() throws IOException {
|
|
||||||
pipeStringToStdin("Hello, World!\n");
|
|
||||||
ByteArrayOutputStream out = pipeStdoutToStream();
|
|
||||||
int exitCode = executeCommand("armor", "--label", "Message");
|
|
||||||
assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, exitCode);
|
|
||||||
assertEquals(0, out.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void armorAlreadyArmoredDataIsIdempotent() throws IOException {
|
public void armorAlreadyArmoredDataIsIdempotent() throws IOException {
|
||||||
pipeStringToStdin(key);
|
pipeStringToStdin(key);
|
||||||
|
|
|
@ -26,4 +26,14 @@ dependencies {
|
||||||
|
|
||||||
// @Nullable, @Nonnull annotations
|
// @Nullable, @Nonnull annotations
|
||||||
implementation "com.google.code.findbugs:jsr305:3.0.2"
|
implementation "com.google.code.findbugs:jsr305:3.0.2"
|
||||||
|
|
||||||
|
// HSRE regex for tests
|
||||||
|
testImplementation project(":hsregex")
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_modular_auto
|
||||||
|
tasks.named('jar') {
|
||||||
|
manifest {
|
||||||
|
attributes('Automatic-Module-Name': 'org.pgpainless.core')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.algorithm;
|
||||||
|
|
||||||
|
import org.pgpainless.key.util.UserId;
|
||||||
|
|
||||||
|
public interface Regex {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true, if the regex matches the given user-id.
|
||||||
|
*
|
||||||
|
* @param userId userId
|
||||||
|
* @return true if matches, false otherwise
|
||||||
|
*/
|
||||||
|
default boolean matches(UserId userId) {
|
||||||
|
return matches(userId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true, if the regex matches the given string.
|
||||||
|
*
|
||||||
|
* @param string string
|
||||||
|
* @return true if matches, false otherwise
|
||||||
|
*/
|
||||||
|
boolean matches(String string);
|
||||||
|
|
||||||
|
static Regex wildcard() {
|
||||||
|
return new Regex() {
|
||||||
|
@Override
|
||||||
|
public boolean matches(String string) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.algorithm;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public abstract class RegexInterpreterFactory {
|
||||||
|
|
||||||
|
private static RegexInterpreterFactory INSTANCE;
|
||||||
|
|
||||||
|
public static RegexInterpreterFactory getInstance() {
|
||||||
|
if (INSTANCE == null) {
|
||||||
|
INSTANCE = new JavaRegexInterpreterFactory();
|
||||||
|
}
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setInstance(@Nonnull RegexInterpreterFactory instance) {
|
||||||
|
INSTANCE = instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Regex create(String regex) {
|
||||||
|
return getInstance().instantiate(regex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex that matches any mail address on the given mail server.
|
||||||
|
* For example, calling this method with parameter <pre>pgpainless.org</pre> will return a regex
|
||||||
|
* that matches any of the following user ids:
|
||||||
|
* <pre>
|
||||||
|
* Alice <alice@pgpainless.org>
|
||||||
|
* <bob@pgpainless.org>
|
||||||
|
* Issuer (code signing) <issuer@pgpainless.org>
|
||||||
|
* </pre>
|
||||||
|
* It will however not match the following mail addresses:
|
||||||
|
* <pre>
|
||||||
|
* Alice <alice@example.org>
|
||||||
|
* alice@pgpainless.org
|
||||||
|
* alice@pgpainless.org <alice@example.org>
|
||||||
|
* Bob <bob@PGPainless.org>
|
||||||
|
* </pre>
|
||||||
|
* Note: This method will not validate the given domain string, so that is your responsibility!
|
||||||
|
*
|
||||||
|
* @param mailDomain domain
|
||||||
|
* @return regex matching the domain
|
||||||
|
*/
|
||||||
|
public static Regex createDefaultMailDomainRegex(String mailDomain) {
|
||||||
|
String escaped = mailDomain.replace(".", "\\.");
|
||||||
|
return create("<[^>]+[@.]" + escaped + ">$");
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Regex instantiate(String regex) throws IllegalArgumentException;
|
||||||
|
|
||||||
|
public static class JavaRegexInterpreterFactory extends RegexInterpreterFactory {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Regex instantiate(String regex) {
|
||||||
|
return new Regex() {
|
||||||
|
|
||||||
|
private final Pattern pattern = Pattern.compile(regex);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(String string) {
|
||||||
|
return pattern.matcher(string).find();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.algorithm;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class RegexSet {
|
||||||
|
|
||||||
|
private final Set<Regex> regexSet = new HashSet<>();
|
||||||
|
|
||||||
|
private RegexSet(Collection<Regex> regexes) {
|
||||||
|
this.regexSet.addAll(regexes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RegexSet matchAnything() {
|
||||||
|
return new RegexSet(Collections.singleton(Regex.wildcard()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RegexSet matchNothing() {
|
||||||
|
return new RegexSet(Collections.emptySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RegexSet matchSome(Collection<Regex> regexes) {
|
||||||
|
return new RegexSet(regexes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matches(String userId) {
|
||||||
|
for (Regex regex : regexSet) {
|
||||||
|
if (regex.matches(userId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import org.pgpainless.PGPainless.Companion.getPolicy
|
||||||
import org.pgpainless.algorithm.CompressionAlgorithm
|
import org.pgpainless.algorithm.CompressionAlgorithm
|
||||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||||
import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity
|
import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity
|
||||||
|
import org.pgpainless.util.NullOutputStream
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
@ -19,6 +20,10 @@ class EncryptionBuilder : EncryptionBuilderInterface {
|
||||||
return WithOptionsImpl(outputStream)
|
return WithOptionsImpl(outputStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun discardOutput(): EncryptionBuilderInterface.WithOptions {
|
||||||
|
return onOutputStream(NullOutputStream())
|
||||||
|
}
|
||||||
|
|
||||||
class WithOptionsImpl(val outputStream: OutputStream) : EncryptionBuilderInterface.WithOptions {
|
class WithOptionsImpl(val outputStream: OutputStream) : EncryptionBuilderInterface.WithOptions {
|
||||||
|
|
||||||
override fun withOptions(options: ProducerOptions): EncryptionStream {
|
override fun withOptions(options: ProducerOptions): EncryptionStream {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import org.bouncycastle.openpgp.PGPException
|
import org.bouncycastle.openpgp.PGPException
|
||||||
|
|
||||||
fun interface EncryptionBuilderInterface {
|
interface EncryptionBuilderInterface {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a [EncryptionStream] wrapping an [OutputStream]. Data that is piped through the
|
* Create a [EncryptionStream] wrapping an [OutputStream]. Data that is piped through the
|
||||||
|
@ -19,6 +19,16 @@ fun interface EncryptionBuilderInterface {
|
||||||
*/
|
*/
|
||||||
fun onOutputStream(outputStream: OutputStream): WithOptions
|
fun onOutputStream(outputStream: OutputStream): WithOptions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an [EncryptionStream] that discards the data after processing it. This is useful, e.g.
|
||||||
|
* for generating detached signatures, where the resulting signature is retrieved from the
|
||||||
|
* [EncryptionResult] once the operation is finished. In this case, the plaintext data does not
|
||||||
|
* need to be retained.
|
||||||
|
*
|
||||||
|
* @return api handle
|
||||||
|
*/
|
||||||
|
fun discardOutput(): WithOptions
|
||||||
|
|
||||||
fun interface WithOptions {
|
fun interface WithOptions {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -169,6 +169,17 @@ class CertificateValidator {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Reject sigs by non-signing keys
|
||||||
|
if (userIdSignatures.none { (_, sigs) ->
|
||||||
|
sigs.any {
|
||||||
|
SignatureSubpacketsUtil.getKeyFlags(it)?.let { f ->
|
||||||
|
KeyFlag.hasKeyFlag(f.flags, KeyFlag.SIGN_DATA)
|
||||||
|
} == true
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
throw SignatureValidationException(
|
||||||
|
"Signature was generated by non-signing key.")
|
||||||
|
}
|
||||||
} else { // signing key is subkey
|
} else { // signing key is subkey
|
||||||
val subkeySigs = mutableListOf<PGPSignature>()
|
val subkeySigs = mutableListOf<PGPSignature>()
|
||||||
signingSubkey
|
signingSubkey
|
||||||
|
@ -183,7 +194,7 @@ class CertificateValidator {
|
||||||
}
|
}
|
||||||
} catch (e: SignatureValidationException) {
|
} catch (e: SignatureValidationException) {
|
||||||
rejections[it] = e
|
rejections[it] = e
|
||||||
LOGGER.debug("REjecting subkey revocation signature: ${e.message}", e)
|
LOGGER.debug("Rejecting subkey revocation signature: ${e.message}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.util
|
||||||
|
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
/** [OutputStream] that simply discards bytes written to it. */
|
||||||
|
class NullOutputStream : OutputStream() {
|
||||||
|
|
||||||
|
override fun write(p0: Int) {
|
||||||
|
// nop
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(b: ByteArray) {
|
||||||
|
// nop
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||||
|
// nop
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
// nop
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun flush() {
|
||||||
|
// nop
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.algorithm;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class RegexSetTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void matchNothingTest() {
|
||||||
|
RegexSet set = RegexSet.matchNothing();
|
||||||
|
assertFalse(set.matches("<alice@pgpainless.org>"));
|
||||||
|
assertFalse(set.matches("Alice"));
|
||||||
|
assertFalse(set.matches("Alice <alice@pgpainless.org>"));
|
||||||
|
assertFalse(set.matches(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void matchAnything() {
|
||||||
|
RegexSet set = RegexSet.matchAnything();
|
||||||
|
assertTrue(set.matches("Alice"));
|
||||||
|
assertTrue(set.matches("<alice@pgpainless.org>"));
|
||||||
|
assertTrue(set.matches("Alice <alice@pgpainless.org>"));
|
||||||
|
assertTrue(set.matches("Alice <alice@example.org>"));
|
||||||
|
assertTrue(set.matches(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void matchSome() {
|
||||||
|
Regex pgpainless_org = RegexInterpreterFactory.createDefaultMailDomainRegex("pgpainless.org");
|
||||||
|
Regex example_org = RegexInterpreterFactory.createDefaultMailDomainRegex("example.org");
|
||||||
|
|
||||||
|
RegexSet set = RegexSet.matchSome(Arrays.asList(pgpainless_org, example_org));
|
||||||
|
assertTrue(set.matches("Alice <alice@pgpainless.org>"));
|
||||||
|
assertTrue(set.matches("<alice@pgpainless.org>"));
|
||||||
|
assertTrue(set.matches("Bob <bob@example.org>"));
|
||||||
|
assertTrue(set.matches("<bob@example.org>"));
|
||||||
|
assertFalse(set.matches("Bob <bob@example.com>"));
|
||||||
|
assertFalse(set.matches("Alice <alice@PGPainless.org>"));
|
||||||
|
assertFalse(set.matches("alice@pgpainless.org"));
|
||||||
|
assertFalse(set.matches("Alice"));
|
||||||
|
assertFalse(set.matches(""));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.algorithm;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Named;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.pgpainless.key.util.UserId;
|
||||||
|
|
||||||
|
public class RegexTest {
|
||||||
|
private static Stream<Arguments> provideRegexInterpreterFactories() {
|
||||||
|
return Stream.of(
|
||||||
|
Arguments.of(Named.of("Default JavaRegexInterpreterFactory",
|
||||||
|
new RegexInterpreterFactory.JavaRegexInterpreterFactory())),
|
||||||
|
Arguments.of(Named.of("HSRegexInterpreterFactory",
|
||||||
|
new HSRegexInterpreterFactory()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideRegexInterpreterFactories")
|
||||||
|
public void simpleTest(RegexInterpreterFactory factory) {
|
||||||
|
Regex regex = factory.instantiate("Alice|Bob");
|
||||||
|
assertTrue(regex.matches("Alice"));
|
||||||
|
assertTrue(regex.matches("Bob"));
|
||||||
|
assertFalse(regex.matches("Charlie"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideRegexInterpreterFactories")
|
||||||
|
public void testEmailRegexMatchesDomain(RegexInterpreterFactory factory) {
|
||||||
|
Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$");
|
||||||
|
assertTrue(regex.matches("Alice <alice@pgpainless.org>"));
|
||||||
|
assertTrue(regex.matches("Bob <bob@pgpainless.org>"));
|
||||||
|
assertFalse(regex.matches("Alice <alice@example.com>"), "wrong domain");
|
||||||
|
assertFalse(regex.matches("Bob <bob@example.com>"), "wrong domain");
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideRegexInterpreterFactories")
|
||||||
|
public void testEmailRegexMatchesOnlyWrappedAddresses(RegexInterpreterFactory factory) {
|
||||||
|
Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$");
|
||||||
|
assertTrue(regex.matches("<alice@pgpainless.org>"));
|
||||||
|
assertFalse(regex.matches("alice@pgpainless.org"), "only match mails in <>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideRegexInterpreterFactories")
|
||||||
|
public void testCaseSensitivity(RegexInterpreterFactory factory) {
|
||||||
|
Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$");
|
||||||
|
assertFalse(regex.matches("Alice <alice@PGPAINLESS.ORG>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideRegexInterpreterFactories")
|
||||||
|
public void testWildCard(RegexInterpreterFactory factory) {
|
||||||
|
Regex regex = factory.instantiate(".*");
|
||||||
|
assertTrue(regex.matches(""));
|
||||||
|
assertTrue(regex.matches("Alice"));
|
||||||
|
assertTrue(regex.matches("<alice@pgpainless.org>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideRegexInterpreterFactories")
|
||||||
|
public void testExclusion(RegexInterpreterFactory factory) {
|
||||||
|
// Test [^>] matches all but '>'
|
||||||
|
Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$");
|
||||||
|
assertFalse(regex.matches("<alice>appendix@pgpainless.org>"));
|
||||||
|
assertFalse(regex.matches("<>alice@pgpainless.org>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideRegexInterpreterFactories")
|
||||||
|
public void testOnlyMatchAtTheEnd(RegexInterpreterFactory factory) {
|
||||||
|
Regex regex = factory.instantiate("<[^>]+[@.]pgpainless\\.org>$");
|
||||||
|
assertFalse(regex.matches("Alice <alice@pgpainless.org><bob@example.org>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideRegexInterpreterFactories")
|
||||||
|
public void testRanges(RegexInterpreterFactory factory) {
|
||||||
|
Regex regex = factory.instantiate("<[^>]+[0-9][@.]pgpainless\\.org>$");
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
String mail = "<user" + i + "@pgpainless.org>";
|
||||||
|
assertTrue(regex.matches(mail));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFalse(regex.matches("<user@pgpainless.org>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideRegexInterpreterFactories")
|
||||||
|
public void testExactMailMatch(RegexInterpreterFactory factory) {
|
||||||
|
Regex exactMail = factory.instantiate("<exact@pgpainless\\.org>$");
|
||||||
|
assertTrue(exactMail.matches("<exact@pgpainless.org>"));
|
||||||
|
assertTrue(exactMail.matches("Exact Match <exact@pgpainless.org>"));
|
||||||
|
assertFalse(exactMail.matches("<roughly-exact@pgpainless.org>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideRegexInterpreterFactories")
|
||||||
|
public void testSetInstance(RegexInterpreterFactory factory) {
|
||||||
|
RegexInterpreterFactory before = RegexInterpreterFactory.getInstance();
|
||||||
|
RegexInterpreterFactory.setInstance(factory);
|
||||||
|
|
||||||
|
Regex regex = RegexInterpreterFactory.create("<[^>]+[@.]pgpainless\\.org>$");
|
||||||
|
assertTrue(regex.matches(UserId.nameAndEmail("Alice", "alice@pgpainless.org")));
|
||||||
|
|
||||||
|
RegexInterpreterFactory.setInstance(before);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideRegexInterpreterFactories")
|
||||||
|
public void testInvalidRegex(RegexInterpreterFactory factory) {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> factory.instantiate("[ab"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,228 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.decryption_verification;
|
||||||
|
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||||
|
import org.bouncycastle.util.io.Streams;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.pgpainless.PGPainless;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
|
||||||
|
public class VerifySignatureByCertificationKeyFailsTest {
|
||||||
|
|
||||||
|
// Key with non-signing primary key and dedicated signing subkey
|
||||||
|
private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
|
||||||
|
"Version: PGPainless\n" +
|
||||||
|
"Comment: ACFC 7FFA 02BE C1E8 5002 2D35 AFE4 67B2 9A41 A0CE\n" +
|
||||||
|
"Comment: Complex RSA <test@test.test>\n" +
|
||||||
|
"\n" +
|
||||||
|
"lQcYBGYQBhIBEACVXJstPdymc/0ZAQYWSy/hOpHFV0YBgon4ymgIrN0xlJgW0Lju\n" +
|
||||||
|
"oSW9pHen2MWEdLTUJ1eXrj10QPkB1oFpilzQFqXRYTxjrbeoRZjDXoGEf7JTFcIx\n" +
|
||||||
|
"R/i385qreJ2ZYw9pUuBCW2R3juiUwzwiwC7/2y2qADOh9TPnJyyoPv3oDuGdLd7h\n" +
|
||||||
|
"ge3loYOySF0d0JZzFr+x3yQNDWUibiJbckZ1jFpLV2oyHNV9lpxH22xW/nhmanqR\n" +
|
||||||
|
"vFe6PZCK4UNQBuqY9pvwp7neoM4j5h862LSuEmG4LjIaYp2DBf31jIsWYk7cCgcP\n" +
|
||||||
|
"p+t6/9/AGwA05n36G2ncnrcrNX25R1F8b0tqZgEe/fOJ4dNCSTeV4pqSotxxeKFx\n" +
|
||||||
|
"dqY6kvOCCauS5ZLjmMUimgOkDk7/3ePEcRoUMpbhETs3hOcfMMLsh81OL8FdvZVc\n" +
|
||||||
|
"xxoYmT0ca2kDwixSQ+lLfDISECIEW080H7J/bez3wSjYLOfypNIGCdrZ+9zwtHYZ\n" +
|
||||||
|
"QxmziDnjprkDmTXKYA1KYvPXhfxHBpmn8spI1MJawHhF7zI9pycWvWc+oty6+/B+\n" +
|
||||||
|
"dVrx9G9TNDl4xDctehCXC9LErVUW4cAJxZlft1dFc/49JpSRPWzAcw8Q5XOZb2yJ\n" +
|
||||||
|
"qjUnzw690T0COD5CCM34Fh9/ZcDZXBF/WJT9lWMDFHq84fW2tP+FLAd68QARAQAB\n" +
|
||||||
|
"AA/7BdEpGGZgyF+bRQ5tjQ/sk3PTTY4/Y1VELlsjsukyMpLvWwqFL7YGLd0D7ZbB\n" +
|
||||||
|
"sPQipS7+RSGiGK79g7Bo/3h5P0yo58vgedLzZJHuIZD14yuRNGJg9JTK/2ioM3MF\n" +
|
||||||
|
"IEr9XkRXof33noU2uOR83EjQpWJR5JCrC7m5bCoLngA/pa2lF8wIWPWiE3lqeaN1\n" +
|
||||||
|
"c1dpiwRUMiDXDjMUjamGz2zwVY+omWdg8wawdRQY9HhIcrLo2YRE6Pwe2cAGd+ib\n" +
|
||||||
|
"oh6pPSZA0h4vw2RPFixv0IjnSiHcVtUyjYu3R4aqN2MvEsLnACbb04sReFh5MsnA\n" +
|
||||||
|
"0YHQkjcTGzNYCuf20SQRhl+HRLq2xJnKzZsjYjElbnOuEDuyeMAgHs7/CO7+72t3\n" +
|
||||||
|
"LCjrEX6JYi1tuNdbu43BFn9TM2aH5ix+XK+/3ZKgmM3fNaLZxM8041MtGtdWTNnp\n" +
|
||||||
|
"gySKhyLChC/2R0p2MY6fAzRi045ED96fSaKpfroHn0Lw09rNlvEwD1rGHQyGHl0c\n" +
|
||||||
|
"rBWHO89Gpfx6lBOuyC+kJG0Cm5BJkXHpFJ8ky+xoEDzHUgNdXsAvEyj2Dqxkzojz\n" +
|
||||||
|
"oC9M7vS66p45wXXsbEcGCG/FCw8XRNaeOUKOBgQ19IdTsoqCPBfcqWYMkqqcmcwF\n" +
|
||||||
|
"62eT/A8AyHrAi+srJawQCAQtrWnQ9A/oOapqPyUhH/QdjAkIALvoejVUd8OwUkHt\n" +
|
||||||
|
"n/C7mgnoAlPBPPlQrCYKR4bZdCkleWjaz8zUXRKSfTWn1rXDHjDx7SR7kykBqLFZ\n" +
|
||||||
|
"tzkXx/dzB6mOS8Mw8t25Asn9kocMlYQebLDf2jw1HjeC9q8+SY2Cub3YQuVJdZI5\n" +
|
||||||
|
"eu0Bo4DgMn1EmQPuJSY2DO6tHu/kyTT35oJfJQ5ntYPUSWMLBWYV2tfcHx3UkaQt\n" +
|
||||||
|
"nXTuiL7IfEeRniv1I6b2xY5So+WbPuSQjSbmqGfGHythzNmAZsWmCXFZFHQqnbHn\n" +
|
||||||
|
"DtZlqzjIJmaJK6qNLCcUC1ZC6GPU7/n09Q/n/Edu5uMi3Ktc+50CPzjs8x7q6ipb\n" +
|
||||||
|
"e03Hqh8IAMt8VAIC6EeFN2MoyExT95QR4HXzDC2iHFLYxzVpaBrTJ9RlVlStTe+a\n" +
|
||||||
|
"W5euSHCWhGA8Nh8+s4NHLkYk3LezC89+japsooVU/F2QHfEuHWBtevw04+vub8KG\n" +
|
||||||
|
"eRg5+7RK0DnCK3MM/yi7/06J+JTV9rAe3qyaCX2mGQK8QeoC3ea6Gk/d8HY/q0y6\n" +
|
||||||
|
"LFP/0PIiyA55nYdZQoSEFEfmg1lJIU0h9D+FaoiVYjd1IVnHlcooFReJ+SudY1rV\n" +
|
||||||
|
"UP4mckKlgbyfOmB1G8fehTGohfRCWCx92pKKk9jxsLcY8jUJpO7Utzg00OwJWRop\n" +
|
||||||
|
"QyajNCChzXcJju85OEQS8noBIGk7WO8IAJude9ei9+M4sKGUN42aRcX2njLggajH\n" +
|
||||||
|
"0BoqcCP6WJGZ+08kZY+PhvUWAloFm/icOtWRJJbgjvVKZU+yoFjdPu0KIDCYp7pZ\n" +
|
||||||
|
"3SzUoZeY8tt1dNCZfcsJB+WeYW3HQXAavUP4+gR8ro7WjNQhvWddLgHo1z+rNoti\n" +
|
||||||
|
"1BUFlQkVKdreM9ll3HUpYy2xcKMylOxMeL/qEbRCO5L7hDGbjLrio3ynpHrVLIi2\n" +
|
||||||
|
"+wWU+JB2pVQnI4+tm2gMAil63wu8WJZz8BVxn3AhAQgQIH9OfuRCpPwvrBDIAyog\n" +
|
||||||
|
"T8razUcCHQtulj9Pchu74isreAwMn1Z4Ddol89ANfIKn14mla1WaGx5wiLQcQ29t\n" +
|
||||||
|
"cGxleCBSU0EgPHRlc3RAdGVzdC50ZXN0PokCUwQTAQoARwUCZhAGFQkQr+RnsppB\n" +
|
||||||
|
"oM4WIQSs/H/6Ar7B6FACLTWv5GeymkGgzgKeAQKbAQUWAgMBAAQLCQgHBRUKCQgL\n" +
|
||||||
|
"BYkJZgGAApkBAADrLw//U5KEArVsiOmsK02Fru9BAAbFf2+otw55d5UFNCd/G93T\n" +
|
||||||
|
"n38lTuThl9Tk3dLGd/tL39fZjB7XlJ2eBcV/8uzBp9F3d14j/GDwkx7gkaA34TMr\n" +
|
||||||
|
"g73XnIdw2V88WEuKhFg0JAGUm6C8LEtlHYie7kS+gDtyQjQw7qUGhCG/QHyIi0iO\n" +
|
||||||
|
"DA31NCOSHJI2rhK8nkS6SGGzUDwWEP92bKnlcqtooCwdEdPuRuNCJ6J5GDdhX06t\n" +
|
||||||
|
"ZngnLRMHt6pN/UDYdNNVJaIV0ZFICMRTPJSSHzIV59cQx20DBZ7cYK2ag6uDx+O+\n" +
|
||||||
|
"tVWamNcb9JR5TuN7PX+Q/EEhiKpaom/lFNcQwqj4kclwraZXQ2HaHDoJoqCIaBVA\n" +
|
||||||
|
"4eGG1nCy2weEgrSPk4GxFjqAiaifR3Y75JqPG8VTpS51iKU4gvs4EeU/8+WFBOjp\n" +
|
||||||
|
"lZH7FAS9vqCPltZm/6hTD7phqRXAZe3J1RoxFFAl3ikzz1Wz7y/m/y4ouz3bIgRR\n" +
|
||||||
|
"gjFmjPYSbh/p4SMj1jELebs7klqp7hKsXFP+mZm/Oh8WK96G6IIt/TwWJ6exOB1Z\n" +
|
||||||
|
"pcS9RiFOGjfpIy0xzMOGj9EFX3qs+jwpc+oRRCAyqRZZNrvRCgY4crHVBQa5AyJb\n" +
|
||||||
|
"TJ4OfofY8Lj+MBNJO5M3FpljnjP89JMxFqtYGzj/qtDv9QFWXan2zbMRfb8q2kWd\n" +
|
||||||
|
"BxgEZhAGFQEQAPqHNmUHC6rw1y/1uXyd0Y7NoprEB2TAWoUxbX3ZFfUCGh881CAU\n" +
|
||||||
|
"8JQiTED5yXZRbwi4JQSBXg+yRjx8puB5AvHAvZn2AWreXaTyDfoXMXg20qm5sp2V\n" +
|
||||||
|
"mVmtr6iI5rXifa0I9kMJvW0jNNsPFgXuo3/1dTM7U11/HDzdmh9arKGB7MnQphmV\n" +
|
||||||
|
"T3L0wFhY9lHMtNn3CmiTqJNDJhHWTMTWeOictqULwIccFoQJEdBzhJZE3+KX+yv/\n" +
|
||||||
|
"a74DJoSa27SQjJEGUmXCEx3GZiwwGP201hP5TKPLLxfd5B+W+uGbPP/T9O3LEDNp\n" +
|
||||||
|
"hUXYmuKSp77+Zq0JHWnvlSvKDr4oDQ8Wgiq1iDD1baYY1EWmn5olVwyi7jS2m5mG\n" +
|
||||||
|
"fMW7MFBssG1nbmUama/pPLqcV4nr6URveGDFwcx6/ulMkN9P0C1qR3K4Asx1ZB49\n" +
|
||||||
|
"Kt+iz1fuzh+lFU35DS/wRT9LyUzaSvaGegThHNAhw24m19vb5mrBUtQOGuW0MCEh\n" +
|
||||||
|
"CzWkhjMaQVRbCrUqT+ab+X/2xA7ETKITtq40IAsk3tW8YLnKfEt+u7BMMGqPJV9D\n" +
|
||||||
|
"oVRQZwW+xc47T6KfmNEw2RzkoxbmZMnSUBjp0MFWs6Tc9a0OMqnwdbrsxlN1AP0e\n" +
|
||||||
|
"XhpRs8Hl2TuloY9j1yDW2aZ8l1g0KQMbkPKH9dgf+XR0+un1p6HsJpRHABEBAAEA\n" +
|
||||||
|
"D/wKn48r64eQIRRO4VGTOjH3pzqc63EQ0aNFAJqO+pSWxhcLeg3YqmqlLWskWjMz\n" +
|
||||||
|
"xDI8IWrYbQ/rBHk7+WEuJZN9YtnnXGok+PbplqYHE9KyMUjvj4NGcWCGT/oh4GRA\n" +
|
||||||
|
"FDGWE8o1f4U7yoFkRJh/eeYO9/6XRI29ajVtU0xExhiJ5LOAv0s7zHwI+N3rISKY\n" +
|
||||||
|
"x2Bn2bTkSFaen/tOSFMLCbkoy/RmvT/VutgtkyDhQPS/Vn5T4nPxIqyT6xhICTUF\n" +
|
||||||
|
"zBdZ0vXNgNREr/QHLabxoyhswmaAj44Yqf0RZdqPlICarIc3SiQOugu/sXan4uYg\n" +
|
||||||
|
"EDOUZM2Nf25I5BGJ+LLND/xG89865BzCiCLtIii6HnB6fabbKpPIEm6JTL8NuS7L\n" +
|
||||||
|
"rjcWefeWWSlrAOjZGC0OySxIVRLWnE3Mw0YLhWdqdb/zit9dP0tYrezdsRmX6I9D\n" +
|
||||||
|
"eRpGHKhFPLwyuv9Q/7opJqBuj8OmuFBQxNOipr9IKF2OJkqLBcy92rThIETliW5G\n" +
|
||||||
|
"6xF7wVYe4leEGzYrp4Zi3meO+CJoyw2vVj7RcZKU9Lyc3MR5VxHjl7aqrfhgtpGS\n" +
|
||||||
|
"3YEmW0O58guXc9hdrVE/dy7r0pW6CZo08w+dv2OSOyvjTdq8SkdE8cKJ52eipR/3\n" +
|
||||||
|
"SbNIu3sgd3+keXqvXvvhIHjvbqoV/c4cMnzj4FaR5w1xuQgA+18DlzS2+BocmotZ\n" +
|
||||||
|
"4uhQPheFrQsmInawLOVDVjMIf1si64DrjeKC9+3SjJseTD3nIy26/s9kOI4ixkjb\n" +
|
||||||
|
"jO4J2/fNxyT5AK6Owcpy8wTOkXaI8MAhPq1IWq8dAZFnWxJNOheNx5HJwhx4Dvrn\n" +
|
||||||
|
"z4ONDFfKBZ6eSSiak2eJ7B07jjyU1yu1gAXRjc61cZKK9V1dY6/HgJzuzEXLYqpD\n" +
|
||||||
|
"MXHz33Uqkg1qRtkqECx2i7Vo78gZcASB8fAsE7Rinub9dJlfdwWGMgwOU88g/aMs\n" +
|
||||||
|
"KaizN4fosqpX+Y+0uofl40lpKQFcmJOMCxCKZeaBD1+UOu2jIgI4UcRfh75FyQn8\n" +
|
||||||
|
"zzmCZQgA/yQ5fwyQtwYiUIlmwAdKzHDoknVmYdlWwBjlFhLTFJccIjGzzGePN7nE\n" +
|
||||||
|
"iwxJfX/LZ4ObprT+q0nfIf37cPqXfPv35S/yutLCX5CjkIsiD93aCsDE58/WlwFQ\n" +
|
||||||
|
"oxi62pAMYl83HfHLtyRFFuXmpnt+Su7tlzEYCRn8JakKy/VzyEEbjRiQgJoB/Mje\n" +
|
||||||
|
"GGlbfY+huTCGM40gtAduOzt15Qxr4JY1QLArX+Dosf2ylAoXcmcHz9wNIAAb40fr\n" +
|
||||||
|
"fO13k5FqXQDJh5GFSOQliQX98D5ip1SDK2Ut8EDuq2NPjMbfI08vf9TELhEmTXy2\n" +
|
||||||
|
"7rCM99in9kFQCkKH7cgTnihyY9N7OwgAmpnlXADaiCQGB7V1QQWhiJCPPB0ptFQi\n" +
|
||||||
|
"ZxFDgJ8cqX46Wg7cK9pC8uLpYMpTGIGQonygKEOVf5QY3CP4mZ2WujCQ7m4sDpw0\n" +
|
||||||
|
"tkpdjTkhz4Kz1xw07toxKiqSjwlKq7TLm/HbqNZLijLLyvjTB1xxlwh7XCAaXzr2\n" +
|
||||||
|
"Ri4dBHVqQIO/sygBVSRPHVqT6nKjo+Bz+qb7Bef71jkANrmIRjzq66X1fYlrQejd\n" +
|
||||||
|
"4W7/+YNzOsKHcgBoF6A1texG59JH1raD5GVnBfFLWoYx4T57oN0mK2+BDSNCQSYe\n" +
|
||||||
|
"/gzxxdbocaWMjW71JBudLKh9vXwhqPLD5828YkTJWCyxIAjk/6BtSXxZiQRSBBgB\n" +
|
||||||
|
"CgI8BQJmEAYYAp4BApsCBRYCAwEABAsJCAcFFQoJCAvBXSAEGQEKAAYFAmYQBhgA\n" +
|
||||||
|
"CgkQiw37fiqkMiCWGxAA99f7sR1epBqiq1oYVOQoj6ZK/Vzstbv31vx1evQSy4/j\n" +
|
||||||
|
"9RhD8pNZWg9Kb2U5vv8ZZoJGBNm071P9/sZE5YJm/2GB7CStC3z5WvHPQZK5a2L5\n" +
|
||||||
|
"d8jp99+fkK87qlWbig4AiRD/nhaoHshsKqp9q+5NEypZapZleQ2HIJ+wW3aqjtj+\n" +
|
||||||
|
"U54JZiXxd95pMbx6JMee6SvpKGZTceem7jOljFwMZ0I+qPmaFpALJfZI3pxKCozg\n" +
|
||||||
|
"72yABVk4ICWJ5xZuxfUvoIkCQ9wcw+D3xYaHWQ1jl8l/mzZaBefa4ZlSk//ajgPY\n" +
|
||||||
|
"HhDrEmhKTQ6Lv0aLC83pVo66IRDZwCrcYFy2cEefxX3FHqGr6sD/cPMMlu/aBzG+\n" +
|
||||||
|
"oAvx3Xseav9zv5eZlRET4MD8QO7bPC7DeGdBPOhKIAoiFCOB9hIlr7MTNFpQkpcn\n" +
|
||||||
|
"dBovK3s21+E2cAzPuhrBuHKPXeGV8bHPdmtZk5wtaBfBwwbtAzirGQOx/aimDK2y\n" +
|
||||||
|
"Tx1Kjyqb6QCdjJopzLLtBGd9PQcpUPenngZ8+8uE2inNlZRczEfB6YtNitAtIx/G\n" +
|
||||||
|
"qkD6DagReyD/gmqQKUGn/6amYkux9dAs4sD/F1NN1hN4BWlvhpXVkiqG9/1MAu77\n" +
|
||||||
|
"n+ne74CJcWJ93fKokMvubyusVJfZfuQuLYz0NcwxN3YMlt8yDL4l0ZK+GEjXd8oA\n" +
|
||||||
|
"CgkQr+RnsppBoM7aUQ//fnTz/4jFGqHssqp3ZQro8Ie4NEmtmjioFzq9FZQX5KAZ\n" +
|
||||||
|
"uL8q6pT1ChV6uqvL9YgfYgbSGaWaVRIJlt+cfz8EfbHpgHvEj1R94TudE0MajDdR\n" +
|
||||||
|
"1V4jpEIHtlftoN9m9n60woAFScN+7LjQ/TRZDf2Ie6lBkpTEHr1gUvb/VzyiOSxg\n" +
|
||||||
|
"sYMbcPPpcymCPJKyzx2DHFryHRS7YzoRHb8Apmlat8ceervNTPzErznsN1LljEA7\n" +
|
||||||
|
"qhghWgkCrpApTGOESpwcoGli6m62tDZvLzpIJhHK3yan6nC7VcQ09FHdMJc+762Q\n" +
|
||||||
|
"ZxPbtZalnCrxrh22F1KcJwMYBo+PeOXueL3fCiPGImEe2DT1SIV8wmO5yCffxGoF\n" +
|
||||||
|
"ylGotT6HrGQPseB8yo0WycVs9PIhyLPc1D6SVerLQn34ru0DxuXX2P6x0UvXxH74\n" +
|
||||||
|
"z2VCGj78oBv2lguy47d3IEllWhFTJUHyH+KR7gUQlH4f4S0/drqH0s9oLl/xGUET\n" +
|
||||||
|
"9IHDK/aPh57DTdpyNur/75cty8f94ScmwclYB7L+z1wMMDe3qA9GhVG9UvL1s8oz\n" +
|
||||||
|
"OQ4T1XwKMf5a4obCkOoyV7B41RdwaKHYVkDGcqvVbQJRQGpyE8fzqFakjjdVN0SN\n" +
|
||||||
|
"diyJvSrr60QeUdnykTmQRZK+hajAz0hKSHT9L9bhgwfMUDd8SwM8D/shHgvVffGd\n" +
|
||||||
|
"BxgEZhAGGAEQALcxdafRMdeNOF21Z6AFLIJ4jcsPcgsJ1sTpMHHJCGBfHB5iE5VF\n" +
|
||||||
|
"LbCYKMES1mhe58JsY2KLDj/9YgzJKfO4tn9SXVWmMCOsVoa9N0OH0H2/QxcipjCt\n" +
|
||||||
|
"LNFg1nOYVSq47TmgAMC+NnAMPYBh0MlZrr++Z4WFxNxKukNmvlO2JGRTMm+p/6qr\n" +
|
||||||
|
"zhXyBaVxI2ZW6wqg79JAFKfC7v2b8Zfp7t2ehlRQ0gweHLOFBjIhaZatk93J9pF4\n" +
|
||||||
|
"epzI6CuBLMwoFKmj/bVbrZPPUabe8vMJt/sZjxQzzfKLnHVRfihs7InFGaf6wgxF\n" +
|
||||||
|
"7cDDA5/x0/wsmOlOaxyt5nInJnMLdUzmIBVu95YjAaBFejF8t9JzhOgDXQ/A7Zx0\n" +
|
||||||
|
"EQHDF56FYvIcHVjUCJ4qeLJYCWMNAHBkUbyCltMJnFB+aYc28eOQW6fHBczo+jmr\n" +
|
||||||
|
"HNjsg+1RWal1ZijzAmCN1aAHY9DPvUZklLn/qpk9EtpI6hLRP94sbbr1bJzibBi9\n" +
|
||||||
|
"mEvsL8TwhpEqwc+HIGqnvVQHqb1mVEmcGVdhiqmNFe94lyYkv7BWim5mu6b62Dkl\n" +
|
||||||
|
"wTAcpAowYzqkZ9UXqBlHMGqrFqtSvT6PBh+s7ZCd1/ueACV5bM9G9lU5DRZOcuAS\n" +
|
||||||
|
"dM0TI6a04PEziV1npOFxcnx+sZApDZ8Rqpa7nkZXsKmxhRoupUi4neAFABEBAAEA\n" +
|
||||||
|
"D/4//TrHv77VODL0KKVls+j0Of/tahu/11P5vCp71GjkoNRFmKSWg2+OO9ggeOAD\n" +
|
||||||
|
"3QK/WvTsOv5jQ7K4HJxW0bKNjsujW0V9cHlY30cqg4pEIkbhEe1TG2qISHcgMZmu\n" +
|
||||||
|
"LqJOeqFIsih5wwzIh2JSsszjlTK75Rn6iO+/E2hv/TOBB76aWps/lnuKFtv6Cib/\n" +
|
||||||
|
"XGUFdWnP2ypb3y9zzsD4+3HAX9s0IHb+XJZR7qlXYWxsgX0g/6bs8VSC53qRl7F6\n" +
|
||||||
|
"LpXpG6tHahqbgtNWopHiawak4yyjNeU+T537LNgQbtvA0+Q+VMzrVJHTv0rI18Pg\n" +
|
||||||
|
"VgOjmwy3G9dfEGXR0bLLhaa2vfvBA54yXTfIN8qn6iJctxRucfJEEYWWIcdXQNmv\n" +
|
||||||
|
"aD2Ozz0IzDDSMyyZiAibhuoOOL4izinFX9O7AxVEkIf3+IHloxjuT6LSllNOO8ZB\n" +
|
||||||
|
"xKsyeLFgIjQFXjY4sR+JleML+YkCq46NdHVmbb6TryDelkduqCHOk2PGvdv/1zrf\n" +
|
||||||
|
"yTRMWQLx80Urd4rOKxNOnqwblj6yt1eOINXA31GkJSIEOkkLdFOZ/H7zp+UqbGE/\n" +
|
||||||
|
"4nGh/JzBur/jTLIx9ElsrV3lLGEtUa47mgbMYAXIIL2RR+LMlT4JoO3SQXEuvkok\n" +
|
||||||
|
"RT7NpyoVTWHYlQiR0XruCt4bvWaEVCdiNj565yI7pqPzkQgAxZ5tKTPW+8uHOQSh\n" +
|
||||||
|
"NuY9Ok2bUcaNvzboOu7t3c9rETf5/XBAjcmjNSxlDLLiZv9Qp3DhcnXu45ZDSh86\n" +
|
||||||
|
"Sd+o9NlQLTAs/J98rRExDISj42V3q740Hf8jXS3lz4fwlr3YHeV0c1A+YsIoY5nb\n" +
|
||||||
|
"Bs2UHO5nX1PgtE17Bgbt1+Y3jamK/2+W7WDAiIq7G4uRpeORqrXsjWZDuIaKR03C\n" +
|
||||||
|
"lxj1c7LqkFUjVpY47mdlBDuXmra7slcYVdabR5oULpRvloAjp+VX3Yfwzl0/I0o6\n" +
|
||||||
|
"/3WZTRhM4PhkP4imkveykv/BkIDFWfho3pJScru8Aqn3XbccrZO8WY6nONzvYIr+\n" +
|
||||||
|
"COgWpwgA7VALxeELI7nJiWeZUZ5HBHOMKQFTdioKD9BgwvEPDOXq+VqAT2n3basQ\n" +
|
||||||
|
"ShYjCot8gnxe3ei+/KQs/a8DkZVDY7XKMqhZyvidxF6weFAbO4Z+sB6LfyJmz5D1\n" +
|
||||||
|
"nA0iv8laP6at1wgn1MSfpXZm1cXuUHDpyTqWXLxG1qbUpQJS27fl6F0ws1k8Qvno\n" +
|
||||||
|
"PLjGbsbEeujbWU2lqZDs3L72e/arIpHdO94LNOwSKrGjO1KTkGgue+uGm1u17gEZ\n" +
|
||||||
|
"3U+rbCvG2lTq7kHZEQHiGaIygxyDJAyIvroPZ9c5nABLGMoH93Pq9tGX2OPxyWLx\n" +
|
||||||
|
"P1db87EzKRQjFGmZSeeKENRvLYAVcwgAqZTCPD9CkUG6O//rZI/1pPArhN8CY/+x\n" +
|
||||||
|
"8L2jksvaTLimdBzXyVXlNAPIynMih/QaxNq+WNOmnihVFAw7ur/nmN9Oea33Ue7v\n" +
|
||||||
|
"6Ryrz9nzi6GonLz0/grzB5XWAiO3+i9WUdVpeTgjlFPXSJsbOjEVRAB2W0heioeM\n" +
|
||||||
|
"M6weKP8i2O+GF3ULoWzmam4EgsanaSCXgDq1RYXyJhfUYTikVqrD+qJYT0asxU0H\n" +
|
||||||
|
"S92KmdZ6UKLOTPZdrIL5X0Cj/ejBX/94xHLM0GfXT9CIHTVIVSa2poSbWhN0O+yT\n" +
|
||||||
|
"NMySr7OHGDUjEUqBL2O5Wm3oyMK/5EoSQ9YJBCrkbkymvoahjrooXZSeiQIzBBgB\n" +
|
||||||
|
"CgAdBQJmEAYYAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQr+RnsppBoM7gBQ//\n" +
|
||||||
|
"YGQ6suANXV0C7TvofcMwubahLriBiWGAkI+pNQ7Tmzgql4ALcf4BFsDffcTCy3ue\n" +
|
||||||
|
"dLAZkEEwT86Ip1qW7mKCN1tScj0g8uU8U78oVShqoyEq0ebemaVmY7gvFWCpGXgr\n" +
|
||||||
|
"JgNjL4QzRbwzKUdvIkTMmUDiPqhmGuCFV76pazzTmhI/RAzVfnroG0kVDVwODlCI\n" +
|
||||||
|
"CcswoEOI32v+B8SJbbsXXQ4E6jrDwPIUV/z54VERbi+JcKaes0a/rOLF0FOnoTFs\n" +
|
||||||
|
"2b2aqt+Knj/RQCP9r3UNl06dONBMaydkhFmMjRjenQb6p+91JyNkXJ6tgD4CDbps\n" +
|
||||||
|
"2kqryCpZf39E0QU75Rvuofc8vhSRbRvSl7D+NhOdk9c6EEOJPbtozaG+/l8g1K42\n" +
|
||||||
|
"aMT+TvccBPK0b4EcZtAyCLjDg0eA/GN5+DIhBQCQA4y4KdEf3IMoS+BeEPBNfNyG\n" +
|
||||||
|
"isVD/f/R/68uGg6S9sKFXOCAXO0rnVIu1Oe123l09FsXVZpFDV3PMbpk7Sxh/20L\n" +
|
||||||
|
"ERTp5jBV+2J2szmYYxcSfVl6h+P3k8Y8l4evEoPPL6Skz9uZI5C3UB1c6dnWBcRm\n" +
|
||||||
|
"G759QEMpJuvyuwFZkcoGxfVvtneZsTGajEnNOB4o46hb9a5DLJ9tc06zG+j+eUeR\n" +
|
||||||
|
"C6mwvn/JYRaGr9uG4zpdxNeFmQ80yitmGGllHenjfaU=\n" +
|
||||||
|
"=hhYV\n" +
|
||||||
|
"-----END PGP PRIVATE KEY BLOCK-----\n";
|
||||||
|
|
||||||
|
private static byte[] DATA = "Hello, World!".getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// Signature by primary key (shall be rejected, as the primary key is not signing capable.
|
||||||
|
// We instead expect the signing subkey to sign).
|
||||||
|
private static final String SIG = "-----BEGIN PGP SIGNATURE-----\n" +
|
||||||
|
"Version: PGPainless\n" +
|
||||||
|
"\n" +
|
||||||
|
"iQIcBAABCAAGBQJmEAYYAAoJEK/kZ7KaQaDOH4MP/2kaK8lQaBU+jChpWPLR2R2+\n" +
|
||||||
|
"dB7j29tFPRAqbzbazxaF+jZQxuuHWtM3bwd9Vdta9zirDc27b7XyufFLBza4Bn+R\n" +
|
||||||
|
"7fT7uHTQQts/zaX8YGxJ90rb06toFXiv/rlm531kLaGXxlACU6SpI8maqpP4im+G\n" +
|
||||||
|
"W0LgBDZiT9udFs3eeJZ8O3yDLP29Rdw8sHPa6pOyyhkkhsvo0bNaBaSt6GDW5UK9\n" +
|
||||||
|
"f5Gz+XF9ZLJgsNqQwWQM55+4ZhdkfEszRcJgAhuSCamk+ZLfvIPhEu21/7weNq3c\n" +
|
||||||
|
"Yp0hvaz27gaW7IkjEgI1FqkPrmJmyk5SVMMvaev9p0WXDgUIeDLI6CwvoXaoMCAX\n" +
|
||||||
|
"pg0Q754ccHu2pELwNb5YIxGSPSMXRVH8xUDqicZpl/50ucy3g348s5HekcVnzBtX\n" +
|
||||||
|
"UKVX3tU6r5HrkVAX7bDGht3WXE1jRE98W3uKpFWzrJBK+uQIyOtOEXeKT+z1BbNy\n" +
|
||||||
|
"CvYeDq4xjpGYB2tJY2LKXrC4+IzJ56e3XU2t75KhO0SBV+Ax1bJ0MBnmAedg0pg5\n" +
|
||||||
|
"0r+mknBqoYu+OHNwMS/N+YH1iZEV0QxP+ldLp+ff2QiIvtiDcOIQ0oNEJy0bqh0p\n" +
|
||||||
|
"TrS9PKMl+kH+FaAPUVb0ruviTd/zPljjiJ2P396bu1JBXdoncn2y4KklQOWHJSVI\n" +
|
||||||
|
"F5ZjatEBixl6pdW7I5Cr\n" +
|
||||||
|
"=vPG/\n" +
|
||||||
|
"-----END PGP SIGNATURE-----";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSignatureByNonSigningPrimaryKeyIsRejected() throws Exception {
|
||||||
|
PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY);
|
||||||
|
|
||||||
|
DecryptionStream verifier = PGPainless.decryptAndOrVerify()
|
||||||
|
.onInputStream(new ByteArrayInputStream(DATA))
|
||||||
|
.withOptions(ConsumerOptions.get()
|
||||||
|
.addVerificationCert(PGPainless.extractCertificate(key))
|
||||||
|
.addVerificationOfDetachedSignatures(new ByteArrayInputStream(SIG.getBytes(StandardCharsets.UTF_8))));
|
||||||
|
|
||||||
|
Streams.drain(verifier);
|
||||||
|
verifier.close();
|
||||||
|
|
||||||
|
MessageMetadata result = verifier.getMetadata();
|
||||||
|
assertFalse(result.isVerifiedSigned());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.key
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||||
|
import org.junit.jupiter.api.Disabled
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.pgpainless.PGPainless
|
||||||
|
|
||||||
|
class KeyWithUnknownSecretKeyEncryptionMethodTest {
|
||||||
|
|
||||||
|
// Test vector from https://gitlab.com/dkg/openpgp-hardware-secrets/-/merge_requests/2
|
||||||
|
val KEY =
|
||||||
|
"""-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
|
||||||
|
xTQEZgWtcxYJKwYBBAHaRw8BAQdAlLK6UPQsVHR2ETk1SwVIG3tBmpiEtikYYlCy
|
||||||
|
1TIiqzb8zR08aGFyZHdhcmUtc2VjcmV0QGV4YW1wbGUub3JnPsKNBBAWCAA1AhkB
|
||||||
|
BQJmBa1zAhsDCAsJCAcKDQwLBRUKCQgLAhYCFiEEXlP8Tur0WZR+f0I33/i9Uh4O
|
||||||
|
HEkACgkQ3/i9Uh4OHEnryAD8CzH2ajJvASp46ApfI4pLPY57rjBX++d/2FQPRyqG
|
||||||
|
HJUA/RLsNNgxiFYmK5cjtQe2/DgzWQ7R6PxPC6oa3XM7xPcCxzkEZgWtcxIKKwYB
|
||||||
|
BAGXVQEFAQEHQE1YXOKeaklwG01Yab4xopP9wbu1E+pCrP1xQpiFZW5KAwEIB/zC
|
||||||
|
eAQYFggAIAUCZgWtcwIbDBYhBF5T/E7q9FmUfn9CN9/4vVIeDhxJAAoJEN/4vVIe
|
||||||
|
DhxJVTgA/1WaFrKdP3AgL0Ffdooc5XXbjQsj0uHo6FZSHRI4pchMAQCyJnKQ3RvW
|
||||||
|
/0gm41JCqImyg2fxWG4hY0N5Q7Rc6PyzDQ==
|
||||||
|
=3w/O
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----"""
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Disabled("Disabled since BC 1.77 chokes on the test key")
|
||||||
|
fun testExtractCertificate() {
|
||||||
|
val key = PGPainless.readKeyRing().secretKeyRing(KEY)!!
|
||||||
|
val cert = PGPainless.extractCertificate(key)
|
||||||
|
|
||||||
|
assertNotNull(cert)
|
||||||
|
// Each secret key got its public key component extracted
|
||||||
|
assertEquals(
|
||||||
|
key.secretKeys.asSequence().map { it.keyID }.toSet(),
|
||||||
|
cert.publicKeys.asSequence().map { it.keyID }.toSet())
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
# PGPainless-SOP
|
# PGPainless-SOP
|
||||||
|
|
||||||
[![Spec Revision: 8](https://img.shields.io/badge/Spec%20Revision-8-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/)
|
[![Spec Revision: 10](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/)
|
||||||
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-sop)](https://search.maven.org/artifact/org.pgpainless/pgpainless-sop)
|
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-sop)](https://search.maven.org/artifact/org.pgpainless/pgpainless-sop)
|
||||||
[![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-sop/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-sop)
|
[![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-sop/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-sop)
|
||||||
|
|
||||||
|
|
|
@ -34,3 +34,10 @@ test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
environment("test.implementation", "sop.testsuite.pgpainless.PGPainlessSopInstanceFactory")
|
environment("test.implementation", "sop.testsuite.pgpainless.PGPainlessSopInstanceFactory")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_modular_auto
|
||||||
|
tasks.named('jar') {
|
||||||
|
manifest {
|
||||||
|
attributes('Automatic-Module-Name': 'org.pgpainless.sop')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
|
||||||
import org.bouncycastle.util.io.Streams;
|
|
||||||
import org.pgpainless.decryption_verification.OpenPgpInputStream;
|
|
||||||
import org.pgpainless.util.ArmoredOutputStreamFactory;
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.enums.ArmorLabel;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.Armor;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the <pre>armor</pre> operation using PGPainless.
|
|
||||||
*/
|
|
||||||
public class ArmorImpl implements Armor {
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
@Deprecated
|
|
||||||
public Armor label(@Nonnull ArmorLabel label) throws SOPGPException.UnsupportedOption {
|
|
||||||
throw new SOPGPException.UnsupportedOption("Setting custom Armor labels not supported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public Ready data(@Nonnull InputStream data) throws SOPGPException.BadData {
|
|
||||||
return new Ready() {
|
|
||||||
@Override
|
|
||||||
public void writeTo(@Nonnull OutputStream outputStream) throws IOException {
|
|
||||||
// By buffering the output stream, we can improve performance drastically
|
|
||||||
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
|
|
||||||
|
|
||||||
// Determine nature of the given data
|
|
||||||
OpenPgpInputStream openPgpIn = new OpenPgpInputStream(data);
|
|
||||||
openPgpIn.reset();
|
|
||||||
|
|
||||||
if (openPgpIn.isAsciiArmored()) {
|
|
||||||
// armoring already-armored data is an idempotent operation
|
|
||||||
Streams.pipeAll(openPgpIn, bufferedOutputStream);
|
|
||||||
bufferedOutputStream.flush();
|
|
||||||
openPgpIn.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(bufferedOutputStream);
|
|
||||||
Streams.pipeAll(openPgpIn, armor);
|
|
||||||
bufferedOutputStream.flush();
|
|
||||||
armor.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
|
||||||
import org.pgpainless.exception.MissingPassphraseException;
|
|
||||||
import org.pgpainless.key.OpenPgpFingerprint;
|
|
||||||
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
|
||||||
import org.pgpainless.key.util.KeyRingUtils;
|
|
||||||
import org.pgpainless.util.ArmoredOutputStreamFactory;
|
|
||||||
import org.pgpainless.util.Passphrase;
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.ChangeKeyPassword;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
public class ChangeKeyPasswordImpl implements ChangeKeyPassword {
|
|
||||||
|
|
||||||
private final MatchMakingSecretKeyRingProtector oldProtector = new MatchMakingSecretKeyRingProtector();
|
|
||||||
private Passphrase newPassphrase = Passphrase.emptyPassphrase();
|
|
||||||
private boolean armor = true;
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public ChangeKeyPassword noArmor() {
|
|
||||||
armor = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public ChangeKeyPassword oldKeyPassphrase(@Nonnull String oldPassphrase) {
|
|
||||||
oldProtector.addPassphrase(Passphrase.fromPassword(oldPassphrase));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public ChangeKeyPassword newKeyPassphrase(@Nonnull String newPassphrase) {
|
|
||||||
this.newPassphrase = Passphrase.fromPassword(newPassphrase);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public Ready keys(@Nonnull InputStream inputStream) throws SOPGPException.KeyIsProtected {
|
|
||||||
SecretKeyRingProtector newProtector = SecretKeyRingProtector.unlockAnyKeyWith(newPassphrase);
|
|
||||||
PGPSecretKeyRingCollection secretKeyRingCollection;
|
|
||||||
try {
|
|
||||||
secretKeyRingCollection = KeyReader.readSecretKeys(inputStream, true);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new SOPGPException.BadData(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<PGPSecretKeyRing> updatedSecretKeys = new ArrayList<>();
|
|
||||||
for (PGPSecretKeyRing secretKeys : secretKeyRingCollection) {
|
|
||||||
oldProtector.addSecretKey(secretKeys);
|
|
||||||
try {
|
|
||||||
PGPSecretKeyRing changed = KeyRingUtils.changePassphrase(null, secretKeys, oldProtector, newProtector);
|
|
||||||
updatedSecretKeys.add(changed);
|
|
||||||
} catch (MissingPassphraseException e) {
|
|
||||||
throw new SOPGPException.KeyIsProtected("Cannot unlock key " + OpenPgpFingerprint.of(secretKeys), e);
|
|
||||||
} catch (PGPException e) {
|
|
||||||
if (e.getMessage().contains("Exception decrypting key")) {
|
|
||||||
throw new SOPGPException.KeyIsProtected("Cannot unlock key " + OpenPgpFingerprint.of(secretKeys), e);
|
|
||||||
}
|
|
||||||
throw new RuntimeException("Cannot change passphrase of key " + OpenPgpFingerprint.of(secretKeys), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final PGPSecretKeyRingCollection changedSecretKeyCollection = new PGPSecretKeyRingCollection(updatedSecretKeys);
|
|
||||||
return new Ready() {
|
|
||||||
@Override
|
|
||||||
public void writeTo(@Nonnull OutputStream outputStream) throws IOException {
|
|
||||||
if (armor) {
|
|
||||||
ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(outputStream);
|
|
||||||
changedSecretKeyCollection.encode(armorOut);
|
|
||||||
armorOut.close();
|
|
||||||
} else {
|
|
||||||
changedSecretKeyCollection.encode(outputStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPUtil;
|
|
||||||
import org.bouncycastle.util.io.Streams;
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.Dearmor;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the <pre>dearmor</pre> operation using PGPainless.
|
|
||||||
*/
|
|
||||||
public class DearmorImpl implements Dearmor {
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public Ready data(@Nonnull InputStream data) {
|
|
||||||
InputStream decoder;
|
|
||||||
try {
|
|
||||||
decoder = PGPUtil.getDecoderStream(data);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new SOPGPException.BadData(e);
|
|
||||||
}
|
|
||||||
return new Ready() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeTo(@Nonnull OutputStream outputStream) throws IOException {
|
|
||||||
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
|
|
||||||
Streams.pipeAll(decoder, bufferedOutputStream);
|
|
||||||
bufferedOutputStream.flush();
|
|
||||||
decoder.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,176 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
|
||||||
import org.bouncycastle.util.io.Streams;
|
|
||||||
import org.pgpainless.PGPainless;
|
|
||||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
|
|
||||||
import org.pgpainless.decryption_verification.ConsumerOptions;
|
|
||||||
import org.pgpainless.decryption_verification.DecryptionStream;
|
|
||||||
import org.pgpainless.decryption_verification.MessageMetadata;
|
|
||||||
import org.pgpainless.decryption_verification.SignatureVerification;
|
|
||||||
import org.pgpainless.exception.MalformedOpenPgpMessageException;
|
|
||||||
import org.pgpainless.exception.MissingDecryptionMethodException;
|
|
||||||
import org.pgpainless.exception.WrongPassphraseException;
|
|
||||||
import org.pgpainless.util.Passphrase;
|
|
||||||
import sop.DecryptionResult;
|
|
||||||
import sop.ReadyWithResult;
|
|
||||||
import sop.SessionKey;
|
|
||||||
import sop.Verification;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.Decrypt;
|
|
||||||
import sop.util.UTF8Util;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the <pre>decrypt</pre> operation using PGPainless.
|
|
||||||
*/
|
|
||||||
public class DecryptImpl implements Decrypt {
|
|
||||||
|
|
||||||
private final ConsumerOptions consumerOptions = ConsumerOptions.get();
|
|
||||||
private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector();
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public DecryptImpl verifyNotBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption {
|
|
||||||
consumerOptions.verifyNotBefore(timestamp);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public DecryptImpl verifyNotAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption {
|
|
||||||
consumerOptions.verifyNotAfter(timestamp);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public DecryptImpl verifyWithCert(@Nonnull InputStream certIn) throws SOPGPException.BadData, IOException {
|
|
||||||
PGPPublicKeyRingCollection certs = KeyReader.readPublicKeys(certIn, true);
|
|
||||||
if (certs != null) {
|
|
||||||
consumerOptions.addVerificationCerts(certs);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public DecryptImpl withSessionKey(@Nonnull SessionKey sessionKey) throws SOPGPException.UnsupportedOption {
|
|
||||||
consumerOptions.setSessionKey(
|
|
||||||
new org.pgpainless.util.SessionKey(
|
|
||||||
SymmetricKeyAlgorithm.requireFromId(sessionKey.getAlgorithm()),
|
|
||||||
sessionKey.getKey()));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public DecryptImpl withPassword(@Nonnull String password) {
|
|
||||||
consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(password));
|
|
||||||
String withoutTrailingWhitespace = removeTrailingWhitespace(password);
|
|
||||||
if (!password.equals(withoutTrailingWhitespace)) {
|
|
||||||
consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(withoutTrailingWhitespace));
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String removeTrailingWhitespace(String passphrase) {
|
|
||||||
int i = passphrase.length() - 1;
|
|
||||||
// Find index of first non-whitespace character from the back
|
|
||||||
while (i > 0 && Character.isWhitespace(passphrase.charAt(i))) {
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
return passphrase.substring(0, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public DecryptImpl withKey(@Nonnull InputStream keyIn) throws SOPGPException.BadData, IOException, SOPGPException.UnsupportedAsymmetricAlgo {
|
|
||||||
PGPSecretKeyRingCollection secretKeyCollection = KeyReader.readSecretKeys(keyIn, true);
|
|
||||||
|
|
||||||
for (PGPSecretKeyRing key : secretKeyCollection) {
|
|
||||||
protector.addSecretKey(key);
|
|
||||||
consumerOptions.addDecryptionKey(key, protector);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public Decrypt withKeyPassword(@Nonnull byte[] password) {
|
|
||||||
String string = new String(password, UTF8Util.UTF8);
|
|
||||||
protector.addPassphrase(Passphrase.fromPassword(string));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public ReadyWithResult<DecryptionResult> ciphertext(@Nonnull InputStream ciphertext)
|
|
||||||
throws SOPGPException.BadData,
|
|
||||||
SOPGPException.MissingArg {
|
|
||||||
|
|
||||||
if (consumerOptions.getDecryptionKeys().isEmpty() && consumerOptions.getDecryptionPassphrases().isEmpty() && consumerOptions.getSessionKey() == null) {
|
|
||||||
throw new SOPGPException.MissingArg("Missing decryption key, passphrase or session key.");
|
|
||||||
}
|
|
||||||
|
|
||||||
DecryptionStream decryptionStream;
|
|
||||||
try {
|
|
||||||
decryptionStream = PGPainless.decryptAndOrVerify()
|
|
||||||
.onInputStream(ciphertext)
|
|
||||||
.withOptions(consumerOptions);
|
|
||||||
} catch (MissingDecryptionMethodException e) {
|
|
||||||
throw new SOPGPException.CannotDecrypt("No usable decryption key or password provided.", e);
|
|
||||||
} catch (WrongPassphraseException e) {
|
|
||||||
throw new SOPGPException.KeyIsProtected();
|
|
||||||
} catch (MalformedOpenPgpMessageException | PGPException | IOException e) {
|
|
||||||
throw new SOPGPException.BadData(e);
|
|
||||||
} finally {
|
|
||||||
// Forget passphrases after decryption
|
|
||||||
protector.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ReadyWithResult<DecryptionResult>() {
|
|
||||||
@Override
|
|
||||||
public DecryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
|
|
||||||
Streams.pipeAll(decryptionStream, outputStream);
|
|
||||||
decryptionStream.close();
|
|
||||||
MessageMetadata metadata = decryptionStream.getMetadata();
|
|
||||||
|
|
||||||
if (!metadata.isEncrypted()) {
|
|
||||||
throw new SOPGPException.BadData("Data is not encrypted.");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Verification> verificationList = new ArrayList<>();
|
|
||||||
for (SignatureVerification signatureVerification : metadata.getVerifiedInlineSignatures()) {
|
|
||||||
verificationList.add(VerificationHelper.mapVerification(signatureVerification));
|
|
||||||
}
|
|
||||||
|
|
||||||
SessionKey sessionKey = null;
|
|
||||||
if (metadata.getSessionKey() != null) {
|
|
||||||
org.pgpainless.util.SessionKey sk = metadata.getSessionKey();
|
|
||||||
sessionKey = new SessionKey(
|
|
||||||
(byte) sk.getAlgorithm().getAlgorithmId(),
|
|
||||||
sk.getKey()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DecryptionResult(sessionKey, verificationList);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,168 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
|
||||||
import org.bouncycastle.openpgp.PGPSignature;
|
|
||||||
import org.bouncycastle.util.io.Streams;
|
|
||||||
import org.pgpainless.PGPainless;
|
|
||||||
import org.pgpainless.algorithm.DocumentSignatureType;
|
|
||||||
import org.pgpainless.encryption_signing.EncryptionResult;
|
|
||||||
import org.pgpainless.encryption_signing.EncryptionStream;
|
|
||||||
import org.pgpainless.encryption_signing.ProducerOptions;
|
|
||||||
import org.pgpainless.encryption_signing.SigningOptions;
|
|
||||||
import org.pgpainless.exception.KeyException;
|
|
||||||
import org.pgpainless.key.OpenPgpFingerprint;
|
|
||||||
import org.pgpainless.key.SubkeyIdentifier;
|
|
||||||
import org.pgpainless.key.info.KeyRingInfo;
|
|
||||||
import org.pgpainless.util.ArmoredOutputStreamFactory;
|
|
||||||
import org.pgpainless.util.Passphrase;
|
|
||||||
import sop.MicAlg;
|
|
||||||
import sop.ReadyWithResult;
|
|
||||||
import sop.SigningResult;
|
|
||||||
import sop.enums.SignAs;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.DetachedSign;
|
|
||||||
import sop.util.UTF8Util;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the <pre>sign</pre> operation using PGPainless.
|
|
||||||
*/
|
|
||||||
public class DetachedSignImpl implements DetachedSign {
|
|
||||||
|
|
||||||
private boolean armor = true;
|
|
||||||
private SignAs mode = SignAs.binary;
|
|
||||||
private final SigningOptions signingOptions = SigningOptions.get();
|
|
||||||
private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector();
|
|
||||||
private final List<PGPSecretKeyRing> signingKeys = new ArrayList<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DetachedSign noArmor() {
|
|
||||||
armor = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public DetachedSign mode(@Nonnull SignAs mode) {
|
|
||||||
this.mode = mode;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public DetachedSign key(@Nonnull InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
|
|
||||||
PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true);
|
|
||||||
for (PGPSecretKeyRing key : keys) {
|
|
||||||
KeyRingInfo info = PGPainless.inspectKeyRing(key);
|
|
||||||
if (!info.isUsableForSigning()) {
|
|
||||||
throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys.");
|
|
||||||
}
|
|
||||||
protector.addSecretKey(key);
|
|
||||||
signingKeys.add(key);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public DetachedSign withKeyPassword(@Nonnull byte[] password) {
|
|
||||||
String string = new String(password, UTF8Util.UTF8);
|
|
||||||
protector.addPassphrase(Passphrase.fromPassword(string));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public ReadyWithResult<SigningResult> data(@Nonnull InputStream data) throws IOException {
|
|
||||||
for (PGPSecretKeyRing key : signingKeys) {
|
|
||||||
try {
|
|
||||||
signingOptions.addDetachedSignature(protector, key, modeToSigType(mode));
|
|
||||||
} catch (KeyException.UnacceptableSigningKeyException | KeyException.MissingSecretKeyException e) {
|
|
||||||
throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(key) + " cannot sign.", e);
|
|
||||||
} catch (PGPException e) {
|
|
||||||
throw new SOPGPException.KeyIsProtected("Key " + OpenPgpFingerprint.of(key) + " cannot be unlocked.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OutputStream sink = new NullOutputStream();
|
|
||||||
try {
|
|
||||||
EncryptionStream signingStream = PGPainless.encryptAndOrSign()
|
|
||||||
.onOutputStream(sink)
|
|
||||||
.withOptions(ProducerOptions.sign(signingOptions)
|
|
||||||
.setAsciiArmor(armor));
|
|
||||||
|
|
||||||
return new ReadyWithResult<SigningResult>() {
|
|
||||||
@Override
|
|
||||||
public SigningResult writeTo(@Nonnull OutputStream outputStream) throws IOException {
|
|
||||||
|
|
||||||
if (signingStream.isClosed()) {
|
|
||||||
throw new IllegalStateException("EncryptionStream is already closed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Streams.pipeAll(data, signingStream);
|
|
||||||
signingStream.close();
|
|
||||||
EncryptionResult encryptionResult = signingStream.getResult();
|
|
||||||
|
|
||||||
// forget passphrases
|
|
||||||
protector.clear();
|
|
||||||
|
|
||||||
List<PGPSignature> signatures = new ArrayList<>();
|
|
||||||
for (SubkeyIdentifier key : encryptionResult.getDetachedSignatures().keySet()) {
|
|
||||||
signatures.addAll(encryptionResult.getDetachedSignatures().get(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
OutputStream out;
|
|
||||||
if (armor) {
|
|
||||||
out = ArmoredOutputStreamFactory.get(outputStream);
|
|
||||||
} else {
|
|
||||||
out = outputStream;
|
|
||||||
}
|
|
||||||
for (PGPSignature sig : signatures) {
|
|
||||||
sig.encode(out);
|
|
||||||
}
|
|
||||||
out.close();
|
|
||||||
outputStream.close(); // armor out does not close underlying stream
|
|
||||||
|
|
||||||
return SigningResult.builder()
|
|
||||||
.setMicAlg(micAlgFromSignatures(signatures))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (PGPException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private MicAlg micAlgFromSignatures(Iterable<PGPSignature> signatures) {
|
|
||||||
int algorithmId = 0;
|
|
||||||
for (PGPSignature signature : signatures) {
|
|
||||||
int sigAlg = signature.getHashAlgorithm();
|
|
||||||
if (algorithmId == 0 || algorithmId == sigAlg) {
|
|
||||||
algorithmId = sigAlg;
|
|
||||||
} else {
|
|
||||||
return MicAlg.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return algorithmId == 0 ? MicAlg.empty() : MicAlg.fromHashAlgorithmId(algorithmId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DocumentSignatureType modeToSigType(SignAs mode) {
|
|
||||||
return mode == SignAs.binary ? DocumentSignatureType.BINARY_DOCUMENT
|
|
||||||
: DocumentSignatureType.CANONICAL_TEXT_DOCUMENT;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
|
||||||
import org.bouncycastle.util.io.Streams;
|
|
||||||
import org.pgpainless.PGPainless;
|
|
||||||
import org.pgpainless.decryption_verification.ConsumerOptions;
|
|
||||||
import org.pgpainless.decryption_verification.DecryptionStream;
|
|
||||||
import org.pgpainless.decryption_verification.MessageMetadata;
|
|
||||||
import org.pgpainless.decryption_verification.SignatureVerification;
|
|
||||||
import org.pgpainless.exception.MalformedOpenPgpMessageException;
|
|
||||||
import sop.Verification;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.DetachedVerify;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the <pre>verify</pre> operation using PGPainless.
|
|
||||||
*/
|
|
||||||
public class DetachedVerifyImpl implements DetachedVerify {
|
|
||||||
|
|
||||||
private final ConsumerOptions options = ConsumerOptions.get();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public DetachedVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption {
|
|
||||||
options.verifyNotBefore(timestamp);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public DetachedVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption {
|
|
||||||
options.verifyNotAfter(timestamp);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public DetachedVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData, IOException {
|
|
||||||
PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true);
|
|
||||||
options.addVerificationCerts(certificates);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public DetachedVerifyImpl signatures(@Nonnull InputStream signatures) throws SOPGPException.BadData {
|
|
||||||
try {
|
|
||||||
options.addVerificationOfDetachedSignatures(signatures);
|
|
||||||
} catch (IOException | PGPException e) {
|
|
||||||
throw new SOPGPException.BadData(e);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public List<Verification> data(@Nonnull InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData {
|
|
||||||
options.forceNonOpenPgpData();
|
|
||||||
|
|
||||||
DecryptionStream decryptionStream;
|
|
||||||
try {
|
|
||||||
decryptionStream = PGPainless.decryptAndOrVerify()
|
|
||||||
.onInputStream(data)
|
|
||||||
.withOptions(options);
|
|
||||||
|
|
||||||
Streams.drain(decryptionStream);
|
|
||||||
decryptionStream.close();
|
|
||||||
|
|
||||||
MessageMetadata metadata = decryptionStream.getMetadata();
|
|
||||||
List<Verification> verificationList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (SignatureVerification signatureVerification : metadata.getVerifiedDetachedSignatures()) {
|
|
||||||
verificationList.add(VerificationHelper.mapVerification(signatureVerification));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) {
|
|
||||||
if (verificationList.isEmpty()) {
|
|
||||||
throw new SOPGPException.NoSignature();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return verificationList;
|
|
||||||
} catch (MalformedOpenPgpMessageException | PGPException e) {
|
|
||||||
throw new SOPGPException.BadData(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,201 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
|
||||||
import org.bouncycastle.util.io.Streams;
|
|
||||||
import org.pgpainless.PGPainless;
|
|
||||||
import org.pgpainless.algorithm.DocumentSignatureType;
|
|
||||||
import org.pgpainless.algorithm.StreamEncoding;
|
|
||||||
import org.pgpainless.encryption_signing.EncryptionOptions;
|
|
||||||
import org.pgpainless.encryption_signing.EncryptionStream;
|
|
||||||
import org.pgpainless.encryption_signing.ProducerOptions;
|
|
||||||
import org.pgpainless.encryption_signing.SigningOptions;
|
|
||||||
import org.pgpainless.exception.KeyException;
|
|
||||||
import org.pgpainless.exception.WrongPassphraseException;
|
|
||||||
import org.pgpainless.key.OpenPgpFingerprint;
|
|
||||||
import org.pgpainless.key.info.KeyRingInfo;
|
|
||||||
import org.pgpainless.util.Passphrase;
|
|
||||||
import sop.EncryptionResult;
|
|
||||||
import sop.Profile;
|
|
||||||
import sop.ReadyWithResult;
|
|
||||||
import sop.enums.EncryptAs;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.Encrypt;
|
|
||||||
import sop.util.ProxyOutputStream;
|
|
||||||
import sop.util.UTF8Util;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the <pre>encrypt</pre> operation using PGPainless.
|
|
||||||
*/
|
|
||||||
public class EncryptImpl implements Encrypt {
|
|
||||||
|
|
||||||
private static final Profile RFC4880_PROFILE = new Profile("rfc4880", "Follow the packet format of rfc4880");
|
|
||||||
|
|
||||||
public static final List<Profile> SUPPORTED_PROFILES = Arrays.asList(RFC4880_PROFILE);
|
|
||||||
|
|
||||||
EncryptionOptions encryptionOptions = EncryptionOptions.get();
|
|
||||||
SigningOptions signingOptions = null;
|
|
||||||
MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector();
|
|
||||||
private final Set<PGPSecretKeyRing> signingKeys = new HashSet<>();
|
|
||||||
private String profile = RFC4880_PROFILE.getName(); // TODO: Use in future releases
|
|
||||||
|
|
||||||
private EncryptAs encryptAs = EncryptAs.binary;
|
|
||||||
boolean armor = true;
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public Encrypt noArmor() {
|
|
||||||
armor = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public Encrypt mode(@Nonnull EncryptAs mode) throws SOPGPException.UnsupportedOption {
|
|
||||||
this.encryptAs = mode;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public Encrypt signWith(@Nonnull InputStream keyIn)
|
|
||||||
throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException {
|
|
||||||
if (signingOptions == null) {
|
|
||||||
signingOptions = SigningOptions.get();
|
|
||||||
}
|
|
||||||
PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true);
|
|
||||||
if (keys.size() != 1) {
|
|
||||||
throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size()));
|
|
||||||
}
|
|
||||||
PGPSecretKeyRing signingKey = keys.iterator().next();
|
|
||||||
|
|
||||||
KeyRingInfo info = PGPainless.inspectKeyRing(signingKey);
|
|
||||||
if (info.getSigningSubkeys().isEmpty()) {
|
|
||||||
throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(signingKey) + " cannot sign.");
|
|
||||||
}
|
|
||||||
|
|
||||||
protector.addSecretKey(signingKey);
|
|
||||||
signingKeys.add(signingKey);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public Encrypt withKeyPassword(@Nonnull byte[] password) {
|
|
||||||
String passphrase = new String(password, UTF8Util.UTF8);
|
|
||||||
protector.addPassphrase(Passphrase.fromPassword(passphrase));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public Encrypt withPassword(@Nonnull String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
|
|
||||||
encryptionOptions.addPassphrase(Passphrase.fromPassword(password));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public Encrypt withCert(@Nonnull InputStream cert) throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
|
|
||||||
try {
|
|
||||||
PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true);
|
|
||||||
encryptionOptions.addRecipients(certificates);
|
|
||||||
} catch (KeyException.UnacceptableEncryptionKeyException e) {
|
|
||||||
throw new SOPGPException.CertCannotEncrypt(e.getMessage(), e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new SOPGPException.BadData(e);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public Encrypt profile(@Nonnull String profileName) {
|
|
||||||
// sanitize profile name to make sure we only accept supported profiles
|
|
||||||
for (Profile profile : SUPPORTED_PROFILES) {
|
|
||||||
if (profile.getName().equals(profileName)) {
|
|
||||||
// profile is supported, return
|
|
||||||
this.profile = profile.getName();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Profile is not supported, throw
|
|
||||||
throw new SOPGPException.UnsupportedProfile("encrypt", profileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public ReadyWithResult<sop.EncryptionResult> plaintext(@Nonnull InputStream plaintext) throws IOException {
|
|
||||||
if (!encryptionOptions.hasEncryptionMethod()) {
|
|
||||||
throw new SOPGPException.MissingArg("Missing encryption method.");
|
|
||||||
}
|
|
||||||
ProducerOptions producerOptions = signingOptions != null ?
|
|
||||||
ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions) :
|
|
||||||
ProducerOptions.encrypt(encryptionOptions);
|
|
||||||
producerOptions.setAsciiArmor(armor);
|
|
||||||
producerOptions.setEncoding(encryptAsToStreamEncoding(encryptAs));
|
|
||||||
|
|
||||||
for (PGPSecretKeyRing signingKey : signingKeys) {
|
|
||||||
try {
|
|
||||||
signingOptions.addInlineSignature(
|
|
||||||
protector,
|
|
||||||
signingKey,
|
|
||||||
(encryptAs == EncryptAs.binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT)
|
|
||||||
);
|
|
||||||
} catch (KeyException.UnacceptableSigningKeyException e) {
|
|
||||||
throw new SOPGPException.KeyCannotSign();
|
|
||||||
} catch (WrongPassphraseException e) {
|
|
||||||
throw new SOPGPException.KeyIsProtected();
|
|
||||||
} catch (PGPException e) {
|
|
||||||
throw new SOPGPException.BadData(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
ProxyOutputStream proxy = new ProxyOutputStream();
|
|
||||||
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
|
|
||||||
.onOutputStream(proxy)
|
|
||||||
.withOptions(producerOptions);
|
|
||||||
|
|
||||||
return new ReadyWithResult<EncryptionResult>() {
|
|
||||||
@Override
|
|
||||||
public EncryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException {
|
|
||||||
proxy.replaceOutputStream(outputStream);
|
|
||||||
Streams.pipeAll(plaintext, encryptionStream);
|
|
||||||
encryptionStream.close();
|
|
||||||
// TODO: Extract and emit SessionKey
|
|
||||||
return new EncryptionResult(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (PGPException e) {
|
|
||||||
throw new IOException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static StreamEncoding encryptAsToStreamEncoding(EncryptAs encryptAs) {
|
|
||||||
switch (encryptAs) {
|
|
||||||
case binary:
|
|
||||||
return StreamEncoding.BINARY;
|
|
||||||
case text:
|
|
||||||
return StreamEncoding.UTF8;
|
|
||||||
}
|
|
||||||
throw new IllegalArgumentException("Invalid value encountered: " + encryptAs);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
|
||||||
import org.pgpainless.PGPainless;
|
|
||||||
import org.pgpainless.util.ArmorUtils;
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.ExtractCert;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the <pre>extract-cert</pre> operation using PGPainless.
|
|
||||||
*/
|
|
||||||
public class ExtractCertImpl implements ExtractCert {
|
|
||||||
|
|
||||||
private boolean armor = true;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public ExtractCert noArmor() {
|
|
||||||
armor = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public Ready key(@Nonnull InputStream keyInputStream) throws IOException, SOPGPException.BadData {
|
|
||||||
PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyInputStream, true);
|
|
||||||
|
|
||||||
List<PGPPublicKeyRing> certs = new ArrayList<>();
|
|
||||||
for (PGPSecretKeyRing key : keys) {
|
|
||||||
PGPPublicKeyRing cert = PGPainless.extractCertificate(key);
|
|
||||||
certs.add(cert);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Ready() {
|
|
||||||
@Override
|
|
||||||
public void writeTo(@Nonnull OutputStream outputStream) throws IOException {
|
|
||||||
|
|
||||||
for (PGPPublicKeyRing cert : certs) {
|
|
||||||
OutputStream out = armor ? ArmorUtils.toAsciiArmoredStream(cert, outputStream) : outputStream;
|
|
||||||
cert.encode(out);
|
|
||||||
|
|
||||||
if (armor) {
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
|
||||||
import org.pgpainless.PGPainless;
|
|
||||||
import org.pgpainless.algorithm.KeyFlag;
|
|
||||||
import org.pgpainless.key.generation.KeyRingBuilder;
|
|
||||||
import org.pgpainless.key.generation.KeySpec;
|
|
||||||
import org.pgpainless.key.generation.type.KeyType;
|
|
||||||
import org.pgpainless.key.generation.type.eddsa.EdDSACurve;
|
|
||||||
import org.pgpainless.key.generation.type.rsa.RsaLength;
|
|
||||||
import org.pgpainless.key.generation.type.xdh.XDHSpec;
|
|
||||||
import org.pgpainless.util.ArmorUtils;
|
|
||||||
import org.pgpainless.util.Passphrase;
|
|
||||||
import sop.Profile;
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.GenerateKey;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the <pre>generate-key</pre> operation using PGPainless.
|
|
||||||
*/
|
|
||||||
public class GenerateKeyImpl implements GenerateKey {
|
|
||||||
|
|
||||||
public static final Profile CURVE25519_PROFILE = new Profile("draft-koch-eddsa-for-openpgp-00", "Generate EdDSA / ECDH keys using Curve25519");
|
|
||||||
public static final Profile RSA4096_PROFILE = new Profile("rfc4880", "Generate 4096-bit RSA keys");
|
|
||||||
|
|
||||||
public static final List<Profile> SUPPORTED_PROFILES = Arrays.asList(CURVE25519_PROFILE, RSA4096_PROFILE);
|
|
||||||
|
|
||||||
private boolean armor = true;
|
|
||||||
private boolean signingOnly = false;
|
|
||||||
private final Set<String> userIds = new LinkedHashSet<>();
|
|
||||||
private Passphrase passphrase = Passphrase.emptyPassphrase();
|
|
||||||
private String profile = CURVE25519_PROFILE.getName();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public GenerateKey noArmor() {
|
|
||||||
this.armor = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public GenerateKey userId(@Nonnull String userId) {
|
|
||||||
this.userIds.add(userId);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public GenerateKey withKeyPassword(@Nonnull String password) {
|
|
||||||
this.passphrase = Passphrase.fromPassword(password);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public GenerateKey profile(@Nonnull String profileName) {
|
|
||||||
// Sanitize the profile name to make sure we support the given profile
|
|
||||||
for (Profile profile : SUPPORTED_PROFILES) {
|
|
||||||
if (profile.getName().equals(profileName)) {
|
|
||||||
this.profile = profileName;
|
|
||||||
// return if we found the profile
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// profile not found, throw
|
|
||||||
throw new SOPGPException.UnsupportedProfile("generate-key", profileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public GenerateKey signingOnly() {
|
|
||||||
signingOnly = true;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo {
|
|
||||||
try {
|
|
||||||
final PGPSecretKeyRing key = generateKeyWithProfile(profile, userIds, passphrase, signingOnly);
|
|
||||||
return new Ready() {
|
|
||||||
@Override
|
|
||||||
public void writeTo(@Nonnull OutputStream outputStream) throws IOException {
|
|
||||||
if (armor) {
|
|
||||||
ArmoredOutputStream armoredOutputStream = ArmorUtils.toAsciiArmoredStream(key, outputStream);
|
|
||||||
key.encode(armoredOutputStream);
|
|
||||||
armoredOutputStream.close();
|
|
||||||
} else {
|
|
||||||
key.encode(outputStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) {
|
|
||||||
throw new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", e);
|
|
||||||
} catch (PGPException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private PGPSecretKeyRing generateKeyWithProfile(String profile, Set<String> userIds, Passphrase passphrase, boolean signingOnly)
|
|
||||||
throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
|
|
||||||
KeyRingBuilder keyBuilder;
|
|
||||||
// XDH + EdDSA
|
|
||||||
if (profile.equals(CURVE25519_PROFILE.getName())) {
|
|
||||||
keyBuilder = PGPainless.buildKeyRing()
|
|
||||||
.setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER))
|
|
||||||
.addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA));
|
|
||||||
if (!signingOnly) {
|
|
||||||
keyBuilder.addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// RSA 4096
|
|
||||||
else if (profile.equals(RSA4096_PROFILE.getName())) {
|
|
||||||
keyBuilder = PGPainless.buildKeyRing()
|
|
||||||
.setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.CERTIFY_OTHER))
|
|
||||||
.addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.SIGN_DATA));
|
|
||||||
if (!signingOnly) {
|
|
||||||
keyBuilder.addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Missing else-if branch for profile. Oops.
|
|
||||||
throw new SOPGPException.UnsupportedProfile("generate-key", profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String userId : userIds) {
|
|
||||||
keyBuilder.addUserId(userId);
|
|
||||||
}
|
|
||||||
if (!passphrase.isEmpty()) {
|
|
||||||
keyBuilder.setPassphrase(passphrase);
|
|
||||||
}
|
|
||||||
return keyBuilder.build();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,156 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import org.bouncycastle.bcpg.ArmoredInputStream;
|
|
||||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
|
||||||
import org.bouncycastle.openpgp.PGPCompressedData;
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
|
||||||
import org.bouncycastle.openpgp.PGPLiteralData;
|
|
||||||
import org.bouncycastle.openpgp.PGPObjectFactory;
|
|
||||||
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
|
|
||||||
import org.bouncycastle.openpgp.PGPSignature;
|
|
||||||
import org.bouncycastle.openpgp.PGPSignatureList;
|
|
||||||
import org.bouncycastle.util.io.Streams;
|
|
||||||
import org.pgpainless.decryption_verification.OpenPgpInputStream;
|
|
||||||
import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil;
|
|
||||||
import org.pgpainless.exception.WrongConsumingMethodException;
|
|
||||||
import org.pgpainless.implementation.ImplementationFactory;
|
|
||||||
import org.pgpainless.util.ArmoredOutputStreamFactory;
|
|
||||||
import sop.ReadyWithResult;
|
|
||||||
import sop.Signatures;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.InlineDetach;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the <pre>inline-detach</pre> operation using PGPainless.
|
|
||||||
*/
|
|
||||||
public class InlineDetachImpl implements InlineDetach {
|
|
||||||
|
|
||||||
private boolean armor = true;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public InlineDetach noArmor() {
|
|
||||||
this.armor = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public ReadyWithResult<Signatures> message(@Nonnull InputStream messageInputStream) {
|
|
||||||
|
|
||||||
return new ReadyWithResult<Signatures>() {
|
|
||||||
|
|
||||||
private final ByteArrayOutputStream sigOut = new ByteArrayOutputStream();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Signatures writeTo(@Nonnull OutputStream messageOutputStream)
|
|
||||||
throws SOPGPException.NoSignature, IOException {
|
|
||||||
|
|
||||||
PGPSignatureList signatures = null;
|
|
||||||
OpenPgpInputStream pgpIn = new OpenPgpInputStream(messageInputStream);
|
|
||||||
|
|
||||||
if (pgpIn.isNonOpenPgp()) {
|
|
||||||
throw new SOPGPException.BadData("Data appears to be non-OpenPGP.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle ASCII armor
|
|
||||||
if (pgpIn.isAsciiArmored()) {
|
|
||||||
ArmoredInputStream armorIn = new ArmoredInputStream(pgpIn);
|
|
||||||
|
|
||||||
// Handle cleartext signature framework
|
|
||||||
if (armorIn.isClearText()) {
|
|
||||||
try {
|
|
||||||
signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, messageOutputStream);
|
|
||||||
if (signatures.isEmpty()) {
|
|
||||||
throw new SOPGPException.BadData("Data did not contain OpenPGP signatures.");
|
|
||||||
}
|
|
||||||
} catch (WrongConsumingMethodException e) {
|
|
||||||
throw new SOPGPException.BadData(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// else just dearmor
|
|
||||||
pgpIn = new OpenPgpInputStream(armorIn);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if data was not using cleartext signatures framework
|
|
||||||
if (signatures == null) {
|
|
||||||
|
|
||||||
if (!pgpIn.isBinaryOpenPgp()) {
|
|
||||||
throw new SOPGPException.BadData("Data was containing ASCII armored non-OpenPGP data.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle binary OpenPGP data
|
|
||||||
PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn);
|
|
||||||
Object next;
|
|
||||||
while ((next = objectFactory.nextObject()) != null) {
|
|
||||||
|
|
||||||
if (next instanceof PGPOnePassSignatureList) {
|
|
||||||
// skip over ops
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next instanceof PGPLiteralData) {
|
|
||||||
// write out contents of literal data packet
|
|
||||||
PGPLiteralData literalData = (PGPLiteralData) next;
|
|
||||||
InputStream literalIn = literalData.getDataStream();
|
|
||||||
Streams.pipeAll(literalIn, messageOutputStream);
|
|
||||||
literalIn.close();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next instanceof PGPCompressedData) {
|
|
||||||
// decompress compressed data
|
|
||||||
PGPCompressedData compressedData = (PGPCompressedData) next;
|
|
||||||
try {
|
|
||||||
objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(compressedData.getDataStream());
|
|
||||||
} catch (PGPException e) {
|
|
||||||
throw new SOPGPException.BadData("Cannot decompress PGPCompressedData", e);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next instanceof PGPSignatureList) {
|
|
||||||
signatures = (PGPSignatureList) next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signatures == null) {
|
|
||||||
throw new SOPGPException.BadData("Data did not contain OpenPGP signatures.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// write out signatures
|
|
||||||
if (armor) {
|
|
||||||
ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(sigOut);
|
|
||||||
for (PGPSignature signature : signatures) {
|
|
||||||
signature.encode(armorOut);
|
|
||||||
}
|
|
||||||
armorOut.close();
|
|
||||||
} else {
|
|
||||||
for (PGPSignature signature : signatures) {
|
|
||||||
signature.encode(sigOut);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Signatures() {
|
|
||||||
@Override
|
|
||||||
public void writeTo(@Nonnull OutputStream signatureOutputStream) throws IOException {
|
|
||||||
Streams.pipeAll(new ByteArrayInputStream(sigOut.toByteArray()), signatureOutputStream);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,135 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
|
||||||
import org.bouncycastle.util.io.Streams;
|
|
||||||
import org.pgpainless.PGPainless;
|
|
||||||
import org.pgpainless.algorithm.DocumentSignatureType;
|
|
||||||
import org.pgpainless.encryption_signing.EncryptionStream;
|
|
||||||
import org.pgpainless.encryption_signing.ProducerOptions;
|
|
||||||
import org.pgpainless.encryption_signing.SigningOptions;
|
|
||||||
import org.pgpainless.exception.KeyException;
|
|
||||||
import org.pgpainless.key.OpenPgpFingerprint;
|
|
||||||
import org.pgpainless.key.info.KeyRingInfo;
|
|
||||||
import org.pgpainless.util.Passphrase;
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.enums.InlineSignAs;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.InlineSign;
|
|
||||||
import sop.util.UTF8Util;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the <pre>inline-sign</pre> operation using PGPainless.
|
|
||||||
*/
|
|
||||||
public class InlineSignImpl implements InlineSign {
|
|
||||||
|
|
||||||
private boolean armor = true;
|
|
||||||
private InlineSignAs mode = InlineSignAs.binary;
|
|
||||||
private final SigningOptions signingOptions = new SigningOptions();
|
|
||||||
private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector();
|
|
||||||
private final List<PGPSecretKeyRing> signingKeys = new ArrayList<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public InlineSign mode(@Nonnull InlineSignAs mode) throws SOPGPException.UnsupportedOption {
|
|
||||||
this.mode = mode;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public InlineSign noArmor() {
|
|
||||||
this.armor = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public InlineSign key(@Nonnull InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
|
|
||||||
PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true);
|
|
||||||
for (PGPSecretKeyRing key : keys) {
|
|
||||||
KeyRingInfo info = PGPainless.inspectKeyRing(key);
|
|
||||||
if (!info.isUsableForSigning()) {
|
|
||||||
throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys.");
|
|
||||||
}
|
|
||||||
protector.addSecretKey(key);
|
|
||||||
signingKeys.add(key);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public InlineSign withKeyPassword(@Nonnull byte[] password) {
|
|
||||||
String string = new String(password, UTF8Util.UTF8);
|
|
||||||
protector.addPassphrase(Passphrase.fromPassword(string));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public Ready data(@Nonnull InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText {
|
|
||||||
for (PGPSecretKeyRing key : signingKeys) {
|
|
||||||
try {
|
|
||||||
if (mode == InlineSignAs.clearsigned) {
|
|
||||||
signingOptions.addDetachedSignature(protector, key, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT);
|
|
||||||
} else {
|
|
||||||
signingOptions.addInlineSignature(protector, key, modeToSigType(mode));
|
|
||||||
}
|
|
||||||
} catch (KeyException.UnacceptableSigningKeyException | KeyException.MissingSecretKeyException e) {
|
|
||||||
throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(key) + " cannot sign.", e);
|
|
||||||
} catch (PGPException e) {
|
|
||||||
throw new SOPGPException.KeyIsProtected("Key " + OpenPgpFingerprint.of(key) + " cannot be unlocked.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProducerOptions producerOptions = ProducerOptions.sign(signingOptions);
|
|
||||||
if (mode == InlineSignAs.clearsigned) {
|
|
||||||
producerOptions.setCleartextSigned();
|
|
||||||
producerOptions.setAsciiArmor(true);
|
|
||||||
} else {
|
|
||||||
producerOptions.setAsciiArmor(armor);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Ready() {
|
|
||||||
@Override
|
|
||||||
public void writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
|
|
||||||
try {
|
|
||||||
EncryptionStream signingStream = PGPainless.encryptAndOrSign()
|
|
||||||
.onOutputStream(outputStream)
|
|
||||||
.withOptions(producerOptions);
|
|
||||||
|
|
||||||
if (signingStream.isClosed()) {
|
|
||||||
throw new IllegalStateException("EncryptionStream is already closed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Streams.pipeAll(data, signingStream);
|
|
||||||
signingStream.close();
|
|
||||||
|
|
||||||
// forget passphrases
|
|
||||||
protector.clear();
|
|
||||||
} catch (PGPException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DocumentSignatureType modeToSigType(InlineSignAs mode) {
|
|
||||||
return mode == InlineSignAs.binary ? DocumentSignatureType.BINARY_DOCUMENT
|
|
||||||
: DocumentSignatureType.CANONICAL_TEXT_DOCUMENT;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
|
||||||
import org.bouncycastle.util.io.Streams;
|
|
||||||
import org.pgpainless.PGPainless;
|
|
||||||
import org.pgpainless.decryption_verification.ConsumerOptions;
|
|
||||||
import org.pgpainless.decryption_verification.DecryptionStream;
|
|
||||||
import org.pgpainless.decryption_verification.MessageMetadata;
|
|
||||||
import org.pgpainless.decryption_verification.SignatureVerification;
|
|
||||||
import org.pgpainless.exception.MalformedOpenPgpMessageException;
|
|
||||||
import org.pgpainless.exception.MissingDecryptionMethodException;
|
|
||||||
import sop.ReadyWithResult;
|
|
||||||
import sop.Verification;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.InlineVerify;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the <pre>inline-verify</pre> operation using PGPainless.
|
|
||||||
*/
|
|
||||||
public class InlineVerifyImpl implements InlineVerify {
|
|
||||||
|
|
||||||
private final ConsumerOptions options = ConsumerOptions.get();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public InlineVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption {
|
|
||||||
options.verifyNotBefore(timestamp);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public InlineVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption {
|
|
||||||
options.verifyNotAfter(timestamp);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public InlineVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData, IOException {
|
|
||||||
PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true);
|
|
||||||
options.addVerificationCerts(certificates);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public ReadyWithResult<List<Verification>> data(@Nonnull InputStream data) throws SOPGPException.NoSignature, SOPGPException.BadData {
|
|
||||||
return new ReadyWithResult<List<Verification>>() {
|
|
||||||
@Override
|
|
||||||
public List<Verification> writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
|
|
||||||
DecryptionStream decryptionStream;
|
|
||||||
try {
|
|
||||||
decryptionStream = PGPainless.decryptAndOrVerify()
|
|
||||||
.onInputStream(data)
|
|
||||||
.withOptions(options);
|
|
||||||
|
|
||||||
Streams.pipeAll(decryptionStream, outputStream);
|
|
||||||
decryptionStream.close();
|
|
||||||
|
|
||||||
MessageMetadata metadata = decryptionStream.getMetadata();
|
|
||||||
List<Verification> verificationList = new ArrayList<>();
|
|
||||||
|
|
||||||
List<SignatureVerification> verifications = metadata.isUsingCleartextSignatureFramework() ?
|
|
||||||
metadata.getVerifiedDetachedSignatures() :
|
|
||||||
metadata.getVerifiedInlineSignatures();
|
|
||||||
|
|
||||||
for (SignatureVerification signatureVerification : verifications) {
|
|
||||||
verificationList.add(VerificationHelper.mapVerification(signatureVerification));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) {
|
|
||||||
if (verificationList.isEmpty()) {
|
|
||||||
throw new SOPGPException.NoSignature();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return verificationList;
|
|
||||||
} catch (MissingDecryptionMethodException e) {
|
|
||||||
throw new SOPGPException.BadData("Cannot verify encrypted message.", e);
|
|
||||||
} catch (MalformedOpenPgpMessageException | PGPException e) {
|
|
||||||
throw new SOPGPException.BadData(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
|
||||||
import org.pgpainless.PGPainless;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reader for OpenPGP keys and certificates with error matching according to the SOP spec.
|
|
||||||
*/
|
|
||||||
class KeyReader {
|
|
||||||
|
|
||||||
static PGPSecretKeyRingCollection readSecretKeys(InputStream keyInputStream, boolean requireContent)
|
|
||||||
throws IOException, SOPGPException.BadData {
|
|
||||||
PGPSecretKeyRingCollection keys;
|
|
||||||
try {
|
|
||||||
keys = PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream);
|
|
||||||
} catch (IOException e) {
|
|
||||||
String message = e.getMessage();
|
|
||||||
if (message == null) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
if (message.startsWith("unknown object in stream:") ||
|
|
||||||
message.startsWith("invalid header encountered")) {
|
|
||||||
throw new SOPGPException.BadData(e);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requireContent && keys.size() == 0) {
|
|
||||||
throw new SOPGPException.BadData(new PGPException("No key data found."));
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PGPPublicKeyRingCollection readPublicKeys(InputStream certIn, boolean requireContent)
|
|
||||||
throws IOException {
|
|
||||||
PGPPublicKeyRingCollection certs;
|
|
||||||
try {
|
|
||||||
certs = PGPainless.readKeyRing().publicKeyRingCollection(certIn);
|
|
||||||
} catch (IOException e) {
|
|
||||||
String msg = e.getMessage();
|
|
||||||
if (msg != null && (msg.startsWith("unknown object in stream:") || msg.startsWith("invalid header encountered"))) {
|
|
||||||
throw new SOPGPException.BadData(e);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
if (requireContent && certs.size() == 0) {
|
|
||||||
throw new SOPGPException.BadData(new PGPException("No cert data found."));
|
|
||||||
}
|
|
||||||
return certs;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import sop.Profile;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.ListProfiles;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the <pre>list-profiles</pre> operation using PGPainless.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class ListProfilesImpl implements ListProfiles {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public List<Profile> subcommand(@Nonnull String command) {
|
|
||||||
|
|
||||||
switch (command) {
|
|
||||||
case "generate-key":
|
|
||||||
return GenerateKeyImpl.SUPPORTED_PROFILES;
|
|
||||||
|
|
||||||
case "encrypt":
|
|
||||||
return EncryptImpl.SUPPORTED_PROFILES;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new SOPGPException.UnsupportedProfile(command);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKey;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
|
||||||
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
|
|
||||||
import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor;
|
|
||||||
import org.pgpainless.implementation.ImplementationFactory;
|
|
||||||
import org.pgpainless.key.info.KeyInfo;
|
|
||||||
import org.pgpainless.key.protection.CachingSecretKeyRingProtector;
|
|
||||||
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
|
||||||
import org.pgpainless.key.protection.UnlockSecretKey;
|
|
||||||
import org.pgpainless.util.Passphrase;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link SecretKeyRingProtector} which can be handed passphrases and keys separately,
|
|
||||||
* and which then matches up passphrases and keys when needed.
|
|
||||||
*/
|
|
||||||
public class MatchMakingSecretKeyRingProtector implements SecretKeyRingProtector {
|
|
||||||
|
|
||||||
private final Set<Passphrase> passphrases = new HashSet<>();
|
|
||||||
private final Set<PGPSecretKeyRing> keys = new HashSet<>();
|
|
||||||
private final CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a single passphrase to the protector.
|
|
||||||
*
|
|
||||||
* @param passphrase passphrase
|
|
||||||
*/
|
|
||||||
public void addPassphrase(Passphrase passphrase) {
|
|
||||||
if (passphrase.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!passphrases.add(passphrase)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (PGPSecretKeyRing key : keys) {
|
|
||||||
for (PGPSecretKey subkey : key) {
|
|
||||||
if (protector.hasPassphrase(subkey.getKeyID())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
testPassphrase(passphrase, subkey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a single {@link PGPSecretKeyRing} to the protector.
|
|
||||||
*
|
|
||||||
* @param key secret keys
|
|
||||||
*/
|
|
||||||
public void addSecretKey(PGPSecretKeyRing key) {
|
|
||||||
if (!keys.add(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (PGPSecretKey subkey : key) {
|
|
||||||
if (KeyInfo.isDecrypted(subkey)) {
|
|
||||||
protector.addPassphrase(subkey.getKeyID(), Passphrase.emptyPassphrase());
|
|
||||||
} else {
|
|
||||||
for (Passphrase passphrase : passphrases) {
|
|
||||||
testPassphrase(passphrase, subkey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void testPassphrase(Passphrase passphrase, PGPSecretKey subkey) {
|
|
||||||
try {
|
|
||||||
PBESecretKeyDecryptor decryptor = ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase);
|
|
||||||
UnlockSecretKey.unlockSecretKey(subkey, decryptor);
|
|
||||||
protector.addPassphrase(subkey.getKeyID(), passphrase);
|
|
||||||
} catch (PGPException e) {
|
|
||||||
// wrong password
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasPassphraseFor(long keyId) {
|
|
||||||
return protector.hasPassphrase(keyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public PBESecretKeyDecryptor getDecryptor(long keyId) throws PGPException {
|
|
||||||
return protector.getDecryptor(keyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public PBESecretKeyEncryptor getEncryptor(long keyId) throws PGPException {
|
|
||||||
return protector.getEncryptor(keyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all known passphrases from the protector.
|
|
||||||
*/
|
|
||||||
public void clear() {
|
|
||||||
for (Passphrase passphrase : passphrases) {
|
|
||||||
passphrase.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (PGPSecretKeyRing key : keys) {
|
|
||||||
protector.forgetPassphrase(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link OutputStream} that simply discards bytes written to it.
|
|
||||||
*/
|
|
||||||
public class NullOutputStream extends OutputStream {
|
|
||||||
@Override
|
|
||||||
public void write(int b) {
|
|
||||||
// NOP
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.charset.CharacterCodingException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.bouncycastle.bcpg.ArmoredOutputStream;
|
|
||||||
import org.bouncycastle.bcpg.PublicKeyPacket;
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
|
||||||
import org.bouncycastle.openpgp.PGPSignature;
|
|
||||||
import org.pgpainless.PGPainless;
|
|
||||||
import org.pgpainless.exception.WrongPassphraseException;
|
|
||||||
import org.pgpainless.key.OpenPgpFingerprint;
|
|
||||||
import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface;
|
|
||||||
import org.pgpainless.key.util.KeyRingUtils;
|
|
||||||
import org.pgpainless.key.util.RevocationAttributes;
|
|
||||||
import org.pgpainless.util.ArmoredOutputStreamFactory;
|
|
||||||
import org.pgpainless.util.Passphrase;
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.RevokeKey;
|
|
||||||
import sop.util.UTF8Util;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
public class RevokeKeyImpl implements RevokeKey {
|
|
||||||
|
|
||||||
private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector();
|
|
||||||
private boolean armor = true;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public RevokeKey noArmor() {
|
|
||||||
this.armor = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide the decryption password for the secret key.
|
|
||||||
*
|
|
||||||
* @param password password
|
|
||||||
* @return builder instance
|
|
||||||
* @throws sop.exception.SOPGPException.UnsupportedOption if the implementation does not support key passwords
|
|
||||||
* @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public RevokeKey withKeyPassword(@Nonnull byte[] password)
|
|
||||||
throws SOPGPException.UnsupportedOption,
|
|
||||||
SOPGPException.PasswordNotHumanReadable {
|
|
||||||
String string;
|
|
||||||
try {
|
|
||||||
string = UTF8Util.decodeUTF8(password);
|
|
||||||
} catch (CharacterCodingException e) {
|
|
||||||
throw new SOPGPException.PasswordNotHumanReadable("Cannot UTF8-decode password.");
|
|
||||||
}
|
|
||||||
protector.addPassphrase(Passphrase.fromPassword(string));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public Ready keys(@Nonnull InputStream keys) throws SOPGPException.BadData {
|
|
||||||
PGPSecretKeyRingCollection secretKeyRings;
|
|
||||||
try {
|
|
||||||
secretKeyRings = KeyReader.readSecretKeys(keys, true);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new SOPGPException.BadData("Cannot decode secret keys.", e);
|
|
||||||
}
|
|
||||||
for (PGPSecretKeyRing secretKeys : secretKeyRings) {
|
|
||||||
protector.addSecretKey(secretKeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<PGPPublicKeyRing> revocationCertificates = new ArrayList<>();
|
|
||||||
for (PGPSecretKeyRing secretKeys : secretKeyRings) {
|
|
||||||
SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys);
|
|
||||||
try {
|
|
||||||
RevocationAttributes revocationAttributes = RevocationAttributes.createKeyRevocation()
|
|
||||||
.withReason(RevocationAttributes.Reason.NO_REASON)
|
|
||||||
.withoutDescription();
|
|
||||||
if (secretKeys.getPublicKey().getVersion() == PublicKeyPacket.VERSION_6) {
|
|
||||||
PGPPublicKeyRing revocation = editor.createMinimalRevocationCertificate(protector, revocationAttributes);
|
|
||||||
revocationCertificates.add(revocation);
|
|
||||||
} else {
|
|
||||||
PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys);
|
|
||||||
PGPSignature revocation = editor.createRevocation(protector, revocationAttributes);
|
|
||||||
certificate = KeyRingUtils.injectCertification(certificate, revocation);
|
|
||||||
revocationCertificates.add(certificate);
|
|
||||||
}
|
|
||||||
} catch (WrongPassphraseException e) {
|
|
||||||
throw new SOPGPException.KeyIsProtected("Missing or wrong passphrase for key " + OpenPgpFingerprint.of(secretKeys), e);
|
|
||||||
}
|
|
||||||
catch (PGPException e) {
|
|
||||||
throw new RuntimeException("Cannot generate revocation certificate.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Ready() {
|
|
||||||
@Override
|
|
||||||
public void writeTo(@Nonnull OutputStream outputStream) throws IOException {
|
|
||||||
PGPPublicKeyRingCollection certificateCollection = new PGPPublicKeyRingCollection(revocationCertificates);
|
|
||||||
if (armor) {
|
|
||||||
ArmoredOutputStream out = ArmoredOutputStreamFactory.get(outputStream);
|
|
||||||
certificateCollection.encode(out);
|
|
||||||
out.close();
|
|
||||||
} else {
|
|
||||||
certificateCollection.encode(outputStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,140 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import org.pgpainless.util.ArmoredOutputStreamFactory;
|
|
||||||
import sop.SOP;
|
|
||||||
import sop.operation.Armor;
|
|
||||||
import sop.operation.ChangeKeyPassword;
|
|
||||||
import sop.operation.Dearmor;
|
|
||||||
import sop.operation.Decrypt;
|
|
||||||
import sop.operation.DetachedSign;
|
|
||||||
import sop.operation.DetachedVerify;
|
|
||||||
import sop.operation.InlineDetach;
|
|
||||||
import sop.operation.Encrypt;
|
|
||||||
import sop.operation.ExtractCert;
|
|
||||||
import sop.operation.GenerateKey;
|
|
||||||
import sop.operation.InlineSign;
|
|
||||||
import sop.operation.InlineVerify;
|
|
||||||
import sop.operation.ListProfiles;
|
|
||||||
import sop.operation.RevokeKey;
|
|
||||||
import sop.operation.Version;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the <pre>sop</pre> API using PGPainless.
|
|
||||||
* <pre> {@code
|
|
||||||
* SOP sop = new SOPImpl();
|
|
||||||
* }</pre>
|
|
||||||
*/
|
|
||||||
public class SOPImpl implements SOP {
|
|
||||||
|
|
||||||
static {
|
|
||||||
ArmoredOutputStreamFactory.setVersionInfo(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public Version version() {
|
|
||||||
return new VersionImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public GenerateKey generateKey() {
|
|
||||||
return new GenerateKeyImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public ExtractCert extractCert() {
|
|
||||||
return new ExtractCertImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public DetachedSign sign() {
|
|
||||||
return detachedSign();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public DetachedSign detachedSign() {
|
|
||||||
return new DetachedSignImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public InlineSign inlineSign() {
|
|
||||||
return new InlineSignImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public DetachedVerify verify() {
|
|
||||||
return detachedVerify();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public DetachedVerify detachedVerify() {
|
|
||||||
return new DetachedVerifyImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public InlineVerify inlineVerify() {
|
|
||||||
return new InlineVerifyImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public Encrypt encrypt() {
|
|
||||||
return new EncryptImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public Decrypt decrypt() {
|
|
||||||
return new DecryptImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public Armor armor() {
|
|
||||||
return new ArmorImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public Dearmor dearmor() {
|
|
||||||
return new DearmorImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public ListProfiles listProfiles() {
|
|
||||||
return new ListProfilesImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public RevokeKey revokeKey() {
|
|
||||||
return new RevokeKeyImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public ChangeKeyPassword changeKeyPassword() {
|
|
||||||
return new ChangeKeyPasswordImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public InlineDetach inlineDetach() {
|
|
||||||
return new InlineDetachImpl();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPSignature;
|
|
||||||
import org.pgpainless.decryption_verification.SignatureVerification;
|
|
||||||
import sop.Verification;
|
|
||||||
import sop.enums.SignatureMode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class for shared methods related to {@link Verification Verifications}.
|
|
||||||
*/
|
|
||||||
public class VerificationHelper {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map a {@link SignatureVerification} object to a {@link Verification}.
|
|
||||||
*
|
|
||||||
* @param sigVerification signature verification
|
|
||||||
* @return verification
|
|
||||||
*/
|
|
||||||
public static Verification mapVerification(SignatureVerification sigVerification) {
|
|
||||||
return new Verification(
|
|
||||||
sigVerification.getSignature().getCreationTime(),
|
|
||||||
sigVerification.getSigningKey().getSubkeyFingerprint().toString(),
|
|
||||||
sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString(),
|
|
||||||
getMode(sigVerification.getSignature()),
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map an OpenPGP signature type to a {@link SignatureMode} enum.
|
|
||||||
* Note: This method only maps {@link PGPSignature#BINARY_DOCUMENT} and {@link PGPSignature#CANONICAL_TEXT_DOCUMENT}.
|
|
||||||
* Other values are mapped to <pre>null</pre>.
|
|
||||||
*
|
|
||||||
* @param signature signature
|
|
||||||
* @return signature mode enum or null
|
|
||||||
*/
|
|
||||||
private static SignatureMode getMode(PGPSignature signature) {
|
|
||||||
|
|
||||||
if (signature.getSignatureType() == PGPSignature.BINARY_DOCUMENT) {
|
|
||||||
return SignatureMode.binary;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signature.getSignatureType() == PGPSignature.CANONICAL_TEXT_DOCUMENT) {
|
|
||||||
return SignatureMode.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.sop;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|
||||||
import sop.operation.Version;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the <pre>version</pre> operation using PGPainless.
|
|
||||||
*/
|
|
||||||
public class VersionImpl implements Version {
|
|
||||||
|
|
||||||
// draft version
|
|
||||||
private static final int SOP_VERSION = 8;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public String getName() {
|
|
||||||
return "PGPainless-SOP";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public String getVersion() {
|
|
||||||
// See https://stackoverflow.com/a/50119235
|
|
||||||
String version;
|
|
||||||
try {
|
|
||||||
Properties properties = new Properties();
|
|
||||||
InputStream propertiesFileIn = getClass().getResourceAsStream("/version.properties");
|
|
||||||
if (propertiesFileIn == null) {
|
|
||||||
throw new IOException("File version.properties not found.");
|
|
||||||
}
|
|
||||||
properties.load(propertiesFileIn);
|
|
||||||
version = properties.getProperty("version");
|
|
||||||
} catch (IOException e) {
|
|
||||||
version = "DEVELOPMENT";
|
|
||||||
}
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public String getBackendVersion() {
|
|
||||||
return "PGPainless " + getVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nonnull
|
|
||||||
public String getExtendedVersion() {
|
|
||||||
double bcVersion = new BouncyCastleProvider().getVersion();
|
|
||||||
String FORMAT_VERSION = String.format("%02d", SOP_VERSION);
|
|
||||||
return getName() + " " + getVersion() + "\n" +
|
|
||||||
"https://codeberg.org/PGPainless/pgpainless/src/branch/master/pgpainless-sop\n" +
|
|
||||||
"\n" +
|
|
||||||
"Implementation of the Stateless OpenPGP Protocol Version " + FORMAT_VERSION + "\n" +
|
|
||||||
"https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-" + FORMAT_VERSION + "\n" +
|
|
||||||
"\n" +
|
|
||||||
"Based on pgpainless-core " + getVersion() + "\n" +
|
|
||||||
"https://pgpainless.org\n" +
|
|
||||||
"\n" +
|
|
||||||
"Using " + String.format(Locale.US, "Bouncy Castle %.2f", bcVersion) + "\n" +
|
|
||||||
"https://www.bouncycastle.org/java.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSopSpecRevisionNumber() {
|
|
||||||
return SOP_VERSION;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSopSpecImplementationIncomplete() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getSopSpecImplementationRemarks() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the java-sop package using pgpainless-core.
|
|
||||||
*/
|
|
||||||
package org.pgpainless.sop;
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import kotlin.jvm.Throws
|
||||||
|
import org.bouncycastle.util.io.Streams
|
||||||
|
import org.pgpainless.decryption_verification.OpenPgpInputStream
|
||||||
|
import org.pgpainless.util.ArmoredOutputStreamFactory
|
||||||
|
import sop.Ready
|
||||||
|
import sop.enums.ArmorLabel
|
||||||
|
import sop.exception.SOPGPException
|
||||||
|
import sop.operation.Armor
|
||||||
|
|
||||||
|
/** Implementation of the `armor` operation using PGPainless. */
|
||||||
|
class ArmorImpl : Armor {
|
||||||
|
|
||||||
|
@Throws(SOPGPException.BadData::class)
|
||||||
|
override fun data(data: InputStream): Ready {
|
||||||
|
return object : Ready() {
|
||||||
|
override fun writeTo(outputStream: OutputStream) {
|
||||||
|
// By buffering the output stream, we can improve performance drastically
|
||||||
|
val bufferedOutputStream = BufferedOutputStream(outputStream)
|
||||||
|
|
||||||
|
// Determine the nature of the given data
|
||||||
|
val openPgpIn = OpenPgpInputStream(data)
|
||||||
|
openPgpIn.reset()
|
||||||
|
|
||||||
|
if (openPgpIn.isAsciiArmored) {
|
||||||
|
// armoring already-armored data is an idempotent operation
|
||||||
|
Streams.pipeAll(openPgpIn, bufferedOutputStream)
|
||||||
|
bufferedOutputStream.flush()
|
||||||
|
openPgpIn.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val armor = ArmoredOutputStreamFactory.get(bufferedOutputStream)
|
||||||
|
Streams.pipeAll(openPgpIn, armor)
|
||||||
|
bufferedOutputStream.flush()
|
||||||
|
armor.close()
|
||||||
|
openPgpIn.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Setting custom labels is not supported.")
|
||||||
|
override fun label(label: ArmorLabel): Armor {
|
||||||
|
throw SOPGPException.UnsupportedOption("Setting custom Armor labels not supported.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import org.bouncycastle.openpgp.PGPException
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection
|
||||||
|
import org.pgpainless.bouncycastle.extensions.openPgpFingerprint
|
||||||
|
import org.pgpainless.exception.MissingPassphraseException
|
||||||
|
import org.pgpainless.key.protection.SecretKeyRingProtector
|
||||||
|
import org.pgpainless.key.util.KeyRingUtils
|
||||||
|
import org.pgpainless.util.ArmoredOutputStreamFactory
|
||||||
|
import org.pgpainless.util.Passphrase
|
||||||
|
import sop.Ready
|
||||||
|
import sop.exception.SOPGPException
|
||||||
|
import sop.operation.ChangeKeyPassword
|
||||||
|
|
||||||
|
/** Implementation of the `change-key-password` operation using PGPainless. */
|
||||||
|
class ChangeKeyPasswordImpl : ChangeKeyPassword {
|
||||||
|
|
||||||
|
private val oldProtector = MatchMakingSecretKeyRingProtector()
|
||||||
|
private var newPassphrase = Passphrase.emptyPassphrase()
|
||||||
|
private var armor = true
|
||||||
|
|
||||||
|
override fun keys(keys: InputStream): Ready {
|
||||||
|
val newProtector = SecretKeyRingProtector.unlockAnyKeyWith(newPassphrase)
|
||||||
|
val secretKeysCollection =
|
||||||
|
try {
|
||||||
|
KeyReader.readSecretKeys(keys, true)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
val updatedSecretKeys =
|
||||||
|
secretKeysCollection
|
||||||
|
.map { secretKeys ->
|
||||||
|
oldProtector.addSecretKey(secretKeys)
|
||||||
|
try {
|
||||||
|
return@map KeyRingUtils.changePassphrase(
|
||||||
|
null, secretKeys, oldProtector, newProtector)
|
||||||
|
} catch (e: MissingPassphraseException) {
|
||||||
|
throw SOPGPException.KeyIsProtected(
|
||||||
|
"Cannot unlock key ${secretKeys.openPgpFingerprint}", e)
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
if (e.message?.contains("Exception decrypting key") == true) {
|
||||||
|
throw SOPGPException.KeyIsProtected(
|
||||||
|
"Cannot unlock key ${secretKeys.openPgpFingerprint}", e)
|
||||||
|
}
|
||||||
|
throw RuntimeException(
|
||||||
|
"Cannot change passphrase of key ${secretKeys.openPgpFingerprint}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.let { PGPSecretKeyRingCollection(it) }
|
||||||
|
|
||||||
|
return object : Ready() {
|
||||||
|
override fun writeTo(outputStream: OutputStream) {
|
||||||
|
if (armor) {
|
||||||
|
ArmoredOutputStreamFactory.get(outputStream).use {
|
||||||
|
updatedSecretKeys.encode(it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updatedSecretKeys.encode(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newKeyPassphrase(newPassphrase: String): ChangeKeyPassword = apply {
|
||||||
|
this.newPassphrase = Passphrase.fromPassword(newPassphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun noArmor(): ChangeKeyPassword = apply { armor = false }
|
||||||
|
|
||||||
|
override fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword = apply {
|
||||||
|
oldProtector.addPassphrase(Passphrase.fromPassword(oldPassphrase))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import org.bouncycastle.openpgp.PGPUtil
|
||||||
|
import org.bouncycastle.util.io.Streams
|
||||||
|
import sop.Ready
|
||||||
|
import sop.exception.SOPGPException
|
||||||
|
import sop.operation.Dearmor
|
||||||
|
|
||||||
|
/** Implementation of the `dearmor` operation using PGPainless. */
|
||||||
|
class DearmorImpl : Dearmor {
|
||||||
|
|
||||||
|
override fun data(data: InputStream): Ready {
|
||||||
|
val decoder =
|
||||||
|
try {
|
||||||
|
PGPUtil.getDecoderStream(data)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return object : Ready() {
|
||||||
|
override fun writeTo(outputStream: OutputStream) {
|
||||||
|
BufferedOutputStream(outputStream).use {
|
||||||
|
Streams.pipeAll(decoder, it)
|
||||||
|
it.flush()
|
||||||
|
decoder.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.*
|
||||||
|
import org.bouncycastle.openpgp.PGPException
|
||||||
|
import org.bouncycastle.util.io.Streams
|
||||||
|
import org.pgpainless.PGPainless
|
||||||
|
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||||
|
import org.pgpainless.decryption_verification.ConsumerOptions
|
||||||
|
import org.pgpainless.exception.MalformedOpenPgpMessageException
|
||||||
|
import org.pgpainless.exception.MissingDecryptionMethodException
|
||||||
|
import org.pgpainless.exception.WrongPassphraseException
|
||||||
|
import org.pgpainless.util.Passphrase
|
||||||
|
import sop.DecryptionResult
|
||||||
|
import sop.ReadyWithResult
|
||||||
|
import sop.SessionKey
|
||||||
|
import sop.exception.SOPGPException
|
||||||
|
import sop.operation.Decrypt
|
||||||
|
import sop.util.UTF8Util
|
||||||
|
|
||||||
|
/** Implementation of the `decrypt` operation using PGPainless. */
|
||||||
|
class DecryptImpl : Decrypt {
|
||||||
|
|
||||||
|
private val consumerOptions = ConsumerOptions.get()
|
||||||
|
private val protector = MatchMakingSecretKeyRingProtector()
|
||||||
|
|
||||||
|
override fun ciphertext(ciphertext: InputStream): ReadyWithResult<DecryptionResult> {
|
||||||
|
if (consumerOptions.getDecryptionKeys().isEmpty() &&
|
||||||
|
consumerOptions.getDecryptionPassphrases().isEmpty() &&
|
||||||
|
consumerOptions.getSessionKey() == null) {
|
||||||
|
throw SOPGPException.MissingArg("Missing decryption key, passphrase or session key.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val decryptionStream =
|
||||||
|
try {
|
||||||
|
PGPainless.decryptAndOrVerify()
|
||||||
|
.onInputStream(ciphertext)
|
||||||
|
.withOptions(consumerOptions)
|
||||||
|
} catch (e: MissingDecryptionMethodException) {
|
||||||
|
throw SOPGPException.CannotDecrypt(
|
||||||
|
"No usable decryption key or password provided.", e)
|
||||||
|
} catch (e: WrongPassphraseException) {
|
||||||
|
throw SOPGPException.KeyIsProtected()
|
||||||
|
} catch (e: MalformedOpenPgpMessageException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
} finally {
|
||||||
|
// Forget passphrases after decryption
|
||||||
|
protector.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
return object : ReadyWithResult<DecryptionResult>() {
|
||||||
|
override fun writeTo(outputStream: OutputStream): DecryptionResult {
|
||||||
|
Streams.pipeAll(decryptionStream, outputStream)
|
||||||
|
decryptionStream.close()
|
||||||
|
|
||||||
|
val metadata = decryptionStream.metadata
|
||||||
|
if (!metadata.isEncrypted) {
|
||||||
|
throw SOPGPException.BadData("Data is not encrypted.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val verificationList =
|
||||||
|
metadata.verifiedInlineSignatures.map { VerificationHelper.mapVerification(it) }
|
||||||
|
|
||||||
|
var sessionKey: SessionKey? = null
|
||||||
|
if (metadata.sessionKey != null) {
|
||||||
|
sessionKey =
|
||||||
|
SessionKey(
|
||||||
|
metadata.sessionKey!!.algorithm.algorithmId.toByte(),
|
||||||
|
metadata.sessionKey!!.key)
|
||||||
|
}
|
||||||
|
return DecryptionResult(sessionKey, verificationList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun verifyNotAfter(timestamp: Date): Decrypt = apply {
|
||||||
|
consumerOptions.verifyNotAfter(timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun verifyNotBefore(timestamp: Date): Decrypt = apply {
|
||||||
|
consumerOptions.verifyNotBefore(timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun verifyWithCert(cert: InputStream): Decrypt = apply {
|
||||||
|
KeyReader.readPublicKeys(cert, true)?.let { consumerOptions.addVerificationCerts(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withKey(key: InputStream): Decrypt = apply {
|
||||||
|
KeyReader.readSecretKeys(key, true).forEach {
|
||||||
|
protector.addSecretKey(it)
|
||||||
|
consumerOptions.addDecryptionKey(it, protector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withKeyPassword(password: ByteArray): Decrypt = apply {
|
||||||
|
protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withPassword(password: String): Decrypt = apply {
|
||||||
|
consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(password))
|
||||||
|
password.trimEnd().let {
|
||||||
|
if (it != password) {
|
||||||
|
consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withSessionKey(sessionKey: SessionKey): Decrypt = apply {
|
||||||
|
consumerOptions.setSessionKey(mapSessionKey(sessionKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapSessionKey(sessionKey: SessionKey): org.pgpainless.util.SessionKey =
|
||||||
|
org.pgpainless.util.SessionKey(
|
||||||
|
SymmetricKeyAlgorithm.requireFromId(sessionKey.algorithm.toInt()), sessionKey.key)
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import org.bouncycastle.openpgp.PGPException
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||||
|
import org.bouncycastle.openpgp.PGPSignature
|
||||||
|
import org.bouncycastle.util.io.Streams
|
||||||
|
import org.pgpainless.PGPainless
|
||||||
|
import org.pgpainless.algorithm.DocumentSignatureType
|
||||||
|
import org.pgpainless.algorithm.HashAlgorithm
|
||||||
|
import org.pgpainless.bouncycastle.extensions.openPgpFingerprint
|
||||||
|
import org.pgpainless.encryption_signing.ProducerOptions
|
||||||
|
import org.pgpainless.encryption_signing.SigningOptions
|
||||||
|
import org.pgpainless.exception.KeyException.MissingSecretKeyException
|
||||||
|
import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException
|
||||||
|
import org.pgpainless.util.ArmoredOutputStreamFactory
|
||||||
|
import org.pgpainless.util.Passphrase
|
||||||
|
import sop.MicAlg
|
||||||
|
import sop.ReadyWithResult
|
||||||
|
import sop.SigningResult
|
||||||
|
import sop.enums.SignAs
|
||||||
|
import sop.exception.SOPGPException
|
||||||
|
import sop.operation.DetachedSign
|
||||||
|
import sop.util.UTF8Util
|
||||||
|
|
||||||
|
/** Implementation of the `sign` operation using PGPainless. */
|
||||||
|
class DetachedSignImpl : DetachedSign {
|
||||||
|
|
||||||
|
private val signingOptions = SigningOptions.get()
|
||||||
|
private val protector = MatchMakingSecretKeyRingProtector()
|
||||||
|
private val signingKeys = mutableListOf<PGPSecretKeyRing>()
|
||||||
|
|
||||||
|
private var armor = true
|
||||||
|
private var mode = SignAs.binary
|
||||||
|
|
||||||
|
override fun data(data: InputStream): ReadyWithResult<SigningResult> {
|
||||||
|
signingKeys.forEach {
|
||||||
|
try {
|
||||||
|
signingOptions.addDetachedSignature(protector, it, modeToSigType(mode))
|
||||||
|
} catch (e: UnacceptableSigningKeyException) {
|
||||||
|
throw SOPGPException.KeyCannotSign("Key ${it.openPgpFingerprint} cannot sign.", e)
|
||||||
|
} catch (e: MissingSecretKeyException) {
|
||||||
|
throw SOPGPException.KeyCannotSign(
|
||||||
|
"Key ${it.openPgpFingerprint} cannot sign. Missing secret key.", e)
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
throw SOPGPException.KeyIsProtected(
|
||||||
|
"Key ${it.openPgpFingerprint} cannot be unlocked.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val signingStream =
|
||||||
|
PGPainless.encryptAndOrSign()
|
||||||
|
.discardOutput()
|
||||||
|
.withOptions(ProducerOptions.sign(signingOptions).setAsciiArmor(armor))
|
||||||
|
|
||||||
|
return object : ReadyWithResult<SigningResult>() {
|
||||||
|
override fun writeTo(outputStream: OutputStream): SigningResult {
|
||||||
|
check(!signingStream.isClosed) { "The operation is a one-shot object." }
|
||||||
|
|
||||||
|
Streams.pipeAll(data, signingStream)
|
||||||
|
signingStream.close()
|
||||||
|
val result = signingStream.result
|
||||||
|
|
||||||
|
// forget passphrases
|
||||||
|
protector.clear()
|
||||||
|
|
||||||
|
val signatures = result.detachedSignatures.map { it.value }.flatten()
|
||||||
|
val out =
|
||||||
|
if (armor) ArmoredOutputStreamFactory.get(outputStream) else outputStream
|
||||||
|
|
||||||
|
signatures.forEach { it.encode(out) }
|
||||||
|
out.close()
|
||||||
|
outputStream.close()
|
||||||
|
|
||||||
|
return SigningResult.builder()
|
||||||
|
.setMicAlg(micAlgFromSignatures(signatures))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun key(key: InputStream): DetachedSign = apply {
|
||||||
|
KeyReader.readSecretKeys(key, true).forEach {
|
||||||
|
val info = PGPainless.inspectKeyRing(it)
|
||||||
|
if (!info.isUsableForSigning) {
|
||||||
|
throw SOPGPException.KeyCannotSign(
|
||||||
|
"Key ${info.fingerprint} does not have valid, signing capable subkeys.")
|
||||||
|
}
|
||||||
|
protector.addSecretKey(it)
|
||||||
|
signingKeys.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mode(mode: SignAs): DetachedSign = apply { this.mode = mode }
|
||||||
|
|
||||||
|
override fun noArmor(): DetachedSign = apply { armor = false }
|
||||||
|
|
||||||
|
override fun withKeyPassword(password: ByteArray): DetachedSign = apply {
|
||||||
|
protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun modeToSigType(mode: SignAs): DocumentSignatureType {
|
||||||
|
return when (mode) {
|
||||||
|
SignAs.binary -> DocumentSignatureType.BINARY_DOCUMENT
|
||||||
|
SignAs.text -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun micAlgFromSignatures(signatures: List<PGPSignature>): MicAlg =
|
||||||
|
signatures
|
||||||
|
.mapNotNull { HashAlgorithm.fromId(it.hashAlgorithm) }
|
||||||
|
.toSet()
|
||||||
|
.singleOrNull()
|
||||||
|
?.let { MicAlg.fromHashAlgorithmId(it.algorithmId) }
|
||||||
|
?: MicAlg.empty()
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.util.*
|
||||||
|
import org.bouncycastle.openpgp.PGPException
|
||||||
|
import org.bouncycastle.util.io.Streams
|
||||||
|
import org.pgpainless.PGPainless
|
||||||
|
import org.pgpainless.decryption_verification.ConsumerOptions
|
||||||
|
import org.pgpainless.exception.MalformedOpenPgpMessageException
|
||||||
|
import sop.Verification
|
||||||
|
import sop.exception.SOPGPException
|
||||||
|
import sop.operation.DetachedVerify
|
||||||
|
import sop.operation.VerifySignatures
|
||||||
|
|
||||||
|
/** Implementation of the `verify` operation using PGPainless. */
|
||||||
|
class DetachedVerifyImpl : DetachedVerify {
|
||||||
|
|
||||||
|
private val options = ConsumerOptions.get().forceNonOpenPgpData()
|
||||||
|
|
||||||
|
override fun cert(cert: InputStream): DetachedVerify = apply {
|
||||||
|
options.addVerificationCerts(KeyReader.readPublicKeys(cert, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun data(data: InputStream): List<Verification> {
|
||||||
|
try {
|
||||||
|
val verificationStream =
|
||||||
|
PGPainless.decryptAndOrVerify().onInputStream(data).withOptions(options)
|
||||||
|
|
||||||
|
Streams.drain(verificationStream)
|
||||||
|
verificationStream.close()
|
||||||
|
|
||||||
|
val result = verificationStream.metadata
|
||||||
|
val verifications =
|
||||||
|
result.verifiedDetachedSignatures.map { VerificationHelper.mapVerification(it) }
|
||||||
|
|
||||||
|
if (options.getCertificateSource().getExplicitCertificates().isNotEmpty() &&
|
||||||
|
verifications.isEmpty()) {
|
||||||
|
throw SOPGPException.NoSignature()
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifications
|
||||||
|
} catch (e: MalformedOpenPgpMessageException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notAfter(timestamp: Date): DetachedVerify = apply {
|
||||||
|
options.verifyNotAfter(timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notBefore(timestamp: Date): DetachedVerify = apply {
|
||||||
|
options.verifyNotBefore(timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun signatures(signatures: InputStream): VerifySignatures = apply {
|
||||||
|
try {
|
||||||
|
options.addVerificationOfDetachedSignatures(signatures)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import org.bouncycastle.openpgp.PGPException
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||||
|
import org.bouncycastle.util.io.Streams
|
||||||
|
import org.pgpainless.PGPainless
|
||||||
|
import org.pgpainless.algorithm.DocumentSignatureType
|
||||||
|
import org.pgpainless.algorithm.StreamEncoding
|
||||||
|
import org.pgpainless.bouncycastle.extensions.openPgpFingerprint
|
||||||
|
import org.pgpainless.encryption_signing.EncryptionOptions
|
||||||
|
import org.pgpainless.encryption_signing.ProducerOptions
|
||||||
|
import org.pgpainless.encryption_signing.SigningOptions
|
||||||
|
import org.pgpainless.exception.KeyException.UnacceptableEncryptionKeyException
|
||||||
|
import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException
|
||||||
|
import org.pgpainless.exception.WrongPassphraseException
|
||||||
|
import org.pgpainless.util.Passphrase
|
||||||
|
import sop.EncryptionResult
|
||||||
|
import sop.Profile
|
||||||
|
import sop.ReadyWithResult
|
||||||
|
import sop.enums.EncryptAs
|
||||||
|
import sop.exception.SOPGPException
|
||||||
|
import sop.operation.Encrypt
|
||||||
|
import sop.util.UTF8Util
|
||||||
|
|
||||||
|
/** Implementation of the `encrypt` operation using PGPainless. */
|
||||||
|
class EncryptImpl : Encrypt {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmField val RFC4880_PROFILE = Profile("rfc4880", "Follow the packet format of rfc4880")
|
||||||
|
|
||||||
|
@JvmField val SUPPORTED_PROFILES = listOf(RFC4880_PROFILE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val encryptionOptions = EncryptionOptions.get()
|
||||||
|
private var signingOptions: SigningOptions? = null
|
||||||
|
private val signingKeys = mutableListOf<PGPSecretKeyRing>()
|
||||||
|
private val protector = MatchMakingSecretKeyRingProtector()
|
||||||
|
|
||||||
|
private var profile = RFC4880_PROFILE.name
|
||||||
|
private var mode = EncryptAs.binary
|
||||||
|
private var armor = true
|
||||||
|
|
||||||
|
override fun mode(mode: EncryptAs): Encrypt = apply { this.mode = mode }
|
||||||
|
|
||||||
|
override fun noArmor(): Encrypt = apply { this.armor = false }
|
||||||
|
|
||||||
|
override fun plaintext(plaintext: InputStream): ReadyWithResult<EncryptionResult> {
|
||||||
|
if (!encryptionOptions.hasEncryptionMethod()) {
|
||||||
|
throw SOPGPException.MissingArg("Missing encryption method.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val options =
|
||||||
|
if (signingOptions != null) {
|
||||||
|
ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions!!)
|
||||||
|
} else {
|
||||||
|
ProducerOptions.encrypt(encryptionOptions)
|
||||||
|
}
|
||||||
|
.setAsciiArmor(armor)
|
||||||
|
.setEncoding(modeToStreamEncoding(mode))
|
||||||
|
|
||||||
|
signingKeys.forEach {
|
||||||
|
try {
|
||||||
|
signingOptions!!.addInlineSignature(protector, it, modeToSignatureType(mode))
|
||||||
|
} catch (e: UnacceptableSigningKeyException) {
|
||||||
|
throw SOPGPException.KeyCannotSign("Key ${it.openPgpFingerprint} cannot sign", e)
|
||||||
|
} catch (e: WrongPassphraseException) {
|
||||||
|
throw SOPGPException.KeyIsProtected("Cannot unlock key ${it.openPgpFingerprint}", e)
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return object : ReadyWithResult<EncryptionResult>() {
|
||||||
|
override fun writeTo(outputStream: OutputStream): EncryptionResult {
|
||||||
|
val encryptionStream =
|
||||||
|
PGPainless.encryptAndOrSign()
|
||||||
|
.onOutputStream(outputStream)
|
||||||
|
.withOptions(options)
|
||||||
|
Streams.pipeAll(plaintext, encryptionStream)
|
||||||
|
encryptionStream.close()
|
||||||
|
// TODO: Extract and emit session key once BC supports that
|
||||||
|
return EncryptionResult(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
throw IOException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun profile(profileName: String): Encrypt = apply {
|
||||||
|
profile =
|
||||||
|
SUPPORTED_PROFILES.find { it.name == profileName }?.name
|
||||||
|
?: throw SOPGPException.UnsupportedProfile("encrypt", profileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun signWith(key: InputStream): Encrypt = apply {
|
||||||
|
if (signingOptions == null) {
|
||||||
|
signingOptions = SigningOptions.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
val signingKey =
|
||||||
|
KeyReader.readSecretKeys(key, true).singleOrNull()
|
||||||
|
?: throw SOPGPException.BadData(
|
||||||
|
AssertionError(
|
||||||
|
"Exactly one secret key at a time expected. Got zero or multiple instead."))
|
||||||
|
|
||||||
|
val info = PGPainless.inspectKeyRing(signingKey)
|
||||||
|
if (info.signingSubkeys.isEmpty()) {
|
||||||
|
throw SOPGPException.KeyCannotSign("Key ${info.fingerprint} cannot sign.")
|
||||||
|
}
|
||||||
|
|
||||||
|
protector.addSecretKey(signingKey)
|
||||||
|
signingKeys.add(signingKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withCert(cert: InputStream): Encrypt = apply {
|
||||||
|
try {
|
||||||
|
encryptionOptions.addRecipients(KeyReader.readPublicKeys(cert, true))
|
||||||
|
} catch (e: UnacceptableEncryptionKeyException) {
|
||||||
|
throw SOPGPException.CertCannotEncrypt(e.message ?: "Cert cannot encrypt", e)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withKeyPassword(password: ByteArray): Encrypt = apply {
|
||||||
|
protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun withPassword(password: String): Encrypt = apply {
|
||||||
|
encryptionOptions.addPassphrase(Passphrase.fromPassword(password))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun modeToStreamEncoding(mode: EncryptAs): StreamEncoding {
|
||||||
|
return when (mode) {
|
||||||
|
EncryptAs.binary -> StreamEncoding.BINARY
|
||||||
|
EncryptAs.text -> StreamEncoding.UTF8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun modeToSignatureType(mode: EncryptAs): DocumentSignatureType {
|
||||||
|
return when (mode) {
|
||||||
|
EncryptAs.binary -> DocumentSignatureType.BINARY_DOCUMENT
|
||||||
|
EncryptAs.text -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import org.pgpainless.PGPainless
|
||||||
|
import org.pgpainless.util.ArmorUtils
|
||||||
|
import org.pgpainless.util.ArmoredOutputStreamFactory
|
||||||
|
import sop.Ready
|
||||||
|
import sop.operation.ExtractCert
|
||||||
|
|
||||||
|
/** Implementation of the `extract-cert` operation using PGPainless. */
|
||||||
|
class ExtractCertImpl : ExtractCert {
|
||||||
|
|
||||||
|
private var armor = true
|
||||||
|
|
||||||
|
override fun key(keyInputStream: InputStream): Ready {
|
||||||
|
val certs =
|
||||||
|
KeyReader.readSecretKeys(keyInputStream, true).map { PGPainless.extractCertificate(it) }
|
||||||
|
|
||||||
|
return object : Ready() {
|
||||||
|
override fun writeTo(outputStream: OutputStream) {
|
||||||
|
if (armor) {
|
||||||
|
if (certs.size == 1) {
|
||||||
|
val cert = certs[0]
|
||||||
|
// This way we have a nice armor header with fingerprint and user-ids
|
||||||
|
val armorOut = ArmorUtils.toAsciiArmoredStream(cert, outputStream)
|
||||||
|
cert.encode(armorOut)
|
||||||
|
armorOut.close()
|
||||||
|
} else {
|
||||||
|
// for multiple certs, add no info headers to the ASCII armor
|
||||||
|
val armorOut = ArmoredOutputStreamFactory.get(outputStream)
|
||||||
|
certs.forEach { it.encode(armorOut) }
|
||||||
|
armorOut.close()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
certs.forEach { it.encode(outputStream) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun noArmor(): ExtractCert = apply { armor = false }
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.lang.RuntimeException
|
||||||
|
import java.security.InvalidAlgorithmParameterException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import org.bouncycastle.openpgp.PGPException
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||||
|
import org.pgpainless.PGPainless
|
||||||
|
import org.pgpainless.algorithm.KeyFlag
|
||||||
|
import org.pgpainless.key.generation.KeyRingBuilder
|
||||||
|
import org.pgpainless.key.generation.KeySpec
|
||||||
|
import org.pgpainless.key.generation.type.KeyType
|
||||||
|
import org.pgpainless.key.generation.type.eddsa.EdDSACurve
|
||||||
|
import org.pgpainless.key.generation.type.rsa.RsaLength
|
||||||
|
import org.pgpainless.key.generation.type.xdh.XDHSpec
|
||||||
|
import org.pgpainless.util.ArmorUtils
|
||||||
|
import org.pgpainless.util.Passphrase
|
||||||
|
import sop.Profile
|
||||||
|
import sop.Ready
|
||||||
|
import sop.exception.SOPGPException
|
||||||
|
import sop.operation.GenerateKey
|
||||||
|
|
||||||
|
/** Implementation of the `generate-key` operation using PGPainless. */
|
||||||
|
class GenerateKeyImpl : GenerateKey {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmField
|
||||||
|
val CURVE25519_PROFILE =
|
||||||
|
Profile(
|
||||||
|
"draft-koch-eddsa-for-openpgp-00", "Generate EdDSA / ECDH keys using Curve25519")
|
||||||
|
@JvmField val RSA4096_PROFILE = Profile("rfc4880", "Generate 4096-bit RSA keys")
|
||||||
|
|
||||||
|
@JvmField val SUPPORTED_PROFILES = listOf(CURVE25519_PROFILE, RSA4096_PROFILE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val userIds = mutableSetOf<String>()
|
||||||
|
private var armor = true
|
||||||
|
private var signingOnly = false
|
||||||
|
private var passphrase = Passphrase.emptyPassphrase()
|
||||||
|
private var profile = CURVE25519_PROFILE.name
|
||||||
|
|
||||||
|
override fun generate(): Ready {
|
||||||
|
try {
|
||||||
|
val key = generateKeyWithProfile(profile, userIds, passphrase, signingOnly)
|
||||||
|
return object : Ready() {
|
||||||
|
override fun writeTo(outputStream: OutputStream) {
|
||||||
|
if (armor) {
|
||||||
|
val armorOut = ArmorUtils.toAsciiArmoredStream(key, outputStream)
|
||||||
|
key.encode(armorOut)
|
||||||
|
armorOut.close()
|
||||||
|
} else {
|
||||||
|
key.encode(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: InvalidAlgorithmParameterException) {
|
||||||
|
throw SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", e)
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
throw SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", e)
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun noArmor(): GenerateKey = apply { armor = false }
|
||||||
|
|
||||||
|
override fun profile(profile: String): GenerateKey = apply {
|
||||||
|
this.profile =
|
||||||
|
SUPPORTED_PROFILES.find { it.name == profile }?.name
|
||||||
|
?: throw SOPGPException.UnsupportedProfile("generate-key", profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun signingOnly(): GenerateKey = apply { signingOnly = true }
|
||||||
|
|
||||||
|
override fun userId(userId: String): GenerateKey = apply { userIds.add(userId) }
|
||||||
|
|
||||||
|
override fun withKeyPassword(password: String): GenerateKey = apply {
|
||||||
|
this.passphrase = Passphrase.fromPassword(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateKeyWithProfile(
|
||||||
|
profile: String,
|
||||||
|
userIds: Set<String>,
|
||||||
|
passphrase: Passphrase,
|
||||||
|
signingOnly: Boolean
|
||||||
|
): PGPSecretKeyRing {
|
||||||
|
val keyBuilder: KeyRingBuilder =
|
||||||
|
when (profile) {
|
||||||
|
CURVE25519_PROFILE.name ->
|
||||||
|
PGPainless.buildKeyRing()
|
||||||
|
.setPrimaryKey(
|
||||||
|
KeySpec.getBuilder(
|
||||||
|
KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER))
|
||||||
|
.addSubkey(
|
||||||
|
KeySpec.getBuilder(
|
||||||
|
KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA))
|
||||||
|
.apply {
|
||||||
|
if (!signingOnly) {
|
||||||
|
addSubkey(
|
||||||
|
KeySpec.getBuilder(
|
||||||
|
KeyType.XDH(XDHSpec._X25519),
|
||||||
|
KeyFlag.ENCRYPT_COMMS,
|
||||||
|
KeyFlag.ENCRYPT_STORAGE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RSA4096_PROFILE.name -> {
|
||||||
|
PGPainless.buildKeyRing()
|
||||||
|
.setPrimaryKey(
|
||||||
|
KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.CERTIFY_OTHER))
|
||||||
|
.addSubkey(
|
||||||
|
KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.SIGN_DATA))
|
||||||
|
.apply {
|
||||||
|
if (!signingOnly) {
|
||||||
|
addSubkey(
|
||||||
|
KeySpec.getBuilder(
|
||||||
|
KeyType.RSA(RsaLength._4096),
|
||||||
|
KeyFlag.ENCRYPT_COMMS,
|
||||||
|
KeyFlag.ENCRYPT_STORAGE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> throw SOPGPException.UnsupportedProfile("generate-key", profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
userIds.forEach { keyBuilder.addUserId(it) }
|
||||||
|
if (!passphrase.isEmpty) {
|
||||||
|
keyBuilder.setPassphrase(passphrase)
|
||||||
|
}
|
||||||
|
return keyBuilder.build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import org.bouncycastle.bcpg.ArmoredInputStream
|
||||||
|
import org.bouncycastle.openpgp.PGPCompressedData
|
||||||
|
import org.bouncycastle.openpgp.PGPException
|
||||||
|
import org.bouncycastle.openpgp.PGPLiteralData
|
||||||
|
import org.bouncycastle.openpgp.PGPOnePassSignatureList
|
||||||
|
import org.bouncycastle.openpgp.PGPSignatureList
|
||||||
|
import org.bouncycastle.util.io.Streams
|
||||||
|
import org.pgpainless.decryption_verification.OpenPgpInputStream
|
||||||
|
import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil
|
||||||
|
import org.pgpainless.exception.WrongConsumingMethodException
|
||||||
|
import org.pgpainless.implementation.ImplementationFactory
|
||||||
|
import org.pgpainless.util.ArmoredOutputStreamFactory
|
||||||
|
import sop.ReadyWithResult
|
||||||
|
import sop.Signatures
|
||||||
|
import sop.exception.SOPGPException
|
||||||
|
import sop.operation.InlineDetach
|
||||||
|
|
||||||
|
/** Implementation of the `inline-detach` operation using PGPainless. */
|
||||||
|
class InlineDetachImpl : InlineDetach {
|
||||||
|
|
||||||
|
private var armor = true
|
||||||
|
|
||||||
|
override fun message(messageInputStream: InputStream): ReadyWithResult<Signatures> {
|
||||||
|
return object : ReadyWithResult<Signatures>() {
|
||||||
|
|
||||||
|
private val sigOut = ByteArrayOutputStream()
|
||||||
|
|
||||||
|
override fun writeTo(messageOutputStream: OutputStream): Signatures {
|
||||||
|
var pgpIn = OpenPgpInputStream(messageInputStream)
|
||||||
|
if (pgpIn.isNonOpenPgp) {
|
||||||
|
throw SOPGPException.BadData("Data appears to be non-OpenPGP.")
|
||||||
|
}
|
||||||
|
var signatures: PGPSignatureList? = null
|
||||||
|
|
||||||
|
// Handle ASCII armor
|
||||||
|
if (pgpIn.isAsciiArmored) {
|
||||||
|
val armorIn = ArmoredInputStream(pgpIn)
|
||||||
|
|
||||||
|
// Handle cleartext signature framework
|
||||||
|
if (armorIn.isClearText) {
|
||||||
|
try {
|
||||||
|
signatures =
|
||||||
|
ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(
|
||||||
|
armorIn, messageOutputStream)
|
||||||
|
if (signatures.isEmpty) {
|
||||||
|
throw SOPGPException.BadData(
|
||||||
|
"Data did not contain OpenPGP signatures.")
|
||||||
|
}
|
||||||
|
} catch (e: WrongConsumingMethodException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// else just dearmor
|
||||||
|
pgpIn = OpenPgpInputStream(armorIn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If data was not using cleartext signature framework
|
||||||
|
if (signatures == null) {
|
||||||
|
if (!pgpIn.isBinaryOpenPgp) {
|
||||||
|
throw SOPGPException.BadData(
|
||||||
|
"Data was containing ASCII armored non-OpenPGP data.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle binary OpenPGP data
|
||||||
|
var objectFactory =
|
||||||
|
ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn)
|
||||||
|
var next: Any?
|
||||||
|
|
||||||
|
while (objectFactory.nextObject().also { next = it } != null) {
|
||||||
|
|
||||||
|
if (next is PGPOnePassSignatureList) {
|
||||||
|
// Skip over OPSs
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next is PGPLiteralData) {
|
||||||
|
// Write out contents of Literal Data packet
|
||||||
|
val literalIn = (next as PGPLiteralData).dataStream
|
||||||
|
Streams.pipeAll(literalIn, messageOutputStream)
|
||||||
|
literalIn.close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next is PGPCompressedData) {
|
||||||
|
// Decompress compressed data
|
||||||
|
try {
|
||||||
|
objectFactory =
|
||||||
|
ImplementationFactory.getInstance()
|
||||||
|
.getPGPObjectFactory((next as PGPCompressedData).dataStream)
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
throw SOPGPException.BadData(
|
||||||
|
"Cannot decompress PGPCompressedData", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next is PGPSignatureList) {
|
||||||
|
signatures = next as PGPSignatureList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signatures == null) {
|
||||||
|
throw SOPGPException.BadData("Data did not contain OpenPGP signatures.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (armor) {
|
||||||
|
ArmoredOutputStreamFactory.get(sigOut).use { armoredOut ->
|
||||||
|
signatures.forEach { it.encode(armoredOut) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
signatures.forEach { it.encode(sigOut) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return object : Signatures() {
|
||||||
|
override fun writeTo(outputStream: OutputStream) {
|
||||||
|
sigOut.writeTo(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun noArmor(): InlineDetach = apply { armor = false }
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.lang.RuntimeException
|
||||||
|
import org.bouncycastle.openpgp.PGPException
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||||
|
import org.bouncycastle.util.io.Streams
|
||||||
|
import org.pgpainless.PGPainless
|
||||||
|
import org.pgpainless.algorithm.DocumentSignatureType
|
||||||
|
import org.pgpainless.bouncycastle.extensions.openPgpFingerprint
|
||||||
|
import org.pgpainless.encryption_signing.ProducerOptions
|
||||||
|
import org.pgpainless.encryption_signing.SigningOptions
|
||||||
|
import org.pgpainless.exception.KeyException.MissingSecretKeyException
|
||||||
|
import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException
|
||||||
|
import org.pgpainless.util.Passphrase
|
||||||
|
import sop.Ready
|
||||||
|
import sop.enums.InlineSignAs
|
||||||
|
import sop.exception.SOPGPException
|
||||||
|
import sop.operation.InlineSign
|
||||||
|
import sop.util.UTF8Util
|
||||||
|
|
||||||
|
/** Implementation of the `inline-sign` operation using PGPainless. */
|
||||||
|
class InlineSignImpl : InlineSign {
|
||||||
|
|
||||||
|
private val signingOptions = SigningOptions.get()
|
||||||
|
private val protector = MatchMakingSecretKeyRingProtector()
|
||||||
|
private val signingKeys = mutableListOf<PGPSecretKeyRing>()
|
||||||
|
|
||||||
|
private var armor = true
|
||||||
|
private var mode = InlineSignAs.binary
|
||||||
|
|
||||||
|
override fun data(data: InputStream): Ready {
|
||||||
|
signingKeys.forEach { key ->
|
||||||
|
try {
|
||||||
|
if (mode == InlineSignAs.clearsigned) {
|
||||||
|
signingOptions.addDetachedSignature(protector, key, modeToSigType(mode))
|
||||||
|
} else {
|
||||||
|
signingOptions.addInlineSignature(protector, key, modeToSigType(mode))
|
||||||
|
}
|
||||||
|
} catch (e: UnacceptableSigningKeyException) {
|
||||||
|
throw SOPGPException.KeyCannotSign("Key ${key.openPgpFingerprint} cannot sign.", e)
|
||||||
|
} catch (e: MissingSecretKeyException) {
|
||||||
|
throw SOPGPException.KeyCannotSign(
|
||||||
|
"Key ${key.openPgpFingerprint} does not have the secret signing key component available.",
|
||||||
|
e)
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
throw SOPGPException.KeyIsProtected(
|
||||||
|
"Key ${key.openPgpFingerprint} cannot be unlocked.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val producerOptions =
|
||||||
|
ProducerOptions.sign(signingOptions).apply {
|
||||||
|
if (mode == InlineSignAs.clearsigned) {
|
||||||
|
setCleartextSigned()
|
||||||
|
setAsciiArmor(true) // CSF is always armored
|
||||||
|
} else {
|
||||||
|
setAsciiArmor(armor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return object : Ready() {
|
||||||
|
override fun writeTo(outputStream: OutputStream) {
|
||||||
|
try {
|
||||||
|
val signingStream =
|
||||||
|
PGPainless.encryptAndOrSign()
|
||||||
|
.onOutputStream(outputStream)
|
||||||
|
.withOptions(producerOptions)
|
||||||
|
|
||||||
|
Streams.pipeAll(data, signingStream)
|
||||||
|
signingStream.close()
|
||||||
|
|
||||||
|
// forget passphrases
|
||||||
|
protector.clear()
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun key(key: InputStream): InlineSign = apply {
|
||||||
|
KeyReader.readSecretKeys(key, true).forEach {
|
||||||
|
val info = PGPainless.inspectKeyRing(it)
|
||||||
|
if (!info.isUsableForSigning) {
|
||||||
|
throw SOPGPException.KeyCannotSign(
|
||||||
|
"Key ${info.fingerprint} does not have valid, signing capable subkeys.")
|
||||||
|
}
|
||||||
|
protector.addSecretKey(it)
|
||||||
|
signingKeys.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mode(mode: InlineSignAs): InlineSign = apply { this.mode = mode }
|
||||||
|
|
||||||
|
override fun noArmor(): InlineSign = apply { armor = false }
|
||||||
|
|
||||||
|
override fun withKeyPassword(password: ByteArray): InlineSign = apply {
|
||||||
|
protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun modeToSigType(mode: InlineSignAs): DocumentSignatureType {
|
||||||
|
return when (mode) {
|
||||||
|
InlineSignAs.binary -> DocumentSignatureType.BINARY_DOCUMENT
|
||||||
|
InlineSignAs.text -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT
|
||||||
|
InlineSignAs.clearsigned -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.util.*
|
||||||
|
import org.bouncycastle.openpgp.PGPException
|
||||||
|
import org.bouncycastle.util.io.Streams
|
||||||
|
import org.pgpainless.PGPainless
|
||||||
|
import org.pgpainless.decryption_verification.ConsumerOptions
|
||||||
|
import org.pgpainless.exception.MalformedOpenPgpMessageException
|
||||||
|
import org.pgpainless.exception.MissingDecryptionMethodException
|
||||||
|
import sop.ReadyWithResult
|
||||||
|
import sop.Verification
|
||||||
|
import sop.exception.SOPGPException
|
||||||
|
import sop.operation.InlineVerify
|
||||||
|
|
||||||
|
/** Implementation of the `inline-verify` operation using PGPainless. */
|
||||||
|
class InlineVerifyImpl : InlineVerify {
|
||||||
|
|
||||||
|
private val options = ConsumerOptions.get()
|
||||||
|
|
||||||
|
override fun cert(cert: InputStream): InlineVerify = apply {
|
||||||
|
options.addVerificationCerts(KeyReader.readPublicKeys(cert, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun data(data: InputStream): ReadyWithResult<List<Verification>> {
|
||||||
|
return object : ReadyWithResult<List<Verification>>() {
|
||||||
|
override fun writeTo(outputStream: OutputStream): List<Verification> {
|
||||||
|
try {
|
||||||
|
val verificationStream =
|
||||||
|
PGPainless.decryptAndOrVerify().onInputStream(data).withOptions(options)
|
||||||
|
|
||||||
|
Streams.pipeAll(verificationStream, outputStream)
|
||||||
|
verificationStream.close()
|
||||||
|
|
||||||
|
val result = verificationStream.metadata
|
||||||
|
val verifications =
|
||||||
|
if (result.isUsingCleartextSignatureFramework) {
|
||||||
|
result.verifiedDetachedSignatures
|
||||||
|
} else {
|
||||||
|
result.verifiedInlineSignatures
|
||||||
|
}
|
||||||
|
.map { VerificationHelper.mapVerification(it) }
|
||||||
|
|
||||||
|
if (options.getCertificateSource().getExplicitCertificates().isNotEmpty() &&
|
||||||
|
verifications.isEmpty()) {
|
||||||
|
throw SOPGPException.NoSignature()
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifications
|
||||||
|
} catch (e: MissingDecryptionMethodException) {
|
||||||
|
throw SOPGPException.BadData("Cannot verify encrypted message.", e)
|
||||||
|
} catch (e: MalformedOpenPgpMessageException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notAfter(timestamp: Date): InlineVerify = apply {
|
||||||
|
options.verifyNotAfter(timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notBefore(timestamp: Date): InlineVerify = apply {
|
||||||
|
options.verifyNotBefore(timestamp)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import org.bouncycastle.openpgp.PGPException
|
||||||
|
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection
|
||||||
|
import org.bouncycastle.openpgp.PGPRuntimeOperationException
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection
|
||||||
|
import org.pgpainless.PGPainless
|
||||||
|
import sop.exception.SOPGPException
|
||||||
|
|
||||||
|
/** Reader for OpenPGP keys and certificates with error matching according to the SOP spec. */
|
||||||
|
class KeyReader {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun readSecretKeys(
|
||||||
|
keyInputStream: InputStream,
|
||||||
|
requireContent: Boolean
|
||||||
|
): PGPSecretKeyRingCollection {
|
||||||
|
val keys =
|
||||||
|
try {
|
||||||
|
PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
if (e.message == null) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
if (e.message!!.startsWith("unknown object in stream:") ||
|
||||||
|
e.message!!.startsWith("invalid header encountered")) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
if (requireContent && keys.none()) {
|
||||||
|
throw SOPGPException.BadData(PGPException("No key data found."))
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun readPublicKeys(
|
||||||
|
certIn: InputStream,
|
||||||
|
requireContent: Boolean
|
||||||
|
): PGPPublicKeyRingCollection {
|
||||||
|
val certs =
|
||||||
|
try {
|
||||||
|
PGPainless.readKeyRing().keyRingCollection(certIn, false)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
if (e.message == null) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
if (e.message!!.startsWith("unknown object in stream:") ||
|
||||||
|
e.message!!.startsWith("invalid header encountered")) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
} catch (e: PGPRuntimeOperationException) {
|
||||||
|
throw SOPGPException.BadData(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certs.pgpSecretKeyRingCollection.any()) {
|
||||||
|
throw SOPGPException.BadData(
|
||||||
|
"Secret key components encountered, while certificates were expected.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireContent && certs.pgpPublicKeyRingCollection.none()) {
|
||||||
|
throw SOPGPException.BadData(PGPException("No cert data found."))
|
||||||
|
}
|
||||||
|
return certs.pgpPublicKeyRingCollection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import sop.Profile
|
||||||
|
import sop.exception.SOPGPException
|
||||||
|
import sop.operation.ListProfiles
|
||||||
|
|
||||||
|
/** Implementation of the `list-profiles` operation using PGPainless. */
|
||||||
|
class ListProfilesImpl : ListProfiles {
|
||||||
|
|
||||||
|
override fun subcommand(command: String): List<Profile> =
|
||||||
|
when (command) {
|
||||||
|
"generate-key" -> GenerateKeyImpl.SUPPORTED_PROFILES
|
||||||
|
"encrypt" -> EncryptImpl.SUPPORTED_PROFILES
|
||||||
|
else -> throw SOPGPException.UnsupportedProfile(command)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import org.bouncycastle.openpgp.PGPException
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKey
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||||
|
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor
|
||||||
|
import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor
|
||||||
|
import org.pgpainless.bouncycastle.extensions.isDecrypted
|
||||||
|
import org.pgpainless.bouncycastle.extensions.unlock
|
||||||
|
import org.pgpainless.key.protection.CachingSecretKeyRingProtector
|
||||||
|
import org.pgpainless.key.protection.SecretKeyRingProtector
|
||||||
|
import org.pgpainless.util.Passphrase
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the [SecretKeyRingProtector] which can be handed passphrases and keys
|
||||||
|
* separately, and which then matches up passphrases and keys when needed.
|
||||||
|
*/
|
||||||
|
class MatchMakingSecretKeyRingProtector : SecretKeyRingProtector {
|
||||||
|
|
||||||
|
private val passphrases = mutableSetOf<Passphrase>()
|
||||||
|
private val keys = mutableSetOf<PGPSecretKeyRing>()
|
||||||
|
private val protector = CachingSecretKeyRingProtector()
|
||||||
|
|
||||||
|
fun addPassphrase(passphrase: Passphrase) = apply {
|
||||||
|
if (passphrase.isEmpty) {
|
||||||
|
return@apply
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!passphrases.add(passphrase)) {
|
||||||
|
return@apply
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.forEach { key ->
|
||||||
|
for (subkey in key) {
|
||||||
|
if (protector.hasPassphrase(subkey.keyID)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testPassphrase(passphrase, subkey)) {
|
||||||
|
protector.addPassphrase(subkey.keyID, passphrase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addSecretKey(key: PGPSecretKeyRing) = apply {
|
||||||
|
if (!keys.add(key)) {
|
||||||
|
return@apply
|
||||||
|
}
|
||||||
|
|
||||||
|
key.forEach { subkey ->
|
||||||
|
if (subkey.isDecrypted()) {
|
||||||
|
protector.addPassphrase(subkey.keyID, Passphrase.emptyPassphrase())
|
||||||
|
} else {
|
||||||
|
passphrases.forEach { passphrase ->
|
||||||
|
if (testPassphrase(passphrase, subkey)) {
|
||||||
|
protector.addPassphrase(subkey.keyID, passphrase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testPassphrase(passphrase: Passphrase, key: PGPSecretKey): Boolean =
|
||||||
|
try {
|
||||||
|
key.unlock(passphrase)
|
||||||
|
true
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
// Wrong passphrase
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasPassphraseFor(keyId: Long): Boolean = protector.hasPassphrase(keyId)
|
||||||
|
|
||||||
|
override fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? = protector.getDecryptor(keyId)
|
||||||
|
|
||||||
|
override fun getEncryptor(keyId: Long): PBESecretKeyEncryptor? = protector.getEncryptor(keyId)
|
||||||
|
|
||||||
|
/** Clear all known passphrases from the protector. */
|
||||||
|
fun clear() {
|
||||||
|
passphrases.forEach { it.clear() }
|
||||||
|
keys.forEach { protector.forgetPassphrase(it) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.lang.RuntimeException
|
||||||
|
import org.bouncycastle.openpgp.PGPException
|
||||||
|
import org.bouncycastle.openpgp.PGPPublicKeyRing
|
||||||
|
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection
|
||||||
|
import org.pgpainless.PGPainless
|
||||||
|
import org.pgpainless.bouncycastle.extensions.openPgpFingerprint
|
||||||
|
import org.pgpainless.exception.WrongPassphraseException
|
||||||
|
import org.pgpainless.key.util.KeyRingUtils
|
||||||
|
import org.pgpainless.key.util.RevocationAttributes
|
||||||
|
import org.pgpainless.util.ArmoredOutputStreamFactory
|
||||||
|
import org.pgpainless.util.Passphrase
|
||||||
|
import sop.Ready
|
||||||
|
import sop.exception.SOPGPException
|
||||||
|
import sop.operation.RevokeKey
|
||||||
|
import sop.util.UTF8Util
|
||||||
|
|
||||||
|
class RevokeKeyImpl : RevokeKey {
|
||||||
|
|
||||||
|
private val protector = MatchMakingSecretKeyRingProtector()
|
||||||
|
private var armor = true
|
||||||
|
|
||||||
|
override fun keys(keys: InputStream): Ready {
|
||||||
|
val secretKeyRings =
|
||||||
|
try {
|
||||||
|
KeyReader.readSecretKeys(keys, true)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw SOPGPException.BadData("Cannot decode secret keys.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretKeyRings.forEach { protector.addSecretKey(it) }
|
||||||
|
|
||||||
|
val revocationCertificates = mutableListOf<PGPPublicKeyRing>()
|
||||||
|
secretKeyRings.forEach { secretKeys ->
|
||||||
|
val editor = PGPainless.modifyKeyRing(secretKeys)
|
||||||
|
try {
|
||||||
|
val attributes =
|
||||||
|
RevocationAttributes.createKeyRevocation()
|
||||||
|
.withReason(RevocationAttributes.Reason.NO_REASON)
|
||||||
|
.withoutDescription()
|
||||||
|
if (secretKeys.publicKey.version == 6) {
|
||||||
|
revocationCertificates.add(
|
||||||
|
editor.createMinimalRevocationCertificate(protector, attributes))
|
||||||
|
} else {
|
||||||
|
val certificate = PGPainless.extractCertificate(secretKeys)
|
||||||
|
val revocation = editor.createRevocation(protector, attributes)
|
||||||
|
revocationCertificates.add(
|
||||||
|
KeyRingUtils.injectCertification(certificate, revocation))
|
||||||
|
}
|
||||||
|
} catch (e: WrongPassphraseException) {
|
||||||
|
throw SOPGPException.KeyIsProtected(
|
||||||
|
"Missing or wrong passphrase for key ${secretKeys.openPgpFingerprint}", e)
|
||||||
|
} catch (e: PGPException) {
|
||||||
|
throw RuntimeException(
|
||||||
|
"Cannot generate revocation certificate for key ${secretKeys.openPgpFingerprint}",
|
||||||
|
e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return object : Ready() {
|
||||||
|
override fun writeTo(outputStream: OutputStream) {
|
||||||
|
val collection = PGPPublicKeyRingCollection(revocationCertificates)
|
||||||
|
if (armor) {
|
||||||
|
val armorOut = ArmoredOutputStreamFactory.get(outputStream)
|
||||||
|
collection.encode(armorOut)
|
||||||
|
armorOut.close()
|
||||||
|
} else {
|
||||||
|
collection.encode(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun noArmor(): RevokeKey = apply { armor = false }
|
||||||
|
|
||||||
|
override fun withKeyPassword(password: ByteArray): RevokeKey = apply {
|
||||||
|
val string =
|
||||||
|
try {
|
||||||
|
UTF8Util.decodeUTF8(password)
|
||||||
|
} catch (e: CharacterCodingException) {
|
||||||
|
// TODO: Add cause
|
||||||
|
throw SOPGPException.PasswordNotHumanReadable(
|
||||||
|
"Cannot UTF8-decode password: ${e.stackTraceToString()}")
|
||||||
|
}
|
||||||
|
protector.addPassphrase(Passphrase.fromPassword(string))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import sop.SOP
|
||||||
|
import sop.SOPV
|
||||||
|
import sop.operation.Armor
|
||||||
|
import sop.operation.ChangeKeyPassword
|
||||||
|
import sop.operation.Dearmor
|
||||||
|
import sop.operation.Decrypt
|
||||||
|
import sop.operation.DetachedSign
|
||||||
|
import sop.operation.DetachedVerify
|
||||||
|
import sop.operation.Encrypt
|
||||||
|
import sop.operation.ExtractCert
|
||||||
|
import sop.operation.GenerateKey
|
||||||
|
import sop.operation.InlineDetach
|
||||||
|
import sop.operation.InlineSign
|
||||||
|
import sop.operation.InlineVerify
|
||||||
|
import sop.operation.ListProfiles
|
||||||
|
import sop.operation.RevokeKey
|
||||||
|
import sop.operation.Version
|
||||||
|
|
||||||
|
class SOPImpl(private val sopv: SOPV = SOPVImpl()) : SOP {
|
||||||
|
|
||||||
|
override fun armor(): Armor = ArmorImpl()
|
||||||
|
|
||||||
|
override fun changeKeyPassword(): ChangeKeyPassword = ChangeKeyPasswordImpl()
|
||||||
|
|
||||||
|
override fun dearmor(): Dearmor = DearmorImpl()
|
||||||
|
|
||||||
|
override fun decrypt(): Decrypt = DecryptImpl()
|
||||||
|
|
||||||
|
override fun detachedSign(): DetachedSign = DetachedSignImpl()
|
||||||
|
|
||||||
|
override fun detachedVerify(): DetachedVerify = sopv.detachedVerify()
|
||||||
|
|
||||||
|
override fun encrypt(): Encrypt = EncryptImpl()
|
||||||
|
|
||||||
|
override fun extractCert(): ExtractCert = ExtractCertImpl()
|
||||||
|
|
||||||
|
override fun generateKey(): GenerateKey = GenerateKeyImpl()
|
||||||
|
|
||||||
|
override fun inlineDetach(): InlineDetach = InlineDetachImpl()
|
||||||
|
|
||||||
|
override fun inlineSign(): InlineSign = InlineSignImpl()
|
||||||
|
|
||||||
|
override fun inlineVerify(): InlineVerify = sopv.inlineVerify()
|
||||||
|
|
||||||
|
override fun listProfiles(): ListProfiles = ListProfilesImpl()
|
||||||
|
|
||||||
|
override fun revokeKey(): RevokeKey = RevokeKeyImpl()
|
||||||
|
|
||||||
|
override fun version(): Version = sopv.version()
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import org.pgpainless.util.ArmoredOutputStreamFactory
|
||||||
|
import sop.SOPV
|
||||||
|
import sop.operation.DetachedVerify
|
||||||
|
import sop.operation.InlineVerify
|
||||||
|
import sop.operation.Version
|
||||||
|
|
||||||
|
class SOPVImpl : SOPV {
|
||||||
|
|
||||||
|
init {
|
||||||
|
ArmoredOutputStreamFactory.setVersionInfo(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun detachedVerify(): DetachedVerify = DetachedVerifyImpl()
|
||||||
|
|
||||||
|
override fun inlineVerify(): InlineVerify = InlineVerifyImpl()
|
||||||
|
|
||||||
|
override fun version(): Version = VersionImpl()
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import org.bouncycastle.openpgp.PGPSignature
|
||||||
|
import org.pgpainless.decryption_verification.SignatureVerification
|
||||||
|
import sop.Verification
|
||||||
|
import sop.enums.SignatureMode
|
||||||
|
|
||||||
|
/** Helper class for shared methods related to [Verification] objects. */
|
||||||
|
class VerificationHelper {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a [SignatureVerification] object to a [Verification].
|
||||||
|
*
|
||||||
|
* @param sigVerification signature verification
|
||||||
|
* @return verification
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun mapVerification(sigVerification: SignatureVerification): Verification =
|
||||||
|
Verification(
|
||||||
|
sigVerification.signature.creationTime,
|
||||||
|
sigVerification.signingKey.subkeyFingerprint.toString(),
|
||||||
|
sigVerification.signingKey.primaryKeyFingerprint.toString(),
|
||||||
|
getMode(sigVerification.signature),
|
||||||
|
null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map an OpenPGP signature type to a [SignatureMode] enum. Note: This method only maps
|
||||||
|
* [PGPSignature.BINARY_DOCUMENT] and [PGPSignature.CANONICAL_TEXT_DOCUMENT]. Other values
|
||||||
|
* are mapped to `null`.
|
||||||
|
*
|
||||||
|
* @param signature signature
|
||||||
|
* @return signature mode enum or null
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun getMode(signature: PGPSignature): SignatureMode? =
|
||||||
|
when (signature.signatureType) {
|
||||||
|
PGPSignature.BINARY_DOCUMENT -> SignatureMode.binary
|
||||||
|
PGPSignature.CANONICAL_TEXT_DOCUMENT -> SignatureMode.text
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.sop
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.util.*
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
|
import sop.operation.Version
|
||||||
|
|
||||||
|
/** Implementation of the `version` operation using PGPainless. */
|
||||||
|
class VersionImpl : Version {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SOP_VERSION = 10
|
||||||
|
const val SOPV_VERSION = "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBackendVersion(): String = "PGPainless ${getVersion()}"
|
||||||
|
|
||||||
|
override fun getExtendedVersion(): String {
|
||||||
|
val bcVersion =
|
||||||
|
String.format(Locale.US, "Bouncy Castle %.2f", BouncyCastleProvider().version)
|
||||||
|
val specVersion = String.format("%02d", SOP_VERSION)
|
||||||
|
return """${getName()} ${getVersion()}
|
||||||
|
https://codeberg.org/PGPainless/pgpainless/src/branch/master/pgpainless-sop
|
||||||
|
|
||||||
|
Implementation of the Stateless OpenPGP Protocol Version $specVersion
|
||||||
|
https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-$specVersion
|
||||||
|
|
||||||
|
Based on pgpainless-core ${getVersion()}
|
||||||
|
https://pgpainless.org
|
||||||
|
|
||||||
|
Using $bcVersion
|
||||||
|
https://www.bouncycastle.org/java.html"""
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String = "PGPainless-SOP"
|
||||||
|
|
||||||
|
override fun getSopSpecImplementationRemarks(): String? = null
|
||||||
|
|
||||||
|
override fun getSopSpecRevisionNumber(): Int = SOP_VERSION
|
||||||
|
|
||||||
|
override fun getSopVVersion(): String = SOPV_VERSION
|
||||||
|
|
||||||
|
override fun getVersion(): String {
|
||||||
|
// See https://stackoverflow.com/a/50119235
|
||||||
|
return try {
|
||||||
|
val resourceIn: InputStream =
|
||||||
|
javaClass.getResourceAsStream("/version.properties")
|
||||||
|
?: throw IOException("File version.properties not found.")
|
||||||
|
|
||||||
|
val properties = Properties().apply { load(resourceIn) }
|
||||||
|
properties.getProperty("version")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
"DEVELOPMENT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isSopSpecImplementationIncomplete(): Boolean = false
|
||||||
|
}
|
|
@ -7,22 +7,14 @@ package org.pgpainless.sop;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.pgpainless.PGPainless;
|
import org.pgpainless.PGPainless;
|
||||||
import org.pgpainless.util.ArmorUtils;
|
import org.pgpainless.util.ArmorUtils;
|
||||||
import sop.enums.ArmorLabel;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
|
|
||||||
public class ArmorTest {
|
public class ArmorTest {
|
||||||
|
|
||||||
@Test
|
|
||||||
public void labelIsNotSupported() {
|
|
||||||
assertThrows(SOPGPException.UnsupportedOption.class, () -> new SOPImpl().armor().label(ArmorLabel.sig));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void armor() throws IOException {
|
public void armor() throws IOException {
|
||||||
byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice").getEncoded();
|
byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice").getEncoded();
|
||||||
|
|
|
@ -72,4 +72,11 @@ public class VersionTest {
|
||||||
assertTrue(fullSopSpecVersion.endsWith(incompletenessRemarks));
|
assertTrue(fullSopSpecVersion.endsWith(incompletenessRemarks));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSopVVersion() {
|
||||||
|
String sopVVersion = sop.version().getSopVVersion();
|
||||||
|
assertNotNull(sopVVersion);
|
||||||
|
assertTrue(sopVVersion.matches("\\d+\\.\\d+(\\.\\d+)*")); // X.Y or X.Y.Z... etc.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for the {@link sop.SOP} API, tailored to the behavior of PGPainless' implementation specifically.
|
||||||
|
* Generalized tests can be found in {@link sop.testsuite.pgpainless}.
|
||||||
|
*/
|
||||||
|
package org.pgpainless.sop;
|
|
@ -0,0 +1,9 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generalized tests for the {@link sop.SOP} API.
|
||||||
|
* For tests tailored specifically to PGPainless' behavior, see {@link org.pgpainless.sop}.
|
||||||
|
*/
|
||||||
|
package sop.testsuite.pgpainless;
|
|
@ -6,5 +6,6 @@ rootProject.name = 'PGPainless'
|
||||||
|
|
||||||
include 'pgpainless-core',
|
include 'pgpainless-core',
|
||||||
'pgpainless-sop',
|
'pgpainless-sop',
|
||||||
'pgpainless-cli'
|
'pgpainless-cli',
|
||||||
|
'hsregex'
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,6 @@ allprojects {
|
||||||
logbackVersion = '1.2.13'
|
logbackVersion = '1.2.13'
|
||||||
mockitoVersion = '4.5.1'
|
mockitoVersion = '4.5.1'
|
||||||
slf4jVersion = '1.7.36'
|
slf4jVersion = '1.7.36'
|
||||||
sopJavaVersion = '8.0.1'
|
sopJavaVersion = '10.0.0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue