mirror of
https://codeberg.org/PGPainless/sop-java.git
synced 2025-02-08 19:26:25 +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 {
|
||||
id 'ru.vyarus.animalsniffer' version '1.5.3'
|
||||
id 'org.jetbrains.kotlin.jvm' version "1.8.10"
|
||||
id 'com.diffplug.spotless' version '6.22.0' apply false
|
||||
}
|
||||
|
||||
apply from: 'version.gradle'
|
||||
|
@ -29,6 +31,8 @@ allprojects {
|
|||
apply plugin: 'eclipse'
|
||||
apply plugin: 'jacoco'
|
||||
apply plugin: 'checkstyle'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'com.diffplug.spotless'
|
||||
|
||||
// For non-cli modules enable android api compatibility check
|
||||
if (it.name.equals('sop-java')) {
|
||||
|
@ -53,6 +57,12 @@ allprojects {
|
|||
toolVersion = '8.18'
|
||||
}
|
||||
|
||||
spotless {
|
||||
kotlin {
|
||||
ktfmt().dropboxStyle()
|
||||
}
|
||||
}
|
||||
|
||||
group 'org.pgpainless'
|
||||
description = "Stateless OpenPGP Protocol API for Java"
|
||||
version = shortVersion
|
||||
|
@ -69,6 +79,13 @@ allprojects {
|
|||
reproducibleFileOrder = true
|
||||
}
|
||||
|
||||
// Compatibility of default implementations in kotlin interfaces with Java implementations.
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += ["-Xjvm-default=all-compatibility"]
|
||||
}
|
||||
}
|
||||
|
||||
project.ext {
|
||||
rootConfigDir = new File(rootDir, 'config')
|
||||
gitCommit = getGitCommit()
|
||||
|
|
|
@ -38,6 +38,7 @@ application {
|
|||
|
||||
jar {
|
||||
dependsOn(":sop-java:jar")
|
||||
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
|
||||
|
||||
manifest {
|
||||
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);
|
||||
}
|
||||
|
||||