Add new implementations

This commit is contained in:
Paul Schaub 2025-07-01 14:30:22 +02:00
parent b54ba8ac1e
commit 43fdac4ac5
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
12 changed files with 518 additions and 18 deletions

View file

@ -4,6 +4,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.api.OpenPGPApi; import org.bouncycastle.openpgp.api.OpenPGPApi;
import org.bouncycastle.openpgp.api.bc.BcOpenPGPApi; import org.bouncycastle.openpgp.api.bc.BcOpenPGPApi;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.pgpainless.bouncycastle.sop.operation.BCArmor; import org.pgpainless.bouncycastle.sop.operation.BCArmor;
import org.pgpainless.bouncycastle.sop.operation.BCDearmor; import org.pgpainless.bouncycastle.sop.operation.BCDearmor;
import org.pgpainless.bouncycastle.sop.operation.BCDecrypt; import org.pgpainless.bouncycastle.sop.operation.BCDecrypt;
@ -15,10 +16,12 @@ import org.pgpainless.bouncycastle.sop.operation.BCGenerateKey;
import org.pgpainless.bouncycastle.sop.operation.BCInlineSign; import org.pgpainless.bouncycastle.sop.operation.BCInlineSign;
import org.pgpainless.bouncycastle.sop.operation.BCInlineVerify; import org.pgpainless.bouncycastle.sop.operation.BCInlineVerify;
import org.pgpainless.bouncycastle.sop.operation.BCListProfiles; import org.pgpainless.bouncycastle.sop.operation.BCListProfiles;
import org.pgpainless.bouncycastle.sop.operation.BCMergeCerts;
import org.pgpainless.bouncycastle.sop.operation.BCVersion; import org.pgpainless.bouncycastle.sop.operation.BCVersion;
import sop.SOP; import sop.SOP;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.operation.Armor; import sop.operation.Armor;
import sop.operation.CertifyUserId;
import sop.operation.ChangeKeyPassword; import sop.operation.ChangeKeyPassword;
import sop.operation.Dearmor; import sop.operation.Dearmor;
import sop.operation.Decrypt; import sop.operation.Decrypt;
@ -31,7 +34,10 @@ import sop.operation.InlineDetach;
import sop.operation.InlineSign; import sop.operation.InlineSign;
import sop.operation.InlineVerify; import sop.operation.InlineVerify;
import sop.operation.ListProfiles; import sop.operation.ListProfiles;
import sop.operation.MergeCerts;
import sop.operation.RevokeKey; import sop.operation.RevokeKey;
import sop.operation.UpdateKey;
import sop.operation.ValidateUserId;
import sop.operation.Version; import sop.operation.Version;
import java.security.Security; import java.security.Security;
@ -136,4 +142,28 @@ public class BouncyCastleSOP implements SOP {
public InlineVerify inlineVerify() { public InlineVerify inlineVerify() {
return new BCInlineVerify(api); return new BCInlineVerify(api);
} }
@Nullable
@Override
public CertifyUserId certifyUserId() {
throw new SOPGPException.UnsupportedSubcommand("certify-userid is not implemented.");
}
@Nullable
@Override
public UpdateKey updateKey() {
throw new SOPGPException.UnsupportedSubcommand("update-key is not implemented.");
}
@Nullable
@Override
public MergeCerts mergeCerts() {
return new BCMergeCerts(api);
}
@Nullable
@Override
public ValidateUserId validateUserId() {
throw new SOPGPException.UnsupportedSubcommand("validate-userid is not implemented.");
}
} }

View file

