Skip to content

Commit

Permalink
Merge pull request #1178 from alexbakker/fix-auth-pro
Browse files Browse the repository at this point in the history
Add support for new Authenticator Pro backup format
  • Loading branch information
michaelschattgen authored Sep 7, 2023
2 parents 27e56d6 + 9cabd9f commit 8164e91
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.Argon2Task;
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;

import org.bouncycastle.crypto.params.Argon2Parameters;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
Expand All @@ -44,9 +46,8 @@
import javax.crypto.spec.IvParameterSpec;

public class AuthenticatorProImporter extends DatabaseImporter {
private static final String HEADER = "AuthenticatorPro";
private static final int ITERATIONS = 64000;
private static final int KEY_SIZE = 32 * Byte.SIZE;
private static final String HEADER = "AUTHENTICATORPRO";
private static final String HEADER_LEGACY = "AuthenticatorPro";
private static final String PKG_NAME = "me.jmh.authenticatorpro";
private static final String PKG_DB_PATH = "files/proauth.db3";

Expand Down Expand Up @@ -90,24 +91,19 @@ private static State readExternal(InputStream stream) throws DatabaseImporterExc
}
}

private static EncryptedState readEncrypted(DataInputStream stream) throws DatabaseImporterException {
private static State readEncrypted(DataInputStream stream) throws DatabaseImporterException {
try {
byte[] headerBytes = new byte[HEADER.getBytes(StandardCharsets.UTF_8).length];
stream.readFully(headerBytes);
String header = new String(headerBytes, StandardCharsets.UTF_8);
if (!header.equals(HEADER)) {
throw new DatabaseImporterException("Invalid file header");
switch (header) {
case HEADER:
return EncryptedState.parseHeader(stream);
case HEADER_LEGACY:
return LegacyEncryptedState.parseHeader(stream);
default:
throw new DatabaseImporterException("Invalid file header");
}

int saltSize = 20;
byte[] salt = new byte[saltSize];
stream.readFully(salt);

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
int ivSize = cipher.getBlockSize();
byte[] iv = new byte[ivSize];
stream.readFully(iv);
return new EncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
} catch (UTFDataFormatException e) {
throw new DatabaseImporterException("Invalid file header");
} catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException e) {
Expand All @@ -130,6 +126,13 @@ private static OtpInfo parseOtpInfo(int type, byte[] secret, Algorithm algo, int
}

static class EncryptedState extends State {
private static final int KEY_SIZE = 32;
private static final int MEMORY_COST = 16; // 2^16 KiB = 64 MiB
private static final int PARALLELISM = 4;
private static final int ITERATIONS = 3;
private static final int SALT_SIZE = 16;
private static final int IV_SIZE = 12;

private final Cipher _cipher;
private final byte[] _salt;
private final byte[] _iv;
Expand All @@ -143,6 +146,81 @@ public EncryptedState(Cipher cipher, byte[] salt, byte[] iv, byte[] data) {
_data = data;
}

public JsonState decrypt(char[] password) throws DatabaseImporterException {
Argon2Task.Params params = getKeyDerivationParams(password);
SecretKey key = Argon2Task.deriveKey(params);
return decrypt(key);
}

public JsonState decrypt(SecretKey key) throws DatabaseImporterException {
try {
_cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(_iv));
byte[] decrypted = _cipher.doFinal(_data);
return new JsonState(new JSONObject(new String(decrypted, StandardCharsets.UTF_8)));
} catch (InvalidAlgorithmParameterException | IllegalBlockSizeException
| JSONException | InvalidKeyException | BadPaddingException e) {
throw new DatabaseImporterException(e);
}
}

@Override
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> {
Argon2Task.Params params = getKeyDerivationParams(password);
Argon2Task task = new Argon2Task(context, key -> {
try {
AuthenticatorProImporter.JsonState state = decrypt(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, dialog -> listener.onCanceled());
}

private Argon2Task.Params getKeyDerivationParams(char[] password) {
Argon2Parameters argon2Params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
.withIterations(ITERATIONS)
.withParallelism(PARALLELISM)
.withMemoryPowOfTwo(MEMORY_COST)
.withSalt(_salt)
.build();
return new Argon2Task.Params(password, argon2Params, KEY_SIZE);
}

private static EncryptedState parseHeader(DataInputStream stream)
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
byte[] salt = new byte[SALT_SIZE];
stream.readFully(salt);

byte[] iv = new byte[IV_SIZE];
stream.readFully(iv);

Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
return new EncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
}
}

static class LegacyEncryptedState extends State {
private static final int ITERATIONS = 64000;
private static final int KEY_SIZE = 32 * Byte.SIZE;
private static final int SALT_SIZE = 20;

private final Cipher _cipher;
private final byte[] _salt;
private final byte[] _iv;
private final byte[] _data;

public LegacyEncryptedState(Cipher cipher, byte[] salt, byte[] iv, byte[] data) {
super(true);
_cipher = cipher;
_salt = salt;
_iv = iv;
_data = data;
}

public JsonState decrypt(char[] password) throws DatabaseImporterException {
PBKDFTask.Params params = getKeyDerivationParams(password);
SecretKey key = PBKDFTask.deriveKey(params);
Expand Down Expand Up @@ -180,6 +258,18 @@ public void decrypt(Context context, DecryptListener listener) throws DatabaseIm
private PBKDFTask.Params getKeyDerivationParams(char[] password) {
return new PBKDFTask.Params("PBKDF2WithHmacSHA1", KEY_SIZE, password, _salt, ITERATIONS);
}

private static LegacyEncryptedState parseHeader(DataInputStream stream)
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
byte[] salt = new byte[SALT_SIZE];
stream.readFully(salt);

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
int ivSize = cipher.getBlockSize();
byte[] iv = new byte[ivSize];
stream.readFully(iv);
return new LegacyEncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
}
}

private static class JsonState extends State {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.beemdevelopment.aegis.ui.tasks;

import android.content.Context;

import com.beemdevelopment.aegis.R;

import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class Argon2Task extends ProgressDialogTask<Argon2Task.Params, SecretKey> {
private final Callback _cb;

public Argon2Task(Context context, Callback cb) {
super(context, context.getString(R.string.unlocking_vault));
_cb = cb;
}

@Override
protected SecretKey doInBackground(Params... args) {
setPriority();

Params params = args[0];
return deriveKey(params);
}

public static SecretKey deriveKey(Params params) {
Argon2BytesGenerator gen = new Argon2BytesGenerator();
gen.init(params.getArgon2Params());

byte[] key = new byte[params.getKeySize()];
gen.generateBytes(params.getPassword(), key);
return new SecretKeySpec(key, 0, key.length, "AES");
}

@Override
protected void onPostExecute(SecretKey key) {
super.onPostExecute(key);
_cb.onTaskFinished(key);
}

public interface Callback {
void onTaskFinished(SecretKey key);
}

public static class Params {
private final char[] _password;
private final Argon2Parameters _argon2Params;
private final int _keySize;

public Params(char[] password, Argon2Parameters argon2Params, int keySize) {
_password = password;
_argon2Params = argon2Params;
_keySize = keySize;
}

public char[] getPassword() {
return _password;
}

public Argon2Parameters getArgon2Params() {
return _argon2Params;
}

public int getKeySize() {
return _keySize;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,16 @@ public void testImportTotpAuthenticatorInternal() throws IOException, DatabaseIm
public void testImportAuthProEncrypted() throws DatabaseImporterException, IOException, OtpInfoException {
List<VaultEntry> entries = importEncrypted(AuthenticatorProImporter.class, "authpro_encrypted.bin", state -> {
char[] password = "test".toCharArray();
try {
return ((AuthenticatorProImporter.EncryptedState) state).decrypt(password);
} catch (DatabaseImporterException e) {
throw new DatabaseImporterException(e);
}
return ((AuthenticatorProImporter.EncryptedState) state).decrypt(password);
});
checkImportedEntries(entries);
}

@Test
public void testImportAuthProEncryptedLegacy() throws DatabaseImporterException, IOException, OtpInfoException {
List<VaultEntry> entries = importEncrypted(AuthenticatorProImporter.class, "authpro_encrypted_legacy.bin", state -> {
char[] password = "test".toCharArray();
return ((AuthenticatorProImporter.LegacyEncryptedState) state).decrypt(password);
});
checkImportedEntries(entries);
}
Expand Down
Binary file not shown.
Binary file not shown.

0 comments on commit 8164e91

Please sign in to comment.