Allow user-supplied secret key backup passphrases

Instead of passing the passphrase back to the user via a
DisplayBackupCodeCallback, we directly return the passphrase
which is now represented by a class.

Also we now allow the user to provide the passphrase.
This commit is contained in:
Paul Schaub 2020-07-15 22:22:42 +02:00
parent 075e65ad40
commit 6e57ea0873
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
8 changed files with 183 additions and 87 deletions

View File

@ -16,6 +16,7 @@
*/
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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@ -26,17 +27,11 @@ import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Arrays;
import java.util.Set;
import java.util.logging.Level;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
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.exception.InvalidBackupCodeException;
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 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
* in XEP-0373 §5.
@ -123,7 +114,7 @@ public class OXSecretKeyBackupIntegrationTest extends AbstractOpenPgpIntegration
OpenPgpStore beforeStore = new FileBasedOpenPgpStore(beforePath);
beforeStore.setKeyRingProtector(new UnprotectedKeysProtector());
PainlessOpenPgpProvider beforeProvider = new PainlessOpenPgpProvider(beforeStore);
openPgpManager = OpenPgpManager.getInstanceFor(aliceConnection);
OpenPgpManager openPgpManager = OpenPgpManager.getInstanceFor(aliceConnection);
openPgpManager.setOpenPgpProvider(beforeProvider);
OpenPgpSelf self = openPgpManager.getOpenPgpSelf();
@ -141,29 +132,15 @@ public class OXSecretKeyBackupIntegrationTest extends AbstractOpenPgpIntegration
PGPPublicKeyRing beforePub = beforeStore.getPublicKeyRing(alice, keyFingerprint);
assertNotNull(beforePub);
openPgpManager.backupSecretKeyToServer(new DisplayBackupCodeCallback() {
@Override
public void displayBackupCode(String backupCode) {
OXSecretKeyBackupIntegrationTest.this.backupCode = backupCode;
}
}, new SecretKeyBackupSelectionCallback() {
@Override
public Set<OpenPgpV4Fingerprint> selectKeysToBackup(Set<OpenPgpV4Fingerprint> availableSecretKeys) {
return availableSecretKeys;
}
});
OpenPgpSecretKeyBackupPassphrase backupPassphrase =
openPgpManager.backupSecretKeyToServer(availableSecretKeys -> availableSecretKeys);
FileBasedOpenPgpStore afterStore = new FileBasedOpenPgpStore(afterPath);
afterStore.setKeyRingProtector(new UnprotectedKeysProtector());
PainlessOpenPgpProvider afterProvider = new PainlessOpenPgpProvider(afterStore);
openPgpManager.setOpenPgpProvider(afterProvider);
OpenPgpV4Fingerprint fingerprint = openPgpManager.restoreSecretKeyServerBackup(new AskForBackupCodeCallback() {
@Override
public String askForBackupCode() {
return backupCode;
}
});
OpenPgpV4Fingerprint fingerprint = openPgpManager.restoreSecretKeyServerBackup(() -> backupPassphrase);
assertEquals(keyFingerprint, fingerprint);
@ -173,10 +150,10 @@ public class OXSecretKeyBackupIntegrationTest extends AbstractOpenPgpIntegration
PGPSecretKeyRing afterSec = afterStore.getSecretKeyRing(alice, keyFingerprint);
assertNotNull(afterSec);
assertTrue(Arrays.equals(beforeSec.getEncoded(), afterSec.getEncoded()));
assertArrayEquals(beforeSec.getEncoded(), afterSec.getEncoded());
PGPPublicKeyRing afterPub = afterStore.getPublicKeyRing(alice, keyFingerprint);
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");
* 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.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.OpenPgpProvider;
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>
*
* @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.
* @return secret key passphrase used to encrypt the backup.
*
* @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.
@ -402,8 +402,38 @@ public final class OpenPgpManager extends Manager {
* @throws PGPException PGP is brittle
* @throws MissingOpenPgpKeyException in case we have no OpenPGP key pair to back up.
*/
public void backupSecretKeyToServer(DisplayBackupCodeCallback displayCodeCallback,
SecretKeyBackupSelectionCallback selectKeyCallback)
public OpenPgpSecretKeyBackupPassphrase backupSecretKeyToServer(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,
XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException,
SmackException.NotLoggedInException, IOException,
@ -413,8 +443,6 @@ public final class OpenPgpManager extends Manager {
BareJid ownJid = connection().getUser().asBareJid();
String backupCode = SecretKeyBackupHelper.generateBackupPassword();
PGPSecretKeyRingCollection secretKeyRings = provider.getStore().getSecretKeysOf(ownJid);
Set<OpenPgpV4Fingerprint> availableKeyPairs = new HashSet<>();
@ -424,10 +452,9 @@ public final class OpenPgpManager extends Manager {
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);
displayCodeCallback.displayBackupCode(backupCode);
}
/**
@ -476,7 +503,7 @@ public final class OpenPgpManager extends Manager {
throw new NoBackupFoundException();
}
String backupCode = codeCallback.askForBackupCode();
OpenPgpSecretKeyBackupPassphrase backupCode = codeCallback.askForBackupCode();
PGPSecretKeyRing secretKeys = SecretKeyBackupHelper.restoreSecretKeyBackup(backup, backupCode);
provider.getStore().importSecretKey(getJidOrThrow(), secretKeys);

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;
import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase;
public interface AskForBackupCodeCallback {
/**
@ -27,5 +29,5 @@ public interface AskForBackupCodeCallback {
*
* @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.stringencoder.Base64;
import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase;
import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider;
import org.jivesoftware.smackx.ox.element.SecretkeyElement;
import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException;
@ -51,8 +52,8 @@ public class SecretKeyBackupHelper {
*
* @return backup code
*/
public static String generateBackupPassword() {
return StringUtils.secureOfflineAttackSafeRandomString();
public static OpenPgpSecretKeyBackupPassphrase generateBackupPassword() {
return new OpenPgpSecretKeyBackupPassphrase(StringUtils.secureOfflineAttackSafeRandomString());
}
/**
@ -73,7 +74,7 @@ public class SecretKeyBackupHelper {
public static SecretkeyElement createSecretkeyElement(OpenPgpProvider provider,
BareJid owner,
Set<OpenPgpV4Fingerprint> fingerprints,
String backupCode)
OpenPgpSecretKeyBackupPassphrase backupCode)
throws PGPException, IOException, MissingOpenPgpKeyException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
@ -105,9 +106,9 @@ public class SecretKeyBackupHelper {
* @throws IOException IO is dangerous
*/
public static SecretkeyElement createSecretkeyElement(byte[] keys,
String backupCode)
OpenPgpSecretKeyBackupPassphrase backupCode)
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);
return new SecretkeyElement(Base64.encode(encrypted));
}
@ -123,13 +124,13 @@ public class SecretKeyBackupHelper {
* @throws IOException IO is dangerous.
* @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 {
byte[] encrypted = Base64.decode(backup.getB64Data());
byte[] decrypted;
try {
decrypted = PGPainless.decryptWithPassword(encrypted, new Passphrase(backupCode.toCharArray()));
decrypted = PGPainless.decryptWithPassword(encrypted, new Passphrase(backupCode.toString().toCharArray()));
} catch (IOException | PGPException 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");
* 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.assertTrue;
import static org.junit.Assert.assertArrayEquals;
import java.io.File;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Arrays;
import java.util.Collections;
import org.jivesoftware.smack.test.util.SmackTestSuite;
import org.jivesoftware.smackx.ox.crypto.PainlessOpenPgpProvider;
import org.jivesoftware.smackx.ox.element.SecretkeyElement;
import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException;
@ -60,7 +59,7 @@ public class SecretKeyBackupHelperTest extends SmackTestSuite {
public void backupPasswordGenerationTest() {
final String alphabet = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ";
String backupCode = SecretKeyBackupHelper.generateBackupPassword();
OpenPgpSecretKeyBackupPassphrase backupCode = SecretKeyBackupHelper.generateBackupPassword();
assertEquals(29, backupCode.length());
for (int i = 0; i < backupCode.length(); i++) {
if ((i + 1) % 5 == 0) {
@ -86,12 +85,13 @@ public class SecretKeyBackupHelperTest extends SmackTestSuite {
provider.getStore().importSecretKey(jid, keyRing.getSecretKeys());
// Create encrypted backup
String backupCode = SecretKeyBackupHelper.generateBackupPassword();
SecretkeyElement element = SecretKeyBackupHelper.createSecretkeyElement(provider, jid, Collections.singleton(new OpenPgpV4Fingerprint(keyRing.getSecretKeys())), backupCode);
OpenPgpSecretKeyBackupPassphrase backupCode = SecretKeyBackupHelper.generateBackupPassword();
SecretkeyElement element = SecretKeyBackupHelper.createSecretkeyElement(provider, jid,
Collections.singleton(new OpenPgpV4Fingerprint(keyRing.getSecretKeys())), backupCode);
// Decrypt backup and compare
PGPSecretKeyRing secretKeyRing = SecretKeyBackupHelper.restoreSecretKeyBackup(element, backupCode);
assertTrue(Arrays.equals(keyRing.getSecretKeys().getEncoded(), secretKeyRing.getEncoded()));
assertArrayEquals(keyRing.getSecretKeys().getEncoded(), secretKeyRing.getEncoded());
}
@AfterClass