@ -0,0 +1,74 @@
package org.pgpainless.bouncycastle.sop.operation;
import org.bouncycastle.openpgp.api.KeyPassphraseProvider;
import org.bouncycastle.openpgp.api.OpenPGPApi;
import org.bouncycastle.util.io.Streams;
import org.jetbrains.annotations.NotNull;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.operation.CertifyUserId;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Set;
public class BCCertifyUserId
extends AbstractBCOperation
implements CertifyUserId {
private final KeyPassphraseProvider.DefaultKeyPassphraseProvider passphraseProvider =
new KeyPassphraseProvider.DefaultKeyPassphraseProvider();
private boolean armor = true;
private boolean requireSelfSig = true;
private final Set<String> userIds = new HashSet<>();
public BCCertifyUserId(OpenPGPApi api) {
super(api);
}
@NotNull
@Override
public Ready certs(@NotNull InputStream inputStream) throws SOPGPException.BadData, IOException, SOPGPException.CertUserIdNoMatch {
return new Ready() {
@Override
public void writeTo(@NotNull OutputStream outputStream) throws IOException {
Streams.pipeAll(inputStream, outputStream);
}
};
}
@NotNull
@Override
public CertifyUserId noArmor() throws SOPGPException.UnsupportedOption {
this.armor = false;
return this;
}
@NotNull
@Override
public CertifyUserId userId(@NotNull String s) throws SOPGPException.UnsupportedOption {
this.userIds.add(s.trim());
return this;
}
@NotNull
@Override
public CertifyUserId withKeyPassword(@NotNull byte[] bytes) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
passphraseProvider.addPassphrase(new String(bytes).toCharArray());
return this;
}
@NotNull
@Override
public CertifyUserId noRequireSelfSig() throws SOPGPException.UnsupportedOption {
this.requireSelfSig = false;
return this;
}
@NotNull
@Override
public CertifyUserId keys(@NotNull InputStream inputStream) throws SOPGPException.BadData, IOException, SOPGPException.KeyIsProtected {
return this;
}
}

View file

@ -7,6 +7,8 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.api.OpenPGPApi; import org.bouncycastle.openpgp.api.OpenPGPApi;
import org.bouncycastle.openpgp.api.OpenPGPCertificate;
import org.bouncycastle.openpgp.api.OpenPGPKey;
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import sop.Ready; import sop.Ready;
@ -16,6 +18,7 @@ import sop.operation.ExtractCert;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@ -33,29 +36,32 @@ public class BCExtractCert
@NotNull @NotNull
@Override @Override
public Ready key(@NotNull InputStream inputStream) throws IOException, SOPGPException.BadData { public Ready key(@NotNull InputStream inputStream) throws IOException, SOPGPException.BadData {
InputStream decodeIn = PGPUtil.getDecoderStream(inputStream); List<OpenPGPKey> keys = api.readKeyOrCertificate().parseKeys(inputStream);
PGPObjectFactory objFac = new BcPGPObjectFactory(decodeIn); List<OpenPGPCertificate> certs = new ArrayList<>();
PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) objFac.nextObject(); for (OpenPGPKey key : keys) {
certs.add(key.toCertificate());
List<PGPPublicKey> list = new ArrayList<>();
Iterator<PGPPublicKey> iterator = secretKeys.getPublicKeys();
while (iterator.hasNext()) {
list.add(iterator.next());
} }
PGPPublicKeyRing publicKeys = new PGPPublicKeyRing(list);
return new Ready() { return new Ready() {
@Override @Override
public void writeTo(@NotNull OutputStream outputStream) throws IOException { public void writeTo(@NotNull OutputStream outputStream) throws IOException {
if (armor) { if (armor) {
ArmoredOutputStream aOut = ArmoredOutputStream.builder() if (certs.size() == 1) {
.clearHeaders() outputStream.write(certs.get(0).toAsciiArmoredString().getBytes(StandardCharsets.UTF_8));
.enableCRC(true) } else {
.build(outputStream); ArmoredOutputStream aOut = ArmoredOutputStream.builder()
publicKeys.encode(aOut); .clearHeaders()
aOut.close(); .enableCRC(true)
.build(outputStream);
for (OpenPGPCertificate cert : certs) {
aOut.write(cert.getEncoded());
}
aOut.close();
}
} else { } else {
publicKeys.encode(outputStream); for (OpenPGPCertificate cert : certs) {
outputStream.write(cert.getEncoded());
}
} }
} }
}; };

View file

@ -3,6 +3,7 @@ package org.pgpainless.bouncycastle.sop.operation;
import org.bouncycastle.openpgp.api.OpenPGPApi; import org.bouncycastle.openpgp.api.OpenPGPApi;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import sop.Profile; import sop.Profile;
import sop.exception.SOPGPException;
import sop.operation.ListProfiles; import sop.operation.ListProfiles;
import java.util.Collections; import java.util.Collections;
@ -22,7 +23,9 @@ public class BCListProfiles
switch (s) { switch (s) {
case "generate-key": case "generate-key":
return BCGenerateKey.PROFILES; return BCGenerateKey.PROFILES;
case "encrypt":
return Collections.emptyList();
} }
return Collections.emptyList(); throw new SOPGPException.UnsupportedProfile(s);
} }
} }

