Skip to content

Commit

Permalink
Add support for importing FreeOTP 2 backups
Browse files Browse the repository at this point in the history
  • Loading branch information
alexbakker committed Sep 28, 2024
1 parent 08d900c commit 71fec5f
Show file tree
Hide file tree
Showing 7 changed files with 390 additions and 12 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public State read(InputStream stream, boolean isInternal) throws DatabaseImporte
entries.add(array.getJSONObject(i));
}

state = new FreeOtpImporter.State(entries);
state = new FreeOtpImporter.DecryptedStateV1(entries);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
import android.content.Context;

import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;

import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;

import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
Expand Down Expand Up @@ -31,10 +36,18 @@ protected SecretKey doInBackground(Params... args) {

public static SecretKey deriveKey(Params params) {
try {
// Some older versions of Android (< 26) do not support PBKDF2withHmacSHA512, so use
// BouncyCastle's implementation instead.
if (params.getAlgorithm().equals("PBKDF2withHmacSHA512")) {
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest());
gen.init(CryptoUtils.toBytes(params.getPassword()), params.getSalt(), params.getIterations());
byte[] key = ((KeyParameter) gen.generateDerivedParameters(params.getKeySize())).getKey();
return new SecretKeySpec(key, "AES");
}

SecretKeyFactory factory = SecretKeyFactory.getInstance(params.getAlgorithm());
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), params.getKeySize());
SecretKey key = factory.generateSecret(spec);
return new SecretKeySpec(key.getEncoded(), "AES");
return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,21 +216,49 @@ public void testImportBitwardenCsv() throws IOException, DatabaseImporterExcepti
}

@Test
public void testImportFreeOtp() throws IOException, DatabaseImporterException, OtpInfoException {
public void testImportFreeOtpV1() throws IOException, DatabaseImporterException, OtpInfoException {
List<VaultEntry> entries = importPlain(FreeOtpImporter.class, "freeotp.xml");
checkImportedFreeOtpEntries(entries);
checkImportedFreeOtpEntriesV1(entries);
}

@Test
public void testImportFreeOtpV2Sha1() throws IOException, DatabaseImporterException, OtpInfoException {
List<VaultEntry> entries = importEncrypted(FreeOtpImporter.class, "freeotp_v2_sha1.xml", encryptedState -> {
final char[] password = "test".toCharArray();
return ((FreeOtpImporter.EncryptedState) encryptedState).decrypt(password);
});
checkImportedEntries(entries);
}

@Test
public void testImportFreeOtpV2Sha512() throws IOException, DatabaseImporterException, OtpInfoException {
List<VaultEntry> entries = importEncrypted(FreeOtpImporter.class, "freeotp_v2_sha512.xml", encryptedState -> {
final char[] password = "test".toCharArray();
return ((FreeOtpImporter.EncryptedState) encryptedState).decrypt(password);
});
checkImportedEntries(entries);
}

@Test
public void testImportFreeOtpV2_27() throws IOException, DatabaseImporterException, OtpInfoException {
List<VaultEntry> entries = importEncrypted(FreeOtpImporter.class, "freeotp_v2_27.xml", encryptedState -> {
final char[] password = "test".toCharArray();
return ((FreeOtpImporter.EncryptedState) encryptedState).decrypt(password);
});
checkImportedEntries(entries);
}


@Test
public void testImportFreeOtpPlus() throws IOException, DatabaseImporterException, OtpInfoException {
List<VaultEntry> entries = importPlain(FreeOtpPlusImporter.class, "freeotp_plus.json");
checkImportedFreeOtpEntries(entries);
checkImportedFreeOtpEntriesV1(entries);
}

@Test
public void testImportFreeOtpPlusInternal() throws IOException, DatabaseImporterException, OtpInfoException {
List<VaultEntry> entries = importPlain(FreeOtpPlusImporter.class, "freeotp_plus_internal.xml", true);
checkImportedFreeOtpEntries(entries);
checkImportedFreeOtpEntriesV1(entries);
}

@Test
Expand Down Expand Up @@ -423,7 +451,7 @@ private void checkImportedTotpAuthenticatorEntries(List<VaultEntry> entries) thr
}
}

private void checkImportedFreeOtpEntries(List<VaultEntry> entries) throws OtpInfoException {
private void checkImportedFreeOtpEntriesV1(List<VaultEntry> entries) throws OtpInfoException {
for (VaultEntry entry : entries) {
// for some reason, FreeOTP adds -1 to the counter
VaultEntry entryVector = getEntryVectorBySecret(entry.getInfo().getSecret());
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 comments on commit 71fec5f

Please sign in to comment.