1
0
Fork 0
mirror of https://codeberg.org/Mercury-IM/Smack synced 2024-11-22 06:12:05 +01:00

Merge remote-tracking branch 'vanitas/secretKeyBackupPassword' into mercuryPatches

This commit is contained in:
Paul Schaub 2020-07-18 13:57:47 +02:00
commit 3dd035f5fc
9 changed files with 196 additions and 95 deletions

View file

@ -270,7 +270,12 @@ public class StringUtils {
/** /**
* 24 upper case characters from the latin alphabet and numbers without '0' and 'O'. * 24 upper case characters from the latin alphabet and numbers without '0' and 'O'.
*/ */
private static final char[] UNAMBIGUOUS_NUMBERS_AND_LETTER = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ".toCharArray(); public static final String UNAMBIGUOUS_NUMBERS_AND_LETTERS_STRING = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ";
/**
* 24 upper case characters from the latin alphabet and numbers without '0' and 'O'.
*/
private static final char[] UNAMBIGUOUS_NUMBERS_AND_LETTERS = UNAMBIGUOUS_NUMBERS_AND_LETTERS_STRING.toCharArray();
/** /**
* Returns a random String of numbers and letters (lower and upper case) * Returns a random String of numbers and letters (lower and upper case)
@ -294,14 +299,14 @@ public class StringUtils {
// See also https://www.grc.com/haystack.htm // See also https://www.grc.com/haystack.htm
final int REQUIRED_LENGTH = 10; final int REQUIRED_LENGTH = 10;
return randomString(RandomUtil.SECURE_RANDOM.get(), UNAMBIGUOUS_NUMBERS_AND_LETTER, REQUIRED_LENGTH); return randomString(RandomUtil.SECURE_RANDOM.get(), UNAMBIGUOUS_NUMBERS_AND_LETTERS, REQUIRED_LENGTH);
} }
public static String secureUniqueRandomString() { public static String secureUniqueRandomString() {
// 34^13 = 8.11e19 possible combinations, which is > 2^64. // 34^13 = 8.11e19 possible combinations, which is > 2^64.
final int REQUIRED_LENGTH = 13; final int REQUIRED_LENGTH = 13;
return randomString(RandomUtil.SECURE_RANDOM.get(), UNAMBIGUOUS_NUMBERS_AND_LETTER, REQUIRED_LENGTH); return randomString(RandomUtil.SECURE_RANDOM.get(), UNAMBIGUOUS_NUMBERS_AND_LETTERS, REQUIRED_LENGTH);
} }
/** /**
@ -324,7 +329,7 @@ public class StringUtils {
// See also https://www.grc.com/haystack.htm // See also https://www.grc.com/haystack.htm
final int REQUIRED_LENGTH = 24; final int REQUIRED_LENGTH = 24;
return randomString(RandomUtil.SECURE_RANDOM.get(), UNAMBIGUOUS_NUMBERS_AND_LETTER, REQUIRED_LENGTH); return randomString(RandomUtil.SECURE_RANDOM.get(), UNAMBIGUOUS_NUMBERS_AND_LETTERS, REQUIRED_LENGTH);
} }
private static final int RANDOM_STRING_CHUNK_SIZE = 4; private static final int RANDOM_STRING_CHUNK_SIZE = 4;
@ -368,8 +373,8 @@ public class StringUtils {
char[] randomChars = new char[length]; char[] randomChars = new char[length];
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
int index = random.nextInt(UNAMBIGUOUS_NUMBERS_AND_LETTER.length); int index = random.nextInt(UNAMBIGUOUS_NUMBERS_AND_LETTERS.length);
randomChars[i] = UNAMBIGUOUS_NUMBERS_AND_LETTER[index]; randomChars[i] = UNAMBIGUOUS_NUMBERS_AND_LETTERS[index];
} }
return new String(randomChars); return new String(randomChars);
} }

View file

@ -16,6 +16,7 @@
*/ */
package org.jivesoftware.smackx.ox; package org.jivesoftware.smackx.ox;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
@ -26,17 +27,11 @@ import java.io.IOException;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException; import java.security.NoSuchProviderException;
import java.util.Arrays;
import java.util.Set;
import java.util.logging.Level; import java.util.logging.Level;
import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.ox.callback.backup.AskForBackupCodeCallback;
import org.jivesoftware.smackx.ox.callback.backup.DisplayBackupCodeCallback;
import org.jivesoftware.smackx.ox.callback.backup.SecretKeyBackupSelectionCallback;
import org.jivesoftware.smackx.ox.crypto.PainlessOpenPgpProvider; import org.jivesoftware.smackx.ox.crypto.PainlessOpenPgpProvider;
import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException; import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException;
import org.jivesoftware.smackx.ox.exception.MissingOpenPgpKeyException; import org.jivesoftware.smackx.ox.exception.MissingOpenPgpKeyException;
@ -64,10 +59,6 @@ public class OXSecretKeyBackupIntegrationTest extends AbstractOpenPgpIntegration
private static final File beforePath = new File(tempDir, "ox_backup_" + sessionId); private static final File beforePath = new File(tempDir, "ox_backup_" + sessionId);
private static final File afterPath = new File(tempDir, "ox_restore_" + sessionId); private static final File afterPath = new File(tempDir, "ox_restore_" + sessionId);
private String backupCode = null;
private OpenPgpManager openPgpManager;
/** /**
* This integration test tests the basic secret key backup and restore functionality as described * This integration test tests the basic secret key backup and restore functionality as described
* in XEP-0373 §5. * in XEP-0373 §5.
@ -123,7 +114,7 @@ public class OXSecretKeyBackupIntegrationTest extends AbstractOpenPgpIntegration
OpenPgpStore beforeStore = new FileBasedOpenPgpStore(beforePath); OpenPgpStore beforeStore = new FileBasedOpenPgpStore(beforePath);
beforeStore.setKeyRingProtector(new UnprotectedKeysProtector()); beforeStore.setKeyRingProtector(new UnprotectedKeysProtector());
PainlessOpenPgpProvider beforeProvider = new PainlessOpenPgpProvider(beforeStore); PainlessOpenPgpProvider beforeProvider = new PainlessOpenPgpProvider(beforeStore);
openPgpManager = OpenPgpManager.getInstanceFor(aliceConnection); OpenPgpManager openPgpManager = OpenPgpManager.getInstanceFor(aliceConnection);
openPgpManager.setOpenPgpProvider(beforeProvider); openPgpManager.setOpenPgpProvider(beforeProvider);
OpenPgpSelf self = openPgpManager.getOpenPgpSelf(); OpenPgpSelf self = openPgpManager.getOpenPgpSelf();
@ -141,29 +132,15 @@ public class OXSecretKeyBackupIntegrationTest extends AbstractOpenPgpIntegration
PGPPublicKeyRing beforePub = beforeStore.getPublicKeyRing(alice, keyFingerprint); PGPPublicKeyRing beforePub = beforeStore.getPublicKeyRing(alice, keyFingerprint);
assertNotNull(beforePub); assertNotNull(beforePub);
openPgpManager.backupSecretKeyToServer(new DisplayBackupCodeCallback() { OpenPgpSecretKeyBackupPassphrase backupPassphrase =
@Override openPgpManager.backupSecretKeyToServer(availableSecretKeys -> availableSecretKeys);
public void displayBackupCode(String backupCode) {
OXSecretKeyBackupIntegrationTest.this.backupCode = backupCode;
}
}, new SecretKeyBackupSelectionCallback() {
@Override
public Set<OpenPgpV4Fingerprint> selectKeysToBackup(Set<OpenPgpV4Fingerprint> availableSecretKeys) {
return availableSecretKeys;
}
});
FileBasedOpenPgpStore afterStore = new FileBasedOpenPgpStore(afterPath); FileBasedOpenPgpStore afterStore = new FileBasedOpenPgpStore(afterPath);
afterStore.setKeyRingProtector(new UnprotectedKeysProtector()); afterStore.setKeyRingProtector(new UnprotectedKeysProtector());
PainlessOpenPgpProvider afterProvider = new PainlessOpenPgpProvider(afterStore); PainlessOpenPgpProvider afterProvider = new PainlessOpenPgpProvider(afterStore);
openPgpManager.setOpenPgpProvider(afterProvider); openPgpManager.setOpenPgpProvider(afterProvider);
OpenPgpV4Fingerprint fingerprint = openPgpManager.restoreSecretKeyServerBackup(new AskForBackupCodeCallback() { OpenPgpV4Fingerprint fingerprint = openPgpManager.restoreSecretKeyServerBackup(() -> backupPassphrase);
@Override
public String askForBackupCode() {
return backupCode;
}
});
assertEquals(keyFingerprint, fingerprint); assertEquals(keyFingerprint, fingerprint);
@ -173,10 +150,10 @@ public class OXSecretKeyBackupIntegrationTest extends AbstractOpenPgpIntegration
PGPSecretKeyRing afterSec = afterStore.getSecretKeyRing(alice, keyFingerprint); PGPSecretKeyRing afterSec = afterStore.getSecretKeyRing(alice, keyFingerprint);
assertNotNull(afterSec); assertNotNull(afterSec);
assertTrue(Arrays.equals(beforeSec.getEncoded(), afterSec.getEncoded())); assertArrayEquals(beforeSec.getEncoded(), afterSec.getEncoded());
PGPPublicKeyRing afterPub = afterStore.getPublicKeyRing(alice, keyFingerprint); PGPPublicKeyRing afterPub = afterStore.getPublicKeyRing(alice, keyFingerprint);
assertNotNull(afterPub); assertNotNull(afterPub);
assertTrue(Arrays.equals(beforePub.getEncoded(), afterPub.getEncoded())); assertArrayEquals(beforePub.getEncoded(), afterPub.getEncoded());
} }
} }

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2017-2020 Florian Schmaus, 2018 Paul Schaub. * Copyright 2018-2020 Paul Schaub, 2017-2020 Florian Schmaus.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -44,7 +44,6 @@ import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smack.xml.XmlPullParserException; import org.jivesoftware.smack.xml.XmlPullParserException;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.ox.callback.backup.AskForBackupCodeCallback; import org.jivesoftware.smackx.ox.callback.backup.AskForBackupCodeCallback;
import org.jivesoftware.smackx.ox.callback.backup.DisplayBackupCodeCallback;
import org.jivesoftware.smackx.ox.callback.backup.SecretKeyBackupSelectionCallback; import org.jivesoftware.smackx.ox.callback.backup.SecretKeyBackupSelectionCallback;
import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider; import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider;
import org.jivesoftware.smackx.ox.element.CryptElement; import org.jivesoftware.smackx.ox.element.CryptElement;
@ -384,8 +383,9 @@ public final class OpenPgpManager extends Manager {
* *
* @see <a href="https://xmpp.org/extensions/xep-0373.html#synchro-pep">XEP-0373 §5</a> * @see <a href="https://xmpp.org/extensions/xep-0373.html#synchro-pep">XEP-0373 §5</a>
* *
* @param displayCodeCallback callback, which will receive the backup password used to encrypt the secret key.
* @param selectKeyCallback callback, which will receive the users choice of which keys will be backed up. * @param selectKeyCallback callback, which will receive the users choice of which keys will be backed up.
* @return secret key passphrase used to encrypt the backup.
*
* @throws InterruptedException if the thread is interrupted. * @throws InterruptedException if the thread is interrupted.
* @throws PubSubException.NotALeafNodeException if the private node is not a {@link LeafNode}. * @throws PubSubException.NotALeafNodeException if the private node is not a {@link LeafNode}.
* @throws XMPPException.XMPPErrorException in case of an XMPP protocol error. * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
@ -397,8 +397,38 @@ public final class OpenPgpManager extends Manager {
* @throws PGPException PGP is brittle * @throws PGPException PGP is brittle
* @throws MissingOpenPgpKeyException in case we have no OpenPGP key pair to back up. * @throws MissingOpenPgpKeyException in case we have no OpenPGP key pair to back up.
*/ */
public void backupSecretKeyToServer(DisplayBackupCodeCallback displayCodeCallback, public OpenPgpSecretKeyBackupPassphrase backupSecretKeyToServer(SecretKeyBackupSelectionCallback selectKeyCallback)
SecretKeyBackupSelectionCallback selectKeyCallback) throws InterruptedException, PubSubException.NotALeafNodeException,
XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException,
SmackException.NotLoggedInException, IOException,
SmackException.FeatureNotSupportedException, PGPException, MissingOpenPgpKeyException {
OpenPgpSecretKeyBackupPassphrase passphrase = SecretKeyBackupHelper.generateBackupPassword();
backupSecretKeyToServer(selectKeyCallback, passphrase);
return passphrase;
}
/**
* Upload the encrypted secret key to a private PEP node.
* The backup is encrypted using the provided secret key passphrase.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#synchro-pep">XEP-0373 §5</a>
*
* @param selectKeyCallback callback, which will receive the users choice of which keys will be backed up. @param selectKeyCallback
* @param passphrase secret key passphrase
*
* @throws InterruptedException if the thread is interrupted.
* @throws PubSubException.NotALeafNodeException if the private node is not a {@link LeafNode}.
* @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
* @throws SmackException.NotConnectedException if we are not connected.
* @throws SmackException.NoResponseException if the server doesn't respond.
* @throws SmackException.NotLoggedInException if we are not logged in.
* @throws IOException IO is dangerous.
* @throws SmackException.FeatureNotSupportedException if the server doesn't support the PubSub whitelist access model.
* @throws PGPException PGP is brittle
* @throws MissingOpenPgpKeyException in case we have no OpenPGP key pair to back up.
*/
public void backupSecretKeyToServer(SecretKeyBackupSelectionCallback selectKeyCallback,
OpenPgpSecretKeyBackupPassphrase passphrase)
throws InterruptedException, PubSubException.NotALeafNodeException, throws InterruptedException, PubSubException.NotALeafNodeException,
XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException,
SmackException.NotLoggedInException, IOException, SmackException.NotLoggedInException, IOException,
@ -408,8 +438,6 @@ public final class OpenPgpManager extends Manager {
BareJid ownJid = connection().getUser().asBareJid(); BareJid ownJid = connection().getUser().asBareJid();
String backupCode = SecretKeyBackupHelper.generateBackupPassword();
PGPSecretKeyRingCollection secretKeyRings = provider.getStore().getSecretKeysOf(ownJid); PGPSecretKeyRingCollection secretKeyRings = provider.getStore().getSecretKeysOf(ownJid);
Set<OpenPgpV4Fingerprint> availableKeyPairs = new HashSet<>(); Set<OpenPgpV4Fingerprint> availableKeyPairs = new HashSet<>();
@ -419,10 +447,9 @@ public final class OpenPgpManager extends Manager {
Set<OpenPgpV4Fingerprint> selectedKeyPairs = selectKeyCallback.selectKeysToBackup(availableKeyPairs); Set<OpenPgpV4Fingerprint> selectedKeyPairs = selectKeyCallback.selectKeysToBackup(availableKeyPairs);
SecretkeyElement secretKey = SecretKeyBackupHelper.createSecretkeyElement(provider, ownJid, selectedKeyPairs, backupCode); SecretkeyElement secretKey = SecretKeyBackupHelper.createSecretkeyElement(provider, ownJid, selectedKeyPairs, passphrase);
OpenPgpPubSubUtil.depositSecretKey(connection(), secretKey); OpenPgpPubSubUtil.depositSecretKey(connection(), secretKey);
displayCodeCallback.displayBackupCode(backupCode);
} }
/** /**
@ -471,7 +498,7 @@ public final class OpenPgpManager extends Manager {
throw new NoBackupFoundException(); throw new NoBackupFoundException();
} }
String backupCode = codeCallback.askForBackupCode(); OpenPgpSecretKeyBackupPassphrase backupCode = codeCallback.askForBackupCode();
PGPSecretKeyRing secretKeys = SecretKeyBackupHelper.restoreSecretKeyBackup(backup, backupCode); PGPSecretKeyRing secretKeys = SecretKeyBackupHelper.restoreSecretKeyBackup(backup, backupCode);
OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(secretKeys); OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(secretKeys);
@ -541,7 +568,7 @@ public final class OpenPgpManager extends Manager {
if (contentElement instanceof SigncryptElement) { if (contentElement instanceof SigncryptElement) {
for (SigncryptElementReceivedListener l : signcryptElementReceivedListeners) { for (SigncryptElementReceivedListener l : signcryptElementReceivedListeners) {
l.signcryptElementReceived(contact, message, (SigncryptElement) contentElement, l.signcryptElementReceived(contact, message, (SigncryptElement) contentElement,
decrypted.getMetadata()); decrypted.getMetadata());
} }
return; return;
} }
@ -556,7 +583,7 @@ public final class OpenPgpManager extends Manager {
if (contentElement instanceof CryptElement) { if (contentElement instanceof CryptElement) {
for (CryptElementReceivedListener l : cryptElementReceivedListeners) { for (CryptElementReceivedListener l : cryptElementReceivedListeners) {
l.cryptElementReceived(contact, message, (CryptElement) contentElement, l.cryptElementReceived(contact, message, (CryptElement) contentElement,
decrypted.getMetadata()); decrypted.getMetadata());
} }
return; return;
} }

View file

@ -0,0 +1,64 @@
/**
*
* 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.jivesoftware.smackx.ox;
import static org.jivesoftware.smack.util.StringUtils.UNAMBIGUOUS_NUMBERS_AND_LETTERS_STRING;
import java.util.regex.Pattern;
/**
* Represents a secret key backup passphrase whose format is described in XEP-0373 §5.3.
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#backup-encryption">
* XEP-0373 §5.4 Encrypting the Secret Key Backup</a>
*/
public class OpenPgpSecretKeyBackupPassphrase implements CharSequence {
private static final Pattern PASSPHRASE_PATTERN = Pattern.compile(
"^([" + UNAMBIGUOUS_NUMBERS_AND_LETTERS_STRING + "]{4}-){5}" +
"[" + UNAMBIGUOUS_NUMBERS_AND_LETTERS_STRING + "]{4}$");
private final String passphrase;
public OpenPgpSecretKeyBackupPassphrase(String passphrase) {
if (!PASSPHRASE_PATTERN.matcher(passphrase).matches()) {
throw new IllegalArgumentException("Passphrase must be 24 upper case letters and numbers from the english " +
"alphabet without 'O' and '0', divided into blocks of 4 and separated with dashes ('-').");
}
this.passphrase = passphrase;
}
@Override
public int length() {
return passphrase.length();
}
@Override
public char charAt(int i) {
return passphrase.charAt(i);
}
@Override
public CharSequence subSequence(int i, int i1) {
return passphrase.subSequence(i, i1);
}
@Override
public String toString() {
return passphrase;
}
}

View file

@ -16,6 +16,8 @@
*/ */
package org.jivesoftware.smackx.ox.callback.backup; package org.jivesoftware.smackx.ox.callback.backup;
import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase;
public interface AskForBackupCodeCallback { public interface AskForBackupCodeCallback {
/** /**
@ -27,5 +29,5 @@ public interface AskForBackupCodeCallback {
* *
* @return backup code provided by the user. * @return backup code provided by the user.
*/ */
String askForBackupCode(); OpenPgpSecretKeyBackupPassphrase askForBackupCode();
} }

View file

@ -1,32 +0,0 @@
/**
*
* Copyright 2018 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.jivesoftware.smackx.ox.callback.backup;
public interface DisplayBackupCodeCallback {
/**
* This method is used to provide a client access to the generated backup code.
* The client can then go ahead and display the code to the user.
* The backup code follows the format described in XEP-0373 §5.3
*
* @see <a href="https://xmpp.org/extensions/xep-0373.html#backup-encryption">
* XEP-0373 §5.4 Encrypting the Secret Key Backup</a>
*
* @param backupCode backup code
*/
void displayBackupCode(String backupCode);
}