View file

@ -0,0 +1,105 @@
package org.pgpainless.bouncycastle.sop.operation;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.KeyIdentifier;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.api.OpenPGPApi;
import org.bouncycastle.openpgp.api.OpenPGPCertificate;
import org.jetbrains.annotations.NotNull;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.operation.MergeCerts;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class BCMergeCerts
extends AbstractBCOperation
implements MergeCerts {
private List<OpenPGPCertificate> updates = new ArrayList<>();
private boolean armor = true;
public BCMergeCerts(OpenPGPApi api) {
super(api);
}
@NotNull
@Override
public Ready baseCertificates(@NotNull InputStream inputStream) throws SOPGPException.BadData, IOException {
return new Ready() {
@Override
public void writeTo(@NotNull OutputStream outputStream) throws IOException {
Map<KeyIdentifier, OpenPGPCertificate> merged = new HashMap<>();
List<OpenPGPCertificate> baseCerts = api.readKeyOrCertificate().parseCertificates(inputStream);
// Merge base certs
for (OpenPGPCertificate base : baseCerts) {
OpenPGPCertificate existing = merged.get(base.getKeyIdentifier());
if (existing != null) {
try {
existing = OpenPGPCertificate.join(existing, base);
merged.put(existing.getKeyIdentifier(), existing);
} catch (PGPException e) {
throw new RuntimeException(e);
}
} else {
merged.put(base.getKeyIdentifier(), base);
}
}
// Merge updates
for (OpenPGPCertificate update : updates) {
OpenPGPCertificate existing = merged.get(update.getKeyIdentifier());
if (existing != null) {
try {
existing = OpenPGPCertificate.join(existing, update);
merged.put(existing.getKeyIdentifier(), existing);
} catch (PGPException e) {
throw new RuntimeException(e);
}
}
}
// output
if (armor) {
if (merged.size() == 1) {
outputStream.write(merged.values().iterator().next().toAsciiArmoredString().getBytes(StandardCharsets.UTF_8));
} else {
ArmoredOutputStream aOut = ArmoredOutputStream.builder()
.clearHeaders()
.build(outputStream);
for (OpenPGPCertificate cert : merged.values()) {
aOut.write(cert.getEncoded());
}
aOut.close();
}
} else {
for (OpenPGPCertificate cert : merged.values()) {
outputStream.write(cert.getEncoded());
}
}
}
};
}
@NotNull
@Override
public MergeCerts noArmor() throws SOPGPException.UnsupportedOption {
armor = false;
return this;
}
@NotNull
@Override
public MergeCerts updates(@NotNull InputStream inputStream) throws SOPGPException.BadData, IOException {
this.updates.addAll(api.readKeyOrCertificate().parseCertificates(inputStream));
return this;
}
}

View file

