From 9cabd9f309ea8820afeb60a31998e582486dc177 Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Thu, 7 Sep 2023 21:41:30 +0200 Subject: [PATCH] Add support for new Authenticator Pro backup format This adds support for Authenticator Pro's latest backup format changes. The format of the content itself has not changed as far as I can tell, but they do use a different cipher and KDF now: AES GCM and Argon2id, respectively. The memory cost is statically set at 64MiB. I suspect that this may cause OOM situations on some lower-end devices, but we'll see, not much we can do about that right now without making more changes. --- .../importers/AuthenticatorProImporter.java | 122 +++++++++++++++--- .../aegis/ui/tasks/Argon2Task.java | 71 ++++++++++ .../aegis/importers/DatabaseImporterTest.java | 15 ++- .../aegis/importers/authpro_encrypted.bin | Bin 1332 -> 1436 bytes .../importers/authpro_encrypted_legacy.bin | Bin 0 -> 1332 bytes 5 files changed, 187 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/beemdevelopment/aegis/ui/tasks/Argon2Task.java create mode 100644 app/src/test/resources/com/beemdevelopment/aegis/importers/authpro_encrypted_legacy.bin diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AuthenticatorProImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AuthenticatorProImporter.java index 354ded2d2f..1f755962e8 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/AuthenticatorProImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AuthenticatorProImporter.java @@ -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; @@ -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"; @@ -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) { @@ -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; @@ -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); @@ -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 { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/tasks/Argon2Task.java b/app/src/main/java/com/beemdevelopment/aegis/ui/tasks/Argon2Task.java new file mode 100644 index 0000000000..98b35b6abb --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/tasks/Argon2Task.java @@ -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 { + 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; + } + } +} diff --git a/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java b/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java index d255f68abc..c9f1030a5f 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java +++ b/app/src/test/java/com/beemdevelopment/aegis/importers/DatabaseImporterTest.java @@ -146,11 +146,16 @@ public void testImportTotpAuthenticatorInternal() throws IOException, DatabaseIm public void testImportAuthProEncrypted() throws DatabaseImporterException, IOException, OtpInfoException { List 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 entries = importEncrypted(AuthenticatorProImporter.class, "authpro_encrypted_legacy.bin", state -> { + char[] password = "test".toCharArray(); + return ((AuthenticatorProImporter.LegacyEncryptedState) state).decrypt(password); }); checkImportedEntries(entries); } diff --git a/app/src/test/resources/com/beemdevelopment/aegis/importers/authpro_encrypted.bin b/app/src/test/resources/com/beemdevelopment/aegis/importers/authpro_encrypted.bin index 25858829537f958a32974cd2bf87f7138ff4f00a..b515a7b318acd5c591ba640b3746d04c52c450c6 100644 GIT binary patch literal 1436 zcmV;N1!MX_Ra8htPE<)lK~zstP*P7Ho1RP+%K&=9j7B{Hf5$sV_M8Z$InO6s<+g2` zI7Kb3Vy$y^>h1-;ecg2+Us`A%k!B1hRZK`*`Njy1(3HHhINJHo)=BF3rwLCDs66s%Yl1bsO_aTRy>ywRc%7ge;6N5P_ zc@7S4iuIv0z?{tvAwDa$FV6BPyK9wjxD^7gXZ(Lx3*|x63o@ii!&jbQ*0Bv-Jz z!?9aJi5PH++1x~I02cao5XR?v6k&0aXmkuV?QMZad9j~IaDC=~KC*;0NUf+osOTr4 zL^VN~{aSJ&2{#f`JY8X7v-4S)uC$S9^5&u=mx-$y)3^;F#qF41${+s}q#ka20hg7A z_hicZ+|x)CP%(x z&KJe8?aspu3BAv}Au=F|jsp_VFMgFzmJ&uW2@GZEu~|ub>g9Oas_OLQ>ofyWH*{3Kq>5fa9P{DX*+)EtB2gD0qOReG2yICN+e4o>J zWaY)0L?)&-xa_*Vc+AOVmxNB7-b>g!$VmWBL`}0?U;9V7N{)apWQt6r0pDyBNQpa0 zq$;~e(+VTFNb8)y{hKX)rzA{okIoj%2QU9p^Sls2I*chop`>;)##!i~0WDtd9!1cZ z8Nn!L^BTxT#WE4P5p39V-JB!g)^irY_a{thcCN>umCKL}J=4?`XG&7Xgbx?8e^WZ{$`7}9#?r`#@8TgSxm*kH-Z$v{N%c-~8GGU@!+b4712;*% z&42-o4v_3WFG+uEKW5$n)8S`qf2aYsS4{Z`mTh28ZX17WLweI8SsC4vX^wo)0j%M7{x3;$TgE9EDapi zCNS0m!Y=Slu21+q_Y8+?`uKLC&7aRiO=zMU8JnF<5Nu6sY#ZvYAN~KSbv*d4Wi;`N zcCDW!7MietzMRp_G(}%oR$0Zi1Sc(?iZHT>-JZPu#DRbi%6OtSR@mgP7fyHnwJ*$| zOoW!&8$M)~tgi$vmmOhq-)223vZC#Legsgj!!bYXRoY|rpCHs=g4lZ1 zs>p$bM__1O+!khWA53n$vNTql0jj?KgeC_hi@Y>h%1C($iY^@&@z%BzAh1=dG|Ki< zRYOiwFz;~7^5`OkkBN&fbE3C*x3-ijPAAXhm`@cj)XMj4%OAJ!XUq;vFhzr@uj^G0 z(UQ%14&6L-^4k9fc4)uf($N1FHh}WpF8jlnDGp-kbB9$=!;H|0=;izo<;G3NfmKT- zoQnI3D!_IDl~Q-+9RJ&ZpR1c8O0S-Zyir q?#Q#~6|uU}gUTk_h(zY8|JkasTt|x54zITr!NOlM$EYOy!5SD#Da9=S literal 1332 zcmV-41uyrB^qj(fGg=FM_tG(ua!I3K6IB!|`gq2O^2gQE1Qm|`H5T+_Q za4;d?gz#pABlqi<7LO3fP_HZbV)jR#Deu;6;}T`lzO|2WnzilJiZGEo z{lu;)6r9@Agks2d7Z-i}s$4F9B*U18+x^RurPGgxlsrn;_z z+g`0+s`xx&h4lJ>U(X~+#amuQ9L;`eLwV814AdCp4M)8jR>^lPrJqCBKp!BgXWIf2 zH@E#tL`1(mmHyI!S-d!Czn^=rR(WILW-T@S`}oS3HMq!kY|NEey|eUrwEyf6pBv9A za<4;D!$IQ2FLcn?WnMp{zpte}j=AL$@6?%3_46w83S?(y0**|hoQ{@Um80DI=d+hA zVl^~{>XHY5vbkIRha9p^$)L6Stu!CnN)LZZr=qT%0-H zx;_0caNpR}xB*;>`og08s>Juu{ve6hVV4s0$mfGW&-&h9ut(G;$LsJBk{NF$h$iOw zA(SsY*m;Ai3v@25`+D{3bu;E(I{wbmQiLV|)+D3xolIq3YW*VCqS8GrPq6(u_|0=x z7$OL08UG|SrSUE{oP*9ueLXerzd_iha-7*jZKT=v8hmqQLE(?-vrNz7|C?9O!1=I} z;hlFZP>Gx)YFH^Us*BsASZht#jj`j}{VNQ?PkZd4uN6kC^+GK-eR*5+Cfvz0vsOQ{ zmt@mSn8_nkh&2*N_nyIQ)wq703% zD^CZ`V|jkhr6Y|~ZU_(Jy=|XBEqsII;(Lqeeb`z{`QR4rwEQMzghD8re7k(shjM_5 z!mZ#+$gg0bYJO4uk2;rLgm>FD9x}4CT4GI1xq)%XCuTHS?MCLzLt_%PAP-E{8GcJc zuS$ZpjAJB%`NJJOPG+-=&Vw-jB&Ygab}Kn~RI^r<6jt17lED&7F3iu9&xLqN7If@2 zuDJ<wQKuc%tT#l>BnNE12$rbM%|MF`Z&yNZ8W7}t8<~) zB)WJt@`5}Sl)dRnlF4M+r8x_eVn8os?a}nU%08R%KU86|Ja$@Te33 diff --git a/app/src/test/resources/com/beemdevelopment/aegis/importers/authpro_encrypted_legacy.bin b/app/src/test/resources/com/beemdevelopment/aegis/importers/authpro_encrypted_legacy.bin new file mode 100644 index 0000000000000000000000000000000000000000..25858829537f958a32974cd2bf87f7138ff4f00a GIT binary patch literal 1332 zcmV-41uyrB^qj(fGg=FM_tG(ua!I3K6IB!|`gq2O^2gQE1Qm|`H5T+_Q za4;d?gz#pABlqi<7LO3fP_HZbV)jR#Deu;6;}T`lzO|2WnzilJiZGEo z{lu;)6r9@Agks2d7Z-i}s$4F9B*U18+x^RurPGgxlsrn;_z z+g`0+s`xx&h4lJ>U(X~+#amuQ9L;`eLwV814AdCp4M)8jR>^lPrJqCBKp!BgXWIf2 zH@E#tL`1(mmHyI!S-d!Czn^=rR(WILW-T@S`}oS3HMq!kY|NEey|eUrwEyf6pBv9A za<4;D!$IQ2FLcn?WnMp{zpte}j=AL$@6?%3_46w83S?(y0**|hoQ{@Um80DI=d+hA zVl^~{>XHY5vbkIRha9p^$)L6Stu!CnN)LZZr=qT%0-H zx;_0caNpR}xB*;>`og08s>Juu{ve6hVV4s0$mfGW&-&h9ut(G;$LsJBk{NF$h$iOw zA(SsY*m;Ai3v@25`+D{3bu;E(I{wbmQiLV|)+D3xolIq3YW*VCqS8GrPq6(u_|0=x z7$OL08UG|SrSUE{oP*9ueLXerzd_iha-7*jZKT=v8hmqQLE(?-vrNz7|C?9O!1=I} z;hlFZP>Gx)YFH^Us*BsASZht#jj`j}{VNQ?PkZd4uN6kC^+GK-eR*5+Cfvz0vsOQ{ zmt@mSn8_nkh&2*N_nyIQ)wq703% zD^CZ`V|jkhr6Y|~ZU_(Jy=|XBEqsII;(Lqeeb`z{`QR4rwEQMzghD8re7k(shjM_5 z!mZ#+$gg0bYJO4uk2;rLgm>FD9x}4CT4GI1xq)%XCuTHS?MCLzLt_%PAP-E{8GcJc zuS$ZpjAJB%`NJJOPG+-=&Vw-jB&Ygab}Kn~RI^r<6jt17lED&7F3iu9&xLqN7If@2 zuDJ<wQKuc%tT#l>BnNE12$rbM%|MF`Z&yNZ8W7}t8<~) zB)WJt@`5}Sl)dRnlF4M+r8x_eVn8os?a}nU%08R%KU86|Ja$@Te33 literal 0 HcmV?d00001