View file

@ -23,6 +23,7 @@ import java.util.Set;
import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.stringencoder.Base64; import org.jivesoftware.smack.util.stringencoder.Base64;
import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase;
import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider; import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider;
import org.jivesoftware.smackx.ox.element.SecretkeyElement; import org.jivesoftware.smackx.ox.element.SecretkeyElement;
import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException; import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException;
@ -51,8 +52,8 @@ public class SecretKeyBackupHelper {
* *
* @return backup code * @return backup code
*/ */
public static String generateBackupPassword() { public static OpenPgpSecretKeyBackupPassphrase generateBackupPassword() {
return StringUtils.secureOfflineAttackSafeRandomString(); return new OpenPgpSecretKeyBackupPassphrase(StringUtils.secureOfflineAttackSafeRandomString());
} }
/** /**
@ -73,7 +74,7 @@ public class SecretKeyBackupHelper {
public static SecretkeyElement createSecretkeyElement(OpenPgpProvider provider, public static SecretkeyElement createSecretkeyElement(OpenPgpProvider provider,
BareJid owner, BareJid owner,
Set<OpenPgpV4Fingerprint> fingerprints, Set<OpenPgpV4Fingerprint> fingerprints,
String backupCode) OpenPgpSecretKeyBackupPassphrase backupCode)
throws PGPException, IOException, MissingOpenPgpKeyException { throws PGPException, IOException, MissingOpenPgpKeyException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream(); ByteArrayOutputStream buffer = new ByteArrayOutputStream();
@ -105,9 +106,9 @@ public class SecretKeyBackupHelper {
* @throws IOException IO is dangerous * @throws IOException IO is dangerous
*/ */
public static SecretkeyElement createSecretkeyElement(byte[] keys, public static SecretkeyElement createSecretkeyElement(byte[] keys,
String backupCode) OpenPgpSecretKeyBackupPassphrase backupCode)
throws PGPException, IOException { throws PGPException, IOException {
byte[] encrypted = PGPainless.encryptWithPassword(keys, new Passphrase(backupCode.toCharArray()), byte[] encrypted = PGPainless.encryptWithPassword(keys, new Passphrase(backupCode.toString().toCharArray()),
SymmetricKeyAlgorithm.AES_256); SymmetricKeyAlgorithm.AES_256);
return new SecretkeyElement(Base64.encode(encrypted)); return new SecretkeyElement(Base64.encode(encrypted));
} }
@ -123,13 +124,13 @@ public class SecretKeyBackupHelper {
* @throws IOException IO is dangerous. * @throws IOException IO is dangerous.
* @throws PGPException PGP is brittle. * @throws PGPException PGP is brittle.
*/ */
public static PGPSecretKeyRing restoreSecretKeyBackup(SecretkeyElement backup, String backupCode) public static PGPSecretKeyRing restoreSecretKeyBackup(SecretkeyElement backup, OpenPgpSecretKeyBackupPassphrase backupCode)
throws InvalidBackupCodeException, IOException, PGPException { throws InvalidBackupCodeException, IOException, PGPException {
byte[] encrypted = Base64.decode(backup.getB64Data()); byte[] encrypted = Base64.decode(backup.getB64Data());
byte[] decrypted; byte[] decrypted;
try { try {
decrypted = PGPainless.decryptWithPassword(encrypted, new Passphrase(backupCode.toCharArray())); decrypted = PGPainless.decryptWithPassword(encrypted, new Passphrase(backupCode.toString().toCharArray()));
} catch (IOException | PGPException e) { } catch (IOException | PGPException e) {
throw new InvalidBackupCodeException("Could not decrypt secret key backup. Possibly wrong passphrase?", e); throw new InvalidBackupCodeException("Could not decrypt secret key backup. Possibly wrong passphrase?", e);
} }

View file

@ -0,0 +1,57 @@
/**
*
* 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.jivesoftware.smackx.ox;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import org.jivesoftware.smackx.ox.util.SecretKeyBackupHelper;
import org.junit.jupiter.api.Test;
public class OpenPgpSecretKeyBackupPassphraseTest {
@Test
public void secretKeyPassphraseConstructorTest() {
OpenPgpSecretKeyBackupPassphrase valid =
new OpenPgpSecretKeyBackupPassphrase("TWNK-KD5Y-MT3T-E1GS-DRDB-KVTW");
assertNotNull(valid);
for (int i = 0; i < 50; i++) {
assertNotNull(SecretKeyBackupHelper.generateBackupPassword());
}
assertThrows(IllegalArgumentException.class,
() -> new OpenPgpSecretKeyBackupPassphrase("TWNKKD5YMT3TE1GSDRDBKVTW"));
assertThrows(IllegalArgumentException.class,
() -> new OpenPgpSecretKeyBackupPassphrase("0123-4567-89AB-CDEF-GHIJ-KLMN"));
assertThrows(IllegalArgumentException.class,
() -> new OpenPgpSecretKeyBackupPassphrase("CONT-AINS-ILLE-GALL-ETTE-RSO0"));
assertThrows(IllegalArgumentException.class,
() -> new OpenPgpSecretKeyBackupPassphrase("TWNK-KD5Y-MT3T-E1GS-DRDB-"));
assertThrows(IllegalArgumentException.class,
() -> new OpenPgpSecretKeyBackupPassphrase("TWNK-KD5Y-MT3T-E1GS-DRDB-KVTW-ADDD"));
assertThrows(IllegalArgumentException.class,
() -> new OpenPgpSecretKeyBackupPassphrase("TWNK KD5Y MT3T E1GS DRDB KVTW"));
}
}

View file

@ -1,6 +1,6 @@
/** /**
* *
* Copyright 2018 Paul Schaub. * Copyright 2018-2020 Paul Schaub.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,17 +18,16 @@ package org.jivesoftware.smackx.ox;
import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertTrue; import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertArrayEquals;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException; import java.security.NoSuchProviderException;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import org.jivesoftware.smack.test.util.SmackTestSuite; import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smackx.ox.crypto.PainlessOpenPgpProvider; import org.jivesoftware.smackx.ox.crypto.PainlessOpenPgpProvider;
import org.jivesoftware.smackx.ox.element.SecretkeyElement; import org.jivesoftware.smackx.ox.element.SecretkeyElement;
import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException; import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException;
@ -60,7 +59,7 @@ public class SecretKeyBackupHelperTest extends SmackTestSuite {
public void backupPasswordGenerationTest() { public void backupPasswordGenerationTest() {
final String alphabet = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ"; final String alphabet = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ";
String backupCode = SecretKeyBackupHelper.generateBackupPassword(); OpenPgpSecretKeyBackupPassphrase backupCode = SecretKeyBackupHelper.generateBackupPassword();
assertEquals(29, backupCode.length()); assertEquals(29, backupCode.length());
for (int i = 0; i < backupCode.length(); i++) { for (int i = 0; i < backupCode.length(); i++) {
if ((i + 1) % 5 == 0) { if ((i + 1) % 5 == 0) {
@ -86,12 +85,13 @@ public class SecretKeyBackupHelperTest extends SmackTestSuite {
provider.getStore().importSecretKey(jid, keyRing.getSecretKeys()); provider.getStore().importSecretKey(jid, keyRing.getSecretKeys());
// Create encrypted backup // Create encrypted backup
String backupCode = SecretKeyBackupHelper.generateBackupPassword(); OpenPgpSecretKeyBackupPassphrase backupCode = SecretKeyBackupHelper.generateBackupPassword();
SecretkeyElement element = SecretKeyBackupHelper.createSecretkeyElement(provider, jid, Collections.singleton(new OpenPgpV4Fingerprint(keyRing.getSecretKeys())), backupCode); SecretkeyElement element = SecretKeyBackupHelper.createSecretkeyElement(provider, jid,
Collections.singleton(new OpenPgpV4Fingerprint(keyRing.getSecretKeys())), backupCode);
// Decrypt backup and compare // Decrypt backup and compare
PGPSecretKeyRing secretKeyRing = SecretKeyBackupHelper.restoreSecretKeyBackup(element, backupCode); PGPSecretKeyRing secretKeyRing = SecretKeyBackupHelper.restoreSecretKeyBackup(element, backupCode);
assertTrue(Arrays.equals(keyRing.getSecretKeys().getEncoded(), secretKeyRing.getEncoded())); assertArrayEquals(keyRing.getSecretKeys().getEncoded(), secretKeyRing.getEncoded());
} }
@AfterClass @AfterClass