From 2a672aaf030fc893424a282638e9cf381ac90fc3 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Fri, 27 Nov 2020 13:00:06 +0100 Subject: [PATCH] Add ability to change expiration date for the primary key --- .../secretkeyring/SecretKeyRingEditor.java | 52 +++++++++++++++++ .../SecretKeyRingEditorInterface.java | 14 +++++ .../modification/ChangeExpirationTest.java | 58 +++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index dccd3c7a..057d8b36 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -21,6 +21,7 @@ import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -286,6 +287,57 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { return this; } + @Override + public SecretKeyRingEditorInterface setExpirationDate(OpenPgpV4Fingerprint fingerprint, + Date expiration, + SecretKeyRingProtector secretKeyRingProtector) + throws PGPException { + Iterator secretKeyIterator = secretKeyRing.getSecretKeys(); + + if (!secretKeyIterator.hasNext()) { + throw new NoSuchElementException("No secret keys in the ring."); + } + + PGPSecretKey secretKey = secretKeyIterator.next(); + PGPPublicKey publicKey = secretKey.getPublicKey(); + + if (!new OpenPgpV4Fingerprint(publicKey).equals(fingerprint)) { + throw new IllegalArgumentException("Currently it is possible to adjust expiration date for primary key only."); + } + + List secretKeyList = new ArrayList<>(); + PGPPrivateKey privateKey = unlockSecretKey(secretKey, secretKeyRingProtector); + + PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); + PGPSignatureGenerator signatureGenerator = SignatureUtils.getSignatureGeneratorFor(primaryKey); + PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(); + + long secondsToExpire = 0; // 0 means "no expiration" + if (expiration != null) { + secondsToExpire = (expiration.getTime() - primaryKey.getPublicKey().getCreationTime().getTime()) / 1000; + } + subpacketGenerator.setKeyExpirationTime(false, secondsToExpire); + + PGPSignatureSubpacketVector subPackets = subpacketGenerator.generate(); + signatureGenerator.setHashedSubpackets(subPackets); + + signatureGenerator.init(PGPSignature.POSITIVE_CERTIFICATION, privateKey); + + Iterator users = publicKey.getUserIDs(); + while (users.hasNext()) { + String user = users.next(); + PGPSignature signature = signatureGenerator.generateCertification(user, primaryKey.getPublicKey()); + publicKey = PGPPublicKey.addCertification(publicKey, user, signature); + } + + secretKey = PGPSecretKey.replacePublicKey(secretKey, publicKey); + secretKeyList.add(secretKey); + + secretKeyRing = new PGPSecretKeyRing(secretKeyList); + + return this; + } + @Override public PGPSignature createRevocationCertificate(OpenPgpV4Fingerprint fingerprint, SecretKeyRingProtector secretKeyRingProtector, diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java index 18a49b39..36949aa4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java @@ -15,6 +15,7 @@ */ package org.pgpainless.key.modification.secretkeyring; +import java.util.Date; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import javax.annotation.Nonnull; @@ -189,6 +190,19 @@ public interface SecretKeyRingEditorInterface { RevocationAttributes revocationAttributes) throws PGPException; + /** + * Set key expiration time. + * + * @param fingerprint key that will have its expiration date adjusted + * @param expiration target expiration time or @{code null} for no expiration + * @param secretKeyRingProtector protector to unlock the priary key + * @return the builder + */ + SecretKeyRingEditorInterface setExpirationDate(OpenPgpV4Fingerprint fingerprint, + Date expiration, + SecretKeyRingProtector secretKeyRingProtector) + throws PGPException; + /** * Create a detached revocation certificate, which can be used to revoke the specified key. * diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java new file mode 100644 index 00000000..b0dab2ac --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pgpainless.key.modification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.UnprotectedKeysProtector; + +import java.io.IOException; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.*; + +public class ChangeExpirationTest { + + @Test + public void setExpirationDateAndThenUnsetIt() throws PGPException, IOException, InterruptedException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + + KeyRingInfo sInfo = PGPainless.inspectKeyRing(secretKeys); + OpenPgpV4Fingerprint fingerprint = sInfo.getFingerprint(); + + assertNull(sInfo.getExpirationDate()); + + Date date = new Date(1606493432000L); + secretKeys = PGPainless.modifyKeyRing(secretKeys).setExpirationDate(fingerprint, date, new UnprotectedKeysProtector()).done(); + sInfo = PGPainless.inspectKeyRing(secretKeys); + assertNotNull(sInfo.getExpirationDate()); + assertEquals(date.getTime(), sInfo.getExpirationDate().getTime()); + + // We need to wait for one second as OpenPGP signatures have coarse-grained (up to a second) + // accuracy. Creating two signatures within a short amount of time will make the second one + // "invisible" + Thread.sleep(1000); + + secretKeys = PGPainless.modifyKeyRing(secretKeys).setExpirationDate(fingerprint, null, new UnprotectedKeysProtector()).done(); + sInfo = PGPainless.inspectKeyRing(secretKeys); + assertNull(sInfo.getExpirationDate()); + } +}