mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-11-16 01:12:05 +01:00
Add SecretKeyRingEditor.replaceUserId(old,new,protector)
This commit is contained in:
parent
4730ac427b
commit
dec3c8be60
3 changed files with 149 additions and 2 deletions
|
@ -205,12 +205,61 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface {
|
||||||
@Override
|
@Override
|
||||||
public SecretKeyRingEditorInterface removeUserId(
|
public SecretKeyRingEditorInterface removeUserId(
|
||||||
CharSequence userId,
|
CharSequence userId,
|
||||||
SecretKeyRingProtector protector) throws PGPException {
|
SecretKeyRingProtector protector)
|
||||||
|
throws PGPException {
|
||||||
return removeUserId(
|
return removeUserId(
|
||||||
SelectUserId.exactMatch(userId.toString()),
|
SelectUserId.exactMatch(userId.toString()),
|
||||||
protector);
|
protector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SecretKeyRingEditorInterface replaceUserId(@Nonnull CharSequence oldUserId,
|
||||||
|
@Nonnull CharSequence newUserId,
|
||||||
|
@Nonnull SecretKeyRingProtector protector)
|
||||||
|
throws PGPException {
|
||||||
|
String oldUID = oldUserId.toString().trim();
|
||||||
|
String newUID = newUserId.toString().trim();
|
||||||
|
if (oldUID.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Old user-id cannot be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newUID.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("New user-id cannot be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing);
|
||||||
|
if (!info.isUserIdValid(oldUID)) {
|
||||||
|
throw new NoSuchElementException("Key does not carry user-id '" + oldUID + "', or it is not valid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
PGPSignature oldCertification = info.getLatestUserIdCertification(oldUID);
|
||||||
|
if (oldCertification == null) {
|
||||||
|
throw new AssertionError("Certification for old user-id MUST NOT be null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind new user-id
|
||||||
|
addUserId(newUserId, new SelfSignatureSubpackets.Callback() {
|
||||||
|
@Override
|
||||||
|
public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) {
|
||||||
|
SignatureSubpacketsHelper.applyFrom(oldCertification.getHashedSubPackets(), (SignatureSubpackets) hashedSubpackets);
|
||||||
|
// Primary user-id
|
||||||
|
if (oldUID.equals(info.getPrimaryUserId())) {
|
||||||
|
// Implicit primary user-id
|
||||||
|
if (!oldCertification.getHashedSubPackets().isPrimaryUserID()) {
|
||||||
|
hashedSubpackets.setPrimaryUserId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modifyUnhashedSubpackets(SelfSignatureSubpackets unhashedSubpackets) {
|
||||||
|
SignatureSubpacketsHelper.applyFrom(oldCertification.getUnhashedSubPackets(), (SignatureSubpackets) unhashedSubpackets);
|
||||||
|
}
|
||||||
|
}, protector);
|
||||||
|
|
||||||
|
return revokeUserId(oldUID, protector);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Move to utility class?
|
// TODO: Move to utility class?
|
||||||
private String sanitizeUserId(@Nonnull CharSequence userId) {
|
private String sanitizeUserId(@Nonnull CharSequence userId) {
|
||||||
// TODO: Further research how to sanitize user IDs.
|
// TODO: Further research how to sanitize user IDs.
|
||||||
|
|
|
@ -104,6 +104,26 @@ public interface SecretKeyRingEditorInterface {
|
||||||
SecretKeyRingProtector protector)
|
SecretKeyRingProtector protector)
|
||||||
throws PGPException;
|
throws PGPException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace a user-id on the key with a new one.
|
||||||
|
* The old user-id gets soft revoked and the new user-id gets bound with the same signature subpackets as the
|
||||||
|
* old one, with one exception:
|
||||||
|
* If the old user-id was implicitly primary (did not carry a {@link org.bouncycastle.bcpg.sig.PrimaryUserID} packet,
|
||||||
|
* but effectively was primary, then the new user-id will be explicitly marked as primary.
|
||||||
|
*
|
||||||
|
* @param oldUserId old user-id
|
||||||
|
* @param newUserId new user-id
|
||||||
|
* @param protector protector to unlock the secret key
|
||||||
|
* @return the builder
|
||||||
|
* @throws PGPException in case we cannot generate a revocation and certification signature
|
||||||
|
* @throws java.util.NoSuchElementException if the old user-id was not found on the key; or if the oldUserId
|
||||||
|
* was already invalid
|
||||||
|
*/
|
||||||
|
SecretKeyRingEditorInterface replaceUserId(CharSequence oldUserId,
|
||||||
|
CharSequence newUserId,
|
||||||
|
SecretKeyRingProtector protector)
|
||||||
|
throws PGPException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a subkey to the key ring.
|
* Add a subkey to the key ring.
|
||||||
* The subkey will be generated from the provided {@link KeySpec}.
|
* The subkey will be generated from the provided {@link KeySpec}.
|
||||||
|
|
|
@ -5,16 +5,31 @@
|
||||||
package org.pgpainless.key.modification;
|
package org.pgpainless.key.modification;
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
import org.bouncycastle.openpgp.PGPException;
|
||||||
|
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||||
|
import org.bouncycastle.util.io.Streams;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.pgpainless.PGPainless;
|
import org.pgpainless.PGPainless;
|
||||||
|
import org.pgpainless.decryption_verification.ConsumerOptions;
|
||||||
|
import org.pgpainless.decryption_verification.DecryptionStream;
|
||||||
|
import org.pgpainless.decryption_verification.OpenPgpMetadata;
|
||||||
|
import org.pgpainless.encryption_signing.EncryptionOptions;
|
||||||
|
import org.pgpainless.encryption_signing.EncryptionResult;
|
||||||
|
import org.pgpainless.encryption_signing.EncryptionStream;
|
||||||
|
import org.pgpainless.encryption_signing.ProducerOptions;
|
||||||
import org.pgpainless.key.info.KeyRingInfo;
|
import org.pgpainless.key.info.KeyRingInfo;
|
||||||
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
||||||
import org.pgpainless.signature.subpackets.SelfSignatureSubpackets;
|
import org.pgpainless.signature.subpackets.SelfSignatureSubpackets;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,7 +70,7 @@ public class FixUserIdDoesNotBreakEncryptionCapabilityTest {
|
||||||
private static final String userIdAfter = "\"(B)ob (J)ohnson\" <bj@evaluation.test>";
|
private static final String userIdAfter = "\"(B)ob (J)ohnson\" <bj@evaluation.test>";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void replaceUserIdWithFixedVersionDoesNotHinderEncryptionCapability() throws IOException, PGPException {
|
public void manualReplaceUserIdWithFixedVersionDoesNotHinderEncryptionCapability() throws IOException, PGPException {
|
||||||
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY);
|
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY);
|
||||||
SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys();
|
SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys();
|
||||||
PGPSecretKeyRing modified = PGPainless.modifyKeyRing(secretKeys)
|
PGPSecretKeyRing modified = PGPainless.modifyKeyRing(secretKeys)
|
||||||
|
@ -81,4 +96,67 @@ public class FixUserIdDoesNotBreakEncryptionCapabilityTest {
|
||||||
assertFalse(after.isUserIdValid(userIdBefore));
|
assertFalse(after.isUserIdValid(userIdBefore));
|
||||||
assertTrue(after.isUserIdValid(userIdAfter));
|
assertTrue(after.isUserIdValid(userIdAfter));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReplaceUserId_missingOldUserIdThrows() throws IOException {
|
||||||
|
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY);
|
||||||
|
assertThrows(NoSuchElementException.class, () -> PGPainless.modifyKeyRing(secretKeys)
|
||||||
|
.replaceUserId("missing", userIdAfter, SecretKeyRingProtector.unprotectedKeys()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReplaceUserId_emptyOldUserIdThrows() throws IOException {
|
||||||
|
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY);
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> PGPainless.modifyKeyRing(secretKeys)
|
||||||
|
.replaceUserId(" ", userIdAfter, SecretKeyRingProtector.unprotectedKeys()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReplaceUserId_emptyNewUserIdThrows() throws IOException {
|
||||||
|
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY);
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> PGPainless.modifyKeyRing(secretKeys)
|
||||||
|
.replaceUserId(userIdBefore, " ", SecretKeyRingProtector.unprotectedKeys()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReplaceImplicitUserIdDoesNotBreakStuff() throws IOException, PGPException {
|
||||||
|
PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(SECRET_KEY);
|
||||||
|
|
||||||
|
PGPSecretKeyRing edited = PGPainless.modifyKeyRing(secretKeys)
|
||||||
|
.replaceUserId(userIdBefore, userIdAfter, SecretKeyRingProtector.unprotectedKeys())
|
||||||
|
.done();
|
||||||
|
|
||||||
|
KeyRingInfo info = PGPainless.inspectKeyRing(edited);
|
||||||
|
assertTrue(info.isUserIdValid(userIdAfter));
|
||||||
|
assertEquals(userIdAfter, info.getPrimaryUserId());
|
||||||
|
|
||||||
|
assertTrue(info.getLatestUserIdCertification(userIdAfter).getHashedSubPackets().isPrimaryUserID());
|
||||||
|
|
||||||
|
PGPPublicKeyRing cert = PGPainless.extractCertificate(edited);
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
|
||||||
|
.onOutputStream(out)
|
||||||
|
.withOptions(ProducerOptions.encrypt(new EncryptionOptions()
|
||||||
|
.addRecipient(cert)));
|
||||||
|
|
||||||
|
encryptionStream.write("Hello".getBytes(StandardCharsets.UTF_8));
|
||||||
|
encryptionStream.close();
|
||||||
|
|
||||||
|
EncryptionResult result = encryptionStream.getResult();
|
||||||
|
assertTrue(result.isEncryptedFor(cert));
|
||||||
|
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
|
||||||
|
ByteArrayOutputStream plain = new ByteArrayOutputStream();
|
||||||
|
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
|
||||||
|
.onInputStream(in)
|
||||||
|
.withOptions(new ConsumerOptions()
|
||||||
|
.addDecryptionKey(edited));
|
||||||
|
|
||||||
|
Streams.pipeAll(decryptionStream, plain);
|
||||||
|
decryptionStream.close();
|
||||||
|
|
||||||
|
OpenPgpMetadata metadata = decryptionStream.getResult();
|
||||||
|
assertTrue(metadata.isEncrypted());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue