Merge pull request #21 from pgpainless/kotlin

Rewrite sop-java in Kotlin
This commit is contained in:
Paul Schaub 2023-10-31 15:59:36 +01:00 committed by GitHub
commit a8829350a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
90 changed files with 2397 additions and 3230 deletions

View file

@ -19,6 +19,8 @@ buildscript {
plugins {
id 'ru.vyarus.animalsniffer' version '1.5.3'
id 'org.jetbrains.kotlin.jvm' version "1.8.10"
id 'com.diffplug.spotless' version '6.22.0' apply false
}
apply from: 'version.gradle'
@ -29,6 +31,8 @@ allprojects {
apply plugin: 'eclipse'
apply plugin: 'jacoco'
apply plugin: 'checkstyle'
apply plugin: 'kotlin'
apply plugin: 'com.diffplug.spotless'
// For non-cli modules enable android api compatibility check
if (it.name.equals('sop-java')) {
@ -53,6 +57,12 @@ allprojects {
toolVersion = '8.18'
}
spotless {
kotlin {
ktfmt().dropboxStyle()
}
}
group 'org.pgpainless'
description = "Stateless OpenPGP Protocol API for Java"
version = shortVersion
@ -69,6 +79,13 @@ allprojects {
reproducibleFileOrder = true
}
// Compatibility of default implementations in kotlin interfaces with Java implementations.
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += ["-Xjvm-default=all-compatibility"]
}
}
project.ext {
rootConfigDir = new File(rootDir, 'config')
gitCommit = getGitCommit()

View file

@ -38,6 +38,7 @@ application {
jar {
dependsOn(":sop-java:jar")
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
manifest {
attributes 'Main-Class': "$mainClassName"

View file

@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* Tuple of a byte array and associated result object.
* @param <T> type of result
*/
public class ByteArrayAndResult<T> {
private final byte[] bytes;
private final T result;
public ByteArrayAndResult(byte[] bytes, T result) {
this.bytes = bytes;
this.result = result;
}
/**
* Return the byte array part.
*
* @return bytes
*/
public byte[] getBytes() {
return bytes;
}
/**
* Return the result part.
*
* @return result
*/
public T getResult() {
return result;
}
/**
* Return the byte array part as an {@link InputStream}.
*
* @return input stream
*/
public InputStream getInputStream() {
return new ByteArrayInputStream(getBytes());
}
}

View file

@ -1,29 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import sop.util.Optional;
public class DecryptionResult {
private final Optional<SessionKey> sessionKey;
private final List<Verification> verifications;
public DecryptionResult(SessionKey sessionKey, List<Verification> verifications) {
this.sessionKey = Optional.ofNullable(sessionKey);
this.verifications = Collections.unmodifiableList(verifications);
}
public Optional<SessionKey> getSessionKey() {
return sessionKey;
}
public List<Verification> getVerifications() {
return new ArrayList<>(verifications);
}
}

View file

@ -1,55 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
import java.io.OutputStream;
import java.io.PrintWriter;
public class MicAlg {
private final String micAlg;
public MicAlg(String micAlg) {
if (micAlg == null) {
throw new IllegalArgumentException("MicAlg String cannot be null.");
}
this.micAlg = micAlg;
}
public static MicAlg empty() {
return new MicAlg("");
}
public static MicAlg fromHashAlgorithmId(int id) {
switch (id) {
case 1:
return new MicAlg("pgp-md5");
case 2:
return new MicAlg("pgp-sha1");
case 3:
return new MicAlg("pgp-ripemd160");
case 8:
return new MicAlg("pgp-sha256");
case 9:
return new MicAlg("pgp-sha384");
case 10:
return new MicAlg("pgp-sha512");
case 11:
return new MicAlg("pgp-sha224");
default:
throw new IllegalArgumentException("Unsupported hash algorithm ID: " + id);
}
}
public String getMicAlg() {
return micAlg;
}
public void writeTo(OutputStream outputStream) {
PrintWriter pw = new PrintWriter(outputStream);
pw.write(getMicAlg());
pw.close();
}
}

View file

@ -1,143 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
import sop.util.Optional;
import sop.util.UTF8Util;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Tuple class bundling a profile name and description.
*
* @see <a href="https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html#name-profile">
* SOP Spec - Profile</a>
*/
public class Profile {
private final String name;
private final Optional<String> description;
/**
* Create a new {@link Profile} object.
* The {@link #toString()} representation MUST NOT exceed a length of 1000 bytes.
*
* @param name profile name
* @param description profile description
*/
public Profile(@Nonnull String name, @Nullable String description) {
if (name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty.");
}
if (name.contains(":")) {
throw new IllegalArgumentException("Name cannot contain ':'.");
}
if (name.contains(" ") || name.contains("\n") || name.contains("\t") || name.contains("\r")) {
throw new IllegalArgumentException("Name cannot contain whitespace characters.");
}
this.name = name;
if (description == null) {
this.description = Optional.ofEmpty();
} else {
String trimmedDescription = description.trim();
if (trimmedDescription.isEmpty()) {
this.description = Optional.ofEmpty();
} else {
this.description = Optional.of(trimmedDescription);
}
}
if (exceeds1000CharLineLimit(this)) {
throw new IllegalArgumentException("The line representation of a profile MUST NOT exceed 1000 bytes.");
}
}
public Profile(String name) {
this(name, null);
}
/**
* Parse a {@link Profile} from its string representation.
*
* @param string string representation
* @return profile
*/
public static Profile parse(String string) {
if (string.contains(": ")) {
// description after colon, e.g. "default: Use implementers recommendations."
String name = string.substring(0, string.indexOf(": "));
String description = string.substring(string.indexOf(": ") + 2);
return new Profile(name, description.trim());
}
if (string.endsWith(":")) {
// empty description, e.g. "default:"
return new Profile(string.substring(0, string.length() - 1));
}
// no description
return new Profile(string.trim());
}
/**
* Return the name (also known as identifier) of the profile.
* A profile name is a UTF-8 string that has no whitespace in it.
* Similar to OpenPGP Notation names, profile names are divided into two namespaces:
* The IETF namespace and the user namespace.
* A profile name in the user namespace ends with the <pre>@</pre> character (0x40) followed by a DNS domain name.
* A profile name in the IETF namespace does not have an <pre>@</pre> character.
* A profile name in the user space is owned and controlled by the owner of the domain in the suffix.
* A profile name in the IETF namespace that begins with the string <pre>rfc</pre> should have semantics that hew as
* closely as possible to the referenced RFC.
* Similarly, a profile name in the IETF namespace that begins with the string <pre>draft-</pre> should have
* semantics that hew as closely as possible to the referenced Internet Draft.
*
* @return name
*/
@Nonnull
public String getName() {
return name;
}
/**
* Return a free-form description of the profile.
*
* @return description
*/
@Nonnull
public Optional<String> getDescription() {
return description;
}
public boolean hasDescription() {
return description.isPresent();
}
/**
* Convert the profile into a String for displaying.
*
* @return string
*/
@Override
public String toString() {
if (getDescription().isEmpty()) {
return getName();
}
return getName() + ": " + getDescription().get();
}
/**
* Test if the string representation of the profile exceeds the limit of 1000 bytes length.
* @param profile profile
* @return <pre>true</pre> if the profile exceeds 1000 bytes, <pre>false</pre> otherwise.
*/
private static boolean exceeds1000CharLineLimit(Profile profile) {
String line = profile.toString();
return line.getBytes(UTF8Util.UTF8).length > 1000;
}
}

View file

@ -1,45 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public abstract class Ready {
/**
* Write the data to the provided output stream.
*
* @param outputStream output stream
* @throws IOException in case of an IO error
*/
public abstract void writeTo(OutputStream outputStream) throws IOException;
/**
* Return the data as a byte array by writing it to a {@link ByteArrayOutputStream} first and then returning
* the array.
*
* @return data as byte array
* @throws IOException in case of an IO error
*/
public byte[] getBytes() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
writeTo(bytes);
return bytes.toByteArray();
}
/**
* Return an input stream containing the data.
*
* @return input stream
* @throws IOException in case of an IO error
*/
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(getBytes());
}
}

View file

@ -1,41 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import sop.exception.SOPGPException;
public abstract class ReadyWithResult<T> {
/**
* Write the data e.g. decrypted plaintext to the provided output stream and return the result of the
* processing operation.
*
* @param outputStream output stream
* @return result, eg. signatures
*
* @throws IOException in case of an IO error
* @throws SOPGPException.NoSignature if there are no valid signatures found
*/
public abstract T writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature;
/**
* Return the data as a {@link ByteArrayAndResult}.
* Calling {@link ByteArrayAndResult#getBytes()} will give you access to the data as byte array, while
* {@link ByteArrayAndResult#getResult()} will grant access to the appended result.
*
* @return byte array and result
* @throws IOException in case of an IO error
* @throws SOPGPException.NoSignature if there are no valid signatures found
*/
public ByteArrayAndResult<T> toByteArrayAndResult() throws IOException, SOPGPException.NoSignature {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
T result = writeTo(bytes);
return new ByteArrayAndResult<>(bytes.toByteArray(), result);
}
}

View file

@ -1,177 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
import sop.operation.Armor;
import sop.operation.ChangeKeyPassword;
import sop.operation.Dearmor;
import sop.operation.Decrypt;
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.DetachedSign;
import sop.operation.DetachedVerify;
import sop.operation.ListProfiles;
import sop.operation.RevokeKey;
import sop.operation.Version;
/**
* Stateless OpenPGP Interface.
* This class provides a stateless interface to various OpenPGP related operations.
* <br>
* Note: Subcommand objects acquired by calling any method of this interface are not intended for reuse.
* If you for example need to generate multiple keys, make a dedicated call to {@link #generateKey()} once per
* key generation.
*/
public interface SOP {
/**
* Get information about the implementations name and version.
*
* @return version
*/
Version version();
/**
* Generate a secret key.
* Customize the operation using the builder {@link GenerateKey}.
*
* @return builder instance
*/
GenerateKey generateKey();
/**
* Extract a certificate (public key) from a secret key.
* Customize the operation using the builder {@link ExtractCert}.
*
* @return builder instance
*/
ExtractCert extractCert();
/**
* Create detached signatures.
* Customize the operation using the builder {@link DetachedSign}.
* <p>
* If you want to sign a message inline, use {@link #inlineSign()} instead.
*
* @return builder instance
*/
default DetachedSign sign() {
return detachedSign();
}
/**
* Create detached signatures.
* Customize the operation using the builder {@link DetachedSign}.
* <p>
* If you want to sign a message inline, use {@link #inlineSign()} instead.
*
* @return builder instance
*/
DetachedSign detachedSign();
/**
* Sign a message using inline signatures.
* <p>
* If you need to create detached signatures, use {@link #detachedSign()} instead.
*
* @return builder instance
*/
InlineSign inlineSign();
/**
* Verify detached signatures.
* Customize the operation using the builder {@link DetachedVerify}.
* <p>
* If you need to verify an inline-signed message, use {@link #inlineVerify()} instead.
*
* @return builder instance
*/
default DetachedVerify verify() {
return detachedVerify();
}
/**
* Verify detached signatures.
* Customize the operation using the builder {@link DetachedVerify}.
* <p>
* If you need to verify an inline-signed message, use {@link #inlineVerify()} instead.
*
* @return builder instance
*/
DetachedVerify detachedVerify();
/**
* Verify signatures of an inline-signed message.
* <p>
* If you need to verify detached signatures over a message, use {@link #detachedVerify()} instead.
*
* @return builder instance
*/
InlineVerify inlineVerify();
/**
* Detach signatures from an inline signed message.
*
* @return builder instance
*/
InlineDetach inlineDetach();
/**
* Encrypt a message.
* Customize the operation using the builder {@link Encrypt}.
*
* @return builder instance
*/
Encrypt encrypt();
/**
* Decrypt a message.
* Customize the operation using the builder {@link Decrypt}.
*
* @return builder instance
*/
Decrypt decrypt();
/**
* Convert binary OpenPGP data to ASCII.
* Customize the operation using the builder {@link Armor}.
*
* @return builder instance
*/
Armor armor();
/**
* Converts ASCII armored OpenPGP data to binary.
* Customize the operation using the builder {@link Dearmor}.
*
* @return builder instance
*/
Dearmor dearmor();
/**
* List supported {@link Profile Profiles} of a subcommand.
*
* @return builder instance
*/
ListProfiles listProfiles();
/**
* Revoke one or more secret keys.
*
* @return builder instance
*/
RevokeKey revokeKey();
/**
* Update a key's password.
*
* @return builder instance
*/
ChangeKeyPassword changeKeyPassword();
}

View file

@ -1,80 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import sop.util.HexUtil;
public class SessionKey {
private static final Pattern PATTERN = Pattern.compile("^(\\d):([0-9A-F]+)$");
private final byte algorithm;
private final byte[] sessionKey;
public SessionKey(byte algorithm, byte[] sessionKey) {
this.algorithm = algorithm;
this.sessionKey = sessionKey;
}
/**
* Return the symmetric algorithm octet.
*
* @return algorithm id
*/
public byte getAlgorithm() {
return algorithm;
}
/**
* Return the session key.
*
* @return session key
*/
public byte[] getKey() {
return sessionKey;
}
@Override
public int hashCode() {
return getAlgorithm() * 17 + Arrays.hashCode(getKey());
}
@Override
public boolean equals(Object other) {
if (other == null) {
return false;
}
if (this == other) {
return true;
}
if (!(other instanceof SessionKey)) {
return false;
}
SessionKey otherKey = (SessionKey) other;
return getAlgorithm() == otherKey.getAlgorithm() && Arrays.equals(getKey(), otherKey.getKey());
}
public static SessionKey fromString(String string) {
string = string.trim().toUpperCase().replace("\n", "");
Matcher matcher = PATTERN.matcher(string);
if (!matcher.matches()) {
throw new IllegalArgumentException("Provided session key does not match expected format.");
}
byte algorithm = Byte.parseByte(matcher.group(1));
String key = matcher.group(2);
return new SessionKey(algorithm, HexUtil.hexToBytes(key));
}
@Override
public String toString() {
return Integer.toString(getAlgorithm()) + ':' + HexUtil.bytesToHex(sessionKey);
}
}

View file

@ -1,21 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
import java.io.IOException;
import java.io.OutputStream;
public abstract class Signatures extends Ready {
/**
* Write OpenPGP signatures to the provided output stream.
*
* @param signatureOutputStream output stream
* @throws IOException in case of an IO error
*/
@Override
public abstract void writeTo(OutputStream signatureOutputStream) throws IOException;
}

View file

@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
/**
* This class contains various information about a signed message.
*/
public final class SigningResult {
private final MicAlg micAlg;
private SigningResult(MicAlg micAlg) {
this.micAlg = micAlg;
}
/**
* Return a string identifying the digest mechanism used to create the signed message.
* This is useful for setting the micalg= parameter for the multipart/signed
* content type of a PGP/MIME object as described in section 5 of [RFC3156].
* <p>
* If more than one signature was generated and different digest mechanisms were used,
* the value of the micalg object is an empty string.
*
* @return micalg
*/
public MicAlg getMicAlg() {
return micAlg;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private MicAlg micAlg;
public Builder setMicAlg(MicAlg micAlg) {
this.micAlg = micAlg;
return this;
}
public SigningResult build() {
SigningResult signingResult = new SigningResult(micAlg);
return signingResult;
}
}
}

View file

@ -1,222 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
import sop.enums.SignatureMode;
import sop.util.Optional;
import sop.util.UTCUtil;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.text.ParseException;
import java.util.Date;
/**
* Class bundling information about a verified signature.
*/
public class Verification {
private final Date creationTime;
private final String signingKeyFingerprint;
private final String signingCertFingerprint;
private final Optional<SignatureMode> signatureMode;
private final Optional<String> description;
private static final String MODE = "mode:";
/**
* Create a new {@link Verification} without mode and description.
*
* @param creationTime signature creation time
* @param signingKeyFingerprint fingerprint of the signing (sub-) key
* @param signingCertFingerprint fingerprint of the certificate
*/
public Verification(@Nonnull Date creationTime,
@Nonnull String signingKeyFingerprint,
@Nonnull String signingCertFingerprint) {
this(creationTime, signingKeyFingerprint, signingCertFingerprint, Optional.ofEmpty(), Optional.ofEmpty());
}
/**
* Create a new {@link Verification}.
*
* @param creationTime signature creation time
* @param signingKeyFingerprint fingerprint of the signing (sub-) key
* @param signingCertFingerprint fingerprint of the certificate
* @param signatureMode signature mode (optional, may be <pre>null</pre>)
* @param description free-form description, e.g. <pre>certificate from dkg.asc</pre> (optional, may be <pre>null</pre>)
*/
public Verification(@Nonnull Date creationTime,
@Nonnull String signingKeyFingerprint,
@Nonnull String signingCertFingerprint,
@Nullable SignatureMode signatureMode,
@Nullable String description) {
this(
creationTime,
signingKeyFingerprint,
signingCertFingerprint,
Optional.ofNullable(signatureMode),
Optional.ofNullable(nullSafeTrim(description))
);
}
private Verification(@Nonnull Date creationTime,
@Nonnull String signingKeyFingerprint,
@Nonnull String signingCertFingerprint,
@Nonnull Optional<SignatureMode> signatureMode,
@Nonnull Optional<String> description) {
this.creationTime = creationTime;
this.signingKeyFingerprint = signingKeyFingerprint;
this.signingCertFingerprint = signingCertFingerprint;
this.signatureMode = signatureMode;
this.description = description;
}
private static String nullSafeTrim(@Nullable String string) {
if (string == null) {
return null;
}
return string.trim();
}
@Nonnull
public static Verification fromString(@Nonnull String toString) {
String[] split = toString.trim().split(" ");
if (split.length < 3) {
throw new IllegalArgumentException("Verification must be of the format 'UTC-DATE OpenPGPFingerprint OpenPGPFingerprint [mode] [info]'");
}
if (split.length == 3) {
return new Verification(
parseUTCDate(split[0]), // timestamp
split[1], // key FP
split[2] // cert FP
);
}
SignatureMode mode = null;
int index = 3;
if (split[index].startsWith(MODE)) {
mode = SignatureMode.valueOf(split[3].substring(MODE.length()));
index++;
}
StringBuilder sb = new StringBuilder();
for (int i = index; i < split.length; i++) {
if (sb.length() != 0) {
sb.append(' ');
}
sb.append(split[i]);
}
return new Verification(
parseUTCDate(split[0]), // timestamp
split[1], // key FP
split[2], // cert FP
mode, // signature mode
sb.length() != 0 ? sb.toString() : null // description
);
}
private static Date parseUTCDate(String utcFormatted) {
try {
return UTCUtil.parseUTCDate(utcFormatted);
} catch (ParseException e) {
throw new IllegalArgumentException("Malformed UTC timestamp.", e);
}
}
/**
* Return the signatures' creation time.
*
* @return signature creation time
*/
@Nonnull
public Date getCreationTime() {
return creationTime;
}
/**
* Return the fingerprint of the signing (sub)key.
*
* @return signing key fingerprint
*/
@Nonnull
public String getSigningKeyFingerprint() {
return signingKeyFingerprint;
}
/**
* Return the fingerprint fo the signing certificate.
*
* @return signing certificate fingerprint
*/
@Nonnull
public String getSigningCertFingerprint() {
return signingCertFingerprint;
}
/**
* Return the mode of the signature.
* Optional, may return <pre>null</pre>.
*
* @return signature mode
*/
@Nonnull
public Optional<SignatureMode> getSignatureMode() {
return signatureMode;
}
/**
* Return an optional description.
* Optional, may return <pre>null</pre>.
*
* @return description
*/
@Nonnull
public Optional<String> getDescription() {
return description;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder()
.append(UTCUtil.formatUTCDate(getCreationTime()))
.append(' ')
.append(getSigningKeyFingerprint())
.append(' ')
.append(getSigningCertFingerprint());
if (signatureMode.isPresent()) {
sb.append(' ').append(MODE).append(signatureMode.get());
}
if (description.isPresent()) {
sb.append(' ').append(description.get());
}
return sb.toString();
}
@Override
public int hashCode() {
return toString().hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof Verification)) {
return false;
}
Verification other = (Verification) obj;
return toString().equals(other.toString());
}
}

View file

@ -1,19 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.enums;
public enum ArmorLabel {
Auto,
Sig,
Key,
Cert,
Message,
;
@Override
public String toString() {
return super.toString().toLowerCase();
}
}

View file

@ -1,16 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.enums;
public enum EncryptAs {
Binary,
Text,
;
@Override
public String toString() {
return super.toString().toLowerCase();
}
}

View file

@ -1,24 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.enums;
public enum InlineSignAs {
/**
* Signature is made over the binary message.
*/
binary,
/**
* Signature is made over the message in text mode.
*/
text,
/**
* Signature is made using the Cleartext Signature Framework.
*/
clearsigned,
}

View file

@ -1,23 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.enums;
public enum SignAs {
/**
* Signature is made over the binary message.
*/
Binary,
/**
* Signature is made over the message in text mode.
*/
Text,
;
@Override
public String toString() {
return super.toString().toLowerCase();
}
}

View file

@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.enums;
/**
* Enum referencing relevant signature types.
*
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-5.2.1">
* RFC4880 §5.2.1 - Signature Types</a>
*/
public enum SignatureMode {
/**
* Signature of a binary document (<pre>0x00</pre>).
*/
binary,
/**
* Signature of a canonical text document (<pre>0x01</pre>).
*/
text
// Other Signature Types are irrelevant.
}

View file

@ -1,9 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Stateless OpenPGP Interface for Java.
* Enumerations.
*/
package sop.enums;

View file

@ -1,473 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.exception;
public abstract class SOPGPException extends RuntimeException {
public SOPGPException() {
super();
}
public SOPGPException(String message) {
super(message);
}
public SOPGPException(Throwable e) {
super(e);
}
public SOPGPException(String message, Throwable cause) {
super(message, cause);
}
public abstract int getExitCode();
/**
* No acceptable signatures found (sop verify, inline-verify).
*/
public static class NoSignature extends SOPGPException {
public static final int EXIT_CODE = 3;
public NoSignature() {
this("No verifiable signature found.");
}
public NoSignature(String message) {
super(message);
}
public NoSignature(String errorMsg, NoSignature e) {
super(errorMsg, e);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* Asymmetric algorithm unsupported (sop encrypt, sign, inline-sign).
*/
public static class UnsupportedAsymmetricAlgo extends SOPGPException {
public static final int EXIT_CODE = 13;
public UnsupportedAsymmetricAlgo(String message, Throwable e) {
super(message, e);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* Certificate not encryption capable (e,g, expired, revoked, unacceptable usage).
*/
public static class CertCannotEncrypt extends SOPGPException {
public static final int EXIT_CODE = 17;
public CertCannotEncrypt(String message, Throwable cause) {
super(message, cause);
}
public CertCannotEncrypt(String message) {
super(message);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* Missing required argument.
*/
public static class MissingArg extends SOPGPException {
public static final int EXIT_CODE = 19;
public MissingArg() {
}
public MissingArg(String message) {
super(message);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* Incomplete verification instructions (sop decrypt).
*/
public static class IncompleteVerification extends SOPGPException {
public static final int EXIT_CODE = 23;
public IncompleteVerification(String message) {
super(message);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* Unable to decrypt (sop decrypt).
*/
public static class CannotDecrypt extends SOPGPException {
public static final int EXIT_CODE = 29;
public CannotDecrypt() {
}
public CannotDecrypt(String errorMsg, Throwable e) {
super(errorMsg, e);
}
public CannotDecrypt(String message) {
super(message);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* Non-UTF-8 or otherwise unreliable password (sop encrypt).
*/
public static class PasswordNotHumanReadable extends SOPGPException {
public static final int EXIT_CODE = 31;
public PasswordNotHumanReadable() {
super();
}
public PasswordNotHumanReadable(String message) {
super(message);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* Unsupported option.
*/
public static class UnsupportedOption extends SOPGPException {
public static final int EXIT_CODE = 37;
public UnsupportedOption(String message) {
super(message);
}
public UnsupportedOption(String message, Throwable cause) {
super(message, cause);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* Invalid data type (no secret key where KEYS expected, etc.).
*/
public static class BadData extends SOPGPException {
public static final int EXIT_CODE = 41;
public BadData(String message) {
super(message);
}
public BadData(Throwable throwable) {
super(throwable);
}
public BadData(String message, Throwable throwable) {
super(message, throwable);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* Non-Text input where text expected.
*/
public static class ExpectedText extends SOPGPException {
public static final int EXIT_CODE = 53;
public ExpectedText() {
super();
}
public ExpectedText(String message) {
super(message);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* Output file already exists.
*/
public static class OutputExists extends SOPGPException {
public static final int EXIT_CODE = 59;
public OutputExists(String message) {
super(message);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* Input file does not exist.
*/
public static class MissingInput extends SOPGPException {
public static final int EXIT_CODE = 61;
public MissingInput(String message, Throwable cause) {
super(message, cause);
}
public MissingInput(String errorMsg) {
super(errorMsg);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* A KEYS input is protected (locked) with a password and sop failed to unlock it.
*/
public static class KeyIsProtected extends SOPGPException {
public static final int EXIT_CODE = 67;
public KeyIsProtected() {
super();
}
public KeyIsProtected(String message) {
super(message);
}
public KeyIsProtected(String message, Throwable cause) {
super(message, cause);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* Unsupported subcommand.
*/
public static class UnsupportedSubcommand extends SOPGPException {
public static final int EXIT_CODE = 69;
public UnsupportedSubcommand(String message) {
super(message);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* An indirect parameter is a special designator (it starts with @), but sop does not know how to handle the prefix.
*/
public static class UnsupportedSpecialPrefix extends SOPGPException {
public static final int EXIT_CODE = 71;
public UnsupportedSpecialPrefix(String errorMsg) {
super(errorMsg);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* Exception that gets thrown if a special designator (starting with @) is given, but the filesystem contains
* a file matching the designator.
* <p>
* E.g. <pre>@ENV:FOO</pre> is given, but <pre>./@ENV:FOO</pre> exists on the filesystem.
*/
public static class AmbiguousInput extends SOPGPException {
public static final int EXIT_CODE = 73;
public AmbiguousInput(String message) {
super(message);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* Key not signature-capable (e.g. expired, revoked, unacceptable usage flags).
*/
public static class KeyCannotSign extends SOPGPException {
public static final int EXIT_CODE = 79;
public KeyCannotSign() {
super();
}
public KeyCannotSign(String message) {
super(message);
}
public KeyCannotSign(String s, Throwable throwable) {
super(s, throwable);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* User provided incompatible options (e.g. "--as=clearsigned --no-armor").
*/
public static class IncompatibleOptions extends SOPGPException {
public static final int EXIT_CODE = 83;
public IncompatibleOptions() {
super();
}
public IncompatibleOptions(String errorMsg) {
super(errorMsg);
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
/**
* The user provided a subcommand with an unsupported profile ("--profile=XYZ"),
* or the user tried to list profiles of a subcommand that does not support profiles at all.
*/
public static class UnsupportedProfile extends SOPGPException {
public static final int EXIT_CODE = 89;
private final String subcommand;
private final String profile;
/**
* Create an exception signalling a subcommand that does not support any profiles.
*
* @param subcommand subcommand
*/
public UnsupportedProfile(String subcommand) {
super("Subcommand '" + subcommand + "' does not support any profiles.");
this.subcommand = subcommand;
this.profile = null;
}
/**
* Create an exception signalling a subcommand does not support a specific profile.
*
* @param subcommand subcommand
* @param profile unsupported profile
*/
public UnsupportedProfile(String subcommand, String profile) {
super("Subcommand '" + subcommand + "' does not support profile '" + profile + "'.");
this.subcommand = subcommand;
this.profile = profile;
}
/**
* Wrap an exception into another instance with a possibly translated error message.
*
* @param errorMsg error message
* @param e exception
*/
public UnsupportedProfile(String errorMsg, UnsupportedProfile e) {
super(errorMsg, e);
this.subcommand = e.getSubcommand();
this.profile = e.getProfile();
}
/**
* Return the subcommand name.
*
* @return subcommand
*/
public String getSubcommand() {
return subcommand;
}
/**
* Return the profile name.
* May return <pre>null</pre>.
*
* @return profile name
*/
public String getProfile() {
return profile;
}
@Override
public int getExitCode() {
return EXIT_CODE;
}
}
}

View file

@ -1,9 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Stateless OpenPGP Interface for Java.
* Exception classes.
*/
package sop.exception;

View file

@ -1,85 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import sop.exception.SOPGPException;
import sop.util.UTF8Util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public interface AbstractSign<T> {
/**
* Disable ASCII armor encoding.
*
* @return builder instance
*/
T noArmor();
/**
* Add one or more signing keys.
*
* @param key input stream containing encoded keys
* @return builder instance
*
* @throws sop.exception.SOPGPException.KeyCannotSign if the key cannot be used for signing
* @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not contain an OpenPGP key
* @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm
* @throws IOException in case of an IO error
*/
T key(InputStream key)
throws SOPGPException.KeyCannotSign,
SOPGPException.BadData,
SOPGPException.UnsupportedAsymmetricAlgo,
IOException;
/**
* Add one or more signing keys.
*
* @param key byte array containing encoded keys
* @return builder instance
*
* @throws sop.exception.SOPGPException.KeyCannotSign if the key cannot be used for signing
* @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP key
* @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm
* @throws IOException in case of an IO error
*/
default T key(byte[] key)
throws SOPGPException.KeyCannotSign,
SOPGPException.BadData,
SOPGPException.UnsupportedAsymmetricAlgo,
IOException {
return key(new ByteArrayInputStream(key));
}
/**
* Provide the password for the secret key used for signing.
*
* @param password password
* @return builder instance
* @throws sop.exception.SOPGPException.UnsupportedOption if key passwords are not supported
* @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the provided passphrase is not human-readable
*/
default T withKeyPassword(String password)
throws SOPGPException.UnsupportedOption,
SOPGPException.PasswordNotHumanReadable {
return withKeyPassword(password.getBytes(UTF8Util.UTF8));
}
/**
* Provide the password for the secret key used for signing.
*
* @param password password
* @return builder instance
* @throws sop.exception.SOPGPException.UnsupportedOption if key passwords are not supported
* @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the provided passphrase is not human-readable
*/
T withKeyPassword(byte[] password)
throws SOPGPException.UnsupportedOption,
SOPGPException.PasswordNotHumanReadable;
}

View file

@ -1,68 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import sop.exception.SOPGPException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
/**
* Common API methods shared between verification of inline signatures ({@link InlineVerify})
* and verification of detached signatures ({@link DetachedVerify}).
*
* @param <T> Builder type ({@link DetachedVerify}, {@link InlineVerify})
*/
public interface AbstractVerify<T> {
/**
* Makes the SOP implementation consider signatures before this date invalid.
*
* @param timestamp timestamp
* @return builder instance
*/
T notBefore(Date timestamp)
throws SOPGPException.UnsupportedOption;
/**
* Makes the SOP implementation consider signatures after this date invalid.
*
* @param timestamp timestamp
* @return builder instance
*/
T notAfter(Date timestamp)
throws SOPGPException.UnsupportedOption;
/**
* Add one or more verification cert.
*
* @param cert input stream containing the encoded certs
* @return builder instance
*
* @throws sop.exception.SOPGPException.BadData if the input stream does not contain an OpenPGP certificate
* @throws IOException in case of an IO error
*/
T cert(InputStream cert)
throws SOPGPException.BadData,
IOException;
/**
* Add one or more verification cert.
*
* @param cert byte array containing the encoded certs
* @return builder instance
*
* @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP certificate
* @throws IOException in case of an IO error
*/
default T cert(byte[] cert)
throws SOPGPException.BadData,
IOException {
return cert(new ByteArrayInputStream(cert));
}
}

View file

@ -1,53 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import sop.Ready;
import sop.enums.ArmorLabel;
import sop.exception.SOPGPException;
public interface Armor {
/**
* Overrides automatic detection of label.
*
* @param label armor label
* @return builder instance
*/
Armor label(ArmorLabel label)
throws SOPGPException.UnsupportedOption;
/**
* Armor the provided data.
*
* @param data input stream of unarmored OpenPGP data
* @return armored data
*
* @throws sop.exception.SOPGPException.BadData if the data appears to be OpenPGP packets, but those are broken
* @throws IOException in case of an IO error
*/
Ready data(InputStream data)
throws SOPGPException.BadData,
IOException;
/**
* Armor the provided data.
*
* @param data unarmored OpenPGP data
* @return armored data
*
* @throws sop.exception.SOPGPException.BadData if the data appears to be OpenPGP packets, but those are broken
* @throws IOException in case of an IO error
*/
default Ready data(byte[] data)
throws SOPGPException.BadData,
IOException {
return data(new ByteArrayInputStream(data));
}
}

View file

@ -1,83 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.util.UTF8Util;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.CharacterCodingException;
public interface ChangeKeyPassword {
/**
* Disable ASCII armoring of the output.
*
* @return builder instance
*/
ChangeKeyPassword noArmor();
default ChangeKeyPassword oldKeyPassphrase(byte[] password) {
try {
return oldKeyPassphrase(UTF8Util.decodeUTF8(password));
} catch (CharacterCodingException e) {
throw new SOPGPException.PasswordNotHumanReadable("Password MUST be a valid UTF8 string.");
}
}
/**
* Provide a passphrase to unlock the secret key.
* This method can be provided multiple times to provide separate passphrases that are tried as a
* means to unlock any secret key material encountered.
*
* @param oldPassphrase old passphrase
* @return builder instance
*/
ChangeKeyPassword oldKeyPassphrase(String oldPassphrase);
/**
* Provide a passphrase to re-lock the secret key with.
* This method can only be used once, and all key material encountered will be encrypted with the given passphrase.
* If this method is not called, the key material will not be protected.
*
* @param newPassphrase new passphrase
* @return builder instance
*/
default ChangeKeyPassword newKeyPassphrase(byte[] newPassphrase) {
try {
return newKeyPassphrase(UTF8Util.decodeUTF8(newPassphrase));
} catch (CharacterCodingException e) {
throw new SOPGPException.PasswordNotHumanReadable("Password MUST be a valid UTF8 string.");
}
}
/**
* Provide a passphrase to re-lock the secret key with.
* This method can only be used once, and all key material encountered will be encrypted with the given passphrase.
* If this method is not called, the key material will not be protected.
*
* @param newPassphrase new passphrase
* @return builder instance
*/
ChangeKeyPassword newKeyPassphrase(String newPassphrase);
default Ready keys(byte[] keys) throws SOPGPException.KeyIsProtected, SOPGPException.BadData {
return keys(new ByteArrayInputStream(keys));
}
/**
* Provide the key material.
*
* @param inputStream input stream of secret key material
* @return ready
*
* @throws sop.exception.SOPGPException.KeyIsProtected if any (sub-) key encountered cannot be unlocked.
* @throws sop.exception.SOPGPException.BadData if the key material is malformed
*/
Ready keys(InputStream inputStream) throws SOPGPException.KeyIsProtected, SOPGPException.BadData;
}

View file

@ -1,59 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.util.UTF8Util;
public interface Dearmor {
/**
* Dearmor armored OpenPGP data.
*
* @param data armored OpenPGP data
* @return input stream of unarmored data
*
* @throws SOPGPException.BadData in case of non-OpenPGP data
* @throws IOException in case of an IO error
*/
Ready data(InputStream data)
throws SOPGPException.BadData,
IOException;
/**
* Dearmor armored OpenPGP data.
*
* @param data armored OpenPGP data
* @return input stream of unarmored data
*
* @throws SOPGPException.BadData in case of non-OpenPGP data
* @throws IOException in case of an IO error
*/
default Ready data(byte[] data)
throws SOPGPException.BadData,
IOException {
return data(new ByteArrayInputStream(data));
}
/**
* Dearmor amored OpenPGP data.
*
* @param data armored OpenPGP data
* @return input stream of unarmored data
*
* @throws SOPGPException.BadData in case of non-OpenPGP data
* @throws IOException in case of an IO error
*/
default Ready data(String data)
throws SOPGPException.BadData,
IOException {
return data(data.getBytes(UTF8Util.UTF8));
}
}

View file

@ -1,193 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import sop.DecryptionResult;
import sop.ReadyWithResult;
import sop.SessionKey;
import sop.exception.SOPGPException;
import sop.util.UTF8Util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
public interface Decrypt {
/**
* Makes the SOP consider signatures before this date invalid.
*
* @param timestamp timestamp
* @return builder instance
*
* @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported
*/
Decrypt verifyNotBefore(Date timestamp)
throws SOPGPException.UnsupportedOption;
/**
* Makes the SOP consider signatures after this date invalid.
*
* @param timestamp timestamp
* @return builder instance
*
* @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported
*/
Decrypt verifyNotAfter(Date timestamp)
throws SOPGPException.UnsupportedOption;
/**
* Adds one or more verification cert.
*
* @param cert input stream containing the cert(s)
* @return builder instance
*
* @throws sop.exception.SOPGPException.BadData if the {@link InputStream} doesn't provide an OpenPGP certificate
* @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the cert uses an unsupported asymmetric algorithm
* @throws IOException in case of an IO error
*/
Decrypt verifyWithCert(InputStream cert)
throws SOPGPException.BadData,
SOPGPException.UnsupportedAsymmetricAlgo,
IOException;
/**
* Adds one or more verification cert.
*
* @param cert byte array containing the cert(s)
* @return builder instance
*
* @throws sop.exception.SOPGPException.BadData if the byte array doesn't contain an OpenPGP certificate
* @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the cert uses an unsupported asymmetric algorithm
* @throws IOException in case of an IO error
*/
default Decrypt verifyWithCert(byte[] cert)
throws SOPGPException.BadData,
SOPGPException.UnsupportedAsymmetricAlgo,
IOException {
return verifyWithCert(new ByteArrayInputStream(cert));
}
/**
* Tries to decrypt with the given session key.
*
* @param sessionKey session key
* @return builder instance
*
* @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported
*/
Decrypt withSessionKey(SessionKey sessionKey)
throws SOPGPException.UnsupportedOption;
/**
* Tries to decrypt with the given password.
*
* @param password password
* @return builder instance
*
* @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable
* @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported
*/
Decrypt withPassword(String password)
throws SOPGPException.PasswordNotHumanReadable,
SOPGPException.UnsupportedOption;
/**
* Adds one or more decryption key.
*
* @param key input stream containing the key(s)
* @return builder instance
*
* @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not provide an OpenPGP key
* @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm
* @throws IOException in case of an IO error
*/
Decrypt withKey(InputStream key)
throws SOPGPException.BadData,
SOPGPException.UnsupportedAsymmetricAlgo,
IOException;
/**
* Adds one or more decryption key.
*
* @param key byte array containing the key(s)
* @return builder instance
*
* @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP key
* @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm
* @throws IOException in case of an IO error
*/
default Decrypt withKey(byte[] key)
throws SOPGPException.BadData,
SOPGPException.UnsupportedAsymmetricAlgo,
IOException {
return withKey(new ByteArrayInputStream(key));
}
/**
* 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
*/
default Decrypt withKeyPassword(String password)
throws SOPGPException.UnsupportedOption,
SOPGPException.PasswordNotHumanReadable {
return withKeyPassword(password.getBytes(UTF8Util.UTF8));
}
/**
* 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
*/
Decrypt withKeyPassword(byte[] password)
throws SOPGPException.UnsupportedOption,
SOPGPException.PasswordNotHumanReadable;
/**
* Decrypts the given ciphertext, returning verification results and plaintext.
* @param ciphertext ciphertext
* @return ready with result
*
* @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not provide an OpenPGP message
* @throws sop.exception.SOPGPException.MissingArg if an argument required for decryption was not provided
* @throws sop.exception.SOPGPException.CannotDecrypt in case decryption fails for some reason
* @throws sop.exception.SOPGPException.KeyIsProtected if the decryption key cannot be unlocked (e.g. missing passphrase)
* @throws IOException in case of an IO error
*/
ReadyWithResult<DecryptionResult> ciphertext(InputStream ciphertext)
throws SOPGPException.BadData,
SOPGPException.MissingArg,
SOPGPException.CannotDecrypt,
SOPGPException.KeyIsProtected,
IOException;
/**
* Decrypts the given ciphertext, returning verification results and plaintext.
* @param ciphertext ciphertext
* @return ready with result
*
* @throws sop.exception.SOPGPException.BadData if the byte array does not contain an encrypted OpenPGP message
* @throws sop.exception.SOPGPException.MissingArg in case of missing decryption method (password or key required)
* @throws sop.exception.SOPGPException.CannotDecrypt in case decryption fails for some reason
* @throws sop.exception.SOPGPException.KeyIsProtected if the decryption key cannot be unlocked (e.g. missing passphrase)
* @throws IOException in case of an IO error
*/
default ReadyWithResult<DecryptionResult> ciphertext(byte[] ciphertext)
throws SOPGPException.BadData,
SOPGPException.MissingArg,
SOPGPException.CannotDecrypt,
SOPGPException.KeyIsProtected,
IOException {
return ciphertext(new ByteArrayInputStream(ciphertext));
}
}

View file

@ -1,61 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import sop.ReadyWithResult;
import sop.SigningResult;
import sop.enums.SignAs;
import sop.exception.SOPGPException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public interface DetachedSign extends AbstractSign<DetachedSign> {
/**
* Sets the signature mode.
* Note: This method has to be called before {@link #key(InputStream)} is called.
*
* @param mode signature mode
* @return builder instance
*
* @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported
*/
DetachedSign mode(SignAs mode)
throws SOPGPException.UnsupportedOption;
/**
* Signs data.
*
* @param data input stream containing data
* @return ready
*
* @throws IOException in case of an IO error
* @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked
* @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered
*/
ReadyWithResult<SigningResult> data(InputStream data)
throws IOException,
SOPGPException.KeyIsProtected,
SOPGPException.ExpectedText;
/**
* Signs data.
*
* @param data byte array containing data
* @return ready
*
* @throws IOException in case of an IO error
* @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked
* @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered
*/
default ReadyWithResult<SigningResult> data(byte[] data)
throws IOException,
SOPGPException.KeyIsProtected,
SOPGPException.ExpectedText {
return data(new ByteArrayInputStream(data));
}
}

View file

@ -1,45 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import sop.exception.SOPGPException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* API for verifying detached signatures.
*/
public interface DetachedVerify extends AbstractVerify<DetachedVerify>, VerifySignatures {
/**
* Provides the detached signatures.
* @param signatures input stream containing encoded, detached signatures.
*
* @return builder instance
*
* @throws sop.exception.SOPGPException.BadData if the input stream does not contain OpenPGP signatures
* @throws IOException in case of an IO error
*/
VerifySignatures signatures(InputStream signatures)
throws SOPGPException.BadData,
IOException;
/**
* Provides the detached signatures.
* @param signatures byte array containing encoded, detached signatures.
*
* @return builder instance
*
* @throws sop.exception.SOPGPException.BadData if the byte array does not contain OpenPGP signatures
* @throws IOException in case of an IO error
*/
default VerifySignatures signatures(byte[] signatures)
throws SOPGPException.BadData,
IOException {
return signatures(new ByteArrayInputStream(signatures));
}
}

View file

@ -1,193 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import sop.Profile;
import sop.Ready;
import sop.enums.EncryptAs;
import sop.exception.SOPGPException;
import sop.util.UTF8Util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public interface Encrypt {
/**
* Disable ASCII armor encoding.
*
* @return builder instance
*/
Encrypt noArmor();
/**
* Sets encryption mode.
*
* @param mode mode
* @return builder instance
*
* @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported
*/
Encrypt mode(EncryptAs mode)
throws SOPGPException.UnsupportedOption;
/**
* Adds the signer key.
*
* @param key input stream containing the encoded signer key
* @return builder instance
*
* @throws sop.exception.SOPGPException.KeyCannotSign if the key cannot be used for signing
* @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm
* @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not contain an OpenPGP key
* @throws IOException in case of an IO error
*/
Encrypt signWith(InputStream key)
throws SOPGPException.KeyCannotSign,
SOPGPException.UnsupportedAsymmetricAlgo,
SOPGPException.BadData,
IOException;
/**
* Adds the signer key.
*
* @param key byte array containing the encoded signer key
* @return builder instance
*
* @throws sop.exception.SOPGPException.KeyCannotSign if the key cannot be used for signing
* @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm
* @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP key
* @throws IOException in case of an IO error
*/
default Encrypt signWith(byte[] key)
throws SOPGPException.KeyCannotSign,
SOPGPException.UnsupportedAsymmetricAlgo,
SOPGPException.BadData,
IOException {
return signWith(new ByteArrayInputStream(key));
}
/**
* Provide the password for the secret key used for signing.
*
* @param password password
* @return builder instance
*
* @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable
* @throws sop.exception.SOPGPException.UnsupportedOption if key password are not supported
*/
default Encrypt withKeyPassword(String password)
throws SOPGPException.PasswordNotHumanReadable,
SOPGPException.UnsupportedOption {
return withKeyPassword(password.getBytes(UTF8Util.UTF8));
}
/**
* Provide the password for the secret key used for signing.
*
* @param password password
* @return builder instance
*
* @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable
* @throws sop.exception.SOPGPException.UnsupportedOption if key password are not supported
*/
Encrypt withKeyPassword(byte[] password)
throws SOPGPException.PasswordNotHumanReadable,
SOPGPException.UnsupportedOption;
/**
* Encrypt with the given password.
*
* @param password password
* @return builder instance
*
* @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable
* @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported
*/
Encrypt withPassword(String password)
throws SOPGPException.PasswordNotHumanReadable,
SOPGPException.UnsupportedOption;
/**
* Encrypt with the given cert.
*
* @param cert input stream containing the encoded cert.
* @return builder instance
*
* @throws sop.exception.SOPGPException.CertCannotEncrypt if the certificate is not encryption capable
* @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the certificate uses an unsupported asymmetric algorithm
* @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not contain an OpenPGP certificate
* @throws IOException in case of an IO error
*/
Encrypt withCert(InputStream cert)
throws SOPGPException.CertCannotEncrypt,
SOPGPException.UnsupportedAsymmetricAlgo,
SOPGPException.BadData,
IOException;
/**
* Encrypt with the given cert.
*
* @param cert byte array containing the encoded cert.
* @return builder instance
*
* @throws sop.exception.SOPGPException.CertCannotEncrypt if the certificate is not encryption capable
* @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the certificate uses an unsupported asymmetric algorithm
* @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP certificate
* @throws IOException in case of an IO error
*/
default Encrypt withCert(byte[] cert)
throws SOPGPException.CertCannotEncrypt,
SOPGPException.UnsupportedAsymmetricAlgo,
SOPGPException.BadData,
IOException {
return withCert(new ByteArrayInputStream(cert));
}
/**
* Pass in a profile.
*
* @param profile profile
* @return builder instance
*/
default Encrypt profile(Profile profile) {
return profile(profile.getName());
}
/**
* Pass in a profile identifier.
*
* @param profileName profile identifier
* @return builder instance
*/
Encrypt profile(String profileName);
/**
* Encrypt the given data yielding the ciphertext.
* @param plaintext plaintext
* @return input stream containing the ciphertext
*
* @throws IOException in case of an IO error
* @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked
*/
Ready plaintext(InputStream plaintext)
throws IOException,
SOPGPException.KeyIsProtected;
/**
* Encrypt the given data yielding the ciphertext.
* @param plaintext plaintext
* @return input stream containing the ciphertext
*
* @throws IOException in case of an IO error
* @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked
*/
default Ready plaintext(byte[] plaintext)
throws IOException,
SOPGPException.KeyIsProtected {
return plaintext(new ByteArrayInputStream(plaintext));
}
}

View file

@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import sop.Ready;
import sop.exception.SOPGPException;
public interface ExtractCert {
/**
* Disable ASCII armor encoding.
*
* @return builder instance
*/
ExtractCert noArmor();
/**
* Extract the cert(s) from the provided key(s).
*
* @param keyInputStream input stream containing the encoding of one or more OpenPGP keys
* @return result containing the encoding of the keys certs
*
* @throws IOException in case of an IO error
* @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not contain an OpenPGP key
*/
Ready key(InputStream keyInputStream)
throws IOException,
SOPGPException.BadData;
/**
* Extract the cert(s) from the provided key(s).
*
* @param key byte array containing the encoding of one or more OpenPGP key
* @return result containing the encoding of the keys certs
*
* @throws IOException in case of an IO error
* @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP key
*/
default Ready key(byte[] key)
throws IOException,
SOPGPException.BadData {
return key(new ByteArrayInputStream(key));
}
}

View file

@ -1,103 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.CharacterCodingException;
import sop.Profile;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.util.UTF8Util;
public interface GenerateKey {
/**
* Disable ASCII armor encoding.
*
* @return builder instance
*/
GenerateKey noArmor();
/**
* Adds a user-id.
*
* @param userId user-id
* @return builder instance
*/
GenerateKey userId(String userId);
/**
* Set a password for the key.
*
* @param password password to protect the key
* @return builder instance
*
* @throws sop.exception.SOPGPException.UnsupportedOption if key passwords are not supported
* @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable
*/
GenerateKey withKeyPassword(String password)
throws SOPGPException.PasswordNotHumanReadable,
SOPGPException.UnsupportedOption;
/**
* Set a password for the key.
*
* @param password password to protect the key
* @return builder instance
*
* @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable
* @throws sop.exception.SOPGPException.UnsupportedOption if key passwords are not supported
*/
default GenerateKey withKeyPassword(byte[] password)
throws SOPGPException.PasswordNotHumanReadable,
SOPGPException.UnsupportedOption {
try {
return withKeyPassword(UTF8Util.decodeUTF8(password));
} catch (CharacterCodingException e) {
throw new SOPGPException.PasswordNotHumanReadable();
}
}
/**
* Pass in a profile.
*
* @param profile profile
* @return builder instance
*/
default GenerateKey profile(Profile profile) {
return profile(profile.getName());
}
/**
* Pass in a profile identifier.
*
* @param profile profile identifier
* @return builder instance
*/
GenerateKey profile(String profile);
/**
* If this options is set, the generated key will not be capable of encryption / decryption.
*
* @return builder instance
*/
GenerateKey signingOnly();
/**
* Generate the OpenPGP key and return it encoded as an {@link InputStream}.
*
* @return key
*
* @throws sop.exception.SOPGPException.MissingArg if no user-id was provided
* @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the generated key uses an unsupported asymmetric algorithm
* @throws IOException in case of an IO error
*/
Ready generate()
throws SOPGPException.MissingArg,
SOPGPException.UnsupportedAsymmetricAlgo,
IOException;
}

View file

@ -1,52 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import sop.ReadyWithResult;
import sop.Signatures;
import sop.exception.SOPGPException;
/**
* Split cleartext signed messages up into data and signatures.
*/
public interface InlineDetach {
/**
* Do not wrap the signatures in ASCII armor.
* @return builder
*/
InlineDetach noArmor();
/**
* Detach the provided signed message from its signatures.
*
* @param messageInputStream input stream containing the signed message
* @return result containing the detached message
*
* @throws IOException in case of an IO error
* @throws sop.exception.SOPGPException.BadData if the input stream does not contain a signed message
*/
ReadyWithResult<Signatures> message(InputStream messageInputStream)
throws IOException,
SOPGPException.BadData;
/**
* Detach the provided cleartext signed message from its signatures.
*
* @param message byte array containing the signed message
* @return result containing the detached message
* @throws IOException in case of an IO error
* @throws sop.exception.SOPGPException.BadData if the byte array does not contain a signed message
*/
default ReadyWithResult<Signatures> message(byte[] message)
throws IOException,
SOPGPException.BadData {
return message(new ByteArrayInputStream(message));
}
}

View file

@ -1,60 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import sop.Ready;
import sop.enums.InlineSignAs;
import sop.exception.SOPGPException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public interface InlineSign extends AbstractSign<InlineSign> {
/**
* Sets the signature mode.
* Note: This method has to be called before {@link #key(InputStream)} is called.
*
* @param mode signature mode
* @return builder instance
*
* @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported
*/
InlineSign mode(InlineSignAs mode)
throws SOPGPException.UnsupportedOption;
/**
* Signs data.
*
* @param data input stream containing data
* @return ready
*
* @throws IOException in case of an IO error
* @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked
* @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered
*/
Ready data(InputStream data)
throws IOException,
SOPGPException.KeyIsProtected,
SOPGPException.ExpectedText;
/**
* Signs data.
*
* @param data byte array containing data
* @return ready
*
* @throws IOException in case of an IO error
* @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked
* @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered
*/
default Ready data(byte[] data)
throws IOException,
SOPGPException.KeyIsProtected,
SOPGPException.ExpectedText {
return data(new ByteArrayInputStream(data));
}
}

View file

@ -1,54 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import sop.ReadyWithResult;
import sop.Verification;
import sop.exception.SOPGPException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* API for verification of inline-signed messages.
*/
public interface InlineVerify extends AbstractVerify<InlineVerify> {
/**
* Provide the inline-signed data.
* The result can be used to write the plaintext message out and to get the verifications.
*
* @param data signed data
* @return list of signature verifications
*
* @throws IOException in case of an IO error
* @throws SOPGPException.NoSignature when no signature is found
* @throws SOPGPException.BadData when the data is invalid OpenPGP data
*/
ReadyWithResult<List<Verification>> data(InputStream data)
throws IOException,
SOPGPException.NoSignature,
SOPGPException.BadData;
/**
* Provide the inline-signed data.
* The result can be used to write the plaintext message out and to get the verifications.
*
* @param data signed data
* @return list of signature verifications
*
* @throws IOException in case of an IO error
* @throws SOPGPException.NoSignature when no signature is found
* @throws SOPGPException.BadData when the data is invalid OpenPGP data
*/
default ReadyWithResult<List<Verification>> data(byte[] data)
throws IOException,
SOPGPException.NoSignature,
SOPGPException.BadData {
return data(new ByteArrayInputStream(data));
}
}

View file

@ -1,43 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import sop.Profile;
import java.util.List;
/**
* Subcommand to list supported profiles of other subcommands.
*/
public interface ListProfiles {
/**
* Provide the name of the subcommand for which profiles shall be listed.
* The returned list of profiles MUST NOT contain more than 4 entries.
*
* @param command command name (e.g. <pre>generate-key</pre>)
* @return list of profiles.
*/
List<Profile> subcommand(String command);
/**
* Return a list of {@link Profile Profiles} supported by the {@link GenerateKey} implementation.
*
* @return profiles
*/
default List<Profile> generateKey() {
return subcommand("generate-key");
}
/**
* Return a list of {@link Profile Profiles} supported by the {@link Encrypt} implementation.
*
* @return profiles
*/
default List<Profile> encrypt() {
return subcommand("encrypt");
}
}

View file

@ -1,54 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.util.UTF8Util;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
public interface RevokeKey {
/**
* Disable ASCII armor encoding.
*
* @return builder instance
*/
RevokeKey noArmor();
/**
* Provide the decryption password for the secret key.
*
* @param password password
* @return builder instance
* @throws SOPGPException.UnsupportedOption if the implementation does not support key passwords
* @throws SOPGPException.PasswordNotHumanReadable if the password is not human-readable
*/
default RevokeKey withKeyPassword(String password)
throws SOPGPException.UnsupportedOption,
SOPGPException.PasswordNotHumanReadable {
return withKeyPassword(password.getBytes(UTF8Util.UTF8));
}
/**
* Provide the decryption password for the secret key.
*
* @param password password
* @return builder instance
* @throws SOPGPException.UnsupportedOption if the implementation does not support key passwords
* @throws SOPGPException.PasswordNotHumanReadable if the password is not human-readable
*/
RevokeKey withKeyPassword(byte[] password)
throws SOPGPException.UnsupportedOption,
SOPGPException.PasswordNotHumanReadable;
default Ready keys(byte[] bytes) {
return keys(new ByteArrayInputStream(bytes));
}
Ready keys(InputStream keys);
}

View file

@ -1,46 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import sop.Verification;
import sop.exception.SOPGPException;
public interface VerifySignatures {
/**
* Provide the signed data (without signatures).
*
* @param data signed data
* @return list of signature verifications
* @throws IOException in case of an IO error
* @throws SOPGPException.NoSignature when no valid signature is found
* @throws SOPGPException.BadData when the data is invalid OpenPGP data
*/
List<Verification> data(InputStream data)
throws IOException,
SOPGPException.NoSignature,
SOPGPException.BadData;
/**
* Provide the signed data (without signatures).
*
* @param data signed data
* @return list of signature verifications
* @throws IOException in case of an IO error
* @throws SOPGPException.NoSignature when no valid signature is found
* @throws SOPGPException.BadData when the data is invalid OpenPGP data
*/
default List<Verification> data(byte[] data)
throws IOException,
SOPGPException.NoSignature,
SOPGPException.BadData {
return data(new ByteArrayInputStream(data));
}
}

View file

@ -1,109 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation;
public interface Version {
/**
* Return the implementations name.
* e.g. "SOP",
*
* @return implementation name
*/
String getName();
/**
* Return the implementations short version string.
* e.g. "1.0"
*
* @return version string
*/
String getVersion();
/**
* Return version information about the used OpenPGP backend.
* e.g. "Bouncycastle 1.70"
*
* @return backend version string
*/
String getBackendVersion();
/**
* Return an extended version string containing multiple lines of version information.
* The first line MUST match the information produced by {@link #getName()} and {@link #getVersion()}, but the rest of the text
* has no defined structure.
* Example:
* <pre>
* "SOP 1.0
* Awesome PGP!
* Using Bouncycastle 1.70
* LibFoo 1.2.2
* See https://pgp.example.org/sop/ for more information"
* </pre>
*
* @return extended version string
*/
String getExtendedVersion();
/**
* Return the revision of the SOP specification that this implementation is implementing, for example,
* <pre>draft-dkg-openpgp-stateless-cli-06</pre>.
* If the implementation targets a specific draft but the implementer knows the implementation is incomplete,
* it should prefix the draft title with a "~" (TILDE, U+007E), for example:
* <pre>~draft-dkg-openpgp-stateless-cli-06</pre>.
* The implementation MAY emit additional text about its relationship to the targeted draft on the lines following
* the versioned title.
*
* @return implemented SOP spec version
*/
default String getSopSpecVersion() {
StringBuilder sb = new StringBuilder();
if (isSopSpecImplementationIncomplete()) {
sb.append('~');
}
sb.append(getSopSpecRevisionName());
if (getSopSpecImplementationRemarks() != null) {
sb.append('\n')
.append('\n')
.append(getSopSpecImplementationRemarks());
}
return sb.toString();
}
/**
* Return the version number of the latest targeted SOP spec revision.
*
* @return SOP spec revision number
*/
int getSopSpecRevisionNumber();
/**
* Return the name of the latest targeted revision of the SOP spec.
*
* @return SOP spec revision string
*/
default String getSopSpecRevisionName() {
return "draft-dkg-openpgp-stateless-cli-" + String.format("%02d", getSopSpecRevisionNumber());
}
/**
* Return <pre>true</pre>, if this implementation of the SOP spec is known to be incomplete or defective.
*
* @return true if incomplete, false otherwise
*/
boolean isSopSpecImplementationIncomplete();
/**
* Return free-form text containing remarks about the completeness of the SOP implementation.
* If there are no remarks, this method returns <pre>null</pre>.
*
* @return remarks or null
*/
String getSopSpecImplementationRemarks();
}

View file

@ -1,9 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Stateless OpenPGP Interface for Java.
* Different cryptographic operations.
*/
package sop.operation;

View file

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Stateless OpenPGP Interface for Java.
*/
package sop;

View file

@ -1,47 +0,0 @@
// Copyright 2021 Paul Schaub, @maybeWeCouldStealAVan, @Dave L.
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.util;
public class HexUtil {
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
/**
* Encode a byte array to a hex string.
*
* @see <a href="https://stackoverflow.com/a/9855338">
* How to convert a byte array to a hex string in Java?</a>
* @param bytes bytes
* @return hex encoding
*/
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
/**
* Decode a hex string into a byte array.
*
* @see <a href="https://stackoverflow.com/a/140861">
* Convert a string representation of a hex dump to a byte array using Java?</a>
* @param s hex string
* @return decoded byte array
*/
public static byte[] hexToBytes(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i + 1), 16));
}
return data;
}
}

View file

@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.util;
/**
* Backport of java.util.Optional for older Android versions.
*
* @param <T> item type
*/
public class Optional<T> {
private final T item;
public Optional() {
this(null);
}
public Optional(T item) {
this.item = item;
}
public static <T> Optional<T> of(T item) {
if (item == null) {
throw new NullPointerException("Item cannot be null.");
}
return new Optional<>(item);
}
public static <T> Optional<T> ofNullable(T item) {
return new Optional<>(item);
}
public static <T> Optional<T> ofEmpty() {
return new Optional<>(null);
}
public T get() {
return item;
}
public boolean isPresent() {
return item != null;
}
public boolean isEmpty() {
return item == null;
}
}

View file

@ -1,80 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* {@link OutputStream} that buffers data being written into it, until its underlying output stream is being replaced.
* At that point, first all the buffered data is being written to the underlying stream, followed by any successive
* data that may get written to the {@link ProxyOutputStream}.
* <p>
* This class is useful if we need to provide an {@link OutputStream} at one point in time when the final
* target output stream is not yet known.
*/
public class ProxyOutputStream extends OutputStream {
private final ByteArrayOutputStream buffer;
private OutputStream swapped;
public ProxyOutputStream() {
this.buffer = new ByteArrayOutputStream();
}
public synchronized void replaceOutputStream(OutputStream underlying) throws IOException {
if (underlying == null) {
throw new NullPointerException("Underlying OutputStream cannot be null.");
}
this.swapped = underlying;
byte[] bufferBytes = buffer.toByteArray();
swapped.write(bufferBytes);
}
@Override
public synchronized void write(byte[] b) throws IOException {
if (swapped == null) {
buffer.write(b);
} else {
swapped.write(b);
}
}
@Override
public synchronized void write(byte[] b, int off, int len) throws IOException {
if (swapped == null) {
buffer.write(b, off, len);
} else {
swapped.write(b, off, len);
}
}
@Override
public synchronized void flush() throws IOException {
buffer.flush();
if (swapped != null) {
swapped.flush();
}
}
@Override
public synchronized void close() throws IOException {
buffer.close();
if (swapped != null) {
swapped.close();
}
}
@Override
public synchronized void write(int i) throws IOException {
if (swapped == null) {
buffer.write(i);
} else {
swapped.write(i);
}
}
}

View file

@ -1,65 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.util;
import javax.annotation.Nonnull;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
/**
* Utility class to parse and format dates as ISO-8601 UTC timestamps.
*/
public class UTCUtil {
public static final SimpleDateFormat UTC_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
public static final SimpleDateFormat[] UTC_PARSERS = new SimpleDateFormat[] {
UTC_FORMATTER,
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"),
new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"),
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
};
static {
for (SimpleDateFormat f : UTC_PARSERS) {
f.setTimeZone(TimeZone.getTimeZone("UTC"));
}
}
/**
* Parse an ISO-8601 UTC timestamp from a string.
*
* @param dateString string
* @return date
* @throws ParseException if the date string is malformed and cannot be parsed
*/
@Nonnull
public static Date parseUTCDate(String dateString) throws ParseException {
ParseException exception = null;
for (SimpleDateFormat parser : UTC_PARSERS) {
try {
return parser.parse(dateString);
} catch (ParseException e) {
// Store first exception (that of UTC_FORMATTER) to throw if we fail to parse the date
if (exception == null) {
exception = e;
}
// Try next parser
}
}
// No parser worked, so we throw the store exception
throw exception;
}
/**
* Format a date as ISO-8601 UTC timestamp.
*
* @param date date
* @return timestamp string
*/
public static String formatUTCDate(Date date) {
return UTC_FORMATTER.format(date);
}
}

View file

@ -1,37 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.util;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
public class UTF8Util {
public static final Charset UTF8 = Charset.forName("UTF8");
private static final CharsetDecoder UTF8Decoder = UTF8
.newDecoder()
.onUnmappableCharacter(CodingErrorAction.REPORT)
.onMalformedInput(CodingErrorAction.REPORT);
/**
* Detect non-valid UTF8 data.
*
* @see <a href="https://stackoverflow.com/a/1471193">ante on StackOverflow</a>
* @param data utf-8 encoded bytes
*
* @return decoded string
* @throws CharacterCodingException if the input data does not resemble UTF8
*/
public static String decodeUTF8(byte[] data)
throws CharacterCodingException {
ByteBuffer byteBuffer = ByteBuffer.wrap(data);
CharBuffer charBuffer = UTF8Decoder.decode(byteBuffer);
return charBuffer.toString();
}
}

View file

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Utility classes.
*/
package sop.util;

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import java.io.InputStream
/**
* Tuple of a [ByteArray] and associated result object.
*
* @param bytes byte array
* @param result result object
* @param <T> type of result
*/
data class ByteArrayAndResult<T>(val bytes: ByteArray, val result: T) {
/**
* [InputStream] returning the contents of [bytes].
*
* @return input stream
*/
val inputStream: InputStream
get() = bytes.inputStream()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ByteArrayAndResult<*>
if (!bytes.contentEquals(other.bytes)) return false
if (result != other.result) return false
return true
}
override fun hashCode(): Int {
var hashCode = bytes.contentHashCode()
hashCode = 31 * hashCode + (result?.hashCode() ?: 0)
return hashCode
}
}

View file

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import sop.util.Optional
class DecryptionResult(sessionKey: SessionKey?, val verifications: List<Verification>) {
val sessionKey: Optional<SessionKey>
init {
this.sessionKey = Optional.ofNullable(sessionKey)
}
}

View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import java.io.OutputStream
import java.io.PrintWriter
data class MicAlg(val micAlg: String) {
fun writeTo(outputStream: OutputStream) {
PrintWriter(outputStream).use { it.write(micAlg) }
}
companion object {
@JvmStatic fun empty() = MicAlg("")
@JvmStatic
fun fromHashAlgorithmId(id: Int) =
when (id) {
1 -> "pgp-md5"
2 -> "pgp-sha1"
3 -> "pgp-ripemd160"
8 -> "pgp-sha256"
9 -> "pgp-sha384"
10 -> "pgp-sha512"
11 -> "pgp-sha224"
12 -> "pgp-sha3-256"
14 -> "pgp-sha3-512"
else -> throw IllegalArgumentException("Unsupported hash algorithm ID: $id")
}.let { MicAlg(it) }
}
}

View file

@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import sop.util.Optional
import sop.util.UTF8Util
/**
* Tuple class bundling a profile name and description.
*
* @param name profile name. A profile name is a UTF-8 string that has no whitespace in it. Similar
* to OpenPGP Notation names, profile names are divided into two namespaces: The IETF namespace
* and the user namespace. A profile name in the user namespace ends with the `@` character (0x40)
* followed by a DNS domain name. A profile name in the IETF namespace does not have an `@`
* character. A profile name in the user space is owned and controlled by the owner of the domain
* in the suffix. A profile name in the IETF namespace that begins with the string `rfc` should
* have semantics that hew as closely as possible to the referenced RFC. Similarly, a profile name
* in the IETF namespace that begins with the string `draft-` should have semantics that hew as
* closely as possible to the referenced Internet Draft.
* @param description a free-form description of the profile.
* @see <a
* href="https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html#name-profile">
* SOP Spec - Profile</a>
*/
data class Profile(val name: String, val description: Optional<String>) {
@JvmOverloads
constructor(
name: String,
description: String? = null
) : this(name, Optional.ofNullable(description?.trim()?.ifBlank { null }))
init {
require(name.trim().isNotBlank()) { "Name cannot be empty." }
require(!name.contains(":")) { "Name cannot contain ':'." }
require(listOf(" ", "\n", "\t", "\r").none { name.contains(it) }) {
"Name cannot contain whitespace characters."
}
require(!exceeds1000CharLineLimit(this)) {
"The line representation of a profile MUST NOT exceed 1000 bytes."
}
}
fun hasDescription() = description.isPresent
/**
* Convert the profile into a String for displaying.
*
* @return string
*/
override fun toString(): String =
if (description.isEmpty) name else "$name: ${description.get()}"
companion object {
/**
* Parse a [Profile] from its string representation.
*
* @param string string representation
* @return profile
*/
@JvmStatic
fun parse(string: String): Profile {
return if (string.contains(": ")) {
Profile(
string.substring(0, string.indexOf(": ")),
string.substring(string.indexOf(": ") + 2).trim())
} else if (string.endsWith(":")) {
Profile(string.substring(0, string.length - 1))
} else {
Profile(string.trim())
}
}
/**
* Test if the string representation of the profile exceeds the limit of 1000 bytes length.
*
* @param profile profile
* @return `true` if the profile exceeds 1000 bytes, `false` otherwise.
*/
@JvmStatic
private fun exceeds1000CharLineLimit(profile: Profile): Boolean =
profile.toString().toByteArray(UTF8Util.UTF8).size > 1000
}
}

View file

@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
/** Abstract class that encapsulates output data, waiting to be consumed. */
abstract class Ready {
/**
* Write the data to the provided output stream.
*
* @param outputStream output stream
* @throws IOException in case of an IO error
*/
@Throws(IOException::class) abstract fun writeTo(outputStream: OutputStream)
/**
* Return the data as a byte array by writing it to a [ByteArrayOutputStream] first and then
* returning the array.
*
* @return data as byte array
* @throws IOException in case of an IO error
*/
val bytes: ByteArray
@Throws(IOException::class)
get() =
ByteArrayOutputStream()
.let {
writeTo(it)
it
}
.toByteArray()
/**
* Return an input stream containing the data.
*
* @return input stream
* @throws IOException in case of an IO error
*/
val inputStream: InputStream
@Throws(IOException::class) get() = ByteArrayInputStream(bytes)
}

View file

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.OutputStream
import sop.exception.SOPGPException
abstract class ReadyWithResult<T> {
/**
* Write the data e.g. decrypted plaintext to the provided output stream and return the result
* of the processing operation.
*
* @param outputStream output stream
* @return result, eg. signatures
* @throws IOException in case of an IO error
* @throws SOPGPException in case of a SOP protocol error
*/
@Throws(IOException::class, SOPGPException::class)
abstract fun writeTo(outputStream: OutputStream): T
/**
* Return the data as a [ByteArrayAndResult]. Calling [ByteArrayAndResult.bytes] will give you
* access to the data as byte array, while [ByteArrayAndResult.result] will grant access to the
* appended result.
*
* @return byte array and result
* @throws IOException in case of an IO error
* @throws SOPGPException.NoSignature if there are no valid signatures found
*/
@Throws(IOException::class, SOPGPException::class)
fun toByteArrayAndResult() =
ByteArrayOutputStream().let {
val result = writeTo(it)
ByteArrayAndResult(it.toByteArray(), result)
}
}

View file

@ -0,0 +1,97 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package 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.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
/**
* Stateless OpenPGP Interface. This class provides a stateless interface to various OpenPGP related
* operations. Note: Subcommand objects acquired by calling any method of this interface are not
* intended for reuse. If you for example need to generate multiple keys, make a dedicated call to
* [generateKey] once per key generation.
*/
interface SOP {
/** Get information about the implementations name and version. */
fun version(): Version
/** Generate a secret key. */
fun generateKey(): GenerateKey
/** Extract a certificate (public key) from a secret key. */
fun extractCert(): ExtractCert
/**
* Create detached signatures. If you want to sign a message inline, use [inlineSign] instead.
*/
fun sign(): DetachedSign = detachedSign()
/**
* Create detached signatures. If you want to sign a message inline, use [inlineSign] instead.
*/
fun detachedSign(): DetachedSign
/**
* Sign a message using inline signatures. If you need to create detached signatures, use
* [detachedSign] instead.
*/
fun inlineSign(): InlineSign
/**
* Verify detached signatures. If you need to verify an inline-signed message, use
* [inlineVerify] instead.
*/
fun verify(): DetachedVerify = detachedVerify()
/**
* Verify detached signatures. If you need to verify an inline-signed message, use
* [inlineVerify] instead.
*/
fun detachedVerify(): DetachedVerify
/**
* Verify signatures of an inline-signed message. If you need to verify detached signatures over
* a message, use [detachedVerify] instead.
*/
fun inlineVerify(): InlineVerify
/** Detach signatures from an inline signed message. */
fun inlineDetach(): InlineDetach
/** Encrypt a message. */
fun encrypt(): Encrypt
/** Decrypt a message. */
fun decrypt(): Decrypt
/** Convert binary OpenPGP data to ASCII. */
fun armor(): Armor
/** Converts ASCII armored OpenPGP data to binary. */
fun dearmor(): Dearmor
/** List supported [Profiles][Profile] of a subcommand. */
fun listProfiles(): ListProfiles
/** Revoke one or more secret keys. */
fun revokeKey(): RevokeKey
/** Update a key's password. */
fun changeKeyPassword(): ChangeKeyPassword
}

View file

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import sop.util.HexUtil
/**
* Class representing a symmetric session key.
*
* @param algorithm symmetric key algorithm ID
* @param key [ByteArray] containing the session key
*/
data class SessionKey(val algorithm: Byte, val key: ByteArray) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SessionKey
if (algorithm != other.algorithm) return false
if (!key.contentEquals(other.key)) return false
return true
}
override fun hashCode(): Int {
var hashCode = algorithm.toInt()
hashCode = 31 * hashCode + key.contentHashCode()
return hashCode
}
override fun toString(): String = "$algorithm:${HexUtil.bytesToHex(key)}"
companion object {
@JvmStatic private val PATTERN = "^(\\d):([0-9A-F]+)$".toPattern()
@JvmStatic
fun fromString(string: String): SessionKey {
val matcher = PATTERN.matcher(string.trim().uppercase().replace("\n", ""))
require(matcher.matches()) { "Provided session key does not match expected format." }
return SessionKey(matcher.group(1).toByte(), HexUtil.hexToBytes(matcher.group(2)))
}
}
}

View file

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import java.io.IOException
import java.io.OutputStream
abstract class Signatures : Ready() {
/**
* Write OpenPGP signatures to the provided output stream.
*
* @param outputStream signature output stream
* @throws IOException in case of an IO error
*/
@Throws(IOException::class) abstract override fun writeTo(outputStream: OutputStream)
}

View file

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
/**
* This class contains various information about a signed message.
*
* @param micAlg string identifying the digest mechanism used to create the signed message. This is
* useful for setting the `micalg=` parameter for the multipart/signed content-type of a PGP/MIME
* object as described in section 5 of [RFC3156]. If more than one signature was generated and
* different digest mechanisms were used, the value of the micalg object is an empty string.
*/
data class SigningResult(val micAlg: MicAlg) {
class Builder internal constructor() {
private var micAlg = MicAlg.empty()
fun setMicAlg(micAlg: MicAlg) = apply { this.micAlg = micAlg }
fun build() = SigningResult(micAlg)
}
companion object {
@JvmStatic fun builder() = Builder()
}
}

View file

@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import java.text.ParseException
import java.util.Date
import sop.enums.SignatureMode
import sop.util.Optional
import sop.util.UTCUtil
data class Verification(
val creationTime: Date,
val signingKeyFingerprint: String,
val signingCertFingerprint: String,
val signatureMode: Optional<SignatureMode>,
val description: Optional<String>
) {
@JvmOverloads
constructor(
creationTime: Date,
signingKeyFingerprint: String,
signingCertFingerprint: String,
signatureMode: SignatureMode? = null,
description: String? = null
) : this(
creationTime,
signingKeyFingerprint,
signingCertFingerprint,
Optional.ofNullable(signatureMode),
Optional.ofNullable(description?.trim()))
override fun toString(): String =
"${UTCUtil.formatUTCDate(creationTime)} $signingKeyFingerprint $signingCertFingerprint" +
(if (signatureMode.isPresent) " mode:${signatureMode.get()}" else "") +
(if (description.isPresent) " ${description.get()}" else "")
companion object {
@JvmStatic
fun fromString(string: String): Verification {
val split = string.trim().split(" ")
require(split.size >= 3) {
"Verification must be of the format 'UTC-DATE OpenPGPFingerprint OpenPGPFingerprint [mode] [info]'."
}
if (split.size == 3) {
return Verification(parseUTCDate(split[0]), split[1], split[2])
}
var index = 3
val mode =
if (split[3].startsWith("mode:")) {
index += 1
SignatureMode.valueOf(split[3].substring("mode:".length))
} else null
val description = split.subList(index, split.size).joinToString(" ").ifBlank { null }
return Verification(
parseUTCDate(split[0]),
split[1],
split[2],
Optional.ofNullable(mode),
Optional.ofNullable(description))
}
@JvmStatic
private fun parseUTCDate(string: String): Date {
return try {
UTCUtil.parseUTCDate(string)
} catch (e: ParseException) {
throw IllegalArgumentException("Malformed UTC timestamp.", e)
}
}
}
}

View file

@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.enums
@Deprecated("Use of armor labels is deprecated.")
enum class ArmorLabel {
auto,
sig,
key,
cert,
message
}

View file

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.enums
enum class EncryptAs {
binary,
text
}

View file

@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.enums
enum class InlineSignAs {
/** Signature is made over the binary message. */
binary,
/** Signature is made over the message in text mode. */
text,
/** Signature is made using the Cleartext Signature Framework. */
clearsigned
}

View file

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.enums
enum class SignAs {
/** Signature is made over the binary message. */
binary,
/** Signature is made over the message in text mode. */
text
}

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.enums
/**
* Enum referencing relevant signature types.
*
* @see <a href="https://www.rfc-editor.org/rfc/rfc4880#section-5.2.1"> RFC4880 §5.2.1 - Signature
* Types</a>
*/
enum class SignatureMode {
/** Signature of a binary document (type `0x00`). */
binary,
/** Signature of a canonical text document (type `0x01`). */
text
}

View file

@ -0,0 +1,308 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.exception
abstract class SOPGPException : RuntimeException {
constructor() : super()
constructor(message: String) : super(message)
constructor(cause: Throwable) : super(cause)
constructor(message: String, cause: Throwable) : super(message, cause)
abstract fun getExitCode(): Int
/** No acceptable signatures found (sop verify, inline-verify). */
class NoSignature : SOPGPException {
@JvmOverloads
constructor(message: String = "No verifiable signature found.") : super(message)
constructor(errorMsg: String, e: NoSignature) : super(errorMsg, e)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 3
}
}
/** Asymmetric algorithm unsupported (sop encrypt, sign, inline-sign). */
class UnsupportedAsymmetricAlgo(message: String, e: Throwable) : SOPGPException(message, e) {
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 13
}
}
/** Certificate not encryption capable (e,g, expired, revoked, unacceptable usage). */
class CertCannotEncrypt : SOPGPException {
constructor(message: String, cause: Throwable) : super(message, cause)
constructor(message: String) : super(message)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 17
}
}
/** Missing required argument. */
class MissingArg : SOPGPException {
constructor()
constructor(message: String) : super(message)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 19
}
}
/** Incomplete verification instructions (sop decrypt). */
class IncompleteVerification(message: String) : SOPGPException(message) {
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 23
}
}
/** Unable to decrypt (sop decrypt). */
class CannotDecrypt : SOPGPException {
constructor()
constructor(errorMsg: String, e: Throwable) : super(errorMsg, e)
constructor(message: String) : super(message)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 29
}
}
/** Non-UTF-8 or otherwise unreliable password (sop encrypt). */
class PasswordNotHumanReadable : SOPGPException {
constructor() : super()
constructor(message: String) : super(message)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 31
}
}
/** Unsupported option. */
class UnsupportedOption : SOPGPException {
constructor(message: String) : super(message)
constructor(message: String, cause: Throwable) : super(message, cause)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 37
}
}
/** Invalid data type (no secret key where KEYS expected, etc.). */
class BadData : SOPGPException {
constructor(message: String) : super(message)
constructor(throwable: Throwable) : super(throwable)
constructor(message: String, throwable: Throwable) : super(message, throwable)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 41
}
}
/** Non-Text input where text expected. */
class ExpectedText : SOPGPException {
constructor() : super()
constructor(message: String) : super(message)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 53
}
}
/** Output file already exists. */
class OutputExists(message: String) : SOPGPException(message) {
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 59
}
}
/** Input file does not exist. */
class MissingInput : SOPGPException {
constructor(message: String, cause: Throwable) : super(message, cause)
constructor(errorMsg: String) : super(errorMsg)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 61
}
}
/** A KEYS input is protected (locked) with a password and sop failed to unlock it. */
class KeyIsProtected : SOPGPException {
constructor() : super()
constructor(message: String) : super(message)
constructor(message: String, cause: Throwable) : super(message, cause)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 67
}
}
/** Unsupported subcommand. */
class UnsupportedSubcommand(message: String) : SOPGPException(message) {
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 69
}
}
/**
* An indirect parameter is a special designator (it starts with @), but sop does not know how
* to handle the prefix.
*/
class UnsupportedSpecialPrefix(errorMsg: String) : SOPGPException(errorMsg) {
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 71
}
}
/**
* Exception that gets thrown if a special designator (starting with @) is given, but the
* filesystem contains a file matching the designator.
*
* E.g. <pre>@ENV:FOO</pre> is given, but <pre>./@ENV:FOO</pre> exists on the filesystem.
*/
class AmbiguousInput(message: String) : SOPGPException(message) {
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 73
}
}
/** Key not signature-capable (e.g. expired, revoked, unacceptable usage flags). */
class KeyCannotSign : SOPGPException {
constructor() : super()
constructor(message: String) : super(message)
constructor(s: String, throwable: Throwable) : super(s, throwable)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 79
}
}
/** User provided incompatible options (e.g. "--as=clearsigned --no-armor"). */
class IncompatibleOptions : SOPGPException {
constructor() : super()
constructor(errorMsg: String) : super(errorMsg)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 83
}
}
/**
* The user provided a subcommand with an unsupported profile ("--profile=XYZ"), or the user
* tried to list profiles of a subcommand that does not support profiles at all.
*/
class UnsupportedProfile : SOPGPException {
/**
* Return the subcommand name.
*
* @return subcommand
*/
val subcommand: String
/**
* Return the profile name. May return `null`.
*
* @return profile name
*/
val profile: String?
/**
* Create an exception signalling a subcommand that does not support any profiles.
*
* @param subcommand subcommand
*/
constructor(
subcommand: String
) : super("Subcommand '$subcommand' does not support any profiles.") {
this.subcommand = subcommand
profile = null
}
/**
* Create an exception signalling a subcommand does not support a specific profile.
*
* @param subcommand subcommand
* @param profile unsupported profile
*/
constructor(
subcommand: String,
profile: String
) : super("Subcommand '$subcommand' does not support profile '$profile'.") {
this.subcommand = subcommand
this.profile = profile
}
/**
* Wrap an exception into another instance with a possibly translated error message.
*
* @param errorMsg error message
* @param e exception
*/
constructor(errorMsg: String, e: UnsupportedProfile) : super(errorMsg, e) {
subcommand = e.subcommand
profile = e.profile
}
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 89
}
}
}

View file

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.exception.SOPGPException.BadData
import sop.exception.SOPGPException.KeyCannotSign
import sop.exception.SOPGPException.PasswordNotHumanReadable
import sop.exception.SOPGPException.UnsupportedAsymmetricAlgo
import sop.exception.SOPGPException.UnsupportedOption
import sop.util.UTF8Util
/**
* Interface for signing operations.
*
* @param <T> builder subclass
*/
interface AbstractSign<T> {
/**
* Disable ASCII armor encoding.
*
* @return builder instance
*/
fun noArmor(): T
/**
* Add one or more signing keys.
*
* @param key input stream containing encoded keys
* @return builder instance
* @throws KeyCannotSign if the key cannot be used for signing
* @throws BadData if the [InputStream] does not contain an OpenPGP key
* @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm
* @throws IOException in case of an IO error
*/
@Throws(
KeyCannotSign::class, BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class)
fun key(key: InputStream): T
/**
* Add one or more signing keys.
*
* @param key byte array containing encoded keys
* @return builder instance
* @throws KeyCannotSign if the key cannot be used for signing
* @throws BadData if the byte array does not contain an OpenPGP key
* @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm
* @throws IOException in case of an IO error
*/
@Throws(
KeyCannotSign::class, BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class)
fun key(key: ByteArray): T = key(key.inputStream())
/**
* Provide the password for the secret key used for signing.
*
* @param password password
* @return builder instance
* @throws UnsupportedOption if key passwords are not supported
* @throws PasswordNotHumanReadable if the provided passphrase is not human-readable
*/
@Throws(UnsupportedOption::class, PasswordNotHumanReadable::class)
fun withKeyPassword(password: String): T = withKeyPassword(password.toByteArray(UTF8Util.UTF8))
/**
* Provide the password for the secret key used for signing.
*
* @param password password
* @return builder instance
* @throws UnsupportedOption if key passwords are not supported
* @throws PasswordNotHumanReadable if the provided passphrase is not human-readable
*/
@Throws(UnsupportedOption::class, PasswordNotHumanReadable::class)
fun withKeyPassword(password: ByteArray): T
}

View file

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import java.util.*
import sop.exception.SOPGPException.BadData
import sop.exception.SOPGPException.UnsupportedOption
/**
* Common API methods shared between verification of inline signatures ([InlineVerify]) and
* verification of detached signatures ([DetachedVerify]).
*
* @param <T> Builder type ([DetachedVerify], [InlineVerify])
*/
interface AbstractVerify<T> {
/**
* Makes the SOP implementation consider signatures before this date invalid.
*
* @param timestamp timestamp
* @return builder instance
*/
@Throws(UnsupportedOption::class) fun notBefore(timestamp: Date): T
/**
* Makes the SOP implementation consider signatures after this date invalid.
*
* @param timestamp timestamp
* @return builder instance
*/
@Throws(UnsupportedOption::class) fun notAfter(timestamp: Date): T
/**
* Add one or more verification cert.
*
* @param cert input stream containing the encoded certs
* @return builder instance
* @throws BadData if the input stream does not contain an OpenPGP certificate
* @throws IOException in case of an IO error
*/
@Throws(BadData::class, IOException::class) fun cert(cert: InputStream): T
/**
* Add one or more verification cert.
*
* @param cert byte array containing the encoded certs
* @return builder instance
* @throws BadData if the byte array does not contain an OpenPGP certificate
* @throws IOException in case of an IO error
*/
@Throws(BadData::class, IOException::class)
fun cert(cert: ByteArray): T = cert(cert.inputStream())
}

View file

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.Ready
import sop.enums.ArmorLabel
import sop.exception.SOPGPException.BadData
import sop.exception.SOPGPException.UnsupportedOption
interface Armor {
/**
* Overrides automatic detection of label.
*
* @param label armor label
* @return builder instance
*/
@Deprecated("Use of armor labels is deprecated and will be removed in a future release.")
@Throws(UnsupportedOption::class)
fun label(label: ArmorLabel): Armor
/**
* Armor the provided data.
*
* @param data input stream of unarmored OpenPGP data
* @return armored data
* @throws BadData if the data appears to be OpenPGP packets, but those are broken
* @throws IOException in case of an IO error
*/
@Throws(BadData::class, IOException::class) fun data(data: InputStream): Ready
/**
* Armor the provided data.
*
* @param data unarmored OpenPGP data
* @return armored data
* @throws BadData if the data appears to be OpenPGP packets, but those are broken
* @throws IOException in case of an IO error
*/
@Throws(BadData::class, IOException::class)
fun data(data: ByteArray): Ready = data(data.inputStream())
}

View file

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.InputStream
import sop.Ready
import sop.exception.SOPGPException.BadData
import sop.exception.SOPGPException.KeyIsProtected
import sop.exception.SOPGPException.PasswordNotHumanReadable
import sop.util.UTF8Util
interface ChangeKeyPassword {
/**
* Disable ASCII armoring of the output.
*
* @return builder instance
*/
fun noArmor(): ChangeKeyPassword
/**
* Provide a passphrase to unlock the secret key. This method can be provided multiple times to
* provide separate passphrases that are tried as a means to unlock any secret key material
* encountered.
*
* @param oldPassphrase old passphrase
* @return builder instance
*/
@Throws(PasswordNotHumanReadable::class)
fun oldKeyPassphrase(oldPassphrase: ByteArray): ChangeKeyPassword =
try {
oldKeyPassphrase(UTF8Util.decodeUTF8(oldPassphrase))
} catch (e: CharacterCodingException) {
throw PasswordNotHumanReadable("Password MUST be a valid UTF8 string.")
}
/**
* Provide a passphrase to unlock the secret key. This method can be provided multiple times to
* provide separate passphrases that are tried as a means to unlock any secret key material
* encountered.
*
* @param oldPassphrase old passphrase
* @return builder instance
*/
fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword
/**
* Provide a passphrase to re-lock the secret key with. This method can only be used once, and
* all key material encountered will be encrypted with the given passphrase. If this method is
* not called, the key material will not be protected.
*
* @param newPassphrase new passphrase
* @return builder instance
*/
@Throws(PasswordNotHumanReadable::class)
fun newKeyPassphrase(newPassphrase: ByteArray): ChangeKeyPassword =
try {
newKeyPassphrase(UTF8Util.decodeUTF8(newPassphrase))
} catch (e: CharacterCodingException) {
throw PasswordNotHumanReadable("Password MUST be a valid UTF8 string.")
}
/**
* Provide a passphrase to re-lock the secret key with. This method can only be used once, and
* all key material encountered will be encrypted with the given passphrase. If this method is
* not called, the key material will not be protected.
*
* @param newPassphrase new passphrase
* @return builder instance
*/
fun newKeyPassphrase(newPassphrase: String): ChangeKeyPassword
/**
* Provide the key material.
*
* @param keys input stream of secret key material
* @return ready
* @throws KeyIsProtected if any (sub-) key encountered cannot be unlocked.
* @throws BadData if the key material is malformed
*/
@Throws(KeyIsProtected::class, BadData::class)
fun keys(keys: ByteArray): Ready = keys(keys.inputStream())
/**
* Provide the key material.
*
* @param keys input stream of secret key material
* @return ready
* @throws KeyIsProtected if any (sub-) key encountered cannot be unlocked.
* @throws BadData if the key material is malformed
*/
@Throws(KeyIsProtected::class, BadData::class) fun keys(keys: InputStream): Ready
}

View file

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.Ready
import sop.exception.SOPGPException.BadData
import sop.util.UTF8Util
interface Dearmor {
/**
* Dearmor armored OpenPGP data.
*
* @param data armored OpenPGP data
* @return input stream of unarmored data
* @throws BadData in case of non-OpenPGP data
* @throws IOException in case of an IO error
*/
@Throws(BadData::class, IOException::class) fun data(data: InputStream): Ready
/**
* Dearmor armored OpenPGP data.
*
* @param data armored OpenPGP data
* @return input stream of unarmored data
* @throws BadData in case of non-OpenPGP data
* @throws IOException in case of an IO error
*/
@Throws(BadData::class, IOException::class)
fun data(data: ByteArray): Ready = data(data.inputStream())
/**
* Dearmor amored OpenPGP data.
*
* @param data armored OpenPGP data
* @return input stream of unarmored data
* @throws BadData in case of non-OpenPGP data
* @throws IOException in case of an IO error
*/
@Throws(BadData::class, IOException::class)
fun data(data: String): Ready = data(data.toByteArray(UTF8Util.UTF8))
}

View file

@ -0,0 +1,165 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import java.util.*
import sop.DecryptionResult
import sop.ReadyWithResult
import sop.SessionKey
import sop.exception.SOPGPException.*
import sop.util.UTF8Util
interface Decrypt {
/**
* Makes the SOP consider signatures before this date invalid.
*
* @param timestamp timestamp
* @return builder instance
* @throws UnsupportedOption if this option is not supported
*/
@Throws(UnsupportedOption::class) fun verifyNotBefore(timestamp: Date): Decrypt
/**
* Makes the SOP consider signatures after this date invalid.
*
* @param timestamp timestamp
* @return builder instance
* @throws UnsupportedOption if this option is not supported
*/
@Throws(UnsupportedOption::class) fun verifyNotAfter(timestamp: Date): Decrypt
/**
* Adds one or more verification cert.
*
* @param cert input stream containing the cert(s)
* @return builder instance
* @throws BadData if the [InputStream] doesn't provide an OpenPGP certificate
* @throws UnsupportedAsymmetricAlgo if the cert uses an unsupported asymmetric algorithm
* @throws IOException in case of an IO error
*/
@Throws(BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class)
fun verifyWithCert(cert: InputStream): Decrypt
/**
* Adds one or more verification cert.
*
* @param cert byte array containing the cert(s)
* @return builder instance
* @throws BadData if the byte array doesn't contain an OpenPGP certificate
* @throws UnsupportedAsymmetricAlgo if the cert uses an unsupported asymmetric algorithm
* @throws IOException in case of an IO error
*/
@Throws(BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class)
fun verifyWithCert(cert: ByteArray): Decrypt = verifyWithCert(cert.inputStream())
/**
* Tries to decrypt with the given session key.
*
* @param sessionKey session key
* @return builder instance
* @throws UnsupportedOption if this option is not supported
*/
@Throws(UnsupportedOption::class) fun withSessionKey(sessionKey: SessionKey): Decrypt
/**
* Tries to decrypt with the given password.
*
* @param password password
* @return builder instance
* @throws PasswordNotHumanReadable if the password is not human-readable
* @throws UnsupportedOption if this option is not supported
*/
@Throws(PasswordNotHumanReadable::class, UnsupportedOption::class)
fun withPassword(password: String): Decrypt
/**
* Adds one or more decryption key.
*
* @param key input stream containing the key(s)
* @return builder instance
* @throws BadData if the [InputStream] does not provide an OpenPGP key
* @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm
* @throws IOException in case of an IO error
*/
@Throws(BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class)
fun withKey(key: InputStream): Decrypt
/**
* Adds one or more decryption key.
*
* @param key byte array containing the key(s)
* @return builder instance
* @throws BadData if the byte array does not contain an OpenPGP key
* @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm
* @throws IOException in case of an IO error
*/
@Throws(BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class)
fun withKey(key: ByteArray): Decrypt = withKey(key.inputStream())
/**
* Provide the decryption password for the secret key.
*
* @param password password
* @return builder instance
* @throws UnsupportedOption if the implementation does not support key passwords
* @throws PasswordNotHumanReadable if the password is not human-readable
*/
@Throws(UnsupportedOption::class, PasswordNotHumanReadable::class)
fun withKeyPassword(password: String): Decrypt =
withKeyPassword(password.toByteArray(UTF8Util.UTF8))
/**
* Provide the decryption password for the secret key.
*
* @param password password
* @return builder instance
* @throws UnsupportedOption if the implementation does not support key passwords
* @throws PasswordNotHumanReadable if the password is not human-readable
*/
@Throws(UnsupportedOption::class, PasswordNotHumanReadable::class)
fun withKeyPassword(password: ByteArray): Decrypt
/**
* Decrypts the given ciphertext, returning verification results and plaintext.
*
* @param ciphertext ciphertext
* @return ready with result
* @throws BadData if the [InputStream] does not provide an OpenPGP message
* @throws MissingArg if an argument required for decryption was not provided
* @throws CannotDecrypt in case decryption fails for some reason
* @throws KeyIsProtected if the decryption key cannot be unlocked (e.g. missing passphrase)
* @throws IOException in case of an IO error
*/
@Throws(
BadData::class,
MissingArg::class,
CannotDecrypt::class,
KeyIsProtected::class,
IOException::class)
fun ciphertext(ciphertext: InputStream): ReadyWithResult<DecryptionResult>
/**
* Decrypts the given ciphertext, returning verification results and plaintext.
*
* @param ciphertext ciphertext
* @return ready with result
* @throws BadData if the byte array does not contain an encrypted OpenPGP message
* @throws MissingArg in case of missing decryption method (password or key required)
* @throws CannotDecrypt in case decryption fails for some reason
* @throws KeyIsProtected if the decryption key cannot be unlocked (e.g. missing passphrase)
* @throws IOException in case of an IO error
*/
@Throws(
BadData::class,
MissingArg::class,
CannotDecrypt::class,
KeyIsProtected::class,
IOException::class)
fun ciphertext(ciphertext: ByteArray): ReadyWithResult<DecryptionResult> =
ciphertext(ciphertext.inputStream())
}

View file

@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.ReadyWithResult
import sop.SigningResult
import sop.enums.SignAs
import sop.exception.SOPGPException.*
interface DetachedSign : AbstractSign<DetachedSign> {
/**
* Sets the signature mode. Note: This method has to be called before [key] is called.
*
* @param mode signature mode
* @return builder instance
* @throws UnsupportedOption if this option is not supported
*/
@Throws(UnsupportedOption::class) fun mode(mode: SignAs): DetachedSign
/**
* Signs data.
*
* @param data input stream containing data
* @return ready
* @throws IOException in case of an IO error
* @throws KeyIsProtected if at least one signing key cannot be unlocked
* @throws ExpectedText if text data was expected, but binary data was encountered
*/
@Throws(IOException::class, KeyIsProtected::class, ExpectedText::class)
fun data(data: InputStream): ReadyWithResult<SigningResult>
/**
* Signs data.
*
* @param data byte array containing data
* @return ready
* @throws IOException in case of an IO error
* @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be
* unlocked
* @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data
* was encountered
*/
@Throws(IOException::class, KeyIsProtected::class, ExpectedText::class)
fun data(data: ByteArray): ReadyWithResult<SigningResult> = data(data.inputStream())
}

View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.exception.SOPGPException.BadData
interface DetachedVerify : AbstractVerify<DetachedVerify>, VerifySignatures {
/**
* Provides the detached signatures.
*
* @param signatures input stream containing encoded, detached signatures.
* @return builder instance
* @throws BadData if the input stream does not contain OpenPGP signatures
* @throws IOException in case of an IO error
*/
@Throws(BadData::class, IOException::class)
fun signatures(signatures: InputStream): VerifySignatures
/**
* Provides the detached signatures.
*
* @param signatures byte array containing encoded, detached signatures.
* @return builder instance
* @throws BadData if the byte array does not contain OpenPGP signatures
* @throws IOException in case of an IO error
*/
@Throws(BadData::class, IOException::class)
fun signatures(signatures: ByteArray): VerifySignatures = signatures(signatures.inputStream())
}

View file

@ -0,0 +1,165 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.Profile
import sop.Ready
import sop.enums.EncryptAs
import sop.exception.SOPGPException.*
import sop.util.UTF8Util
interface Encrypt {
/**
* Disable ASCII armor encoding.
*
* @return builder instance
*/
fun noArmor(): Encrypt
/**
* Sets encryption mode.
*
* @param mode mode
* @return builder instance
* @throws UnsupportedOption if this option is not supported
*/
@Throws(UnsupportedOption::class) fun mode(mode: EncryptAs): Encrypt
/**
* Adds the signer key.
*
* @param key input stream containing the encoded signer key
* @return builder instance
* @throws KeyCannotSign if the key cannot be used for signing
* @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm
* @throws BadData if the [InputStream] does not contain an OpenPGP key
* @throws IOException in case of an IO error
*/
@Throws(
KeyCannotSign::class, UnsupportedAsymmetricAlgo::class, BadData::class, IOException::class)
fun signWith(key: InputStream): Encrypt
/**
* Adds the signer key.
*
* @param key byte array containing the encoded signer key
* @return builder instance
* @throws KeyCannotSign if the key cannot be used for signing
* @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm
* @throws BadData if the byte array does not contain an OpenPGP key
* @throws IOException in case of an IO error
*/
@Throws(
KeyCannotSign::class, UnsupportedAsymmetricAlgo::class, BadData::class, IOException::class)
fun signWith(key: ByteArray): Encrypt = signWith(key.inputStream())
/**
* Provide the password for the secret key used for signing.
*
* @param password password
* @return builder instance
* @throws PasswordNotHumanReadable if the password is not human-readable
* @throws UnsupportedOption if key password are not supported
*/
@Throws(PasswordNotHumanReadable::class, UnsupportedOption::class)
fun withKeyPassword(password: String): Encrypt =
withKeyPassword(password.toByteArray(UTF8Util.UTF8))
/**
* Provide the password for the secret key used for signing.
*
* @param password password
* @return builder instance
* @throws PasswordNotHumanReadable if the password is not human-readable
* @throws UnsupportedOption if key password are not supported
*/
@Throws(PasswordNotHumanReadable::class, UnsupportedOption::class)
fun withKeyPassword(password: ByteArray): Encrypt
/**
* Encrypt with the given password.
*
* @param password password
* @return builder instance
* @throws PasswordNotHumanReadable if the password is not human-readable
* @throws UnsupportedOption if this option is not supported
*/
@Throws(PasswordNotHumanReadable::class, UnsupportedOption::class)
fun withPassword(password: String): Encrypt
/**
* Encrypt with the given cert.
*
* @param cert input stream containing the encoded cert.
* @return builder instance
* @throws CertCannotEncrypt if the certificate is not encryption capable
* @throws UnsupportedAsymmetricAlgo if the certificate uses an unsupported asymmetric algorithm
* @throws BadData if the [InputStream] does not contain an OpenPGP certificate
* @throws IOException in case of an IO error
*/
@Throws(
CertCannotEncrypt::class,
UnsupportedAsymmetricAlgo::class,
BadData::class,
IOException::class)
fun withCert(cert: InputStream): Encrypt
/**
* Encrypt with the given cert.
*
* @param cert byte array containing the encoded cert.
* @return builder instance
* @throws CertCannotEncrypt if the certificate is not encryption capable
* @throws UnsupportedAsymmetricAlgo if the certificate uses an unsupported asymmetric algorithm
* @throws BadData if the byte array does not contain an OpenPGP certificate
* @throws IOException in case of an IO error
*/
@Throws(
CertCannotEncrypt::class,
UnsupportedAsymmetricAlgo::class,
BadData::class,
IOException::class)
fun withCert(cert: ByteArray): Encrypt = withCert(cert.inputStream())
/**
* Pass in a profile.
*
* @param profile profile
* @return builder instance
*/
fun profile(profile: Profile): Encrypt = profile(profile.name)
/**
* Pass in a profile identifier.
*
* @param profileName profile identifier
* @return builder instance
*/
fun profile(profileName: String): Encrypt
/**
* Encrypt the given data yielding the ciphertext.
*
* @param plaintext plaintext
* @return input stream containing the ciphertext
* @throws IOException in case of an IO error
* @throws KeyIsProtected if at least one signing key cannot be unlocked
*/
@Throws(IOException::class, KeyIsProtected::class) fun plaintext(plaintext: InputStream): Ready
/**
* Encrypt the given data yielding the ciphertext.
*
* @param plaintext plaintext
* @return input stream containing the ciphertext
* @throws IOException in case of an IO error
* @throws KeyIsProtected if at least one signing key cannot be unlocked
*/
@Throws(IOException::class, KeyIsProtected::class)
fun plaintext(plaintext: ByteArray): Ready = plaintext(plaintext.inputStream())
}

View file

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.Ready
import sop.exception.SOPGPException.BadData
interface ExtractCert {
/**
* Disable ASCII armor encoding.
*
* @return builder instance
*/
fun noArmor(): ExtractCert
/**
* Extract the cert(s) from the provided key(s).
*
* @param keyInputStream input stream containing the encoding of one or more OpenPGP keys
* @return result containing the encoding of the keys certs
* @throws IOException in case of an IO error
* @throws BadData if the [InputStream] does not contain an OpenPGP key
*/
@Throws(IOException::class, BadData::class) fun key(keyInputStream: InputStream): Ready
/**
* Extract the cert(s) from the provided key(s).
*
* @param key byte array containing the encoding of one or more OpenPGP key
* @return result containing the encoding of the keys certs
* @throws IOException in case of an IO error
* @throws BadData if the byte array does not contain an OpenPGP key
*/
@Throws(IOException::class, BadData::class)
fun key(key: ByteArray): Ready = key(key.inputStream())
}

View file

@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import sop.Profile
import sop.Ready
import sop.exception.SOPGPException.*
import sop.util.UTF8Util
interface GenerateKey {
/**
* Disable ASCII armor encoding.
*
* @return builder instance
*/
fun noArmor(): GenerateKey
/**
* Adds a user-id.
*
* @param userId user-id
* @return builder instance
*/
fun userId(userId: String): GenerateKey
/**
* Set a password for the key.
*
* @param password password to protect the key
* @return builder instance
* @throws UnsupportedOption if key passwords are not supported
* @throws PasswordNotHumanReadable if the password is not human-readable
*/
@Throws(PasswordNotHumanReadable::class, UnsupportedOption::class)
fun withKeyPassword(password: String): GenerateKey
/**
* Set a password for the key.
*
* @param password password to protect the key
* @return builder instance
* @throws PasswordNotHumanReadable if the password is not human-readable
* @throws UnsupportedOption if key passwords are not supported
*/
@Throws(PasswordNotHumanReadable::class, UnsupportedOption::class)
fun withKeyPassword(password: ByteArray): GenerateKey =
try {
withKeyPassword(UTF8Util.decodeUTF8(password))
} catch (e: CharacterCodingException) {
throw PasswordNotHumanReadable()
}
/**
* Pass in a profile.
*
* @param profile profile
* @return builder instance
*/
fun profile(profile: Profile): GenerateKey = profile(profile.name)
/**
* Pass in a profile identifier.
*
* @param profile profile identifier
* @return builder instance
*/
fun profile(profile: String): GenerateKey
/**
* If this options is set, the generated key will not be capable of encryption / decryption.
*
* @return builder instance
*/
fun signingOnly(): GenerateKey
/**
* Generate the OpenPGP key and return it encoded as an [InputStream].
*
* @return key
* @throws MissingArg if no user-id was provided
* @throws UnsupportedAsymmetricAlgo if the generated key uses an unsupported asymmetric
* algorithm
* @throws IOException in case of an IO error
*/
@Throws(MissingArg::class, UnsupportedAsymmetricAlgo::class, IOException::class)
fun generate(): Ready
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.ReadyWithResult
import sop.Signatures
import sop.exception.SOPGPException.BadData
interface InlineDetach {
/**
* Do not wrap the signatures in ASCII armor.
*
* @return builder
*/
fun noArmor(): InlineDetach
/**
* Detach the provided signed message from its signatures.
*
* @param messageInputStream input stream containing the signed message
* @return result containing the detached message
* @throws IOException in case of an IO error
* @throws BadData if the input stream does not contain a signed message
*/
@Throws(IOException::class, BadData::class)
fun message(messageInputStream: InputStream): ReadyWithResult<Signatures>
/**
* Detach the provided cleartext signed message from its signatures.
*
* @param message byte array containing the signed message
* @return result containing the detached message
* @throws IOException in case of an IO error
* @throws BadData if the byte array does not contain a signed message
*/
@Throws(IOException::class, BadData::class)
fun message(message: ByteArray): ReadyWithResult<Signatures> = message(message.inputStream())
}

View file

@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.Ready
import sop.enums.InlineSignAs
import sop.exception.SOPGPException.*
interface InlineSign : AbstractSign<InlineSign> {
/**
* Sets the signature mode. Note: This method has to be called before [.key] is called.
*
* @param mode signature mode
* @return builder instance
* @throws UnsupportedOption if this option is not supported
*/
@Throws(UnsupportedOption::class) fun mode(mode: InlineSignAs): InlineSign
/**
* Signs data.
*
* @param data input stream containing data
* @return ready
* @throws IOException in case of an IO error
* @throws KeyIsProtected if at least one signing key cannot be unlocked
* @throws ExpectedText if text data was expected, but binary data was encountered
*/
@Throws(IOException::class, KeyIsProtected::class, ExpectedText::class)
fun data(data: InputStream): Ready
/**
* Signs data.
*
* @param data byte array containing data
* @return ready
* @throws IOException in case of an IO error
* @throws KeyIsProtected if at least one signing key cannot be unlocked
* @throws ExpectedText if text data was expected, but binary data was encountered
*/
@Throws(IOException::class, KeyIsProtected::class, ExpectedText::class)
fun data(data: ByteArray): Ready = data(data.inputStream())
}

View file

@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.ReadyWithResult
import sop.Verification
import sop.exception.SOPGPException.BadData
import sop.exception.SOPGPException.NoSignature
/** API for verification of inline-signed messages. */
interface InlineVerify : AbstractVerify<InlineVerify> {
/**
* Provide the inline-signed data. The result can be used to write the plaintext message out and
* to get the verifications.
*
* @param data signed data
* @return list of signature verifications
* @throws IOException in case of an IO error
* @throws NoSignature when no signature is found
* @throws BadData when the data is invalid OpenPGP data
*/
@Throws(IOException::class, NoSignature::class, BadData::class)
fun data(data: InputStream): ReadyWithResult<List<Verification>>
/**
* Provide the inline-signed data. The result can be used to write the plaintext message out and
* to get the verifications.
*
* @param data signed data
* @return list of signature verifications
* @throws IOException in case of an IO error
* @throws NoSignature when no signature is found
* @throws BadData when the data is invalid OpenPGP data
*/
@Throws(IOException::class, NoSignature::class, BadData::class)
fun data(data: ByteArray): ReadyWithResult<List<Verification>> = data(data.inputStream())
}

View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import sop.Profile
/** Subcommand to list supported profiles of other subcommands. */
interface ListProfiles {
/**
* Provide the name of the subcommand for which profiles shall be listed. The returned list of
* profiles MUST NOT contain more than 4 entries.
*
* @param command command name (e.g. `generate-key`)
* @return list of profiles.
*/
fun subcommand(command: String): List<Profile>
/**
* Return a list of [Profiles][Profile] supported by the [GenerateKey] implementation.
*
* @return profiles
*/
fun generateKey(): List<Profile> = subcommand("generate-key")
/**
* Return a list of [Profiles][Profile] supported by the [Encrypt] implementation.
*
* @return profiles
*/
fun encrypt(): List<Profile> = subcommand("encrypt")
}

View file

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.InputStream
import sop.Ready
import sop.exception.SOPGPException.PasswordNotHumanReadable
import sop.exception.SOPGPException.UnsupportedOption
import sop.util.UTF8Util
interface RevokeKey {
/**
* Disable ASCII armor encoding.
*
* @return builder instance
*/
fun noArmor(): RevokeKey
/**
* Provide the decryption password for the secret key.
*
* @param password password
* @return builder instance
* @throws UnsupportedOption if the implementation does not support key passwords
* @throws PasswordNotHumanReadable if the password is not human-readable
*/
@Throws(UnsupportedOption::class, PasswordNotHumanReadable::class)
fun withKeyPassword(password: String): RevokeKey =
withKeyPassword(password.toByteArray(UTF8Util.UTF8))
/**
* Provide the decryption password for the secret key.
*
* @param password password
* @return builder instance
* @throws UnsupportedOption if the implementation does not support key passwords
* @throws PasswordNotHumanReadable if the password is not human-readable
*/
@Throws(UnsupportedOption::class, PasswordNotHumanReadable::class)
fun withKeyPassword(password: ByteArray): RevokeKey
fun keys(bytes: ByteArray): Ready = keys(bytes.inputStream())
fun keys(keys: InputStream): Ready
}

View file

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.Verification
import sop.exception.SOPGPException.BadData
import sop.exception.SOPGPException.NoSignature
interface VerifySignatures {
/**
* Provide the signed data (without signatures).
*
* @param data signed data
* @return list of signature verifications
* @throws IOException in case of an IO error
* @throws NoSignature when no valid signature is found
* @throws BadData when the data is invalid OpenPGP data
*/
@Throws(IOException::class, NoSignature::class, BadData::class)
fun data(data: InputStream): List<Verification>
/**
* Provide the signed data (without signatures).
*
* @param data signed data
* @return list of signature verifications
* @throws IOException in case of an IO error
* @throws NoSignature when no valid signature is found
* @throws BadData when the data is invalid OpenPGP data
*/
@Throws(IOException::class, NoSignature::class, BadData::class)
fun data(data: ByteArray): List<Verification> = data(data.inputStream())
}

View file

@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
interface Version {
/**
* Return the implementations name. e.g. `SOP`,
*
* @return implementation name
*/
fun getName(): String
/**
* Return the implementations short version string. e.g. `1.0`
*
* @return version string
*/
fun getVersion(): String
/**
* Return version information about the used OpenPGP backend. e.g. `Bouncycastle 1.70`
*
* @return backend version string
*/
fun getBackendVersion(): String
/**
* Return an extended version string containing multiple lines of version information. The first
* line MUST match the information produced by [getName] and [getVersion], but the rest of the
* text has no defined structure. Example:
* ```
* "SOP 1.0
* Awesome PGP!
* Using Bouncycastle 1.70
* LibFoo 1.2.2
* See https://pgp.example.org/sop/ for more information"
* ```
*
* @return extended version string
*/
fun getExtendedVersion(): String
/**
* Return the revision of the SOP specification that this implementation is implementing, for
* example, `draft-dkg-openpgp-stateless-cli-06`. If the implementation targets a specific draft
* but the implementer knows the implementation is incomplete, it should prefix the draft title
* with a `~` (TILDE, U+007E), for example: `~draft-dkg-openpgp-stateless-cli-06`. The
* implementation MAY emit additional text about its relationship to the targeted draft on the
* lines following the versioned title.
*
* @return implemented SOP spec version
*/
fun getSopSpecVersion(): String {
return buildString {
if (isSopSpecImplementationIncomplete()) append('~')
append(getSopSpecRevisionName())
if (getSopSpecImplementationRemarks() != null) {
append('\n')
append('\n')
append(getSopSpecImplementationRemarks())
}
}
}
/**
* Return the version number of the latest targeted SOP spec revision.
*
* @return SOP spec revision number
*/
fun getSopSpecRevisionNumber(): Int
/**
* Return the name of the latest targeted revision of the SOP spec.
*
* @return SOP spec revision string
*/
fun getSopSpecRevisionName(): String = buildString {
append("draft-dkg-openpgp-stateless-cli-")
append(String.format("%02d", getSopSpecRevisionNumber()))
}
/**
* Return <pre>true</pre>, if this implementation of the SOP spec is known to be incomplete or
* defective.
*
* @return true if incomplete, false otherwise
*/
fun isSopSpecImplementationIncomplete(): Boolean
/**
* Return free-form text containing remarks about the completeness of the SOP implementation. If
* there are no remarks, this method returns <pre>null</pre>.
*
* @return remarks or null
*/
fun getSopSpecImplementationRemarks(): String?
}

View file

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.util
class HexUtil {
companion object {
/**
* Encode a byte array to a hex string.
*
* @param bytes bytes
* @return hex encoding
* @see
* [Convert Byte Arrays to Hex Strings in Kotlin](https://www.baeldung.com/kotlin/byte-arrays-to-hex-strings)
*/
@JvmStatic fun bytesToHex(bytes: ByteArray): String = bytes.toHex()
/**
* Decode a hex string into a byte array.
*
* @param s hex string
* @return decoded byte array
* @see
* [Kotlin convert hex string to ByteArray](https://stackoverflow.com/a/66614516/11150851)
*/
@JvmStatic fun hexToBytes(s: String): ByteArray = s.decodeHex()
}
}
fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Hex encoding must have even number of digits." }
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02X".format(eachByte) }

View file

@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.util
/**
* Backport of java.util.Optional for older Android versions.
*
* @param <T> item type
*/
data class Optional<T>(val item: T? = null) {
val isPresent: Boolean = item != null
val isEmpty: Boolean = item == null
fun get() = item
companion object {
@JvmStatic fun <T> of(item: T) = Optional(item!!)
@JvmStatic fun <T> ofNullable(item: T?) = Optional(item)
@JvmStatic fun <T> ofEmpty() = Optional(null as T?)
}
}

View file

@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.util
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.OutputStream
/**
* [OutputStream] that buffers data being written into it, until its underlying output stream is
* being replaced. At that point, first all the buffered data is being written to the underlying
* stream, followed by any successive data that may get written to the [ProxyOutputStream]. This
* class is useful if we need to provide an [OutputStream] at one point in time when the final
* target output stream is not yet known.
*/
class ProxyOutputStream {
private val buffer = ByteArrayOutputStream()
private var swapped: OutputStream? = null
@Synchronized
fun replaceOutputStream(underlying: OutputStream) {
this.swapped = underlying
swapped!!.write(buffer.toByteArray())
}
@Synchronized
@Throws(IOException::class)
fun write(b: ByteArray) {
if (swapped == null) {
buffer.write(b)
} else {
swapped!!.write(b)
}
}
@Synchronized
@Throws(IOException::class)
fun write(b: ByteArray, off: Int, len: Int) {
if (swapped == null) {
buffer.write(b, off, len)
} else {
swapped!!.write(b, off, len)
}
}
@Synchronized
@Throws(IOException::class)
fun flush() {
buffer.flush()
if (swapped != null) {
swapped!!.flush()
}
}
@Synchronized
@Throws(IOException::class)
fun close() {
buffer.close()
if (swapped != null) {
swapped!!.close()
}
}
@Synchronized
@Throws(IOException::class)
fun write(i: Int) {
if (swapped == null) {
buffer.write(i)
} else {
swapped!!.write(i)
}
}
}

View file

@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.util
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
class UTCUtil {
companion object {
@JvmField val UTC_FORMATTER = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
@JvmField
val UTC_PARSERS =
arrayOf(
UTC_FORMATTER,
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"),
SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"),
SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"))
.onEach { fmt -> fmt.timeZone = TimeZone.getTimeZone("UTC") }
/**
* Parse an ISO-8601 UTC timestamp from a string.
*
* @param dateString string
* @return date
* @throws ParseException if the date string is malformed and cannot be parsed
*/
@JvmStatic
@Throws(ParseException::class)
fun parseUTCDate(dateString: String): Date {
var exception: ParseException? = null
for (parser in UTC_PARSERS) {
try {
return parser.parse(dateString)
} catch (e: ParseException) {
// Store first exception (that of UTC_FORMATTER) to throw if we fail to parse
// the date
if (exception == null) {
exception = e
}
// Try next parser
}
}
throw exception!!
}
/**
* Format a date as ISO-8601 UTC timestamp.
*
* @param date date
* @return timestamp string
*/
@JvmStatic
fun formatUTCDate(date: Date): String {
return UTC_FORMATTER.format(date)
}
}
}

View file

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.util
import java.nio.ByteBuffer
import java.nio.charset.Charset
import java.nio.charset.CodingErrorAction
class UTF8Util {
companion object {
@JvmField val UTF8: Charset = Charset.forName("UTF8")
@JvmStatic
private val UTF8Decoder =
UTF8.newDecoder()
.onUnmappableCharacter(CodingErrorAction.REPORT)
.onMalformedInput(CodingErrorAction.REPORT)
/**
* Detect non-valid UTF8 data.
*
* @param data utf-8 encoded bytes
* @return decoded string
* @throws CharacterCodingException if the input data does not resemble UTF8
* @see [ante on StackOverflow](https://stackoverflow.com/a/1471193)
*/
@JvmStatic
@Throws(CharacterCodingException::class)
fun decodeUTF8(data: ByteArray): String {
val byteBuffer = ByteBuffer.wrap(data)
val charBuffer = UTF8Decoder.decode(byteBuffer)
return charBuffer.toString()
}
}
}

View file

@ -18,7 +18,7 @@ public class MicAlgTest {
@Test
public void constructorNullArgThrows() {
assertThrows(IllegalArgumentException.class, () -> new MicAlg(null));
assertThrows(NullPointerException.class, () -> new MicAlg(null));
}
@Test

View file

@ -62,7 +62,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
byte[] signature = sop.detachedSign()
.key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.mode(SignAs.Text)
.mode(SignAs.text)
.data(message)
.toByteArrayAndResult()
.getBytes();

View file

@ -142,7 +142,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
byte[] ciphertext = sop.encrypt()
.withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.mode(EncryptAs.Binary)
.mode(EncryptAs.binary)
.plaintext(message)
.getBytes();
@ -173,7 +173,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
byte[] ciphertext = sop.encrypt()
.withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.mode(EncryptAs.Text)
.mode(EncryptAs.text)
.plaintext(message)
.getBytes();