Skip to content

Commit

Permalink
Merge pull request #1506 from alexbakker/freeotp2
Browse files Browse the repository at this point in the history
Add support for importing FreeOTP 2 backups
  • Loading branch information
michaelschattgen authored Nov 11, 2024
2 parents c8d5be6 + cc5ce48 commit e8f0666
Show file tree
Hide file tree
Showing 10 changed files with 409 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public abstract class DatabaseImporter {
_importers.add(new Definition("Bitwarden", BitwardenImporter.class, R.string.importer_help_bitwarden, false));
_importers.add(new Definition("Duo", DuoImporter.class, R.string.importer_help_duo, true));
_importers.add(new Definition("Ente Auth", EnteAuthImporter.class, R.string.importer_help_ente_auth, false));
_importers.add(new Definition("FreeOTP (1.x)", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
_importers.add(new Definition("FreeOTP+ (JSON)", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
_importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true));
Expand Down

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,6 +36,15 @@ 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);
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -531,8 +531,10 @@
<string name="importer_help_battle_net_authenticator">Supply a copy of <b>/data/data/com.blizzard.messenger/shared_prefs/com.blizzard.messenger.authenticator_preferences.xml</b>, located in the internal storage directory of Battle.net Authenticator.</string>
<string name="importer_help_duo">Supply a copy of <b>/data/data/com.duosecurity.duomobile/files/duokit/accounts.json</b>, located in the internal storage directory of DUO.</string>
<string name="importer_help_ente_auth">Supply an Ente Auth export file. Currently only unencrypted files are supported.</string>
<string name="importer_help_freeotp">Supply a copy of <b>/data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml</b>, located in the internal storage directory of FreeOTP (1.x).</string>
<string name="importer_help_freeotp">FreeOTP 2: Supply a backup file.\nFreeOTP 1.x: Supply a copy of <b>/data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml</b>, located in the internal storage directory of FreeOTP.</string>
<string name="importer_help_freeotp_plus">Supply a FreeOTP+ export file.</string>
<string name="importer_warning_title_freeotp2">FreeOTP 2 compatibility</string>
<string name="importer_warning_message_freeotp2">There are a number of issues in FreeOTP 2 that can result in corrupt backups. Aegis will try to salvage as many entries as possible, but it\'s possible that some or even all of them fail to import.</string>
<string name="importer_help_google_authenticator"><b>Only database files from Google Authenticator v5.10 and prior are supported</b>.\n\nSupply a copy of <b>/data/data/com.google.android.apps.authenticator2/databases/databases</b>, located in the internal storage directory of Google Authenticator.</string>
<string name="importer_help_microsoft_authenticator">Supply a copy of <b>/data/data/com.azure.authenticator/databases/PhoneFactor</b>, located in the internal storage directory of Microsoft Authenticator.</string>
<string name="importer_help_plain_text">Supply a plain text file with a Google Authenticator URI on each line.</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,21 +216,57 @@ 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 testImportFreeOtpV2Api23() throws IOException, DatabaseImporterException, OtpInfoException {
List<VaultEntry> entries = importEncrypted(FreeOtpImporter.class, "freeotp_v2_api23.xml", encryptedState -> {
final char[] password = "test".toCharArray();
return ((FreeOtpImporter.EncryptedState) encryptedState).decrypt(password);
});
checkImportedEntries(entries);
}

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

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

@Test
public void testImportFreeOtpV2Api34() throws IOException, DatabaseImporterException, OtpInfoException {
List<VaultEntry> entries = importEncrypted(FreeOtpImporter.class, "freeotp_v2_api34.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 +459,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.
Binary file not shown.

0 comments on commit e8f0666

Please sign in to comment.