mirror of
https://codeberg.org/PGPainless/sop-java.git
synced 2024-12-22 12:57:57 +01:00
Merge pull request #21 from pgpainless/kotlin
Rewrite sop-java in Kotlin
This commit is contained in:
commit
a8829350a8
90 changed files with 2397 additions and 3230 deletions
17
build.gradle
17
build.gradle
|
@ -19,6 +19,8 @@ buildscript {
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'ru.vyarus.animalsniffer' version '1.5.3'
|
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'
|
apply from: 'version.gradle'
|
||||||
|
@ -29,6 +31,8 @@ allprojects {
|
||||||
apply plugin: 'eclipse'
|
apply plugin: 'eclipse'
|
||||||
apply plugin: 'jacoco'
|
apply plugin: 'jacoco'
|
||||||
apply plugin: 'checkstyle'
|
apply plugin: 'checkstyle'
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'com.diffplug.spotless'
|
||||||
|
|
||||||
// For non-cli modules enable android api compatibility check
|
// For non-cli modules enable android api compatibility check
|
||||||
if (it.name.equals('sop-java')) {
|
if (it.name.equals('sop-java')) {
|
||||||
|
@ -53,6 +57,12 @@ allprojects {
|
||||||
toolVersion = '8.18'
|
toolVersion = '8.18'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spotless {
|
||||||
|
kotlin {
|
||||||
|
ktfmt().dropboxStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
group 'org.pgpainless'
|
group 'org.pgpainless'
|
||||||
description = "Stateless OpenPGP Protocol API for Java"
|
description = "Stateless OpenPGP Protocol API for Java"
|
||||||
version = shortVersion
|
version = shortVersion
|
||||||
|
@ -69,6 +79,13 @@ allprojects {
|
||||||
reproducibleFileOrder = true
|
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 {
|
project.ext {
|
||||||
rootConfigDir = new File(rootDir, 'config')
|
rootConfigDir = new File(rootDir, 'config')
|
||||||
gitCommit = getGitCommit()
|
gitCommit = getGitCommit()
|
||||||
|
|
|
@ -38,6 +38,7 @@ application {
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
dependsOn(":sop-java:jar")
|
dependsOn(":sop-java:jar")
|
||||||
|
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
|
||||||
|
|
||||||
manifest {
|
manifest {
|
||||||
attributes 'Main-Class': "$mainClassName"
|
attributes 'Main-Class': "$mainClassName"
|
||||||
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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;
|
|
||||||
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility classes.
|
|
||||||
*/
|
|
||||||
package sop.util;
|
|
43
sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt
Normal file
43
sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
15
sop-java/src/main/kotlin/sop/DecryptionResult.kt
Normal file
15
sop-java/src/main/kotlin/sop/DecryptionResult.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
34
sop-java/src/main/kotlin/sop/MicAlg.kt
Normal file
34
sop-java/src/main/kotlin/sop/MicAlg.kt
Normal 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) }
|
||||||
|
}
|
||||||
|
}
|
87
sop-java/src/main/kotlin/sop/Profile.kt
Normal file
87
sop-java/src/main/kotlin/sop/Profile.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
49
sop-java/src/main/kotlin/sop/Ready.kt
Normal file
49
sop-java/src/main/kotlin/sop/Ready.kt
Normal 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)
|
||||||
|
}
|
41
sop-java/src/main/kotlin/sop/ReadyWithResult.kt
Normal file
41
sop-java/src/main/kotlin/sop/ReadyWithResult.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
97
sop-java/src/main/kotlin/sop/SOP.kt
Normal file
97
sop-java/src/main/kotlin/sop/SOP.kt
Normal 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
|
||||||
|
}
|
48
sop-java/src/main/kotlin/sop/SessionKey.kt
Normal file
48
sop-java/src/main/kotlin/sop/SessionKey.kt
Normal 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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
sop-java/src/main/kotlin/sop/Signatures.kt
Normal file
19
sop-java/src/main/kotlin/sop/Signatures.kt
Normal 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)
|
||||||
|
}
|
28
sop-java/src/main/kotlin/sop/SigningResult.kt
Normal file
28
sop-java/src/main/kotlin/sop/SigningResult.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
76
sop-java/src/main/kotlin/sop/Verification.kt
Normal file
76
sop-java/src/main/kotlin/sop/Verification.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt
Normal file
14
sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt
Normal 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
|
||||||
|
}
|
10
sop-java/src/main/kotlin/sop/enums/EncryptAs.kt
Normal file
10
sop-java/src/main/kotlin/sop/enums/EncryptAs.kt
Normal 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
|
||||||
|
}
|
17
sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt
Normal file
17
sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt
Normal 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
|
||||||
|
}
|
12
sop-java/src/main/kotlin/sop/enums/SignAs.kt
Normal file
12
sop-java/src/main/kotlin/sop/enums/SignAs.kt
Normal 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
|
||||||
|
}
|
18
sop-java/src/main/kotlin/sop/enums/SignatureMode.kt
Normal file
18
sop-java/src/main/kotlin/sop/enums/SignatureMode.kt
Normal 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
|
||||||
|
}
|
308
sop-java/src/main/kotlin/sop/exception/SOPGPException.kt
Normal file
308
sop-java/src/main/kotlin/sop/exception/SOPGPException.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
79
sop-java/src/main/kotlin/sop/operation/AbstractSign.kt
Normal file
79
sop-java/src/main/kotlin/sop/operation/AbstractSign.kt
Normal 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
|
||||||
|
}
|
57
sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt
Normal file
57
sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt
Normal 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())
|
||||||
|
}
|
46
sop-java/src/main/kotlin/sop/operation/Armor.kt
Normal file
46
sop-java/src/main/kotlin/sop/operation/Armor.kt
Normal 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())
|
||||||
|
}
|
95
sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt
Normal file
95
sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt
Normal 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
|
||||||
|
}
|
46
sop-java/src/main/kotlin/sop/operation/Dearmor.kt
Normal file
46
sop-java/src/main/kotlin/sop/operation/Dearmor.kt
Normal 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))
|
||||||
|
}
|
165
sop-java/src/main/kotlin/sop/operation/Decrypt.kt
Normal file
165
sop-java/src/main/kotlin/sop/operation/Decrypt.kt
Normal 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())
|
||||||
|
}
|
50
sop-java/src/main/kotlin/sop/operation/DetachedSign.kt
Normal file
50
sop-java/src/main/kotlin/sop/operation/DetachedSign.kt
Normal 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())
|
||||||
|
}
|
34
sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt
Normal file
34
sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt
Normal 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())
|
||||||
|
}
|
165
sop-java/src/main/kotlin/sop/operation/Encrypt.kt
Normal file
165
sop-java/src/main/kotlin/sop/operation/Encrypt.kt
Normal 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())
|
||||||
|
}
|
41
sop-java/src/main/kotlin/sop/operation/ExtractCert.kt
Normal file
41
sop-java/src/main/kotlin/sop/operation/ExtractCert.kt
Normal 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())
|
||||||
|
}
|
91
sop-java/src/main/kotlin/sop/operation/GenerateKey.kt
Normal file
91
sop-java/src/main/kotlin/sop/operation/GenerateKey.kt
Normal 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
|
||||||
|
}
|
43
sop-java/src/main/kotlin/sop/operation/InlineDetach.kt
Normal file
43
sop-java/src/main/kotlin/sop/operation/InlineDetach.kt
Normal 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())
|
||||||
|
}
|
47
sop-java/src/main/kotlin/sop/operation/InlineSign.kt
Normal file
47
sop-java/src/main/kotlin/sop/operation/InlineSign.kt
Normal 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())
|
||||||
|
}
|
42
sop-java/src/main/kotlin/sop/operation/InlineVerify.kt
Normal file
42
sop-java/src/main/kotlin/sop/operation/InlineVerify.kt
Normal 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())
|
||||||
|
}
|
34
sop-java/src/main/kotlin/sop/operation/ListProfiles.kt
Normal file
34
sop-java/src/main/kotlin/sop/operation/ListProfiles.kt
Normal 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")
|
||||||
|
}
|
48
sop-java/src/main/kotlin/sop/operation/RevokeKey.kt
Normal file
48
sop-java/src/main/kotlin/sop/operation/RevokeKey.kt
Normal 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
|
||||||
|
}
|
38
sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt
Normal file
38
sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt
Normal 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())
|
||||||
|
}
|
100
sop-java/src/main/kotlin/sop/operation/Version.kt
Normal file
100
sop-java/src/main/kotlin/sop/operation/Version.kt
Normal 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?
|
||||||
|
}
|
38
sop-java/src/main/kotlin/sop/util/HexUtil.kt
Normal file
38
sop-java/src/main/kotlin/sop/util/HexUtil.kt
Normal 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) }
|
26
sop-java/src/main/kotlin/sop/util/Optional.kt
Normal file
26
sop-java/src/main/kotlin/sop/util/Optional.kt
Normal 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?)
|
||||||
|
}
|
||||||
|
}
|
75
sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt
Normal file
75
sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
62
sop-java/src/main/kotlin/sop/util/UTCUtil.kt
Normal file
62
sop-java/src/main/kotlin/sop/util/UTCUtil.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
sop-java/src/main/kotlin/sop/util/UTF8Util.kt
Normal file
37
sop-java/src/main/kotlin/sop/util/UTF8Util.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ public class MicAlgTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void constructorNullArgThrows() {
|
public void constructorNullArgThrows() {
|
||||||
assertThrows(IllegalArgumentException.class, () -> new MicAlg(null));
|
assertThrows(NullPointerException.class, () -> new MicAlg(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -62,7 +62,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest {
|
||||||
|
|
||||||
byte[] signature = sop.detachedSign()
|
byte[] signature = sop.detachedSign()
|
||||||
.key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
|
.key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
|
||||||
.mode(SignAs.Text)
|
.mode(SignAs.text)
|
||||||
.data(message)
|
.data(message)
|
||||||
.toByteArrayAndResult()
|
.toByteArrayAndResult()
|
||||||
.getBytes();
|
.getBytes();
|
||||||
|
|
|
@ -142,7 +142,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
|
||||||
byte[] ciphertext = sop.encrypt()
|
byte[] ciphertext = sop.encrypt()
|
||||||
.withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
|
.withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
|
||||||
.signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
|
.signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
|
||||||
.mode(EncryptAs.Binary)
|
.mode(EncryptAs.binary)
|
||||||
.plaintext(message)
|
.plaintext(message)
|
||||||
.getBytes();
|
.getBytes();
|
||||||
|
|
||||||
|
@ -173,7 +173,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
|
||||||
byte[] ciphertext = sop.encrypt()
|
byte[] ciphertext = sop.encrypt()
|
||||||
.withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
|
.withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
|
||||||
.signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
|
.signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
|
||||||
.mode(EncryptAs.Text)
|
.mode(EncryptAs.text)
|
||||||
.plaintext(message)
|
.plaintext(message)
|
||||||
.getBytes();
|
.getBytes();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue