Compare commits

...

21 Commits

Author SHA1 Message Date
Paul Schaub c0b433e638
Merge 3399551cec into dd3ef89a5c 2024-04-14 22:01:45 +05:30
Paul Schaub dd3ef89a5c
Add (failing) test for extracting certificate from key with unknown secret key encryption method 2024-04-10 10:47:13 +02:00
Paul Schaub a6f3a223b1
Reject data signatures made by non-signing primary key 2024-04-10 10:38:50 +02:00
Paul Schaub 741d72eadc
Document nature of tests in pgpainless-sop 2024-03-30 19:20:12 +01:00
Paul Schaub 0b7511a223
Remove tests for armor --label 2024-03-30 19:07:12 +01:00
Paul Schaub eeb5986890
Remove notice about armor's label() option 2024-03-30 19:06:42 +01:00
Paul Schaub 32d62c6610
Update pgpainless-cli usage documentation 2024-03-30 18:52:49 +01:00
Paul Schaub 1f9b65e3d2
Fix missing readthedocs theme 2024-03-30 00:37:51 +01:00
Paul Schaub b96f22d0a9
Add EncryptionBuilder.discardOutput()
Also move NullOutputStream from pgpainless-sop to pgpainless-core
2024-03-29 20:37:24 +01:00
Paul Schaub 80cf1a7446
Merge branch 'sopKotlin' 2024-03-24 16:43:43 +01:00
Paul Schaub fe80b1185e
Update man pages 2024-03-24 16:43:27 +01:00
Paul Schaub b393a90da4
Port pgpainless-sop to Kotlin 2024-03-24 16:16:29 +01:00
Paul Schaub 8066650584
Add comments 2024-03-24 11:00:16 +01:00
Paul Schaub bd1949871a
Update CHANGELOG 2024-03-24 10:52:16 +01:00
Paul Schaub 194e4e1458
Bump sop-java to 10.0.0 2024-03-24 10:52:15 +01:00
Paul Schaub 44be5aa981
Delegate verification operations to SOPVImpl 2024-03-24 10:52:15 +01:00
Paul Schaub 3ac273757a
Bump sop-java to 10.0.0-SNAPSHOT and implement sopv interface subset 2024-03-24 10:52:15 +01:00
Paul Schaub fa5bdfcd82
Throw BadData if KEYS are passed where CERTS are expected 2024-03-24 10:52:14 +01:00
Paul Schaub 89038ebedf
Update CHANGELOG 2024-03-21 14:13:58 +01:00
Paul Schaub 337b5d68b6
Add Automatic-Module-Name to pgpainless-core and pgpainless-sop 2024-03-19 15:56:49 +01:00
Paul Schaub 3399551cec
Add ImageEncoding enum 2024-01-20 19:23:55 +01:00
67 changed files with 2088 additions and 2171 deletions

View File

@ -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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -31,9 +31,9 @@ pgpainless\-cli\-encrypt \- Encrypt a message from standard input
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli encrypt\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP]
[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-sign\-with\fP=\fIKEY\fP]...
[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]...
[\fICERTS\fP...]
[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP]
[\fB\-\-sign\-with\fP=\fIKEY\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]...
[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fICERTS\fP...]
.SH "DESCRIPTION"
.SH "OPTIONS"
@ -53,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

View File

@ -30,7 +30,7 @@
pgpainless\-cli\-version \- Display version information about the tool
.SH "SYNOPSIS"
.sp
\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\fP]
\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\fP | \fB\-\-sopv\fP]
.SH "DESCRIPTION"
.SH "OPTIONS"
@ -50,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

View File

@ -13,12 +13,14 @@ do
SRC="${page##*/}"
DEST="${SRC/sop/pgpainless-cli}"
sed \
-e 's/sopv/PLACEHOLDERV/g' \
-e 's#.\\" Title: sop#.\\" Title: pgpainless-cli#g' \
-e 's/Manual: Sop Manual/Manual: PGPainless-CLI Manual/g' \
-e 's/.TH "SOP/.TH "PGPAINLESS\\-CLI/g' \
-e 's/"Sop Manual"/"PGPainless\\-CLI Manual"/g' \
-e 's/\\fBsop/\\fBpgpainless\\-cli/g' \
-e 's/sop/pgpainless\\-cli/g' \
-e 's/PLACEHOLDERV/sopv/g' \
$page > $DEST_DIR/$DEST
done

View File

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

View File

@ -27,3 +27,10 @@ dependencies {
// @Nullable, @Nonnull annotations
implementation "com.google.code.findbugs:jsr305:3.0.2"
}
// https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_modular_auto
tasks.named('jar') {
manifest {
attributes('Automatic-Module-Name': 'org.pgpainless.core')
}
}

View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <info@pgpainless.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
/**
* Image encoding schemes for user attribute image headers.
* Currently, only [JPEG] is defined.
*/
enum class ImageEncoding(val id: Int) {
/** JPEG File Interchange Format (JFIF). */
JPEG(1)
;
companion object {
@JvmStatic
fun requireFromId(id: Int): ImageEncoding =
fromId(id) ?: throw NoSuchElementException("No ImageEncoding found for id $id")
@JvmStatic
fun fromId(id: Int): ImageEncoding? = values().firstOrNull { id == it.id }
}
}

View File

@ -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 {

View File

@ -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 {
/**

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <info@pgpainless.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.algorithm
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
class ImageEncodingTest {
@Test
fun parseJpeg() {
assertEquals(ImageEncoding.JPEG, ImageEncoding.requireFromId(1))
}
@Test
fun parseUnknown() {
assertNull(ImageEncoding.fromId(11))
assertThrows<NoSuchElementException> { ImageEncoding.requireFromId(11) }
}
}

View File

@ -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())
}
}

View File

@ -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)

View File

@ -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')
}
}

View File

@ -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();
}
};
}
}

View File

@ -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);
}
}
};
}
}

View File

@ -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();
}
};
}
}

View File

@ -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);
}
};
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}
};
}
}

View File

@ -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();
}
}

View File

@ -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);
}
};
}
};
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
};
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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
}
}

View File

@ -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);
}
}
};
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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.")
}
}

View File

@ -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))
}
}

View File

@ -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()
}
}
}
}
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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 }
}

View File

@ -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()
}
}

View File

@ -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 }
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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) }
}
}

View File

@ -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))
}
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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();

View File

@ -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.
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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'
}
}