@ -0,0 +1,95 @@
package org.pgpainless.bouncycastle.sop.operation;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.api.KeyPassphraseProvider;
import org.bouncycastle.openpgp.api.OpenPGPApi;
import org.bouncycastle.openpgp.api.OpenPGPCertificate;
import org.bouncycastle.openpgp.api.OpenPGPKey;
import org.bouncycastle.openpgp.api.OpenPGPKeyEditor;
import org.bouncycastle.openpgp.api.exception.KeyPassphraseException;
import org.jetbrains.annotations.NotNull;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.operation.RevokeKey;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class BCRevokeKey extends AbstractBCOperation implements RevokeKey {
private boolean armor = true;
private final KeyPassphraseProvider.DefaultKeyPassphraseProvider passphraseProvider =
new KeyPassphraseProvider.DefaultKeyPassphraseProvider();
public BCRevokeKey(OpenPGPApi api) {
super(api);
}
@NotNull
@Override
public Ready keys(@NotNull InputStream inputStream) {
return new Ready() {
@Override
public void writeTo(@NotNull OutputStream outputStream) throws IOException {
List<OpenPGPKey> keys;
try {
keys = api.readKeyOrCertificate().parseKeys(inputStream);
} catch (IOException e) {
throw new SOPGPException.BadData(e);
}
List<OpenPGPCertificate> revoked = new ArrayList<>();
for (OpenPGPKey key : keys) {
try {
OpenPGPKeyEditor editor = api.editKey(key, passphraseProvider);
editor.revokeKey();
revoked.add(editor.done().toCertificate());
} catch (KeyPassphraseException e) {
throw new SOPGPException.KeyIsProtected("Cannot unlock secret key", e);
} catch (PGPException e) {
throw new RuntimeException(e);
}
}
if (armor) {
if (revoked.size() == 1) {
outputStream.write(revoked.get(0).toAsciiArmoredString().getBytes(StandardCharsets.UTF_8));
} else {
ArmoredOutputStream aOut = ArmoredOutputStream.builder()
.clearHeaders()
.build(outputStream);
for (OpenPGPCertificate cert : revoked) {
aOut.write(cert.getEncoded());
}
aOut.close();
}
} else {
for (OpenPGPCertificate cert : revoked) {
outputStream.write(cert.getEncoded());
}
}
}
};
}
@NotNull
@Override
public RevokeKey noArmor() {
armor = false;
return this;
}
@NotNull
@Override
public RevokeKey withKeyPassword(@NotNull byte[] bytes)
throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable {
passphraseProvider.addPassphrase(new String(bytes).toCharArray());
return this;
}
}

View file

@ -0,0 +1,77 @@
package org.pgpainless.bouncycastle.sop.operation;
import org.bouncycastle.openpgp.api.KeyPassphraseProvider;
import org.bouncycastle.openpgp.api.OpenPGPApi;
import org.bouncycastle.util.io.Streams;
import org.jetbrains.annotations.NotNull;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.operation.UpdateKey;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class BCUpdateKey
extends AbstractBCOperation
implements UpdateKey {
private final KeyPassphraseProvider.DefaultKeyPassphraseProvider passphraseProvider =
new KeyPassphraseProvider.DefaultKeyPassphraseProvider();
private boolean armor = true;
private boolean signingOnly = false;
private boolean addCapabilities = true;
public BCUpdateKey(OpenPGPApi api) {
super(api);
}
@NotNull
@Override
public Ready key(@NotNull InputStream inputStream)
throws SOPGPException.BadData, IOException, SOPGPException.KeyIsProtected, SOPGPException.PrimaryKeyBad {
return new Ready() {
@Override
public void writeTo(@NotNull OutputStream outputStream) throws IOException {
Streams.pipeAll(inputStream, outputStream);
}
};
}
@NotNull
@Override
public UpdateKey noArmor() {
armor = false;
return this;
}
@NotNull
@Override
public UpdateKey signingOnly()
throws SOPGPException.UnsupportedOption {
signingOnly = true;
return this;
}
@NotNull
@Override
public UpdateKey noAddedCapabilities()
throws SOPGPException.UnsupportedOption {
addCapabilities = false;
return this;
}
@NotNull
@Override
public UpdateKey withKeyPassword(@NotNull byte[] bytes)
throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
passphraseProvider.addPassphrase(new String(bytes).toCharArray());
return this;
}
@NotNull
@Override
public UpdateKey mergeCerts(@NotNull InputStream inputStream)
throws SOPGPException.UnsupportedOption, SOPGPException.BadData, IOException {
return this;
}
}

