66import android .net .Uri ;
77
88import androidx .annotation .NonNull ;
9+ import androidx .lifecycle .Lifecycle ;
910
11+ import com .beemdevelopment .aegis .R ;
12+ import com .beemdevelopment .aegis .encoding .Base32 ;
13+ import com .beemdevelopment .aegis .encoding .Base64 ;
14+ import com .beemdevelopment .aegis .encoding .EncodingException ;
15+ import com .beemdevelopment .aegis .helpers .ContextHelper ;
1016import com .beemdevelopment .aegis .otp .GoogleAuthInfo ;
1117import com .beemdevelopment .aegis .otp .GoogleAuthInfoException ;
12- import com .beemdevelopment .aegis .otp .OtpInfo ;
18+ import com .beemdevelopment .aegis .otp .OtpInfoException ;
19+ import com .beemdevelopment .aegis .otp .SteamInfo ;
20+ import com .beemdevelopment .aegis .ui .dialogs .Dialogs ;
21+ import com .beemdevelopment .aegis .ui .tasks .Argon2Task ;
1322import com .beemdevelopment .aegis .util .IOUtils ;
23+ import com .beemdevelopment .aegis .util .JsonUtils ;
1424import com .beemdevelopment .aegis .vault .VaultEntry ;
25+ import com .google .common .base .Objects ;
1526import com .topjohnwu .superuser .io .SuFile ;
1627
28+ import org .bouncycastle .crypto .params .Argon2Parameters ;
1729import org .json .JSONArray ;
1830import org .json .JSONException ;
1931import org .json .JSONObject ;
2032
2133import java .io .IOException ;
2234import java .io .InputStream ;
35+ import java .security .InvalidAlgorithmParameterException ;
36+ import java .security .InvalidKeyException ;
37+ import java .security .NoSuchAlgorithmException ;
38+ import java .util .Arrays ;
39+
40+ import javax .crypto .BadPaddingException ;
41+ import javax .crypto .Cipher ;
42+ import javax .crypto .IllegalBlockSizeException ;
43+ import javax .crypto .NoSuchPaddingException ;
44+ import javax .crypto .SecretKey ;
45+ import javax .crypto .spec .GCMParameterSpec ;
2346
2447public class ProtonAuthenticatorImporter extends DatabaseImporter {
2548
@@ -38,12 +61,94 @@ protected SuFile getAppPath() {
3861 String contents = new String (IOUtils .readAll (stream ), UTF_8 );
3962 JSONObject json = new JSONObject (contents );
4063
64+ if (json .has ("salt" ) && json .has ("content" )) {
65+ int version = json .getInt ("version" );
66+ if (version != 1 ) {
67+ throw new DatabaseImporterException (String .format ("Unsupported version: %d" , version ));
68+ }
69+ byte [] salt = Base64 .decode (json .getString ("salt" ));
70+ byte [] content = Base64 .decode (json .getString ("content" ));
71+ return new EncryptedState (salt , content );
72+ }
73+
4174 return new DecryptedState (json );
4275 } catch (JSONException | IOException e ) {
4376 throw new DatabaseImporterException (e );
4477 }
4578 }
4679
80+ public static class EncryptedState extends DatabaseImporter .State {
81+ private static final int KEY_SIZE = 32 ;
82+ private static final int MEMORY_KB = 19 * 1024 ;
83+ private static final int ITERATIONS = 2 ;
84+ private static final int PARALLELISM = 1 ;
85+ private static final int NONCE_SIZE = 12 ;
86+ private static final int TAG_SIZE = 16 ;
87+ private static final byte [] AAD = "proton.authenticator.export.v1" .getBytes (UTF_8 );
88+
89+ private final byte [] _salt ;
90+ private final byte [] _content ;
91+
92+ private EncryptedState (byte [] salt , byte [] content ) {
93+ super (true );
94+ _salt = salt ;
95+ _content = content ;
96+ }
97+
98+ public DecryptedState decrypt (char [] password ) throws DatabaseImporterException {
99+ Argon2Task .Params params = getKeyDerivationParams (password );
100+ SecretKey key = Argon2Task .deriveKey (params );
101+ return decrypt (key );
102+ }
103+
104+ private DecryptedState decrypt (SecretKey key ) throws DatabaseImporterException {
105+ try {
106+ byte [] nonce = Arrays .copyOfRange (_content , 0 , NONCE_SIZE );
107+ byte [] ct = Arrays .copyOfRange (_content , NONCE_SIZE , _content .length );
108+
109+ Cipher cipher = Cipher .getInstance ("AES/GCM/NoPadding" );
110+ cipher .init (Cipher .DECRYPT_MODE , key , new GCMParameterSpec (TAG_SIZE * 8 , nonce ));
111+ cipher .updateAAD (AAD );
112+ byte [] plaintext = cipher .doFinal (ct );
113+
114+ return new DecryptedState (new JSONObject (new String (plaintext , UTF_8 )));
115+ } catch (BadPaddingException | JSONException e ) {
116+ throw new DatabaseImporterException (e );
117+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
118+ | InvalidAlgorithmParameterException | IllegalBlockSizeException e ) {
119+ throw new RuntimeException (e );
120+ }
121+ }
122+
123+ @ Override
124+ public void decrypt (Context context , DecryptListener listener ) {
125+ Dialogs .showPasswordInputDialog (context , R .string .enter_password_proton_message , password -> {
126+ Argon2Task .Params params = getKeyDerivationParams (password );
127+ Argon2Task task = new Argon2Task (context , key -> {
128+ try {
129+ DecryptedState state = decrypt (key );
130+ listener .onStateDecrypted (state );
131+ } catch (DatabaseImporterException e ) {
132+ listener .onError (e );
133+ }
134+ });
135+ Lifecycle lifecycle = ContextHelper .getLifecycle (context );
136+ task .execute (lifecycle , params );
137+ }, dialog -> listener .onCanceled ());
138+ }
139+
140+ private Argon2Task .Params getKeyDerivationParams (char [] password ) {
141+ Argon2Parameters argon2Params = new Argon2Parameters .Builder (Argon2Parameters .ARGON2_id )
142+ .withVersion (Argon2Parameters .ARGON2_VERSION_13 )
143+ .withIterations (ITERATIONS )
144+ .withParallelism (PARALLELISM )
145+ .withMemoryAsKB (MEMORY_KB )
146+ .withSalt (_salt )
147+ .build ();
148+ return new Argon2Task .Params (password , argon2Params , KEY_SIZE );
149+ }
150+ }
151+
47152 public static class DecryptedState extends DatabaseImporter .State {
48153 private final JSONObject _json ;
49154
@@ -76,21 +181,27 @@ public DecryptedState(@NonNull JSONObject json) {
76181 private static @ NonNull VaultEntry convertEntry (@ NonNull JSONObject entry ) throws DatabaseImporterEntryException {
77182 try {
78183 JSONObject content = entry .getJSONObject ("content" );
79- String name = content .getString ("name" );
184+ String name = JsonUtils .optString (content , "name" );
185+ if (name == null ) {
186+ name = "" ;
187+ }
80188 String uriString = content .getString ("uri" );
81189
82190 Uri uri = Uri .parse (uriString );
83191 try {
84- GoogleAuthInfo info = GoogleAuthInfo .parseUri (uri );
85- OtpInfo otp = info .getOtpInfo ();
192+ if (Objects .equal (uri .getScheme (), "steam" ) && uri .getHost () != null ) {
193+ SteamInfo otp = new SteamInfo (Base32 .decode (uri .getHost ()));
194+ return new VaultEntry (otp , name , "Steam" );
195+ }
86196
87- return new VaultEntry (otp , name , info .getIssuer ());
88- } catch (GoogleAuthInfoException e ) {
197+ GoogleAuthInfo info = GoogleAuthInfo .parseUri (uri );
198+ return new VaultEntry (info .getOtpInfo (), name , info .getIssuer ());
199+ } catch (GoogleAuthInfoException | OtpInfoException | EncodingException e ) {
89200 throw new DatabaseImporterEntryException (e , uriString );
90201 }
91202 } catch (JSONException e ) {
92203 throw new DatabaseImporterEntryException (e , entry .toString ());
93204 }
94205 }
95206 }
96- }
207+ }
0 commit comments