Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,43 @@
import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;

import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.Argon2Task;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.common.base.Objects;
import com.topjohnwu.superuser.io.SuFile;

import org.bouncycastle.crypto.params.Argon2Parameters;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;

public class ProtonAuthenticatorImporter extends DatabaseImporter {

Expand All @@ -38,12 +61,94 @@ protected SuFile getAppPath() {
String contents = new String(IOUtils.readAll(stream), UTF_8);
JSONObject json = new JSONObject(contents);

if (json.has("salt") && json.has("content")) {
int version = json.getInt("version");
if (version != 1) {
throw new DatabaseImporterException(String.format("Unsupported version: %d", version));
}
byte[] salt = Base64.decode(json.getString("salt"));
byte[] content = Base64.decode(json.getString("content"));
return new EncryptedState(salt, content);
}

return new DecryptedState(json);
} catch (JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}

public static class EncryptedState extends DatabaseImporter.State {
private static final int KEY_SIZE = 32;
private static final int MEMORY_KB = 19 * 1024;
private static final int ITERATIONS = 2;
private static final int PARALLELISM = 1;
private static final int NONCE_SIZE = 12;
private static final int TAG_SIZE = 16;
private static final byte[] AAD = "proton.authenticator.export.v1".getBytes(UTF_8);

private final byte[] _salt;
private final byte[] _content;

private EncryptedState(byte[] salt, byte[] content) {
super(true);
_salt = salt;
_content = content;
}

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

private DecryptedState decrypt(SecretKey key) throws DatabaseImporterException {
try {
byte[] nonce = Arrays.copyOfRange(_content, 0, NONCE_SIZE);
byte[] ct = Arrays.copyOfRange(_content, NONCE_SIZE, _content.length);

Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_SIZE * 8, nonce));
cipher.updateAAD(AAD);
byte[] plaintext = cipher.doFinal(ct);

return new DecryptedState(new JSONObject(new String(plaintext, UTF_8)));
} catch (BadPaddingException | JSONException e) {
throw new DatabaseImporterException(e);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}

@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_proton_message, password -> {
Argon2Task.Params params = getKeyDerivationParams(password);
Argon2Task task = new Argon2Task(context, key -> {
try {
DecryptedState 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)
.withVersion(Argon2Parameters.ARGON2_VERSION_13)
.withIterations(ITERATIONS)
.withParallelism(PARALLELISM)
.withMemoryAsKB(MEMORY_KB)
.withSalt(_salt)
.build();
return new Argon2Task.Params(password, argon2Params, KEY_SIZE);
}
}

public static class DecryptedState extends DatabaseImporter.State {
private final JSONObject _json;

Expand Down Expand Up @@ -76,21 +181,27 @@ public DecryptedState(@NonNull JSONObject json) {
private static @NonNull VaultEntry convertEntry(@NonNull JSONObject entry) throws DatabaseImporterEntryException {
try {
JSONObject content = entry.getJSONObject("content");
String name = content.getString("name");
String name = JsonUtils.optString(content, "name");
if (name == null) {
name = "";
}
String uriString = content.getString("uri");

Uri uri = Uri.parse(uriString);
try {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
OtpInfo otp = info.getOtpInfo();
if (Objects.equal(uri.getScheme(), "steam") && uri.getHost() != null) {
SteamInfo otp = new SteamInfo(Base32.decode(uri.getHost()));
return new VaultEntry(otp, name, "Steam");
}

return new VaultEntry(otp, name, info.getIssuer());
} catch (GoogleAuthInfoException e) {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
return new VaultEntry(info.getOtpInfo(), name, info.getIssuer());
} catch (GoogleAuthInfoException | OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, uriString);
}
} catch (JSONException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@
<string name="password_reminder_freq_biweekly">Biweekly</string>
<string name="password_reminder_freq_monthly">Monthly</string>
<string name="password_reminder_freq_quarterly">Quarterly</string>
<string name="enter_password_proton_message">It looks like this Proton Authenticator backup is encrypted. Please enter the password below.</string>
<string name="enter_password_2fas_message">It looks like this 2FAS backup is encrypted. Please enter the password below.</string>
<string name="enter_password_authy_message">It looks like your Authy tokens are encrypted. Please close Aegis, open Authy and unlock the tokens with your password. Instead, Aegis can also attempt to decrypt your Authy tokens for you, if you enter your password below.</string>
<string name="enter_password_aegis_title">Please enter the import password</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,25 @@ public void testImportProtonAuthenticator() throws IOException, DatabaseImporter
checkImportedEntries(entries);
}

@Test
public void testImportProtonAuthenticatorEncrypted() throws IOException, DatabaseImporterException, OtpInfoException {
List<VaultEntry> entries = importEncrypted(ProtonAuthenticatorImporter.class, "proton_authenticator_encrypted.json", encryptedState -> {
final char[] password = "test".toCharArray();
return ((ProtonAuthenticatorImporter.EncryptedState) encryptedState).decrypt(password);
});
for (VaultEntry entry : entries) {
// Proton Authenticator forgets the name and issuer of Steam entries
if (entry.getInfo().getTypeId().equals(SteamInfo.ID)) {
VaultEntry entryVector = getEntryVectorBySecret(entry.getInfo().getSecret());
entryVector.setName("");
entryVector.setIssuer("Steam");
checkImportedEntry(entryVector, entry);
} else {
checkImportedEntry(entry);
}
}
}

private List<VaultEntry> importPlain(Class<? extends DatabaseImporter> type, String resName)
throws IOException, DatabaseImporterException {
return importPlain(type, resName, false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":1,"salt":"sEXuJLVIa3jqN6tO0MqWWw==","content":"gGNkWhT158vSo5q/6M/BvlaSjSuMbNWkXp8y3Jjp0oWY9N9N9DtG+HNb+AJ1ySyw5G4zbk+g4aDRxu8+/JcFCxA5FSMCBgqvMVAWWZcCMj1BGl9qN6XAgoNDcYVWHkV5MyUudZowkVSsZfKS+Fal70KKYMRJ1+oyUzclrFlSPL6SaAy/O9fD3juEkRVlnDcUqSzo1Pw1u+qqvek4qCatbNyIiPzt+m7cHqEb+WJF4bbddd8vt2vA63uM+2GG9/+veomL149DENw9NpnwgB0/Kx914+DxF8LwFoBC4mqCon6AkYPSFkY+nqyRDwEZ9xscY25xptYLTOLlHNwwrBBQvS+gwK8Ou/RkM3GgCxW1FQxNCXLAfH0ixrIDf9Es1qvH1BqdeNeY47TxSuqZp0nIYhS3uD/q5a2DTiJPOjBpA/xzg8yvMvU1vV8mgn+bxDSDfIUmScCLBlUTkKu9ZoCKXg618AxdPxu+EKqAEYTWMDDD1BqyURbdVznlx4rZukwMVJmcMcVTZfZfX8bfa7IQ1pssESIfluPLGYewO8MKW5mfVhcCynJdhWDF8rYmgMZFCMbM65LjDLVM8AoPbIKud6g1mxKvl37UBPI4Q6n1yOTe+IJwJv68eVIvvp//hIxp0XhiDTBbgcN8GnkpO47ssYYXNmpnrTGbaPC3iDGX6VKGWsIwvGdtmlb6EILusDapVoax9lAPjEwLj1qAVAetQoGxTeRhBGE+bPwD7zCbRuHzgc8wTEVJrthfr8bh1+/RD1c0P9uZfG7vXOMzNhTRjCf0A+R6U+qx4Jnv3nuPMQtSG0nQOa4tewsdnmNfPkYb8fjJSU5j1JXHoaX1dP0p3GuL1hbE78I61DY/9Gq43XgaROoRXWtt2FdxHJ7TrnE3R9xP5Vp8lBMXHgGzIic+ScRSpOkruxEpqwNd1L9w3CtmhvufAG0yB+1mZYdphnZquoKQ5LWOfkJTR2uEr4doDFQRQBgJ0GTNe+24Xt98EnVLMoPfuLg6A3f9GLuuWwccKvHzEgZ8+A8IJDThVS4REnKBNEQqUqKcr3hUaT9jBPabTZGuoWpRy97JMYQZnC2ZI6pB9SzUkLbz/gOeR+Ok+qzMhp5accKXbekq2eDkzafv"}
Loading