diff --git a/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/BouncyCastleSOP.java b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/BouncyCastleSOP.java index e401493..a103332 100644 --- a/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/BouncyCastleSOP.java +++ b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/BouncyCastleSOP.java @@ -4,6 +4,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.api.OpenPGPApi; import org.bouncycastle.openpgp.api.bc.BcOpenPGPApi; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.pgpainless.bouncycastle.sop.operation.BCArmor; import org.pgpainless.bouncycastle.sop.operation.BCDearmor; 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.BCInlineVerify; import org.pgpainless.bouncycastle.sop.operation.BCListProfiles; +import org.pgpainless.bouncycastle.sop.operation.BCMergeCerts; import org.pgpainless.bouncycastle.sop.operation.BCVersion; import sop.SOP; import sop.exception.SOPGPException; import sop.operation.Armor; +import sop.operation.CertifyUserId; import sop.operation.ChangeKeyPassword; import sop.operation.Dearmor; import sop.operation.Decrypt; @@ -31,7 +34,10 @@ import sop.operation.InlineDetach; import sop.operation.InlineSign; import sop.operation.InlineVerify; import sop.operation.ListProfiles; +import sop.operation.MergeCerts; import sop.operation.RevokeKey; +import sop.operation.UpdateKey; +import sop.operation.ValidateUserId; import sop.operation.Version; import java.security.Security; @@ -136,4 +142,28 @@ public class BouncyCastleSOP implements SOP { public InlineVerify inlineVerify() { 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."); + } } diff --git a/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCCertifyUserId.java b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCCertifyUserId.java new file mode 100644 index 0000000..5a6e74e --- /dev/null +++ b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCCertifyUserId.java @@ -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 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; + } +} diff --git a/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCExtractCert.java b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCExtractCert.java index 94a7fd4..adeb28e 100644 --- a/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCExtractCert.java +++ b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCExtractCert.java @@ -7,6 +7,8 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPUtil; 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.jetbrains.annotations.NotNull; import sop.Ready; @@ -16,6 +18,7 @@ import sop.operation.ExtractCert; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -33,29 +36,32 @@ public class BCExtractCert @NotNull @Override public Ready key(@NotNull InputStream inputStream) throws IOException, SOPGPException.BadData { - InputStream decodeIn = PGPUtil.getDecoderStream(inputStream); - PGPObjectFactory objFac = new BcPGPObjectFactory(decodeIn); - PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) objFac.nextObject(); - - List list = new ArrayList<>(); - Iterator iterator = secretKeys.getPublicKeys(); - while (iterator.hasNext()) { - list.add(iterator.next()); + List keys = api.readKeyOrCertificate().parseKeys(inputStream); + List certs = new ArrayList<>(); + for (OpenPGPKey key : keys) { + certs.add(key.toCertificate()); } - PGPPublicKeyRing publicKeys = new PGPPublicKeyRing(list); return new Ready() { @Override public void writeTo(@NotNull OutputStream outputStream) throws IOException { if (armor) { - ArmoredOutputStream aOut = ArmoredOutputStream.builder() - .clearHeaders() - .enableCRC(true) - .build(outputStream); - publicKeys.encode(aOut); - aOut.close(); + if (certs.size() == 1) { + outputStream.write(certs.get(0).toAsciiArmoredString().getBytes(StandardCharsets.UTF_8)); + } else { + ArmoredOutputStream aOut = ArmoredOutputStream.builder() + .clearHeaders() + .enableCRC(true) + .build(outputStream); + for (OpenPGPCertificate cert : certs) { + aOut.write(cert.getEncoded()); + } + aOut.close(); + } } else { - publicKeys.encode(outputStream); + for (OpenPGPCertificate cert : certs) { + outputStream.write(cert.getEncoded()); + } } } }; diff --git a/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCListProfiles.java b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCListProfiles.java index 8d1f8a7..d94b365 100644 --- a/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCListProfiles.java +++ b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCListProfiles.java @@ -3,6 +3,7 @@ package org.pgpainless.bouncycastle.sop.operation; import org.bouncycastle.openpgp.api.OpenPGPApi; import org.jetbrains.annotations.NotNull; import sop.Profile; +import sop.exception.SOPGPException; import sop.operation.ListProfiles; import java.util.Collections; @@ -22,7 +23,9 @@ public class BCListProfiles switch (s) { case "generate-key": return BCGenerateKey.PROFILES; + case "encrypt": + return Collections.emptyList(); } - return Collections.emptyList(); + throw new SOPGPException.UnsupportedProfile(s); } } diff --git a/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCMergeCerts.java b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCMergeCerts.java new file mode 100644 index 0000000..317ea74 --- /dev/null +++ b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCMergeCerts.java @@ -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 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 merged = new HashMap<>(); + List 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; + } +} diff --git a/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCRevokeKey.java b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCRevokeKey.java new file mode 100644 index 0000000..dd6069f --- /dev/null +++ b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCRevokeKey.java @@ -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 keys; + try { + keys = api.readKeyOrCertificate().parseKeys(inputStream); + } catch (IOException e) { + throw new SOPGPException.BadData(e); + } + + List 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; + } +} diff --git a/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCUpdateKey.java b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCUpdateKey.java new file mode 100644 index 0000000..735b76f --- /dev/null +++ b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCUpdateKey.java @@ -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; + } +} diff --git a/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCValidateUserId.java b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCValidateUserId.java new file mode 100644 index 0000000..eaaf098 --- /dev/null +++ b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCValidateUserId.java @@ -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 userIds = new ArrayList<>(); + private final List 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 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; + } +} diff --git a/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCVersion.java b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCVersion.java index 65d89bd..fe31192 100644 --- a/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCVersion.java +++ b/bc-sop-api/src/main/java/org/pgpainless/bouncycastle/sop/operation/BCVersion.java @@ -41,7 +41,9 @@ public class BCVersion @NotNull @Override public String getExtendedVersion() { - return ""; + return getName() + " " + getVersion() + "\n" + + getBackendVersion() + "\n" + + "sop-java " + getSopJavaVersion(); } @Override diff --git a/bc-sop-api/src/test/java/org/pgpainless/bouncycastle/sop/operation/BCSopCertifyValidateUserIdTest.java b/bc-sop-api/src/test/java/org/pgpainless/bouncycastle/sop/operation/BCSopCertifyValidateUserIdTest.java new file mode 100644 index 0000000..f8c7bdf --- /dev/null +++ b/bc-sop-api/src/test/java/org/pgpainless/bouncycastle/sop/operation/BCSopCertifyValidateUserIdTest.java @@ -0,0 +1,7 @@ +package org.pgpainless.bouncycastle.sop.operation; + +import sop.testsuite.operation.CertifyValidateUserIdTest; + +public class BCSopCertifyValidateUserIdTest extends CertifyValidateUserIdTest { + +} diff --git a/bc-sop-api/src/test/java/org/pgpainless/bouncycastle/sop/operation/BCSopListProfilesTest.java b/bc-sop-api/src/test/java/org/pgpainless/bouncycastle/sop/operation/BCSopListProfilesTest.java new file mode 100644 index 0000000..c8d217e --- /dev/null +++ b/bc-sop-api/src/test/java/org/pgpainless/bouncycastle/sop/operation/BCSopListProfilesTest.java @@ -0,0 +1,7 @@ +package org.pgpainless.bouncycastle.sop.operation; + +import sop.testsuite.operation.ListProfilesTest; + +public class BCSopListProfilesTest extends ListProfilesTest { + +} diff --git a/bc-sop-api/src/test/java/org/pgpainless/bouncycastle/sop/operation/BCSopMergeCertsTest.java b/bc-sop-api/src/test/java/org/pgpainless/bouncycastle/sop/operation/BCSopMergeCertsTest.java new file mode 100644 index 0000000..4112f4f --- /dev/null +++ b/bc-sop-api/src/test/java/org/pgpainless/bouncycastle/sop/operation/BCSopMergeCertsTest.java @@ -0,0 +1,7 @@ +package org.pgpainless.bouncycastle.sop.operation; + +import sop.testsuite.operation.MergeCertsTest; + +public class BCSopMergeCertsTest extends MergeCertsTest { + +}