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
|
||||
- Removed `OpenPgpMetadata` (`decryptionStream.getResult()`) in favor of `MessageMetadata` (`decryptionStream.getMetadata()`)
|
||||
- `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
|
||||
- `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty
|
||||
- Separate signature verification operations into `SOPV` interface
|
||||
- Add `version --sopv` option
|
||||
- Throw `BadData` error when passing KEYS where CERTS are expected.
|
||||
- Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice)
|
||||
- Do not choke on unknown signature subpackets (thanks @Jerbell)
|
||||
- Prevent timing issues resuting in subkey binding signatures predating the subkey (@thanks Jerbell)
|
||||
|
@ -24,6 +27,12 @@ SPDX-License-Identifier: CC0-1.0
|
|||
- `GNUPG_AEAD_ENCRYPTED_DATA` -> `LIBREPGP_OCB_ENCRYPTED_DATA`
|
||||
- `GNUPG_VERSION_5_PUBLIC_KEY` -> `LIBREPGP_VERSION_5_PUBLIC_KEY`
|
||||
|
||||
## 1.6.7
|
||||
- SOP: Fix OOM error when detached-signing large amounts of data (fix #432)
|
||||
- Move `CachingBcPublicKeyDataDecryptorFactory` from `org.bouncycastle` packet to `org.pgpainless.decryption_verification` to avoid package split (partially addresses #428)
|
||||
- Basic support for Java Modules for `pgpainless-core` and `pgpainless-sop`
|
||||
- Added `Automatic-Module-Name` directive to gradle build files
|
||||
|
||||
## 1.6.6
|
||||
- Downgrade `logback-core` and `logback-classic` to `1.2.13` to fix #426
|
||||
|
||||
|
@ -179,6 +188,9 @@ SPDX-License-Identifier: CC0-1.0
|
|||
- 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
|
||||
- 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
|
||||
- Bump `bcpg-jdk15to18` to `1.72.3`
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
myst-parser>=0.17
|
||||
sphinxcontrib-mermaid>=0.7.1
|
||||
sphinx_rtd_theme>=2.0.0
|
||||
|
|
|
@ -82,23 +82,26 @@ Stateless OpenPGP Protocol
|
|||
Usage: pgpainless-cli [--stacktrace] [COMMAND]
|
||||
|
||||
Options:
|
||||
--stacktrace Print Stacktrace
|
||||
--stacktrace Print stacktrace
|
||||
|
||||
Commands:
|
||||
help Display usage information for the specified subcommand
|
||||
armor Add ASCII Armor to standard input
|
||||
dearmor Remove ASCII Armor from standard input
|
||||
decrypt Decrypt a message from standard input
|
||||
inline-detach Split signatures from a clearsigned message
|
||||
encrypt Encrypt a message from standard input
|
||||
extract-cert Extract a public key certificate from a secret key from
|
||||
standard input
|
||||
generate-key Generate a secret key
|
||||
sign Create a detached signature on the data from standard input
|
||||
verify Verify a detached signature over the data from standard input
|
||||
inline-sign Create an inline-signed message from data on standard input
|
||||
inline-verify Verify inline-signed data from standard input
|
||||
version Display version information about the tool
|
||||
version Display version information about the tool
|
||||
list-profiles Emit a list of profiles supported by the identified
|
||||
subcommand
|
||||
generate-key Generate a secret key
|
||||
change-key-password Update the password of a key
|
||||
revoke-key Generate revocation certificates
|
||||
extract-cert Extract a public key certificate from a secret key
|
||||
sign Create a detached message signature
|
||||
verify Verify a detached signature
|
||||
encrypt Encrypt a message from standard input
|
||||
decrypt Decrypt a message
|
||||
inline-detach Split signatures from a clearsigned message
|
||||
inline-sign Create an inline-signed message
|
||||
inline-verify Verify an inline-signed message
|
||||
armor Add ASCII Armor to standard input
|
||||
dearmor Remove ASCII Armor from standard input
|
||||
help Display usage information for the specified subcommand
|
||||
|
||||
Exit Codes:
|
||||
0 Successful program execution
|
||||
|
@ -120,6 +123,9 @@ Exit Codes:
|
|||
71 Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter
|
||||
73 Ambiguous input (a filename matching the designator already exists)
|
||||
79 Key is not signing capable
|
||||
83 Options were supplied that are incompatible with each other
|
||||
89 The requested profile is unsupported, or the indicated subcommand does
|
||||
not accept profiles
|
||||
```
|
||||
|
||||
To get help on a subcommand, e.g. `encrypt`, just call the help subcommand followed by the subcommand you
|
||||
|
|
|
@ -180,14 +180,6 @@ byte[] armoredData = sop.armor()
|
|||
|
||||
The `data(_)` method can either be called by providing a byte array, or an `InputStream`.
|
||||
|
||||
:::{note}
|
||||
There is a `label(ArmorLabel label)` method, which could theoretically be used to define the label used in the
|
||||
ASCII armor header.
|
||||
However, this method is not (yet?) supported by `pgpainless-sop` and will currently throw an `UnsupportedOption`
|
||||
exception.
|
||||
Instead, the implementation will figure out the data type and set the respective label on its own.
|
||||
:::
|
||||
|
||||
To remove ASCII armor from armored data, simply use the `dearmor()` API:
|
||||
|
||||
```java
|
||||
|
|
|
@ -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
|
||||
.SH "SYNOPSIS"
|
||||
.sp
|
||||
\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP] [\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP]
|
||||
\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP]
|
||||
.SH "DESCRIPTION"
|
||||
|
||||
.SH "OPTIONS"
|
||||
.sp
|
||||
\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP
|
||||
.RS 4
|
||||
Label to be used in the header and tail of the armoring
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-stacktrace\fP
|
||||
.RS 4
|
||||
Print stacktrace
|
||||
|
|
|
@ -31,8 +31,8 @@ pgpainless\-cli\-decrypt \- Decrypt a message
|
|||
.SH "SYNOPSIS"
|
||||
.sp
|
||||
\fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP]
|
||||
[\fB\-\-verify\-not\-after\fP=\fIDATE\fP] [\fB\-\-verify\-not\-before\fP=\fIDATE\fP]
|
||||
[\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP] [\fB\-\-verify\-with\fP=\fICERT\fP]...
|
||||
[\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP] [\fB\-\-verify\-not\-after\fP=\fIDATE\fP]
|
||||
[\fB\-\-verify\-not\-before\fP=\fIDATE\fP] [\fB\-\-verify\-with\fP=\fICERT\fP]...
|
||||
[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]...
|
||||
[\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP]... [\fIKEY\fP...]
|
||||
.SH "DESCRIPTION"
|
||||
|
@ -49,7 +49,7 @@ Can be used to learn the session key on successful decryption
|
|||
Print stacktrace
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-verify\-not\-after\fP=\fIDATE\fP
|
||||
\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP, \fB\-\-verify\-not\-after\fP=\fIDATE\fP
|
||||
.RS 4
|
||||
ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z)
|
||||
.sp
|
||||
|
@ -69,11 +69,6 @@ Reject signatures with a creation date not in range.
|
|||
Defaults to beginning of time (\(aq\-\(aq).
|
||||
.RE
|
||||
.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
|
||||
.RS 4
|
||||
Certificates for signature verification
|
||||
|
|
|
@ -31,9 +31,9 @@ pgpainless\-cli\-encrypt \- Encrypt a message from standard input
|
|||
.SH "SYNOPSIS"
|
||||
.sp
|
||||
\fBpgpainless\-cli encrypt\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP]
|
||||
[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-sign\-with\fP=\fIKEY\fP]...
|
||||
[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]...
|
||||
[\fICERTS\fP...]
|
||||
[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP]
|
||||
[\fB\-\-sign\-with\fP=\fIKEY\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]...
|
||||
[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fICERTS\fP...]
|
||||
.SH "DESCRIPTION"
|
||||
|
||||
.SH "OPTIONS"
|
||||
|
@ -53,7 +53,7 @@ ASCII armor the output
|
|||
Profile identifier to switch between profiles
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-sign\-with\fP=\fIKEY\fP
|
||||
\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP, \fB\-\-sign\-with\fP=\fIKEY\fP
|
||||
.RS 4
|
||||
Sign the output with a private key
|
||||
.RE
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
pgpainless\-cli\-version \- Display version information about the tool
|
||||
.SH "SYNOPSIS"
|
||||
.sp
|
||||
\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\fP]
|
||||
\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\fP | \fB\-\-sopv\fP]
|
||||
.SH "DESCRIPTION"
|
||||
|
||||
.SH "OPTIONS"
|
||||
|
@ -50,7 +50,7 @@ Print an extended version string
|
|||
Print the latest revision of the SOP specification targeted by the implementation
|
||||
.RE
|
||||
.sp
|
||||
\fB\-\-stacktrace\fP
|
||||
\fB\-\-sopv\fP, \fB\-\-stacktrace\fP
|
||||
.RS 4
|
||||
Print stacktrace
|
||||
.RE
|
|
@ -13,12 +13,14 @@ do
|
|||
SRC="${page##*/}"
|
||||
DEST="${SRC/sop/pgpainless-cli}"
|
||||
sed \
|
||||
-e 's/sopv/PLACEHOLDERV/g' \
|
||||
-e 's#.\\" Title: sop#.\\" Title: pgpainless-cli#g' \
|
||||
-e 's/Manual: Sop Manual/Manual: PGPainless-CLI Manual/g' \
|
||||
-e 's/.TH "SOP/.TH "PGPAINLESS\\-CLI/g' \
|
||||
-e 's/"Sop Manual"/"PGPainless\\-CLI Manual"/g' \
|
||||
-e 's/\\fBsop/\\fBpgpainless\\-cli/g' \
|
||||
-e 's/sop/pgpainless\\-cli/g' \
|
||||
-e 's/PLACEHOLDERV/sopv/g' \
|
||||
$page > $DEST_DIR/$DEST
|
||||
done
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
|||
import org.junit.jupiter.api.Test;
|
||||
import org.pgpainless.PGPainless;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sop.exception.SOPGPException;
|
||||
|
||||
public class ArmorCmdTest extends CLITest {
|
||||
|
||||
|
@ -89,15 +88,6 @@ public class ArmorCmdTest extends CLITest {
|
|||
assertTrue(armored.contains("SGVsbG8sIFdvcmxkIQo="));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void labelNotYetSupported() throws IOException {
|
||||
pipeStringToStdin("Hello, World!\n");
|
||||
ByteArrayOutputStream out = pipeStdoutToStream();
|
||||
int exitCode = executeCommand("armor", "--label", "Message");
|
||||
assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, exitCode);
|
||||
assertEquals(0, out.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void armorAlreadyArmoredDataIsIdempotent() throws IOException {
|
||||
pipeStringToStdin(key);
|
||||
|
|
|
@ -26,4 +26,14 @@ dependencies {
|
|||
|
||||
// @Nullable, @Nonnull annotations
|
||||
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.SymmetricKeyAlgorithm
|
||||
import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity
|
||||
import org.pgpainless.util.NullOutputStream
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
|
@ -19,6 +20,10 @@ class EncryptionBuilder : EncryptionBuilderInterface {
|
|||
return WithOptionsImpl(outputStream)
|
||||
}
|
||||
|
||||
override fun discardOutput(): EncryptionBuilderInterface.WithOptions {
|
||||
return onOutputStream(NullOutputStream())
|
||||
}
|
||||
|
||||
class WithOptionsImpl(val outputStream: OutputStream) : EncryptionBuilderInterface.WithOptions {
|
||||
|
||||
override fun withOptions(options: ProducerOptions): EncryptionStream {
|
||||
|
|
|
@ -8,7 +8,7 @@ import java.io.IOException
|
|||
import java.io.OutputStream
|
||||
import org.bouncycastle.openpgp.PGPException
|
||||
|
||||
fun interface EncryptionBuilderInterface {
|
||||
interface EncryptionBuilderInterface {
|
||||
|
||||
/**
|
||||
* Create a [EncryptionStream] wrapping an [OutputStream]. Data that is piped through the
|
||||
|
@ -19,6 +19,16 @@ fun interface EncryptionBuilderInterface {
|
|||
*/
|
||||
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 {
|
||||
|
||||
/**
|
||||
|
|
|
@ -169,6 +169,17 @@ class CertificateValidator {
|
|||
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
|
||||
val subkeySigs = mutableListOf<PGPSignature>()
|
||||
signingSubkey
|
||||
|
@ -183,7 +194,7 @@ class CertificateValidator {
|
|||
}
|
||||
} catch (e: SignatureValidationException) {
|
||||
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" +
|
||||
"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\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
|
||||
|
||||
[![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)
|
||||
[![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()
|
||||
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.pgpainless.PGPainless;
|
||||
import org.pgpainless.util.ArmorUtils;
|
||||
import sop.enums.ArmorLabel;
|
||||
import sop.exception.SOPGPException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
public class ArmorTest {
|
||||
|
||||
@Test
|
||||
public void labelIsNotSupported() {
|
||||
assertThrows(SOPGPException.UnsupportedOption.class, () -> new SOPImpl().armor().label(ArmorLabel.sig));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void armor() throws IOException {
|
||||
byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice").getEncoded();
|
||||
|
|
|
@ -72,4 +72,11 @@ public class VersionTest {
|
|||
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',
|
||||
'pgpainless-sop',
|
||||
'pgpainless-cli'
|
||||
'pgpainless-cli',
|
||||
'hsregex'
|
||||
|
||||
|
|
|
@ -14,6 +14,6 @@ allprojects {
|
|||
logbackVersion = '1.2.13'
|
||||
mockitoVersion = '4.5.1'
|
||||
slf4jVersion = '1.7.36'
|
||||
sopJavaVersion = '8.0.1'
|
||||
sopJavaVersion = '10.0.0'
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue