Compare commits

..

3 commits

15 changed files with 536 additions and 24 deletions

View file

@ -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.");
}
}

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

@ -20,6 +20,7 @@ public class BCDetachedVerify
{
private final OpenPGPDetachedSignatureProcessor processor;
private boolean hasCerts = false;
public BCDetachedVerify(OpenPGPApi api)
{
@ -50,12 +51,16 @@ public class BCDetachedVerify
@Override
public DetachedVerify cert(@NotNull InputStream inputStream) throws SOPGPException.BadData, IOException {
processor.addVerificationCertificate(parseCertificate(inputStream));
hasCerts = true;
return this;
}
@NotNull
@Override
public List<Verification> data(@NotNull InputStream inputStream) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData {
if (!hasCerts) {
throw new SOPGPException.MissingArg("No certificates provided for signature verification.");
}
List<OpenPGPSignature.OpenPGPDocumentSignature> signatures = processor.process(inputStream);
List<Verification> verifications = getVerifications(signatures);

View file

@ -31,6 +31,7 @@ public class BCEncrypt
private final OpenPGPMessageGenerator mGen;
private final List<OpenPGPKey> signingKeys = new ArrayList<>();
private int signatureMode = PGPSignature.BINARY_DOCUMENT;
private boolean hasEncryptionMethod = false;
public BCEncrypt(OpenPGPApi api) {
super(api);
@ -83,6 +84,7 @@ public class BCEncrypt
public Encrypt withPassword(@NotNull String s)
throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
mGen.addEncryptionPassphrase(s.toCharArray());
hasEncryptionMethod = true;
return this;
}
@ -92,6 +94,7 @@ public class BCEncrypt
throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
try {
mGen.addEncryptionCertificate(parseCertificate(inputStream));
hasEncryptionMethod = true;
} catch (InvalidEncryptionKeyException e) {
throw new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt", e);
}
@ -109,6 +112,10 @@ public class BCEncrypt
@Override
public ReadyWithResult<EncryptionResult> plaintext(@NotNull InputStream inputStream)
throws SOPGPException.KeyIsProtected {
if (!hasEncryptionMethod) {
throw new SOPGPException.MissingArg("No encryption method provided.");
}
for (OpenPGPKey key : signingKeys) {
try {
mGen.addSigningKey(key, new SignatureParameters.Callback() {

View file

@ -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<PGPPublicKey> list = new ArrayList<>();
Iterator<PGPPublicKey> iterator = secretKeys.getPublicKeys();
while (iterator.hasNext()) {
list.add(iterator.next());
List<OpenPGPKey> keys = api.readKeyOrCertificate().parseKeys(inputStream);
List<OpenPGPCertificate> 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());
}
}
}
};

View file

@ -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);
}
}

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
@Override
public String getExtendedVersion() {
return "";
return getName() + " " + getVersion() + "\n"
+ getBackendVersion() + "\n"
+ "sop-java " + getSopJavaVersion();
}
@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 {
}

12
pom.xml
View file

@ -33,32 +33,32 @@
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.80-SNAPSHOT</version>
<version>1.82-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpg-jdk18on</artifactId>
<version>1.80-SNAPSHOT</version>
<version>1.82-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcutil-jdk18on</artifactId>
<version>1.80-SNAPSHOT</version>
<version>1.82-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.pgpainless</groupId>
<artifactId>sop-java</artifactId>
<version>10.1.1-SNAPSHOT</version>
<version>14.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.pgpainless</groupId>
<artifactId>sop-java-picocli</artifactId>
<version>10.1.1-SNAPSHOT</version>
<version>14.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.pgpainless</groupId>
<artifactId>sop-java-testfixtures</artifactId>
<version>10.1.1-SNAPSHOT</version>
<version>14.0.1-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>