View file

@ -0,0 +1,87 @@
package org.pgpainless.bouncycastle.sop.operation;
import org.bouncycastle.openpgp.PGPSignatureException;
import org.bouncycastle.openpgp.api.OpenPGPApi;
import org.bouncycastle.openpgp.api.OpenPGPCertificate;
import org.jetbrains.annotations.NotNull;
import sop.exception.SOPGPException;
import sop.operation.ValidateUserId;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class BCValidateUserId
extends AbstractBCOperation
implements ValidateUserId {
private boolean validateSpecOnly = false;
private Date validateAt = new Date();
private final List<String> userIds = new ArrayList<>();
private final List<OpenPGPCertificate> authorities = new ArrayList<>();
public BCValidateUserId(OpenPGPApi api) {
super(api);
}
@NotNull
@Override
public ValidateUserId addrSpecOnly()
throws SOPGPException.UnsupportedOption {
validateSpecOnly = true;
return this;
}
@NotNull
@Override
public ValidateUserId userId(@NotNull String s) {
userIds.add(s.trim());
return this;
}
@NotNull
@Override
public ValidateUserId authorities(@NotNull InputStream inputStream)
throws SOPGPException.BadData, IOException {
authorities.addAll(api.readKeyOrCertificate().parseCertificates(inputStream));
return this;
}
@Override
public boolean subjects(@NotNull InputStream inputStream)
throws SOPGPException.BadData, IOException, SOPGPException.CertUserIdNoMatch {
List<OpenPGPCertificate> certificates = api.readKeyOrCertificate().parseCertificates(inputStream);
for (OpenPGPCertificate certificate : certificates) {
for (String userId : userIds) {
OpenPGPCertificate.OpenPGPUserId uid = certificate.getUserId(userId);
if (!uid.isBoundAt(validateAt)) {
return false;
}
for (OpenPGPCertificate authority : authorities) {
OpenPGPCertificate.OpenPGPSignatureChain certification = uid.getCertificationBy(authority);
if (certification == null) {
throw new SOPGPException.CertUserIdNoMatch("Could not find certification for UserID '" + userId + "'");
}
try {
if (!certification.isValid()) {
return false;
}
} catch (PGPSignatureException e) {
return false;
}
}
}
}
return true;
}
@NotNull
@Override
public ValidateUserId validateAt(@NotNull Date date) {
validateAt = date;
return this;
}
}

View file

@ -41,7 +41,9 @@ public class BCVersion
@NotNull @NotNull
@Override @Override
public String getExtendedVersion() { public String getExtendedVersion() {
return ""; return getName() + " " + getVersion() + "\n"
+ getBackendVersion() + "\n"
+ "sop-java " + getSopJavaVersion();
} }
@Override @Override

View file

@ -0,0 +1,7 @@
package org.pgpainless.bouncycastle.sop.operation;
import sop.testsuite.operation.CertifyValidateUserIdTest;
public class BCSopCertifyValidateUserIdTest extends CertifyValidateUserIdTest {
}

View file

@ -0,0 +1,7 @@
package org.pgpainless.bouncycastle.sop.operation;
import sop.testsuite.operation.ListProfilesTest;
public class BCSopListProfilesTest extends ListProfilesTest {
}

View file

@ -0,0 +1,7 @@
package org.pgpainless.bouncycastle.sop.operation;
import sop.testsuite.operation.MergeCertsTest;
public class BCSopMergeCertsTest extends MergeCertsTest {
}