mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-11-25 21:42:07 +01:00
Merge pull request #408 from vanitasvitae/secretKeyBackupPassword
Allow user-supplied secret key backup passphrases
This commit is contained in:
commit
81cccaab91
9 changed files with 196 additions and 95 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -46,7 +46,6 @@ 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;
|
||||||
|
@ -389,8 +388,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.
|
||||||
|
@ -402,8 +402,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,
|
||||||
|
@ -413,8 +443,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<>();
|
||||||
|
@ -424,10 +452,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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -476,7 +503,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);
|
||||||
provider.getStore().importSecretKey(getJidOrThrow(), secretKeys);
|
provider.getStore().importSecretKey(getJidOrThrow(), secretKeys);
|
||||||
|
@ -551,7 +578,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;
|
||||||
}
|
}
|
||||||
|
@ -566,7 +593,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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue