mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-16 23:12:51 +00:00
Google Authenticator compatible export
This commit is contained in:
parent
029ca81bbf
commit
bc665b6980
32 changed files with 173 additions and 121 deletions
|
@ -9,6 +9,7 @@ import com.beemdevelopment.aegis.encoding.Base32;
|
|||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.encoding.Hex;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
@ -18,7 +19,7 @@ import java.util.List;
|
|||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class GoogleAuthInfo implements Serializable {
|
||||
public class GoogleAuthInfo implements Transferable, Serializable {
|
||||
public static final String SCHEME = "otpauth";
|
||||
public static final String SCHEME_EXPORT = "otpauth-migration";
|
||||
|
||||
|
@ -267,6 +268,7 @@ public class GoogleAuthInfo implements Serializable {
|
|||
return _info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getUri() {
|
||||
Uri.Builder builder = new Uri.Builder();
|
||||
|
||||
|
@ -319,7 +321,7 @@ public class GoogleAuthInfo implements Serializable {
|
|||
return _accountName;
|
||||
}
|
||||
|
||||
public static class Export {
|
||||
public static class Export implements Transferable, Serializable {
|
||||
private int _batchId;
|
||||
private int _batchIndex;
|
||||
private int _batchSize;
|
||||
|
@ -385,5 +387,72 @@ public class GoogleAuthInfo implements Serializable {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getUri() throws GoogleAuthInfoException {
|
||||
GoogleAuthProtos.MigrationPayload.Builder builder = GoogleAuthProtos.MigrationPayload.newBuilder();
|
||||
builder.setBatchId(_batchId)
|
||||
.setBatchIndex(_batchIndex)
|
||||
.setBatchSize(_batchSize)
|
||||
.setVersion(1);
|
||||
|
||||
for (GoogleAuthInfo info: _entries) {
|
||||
GoogleAuthProtos.MigrationPayload.OtpParameters.Builder parameters = GoogleAuthProtos.MigrationPayload.OtpParameters.newBuilder()
|
||||
.setSecret(ByteString.copyFrom(info.getOtpInfo().getSecret()))
|
||||
.setName(info.getAccountName())
|
||||
.setIssuer(info.getIssuer());
|
||||
|
||||
switch (info.getOtpInfo().getAlgorithm(false)) {
|
||||
case "SHA1":
|
||||
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_SHA1);
|
||||
break;
|
||||
case "SHA256":
|
||||
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_SHA256);
|
||||
break;
|
||||
case "SHA512":
|
||||
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_SHA512);
|
||||
break;
|
||||
case "MD5":
|
||||
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_MD5);
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(info.getUri(), String.format("Unsupported Algorithm: %s", info.getOtpInfo().getAlgorithm(false)));
|
||||
}
|
||||
|
||||
switch (info.getOtpInfo().getDigits()) {
|
||||
case 6:
|
||||
parameters.setDigits(GoogleAuthProtos.MigrationPayload.DigitCount.DIGIT_COUNT_SIX);
|
||||
break;
|
||||
case 8:
|
||||
parameters.setDigits(GoogleAuthProtos.MigrationPayload.DigitCount.DIGIT_COUNT_EIGHT);
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(info.getUri(), String.format("Unsupported number of digits: %s", info.getOtpInfo().getDigits()));
|
||||
}
|
||||
|
||||
switch (info.getOtpInfo().getType().toLowerCase()) {
|
||||
case HotpInfo.ID:
|
||||
parameters.setType(GoogleAuthProtos.MigrationPayload.OtpType.OTP_TYPE_HOTP);
|
||||
parameters.setCounter(((HotpInfo) info.getOtpInfo()).getCounter());
|
||||
break;
|
||||
case TotpInfo.ID:
|
||||
parameters.setType(GoogleAuthProtos.MigrationPayload.OtpType.OTP_TYPE_TOTP);
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(info.getUri(), String.format("Type unsupported by GoogleAuthProtos: %s", info.getOtpInfo().getType()));
|
||||
}
|
||||
|
||||
builder.addOtpParameters(parameters.build());
|
||||
}
|
||||
|
||||
Uri.Builder exportUriBuilder = new Uri.Builder()
|
||||
.scheme(SCHEME_EXPORT)
|
||||
.authority("offline");
|
||||
|
||||
String data = Base64.encode(builder.build().toByteArray());
|
||||
exportUriBuilder.appendQueryParameter("data", data);
|
||||
|
||||
return exportUriBuilder.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package com.beemdevelopment.aegis.otp;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
public interface Transferable {
|
||||
Uri getUri() throws GoogleAuthInfoException;
|
||||
}
|
|
@ -17,6 +17,8 @@ import com.beemdevelopment.aegis.R;
|
|||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.helpers.QrCodeHelper;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.otp.Transferable;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.google.zxing.WriterException;
|
||||
|
||||
|
@ -24,8 +26,9 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
public class TransferEntriesActivity extends AegisActivity {
|
||||
private List<GoogleAuthInfo> _authInfos;
|
||||
private List<Transferable> _authInfos;
|
||||
private ImageView _qrImage;
|
||||
private TextView _description;
|
||||
private TextView _issuer;
|
||||
private TextView _accountName;
|
||||
private TextView _entriesCount;
|
||||
|
@ -43,6 +46,7 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
|
||||
_qrImage = findViewById(R.id.ivQrCode);
|
||||
_description = findViewById(R.id.tvDescription);
|
||||
_issuer = findViewById(R.id.tvIssuer);
|
||||
_accountName = findViewById(R.id.tvAccountName);
|
||||
_entriesCount = findViewById(R.id.tvEntriesCount);
|
||||
|
@ -55,7 +59,7 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
_authInfos = (ArrayList<GoogleAuthInfo>) intent.getSerializableExtra("authInfos");
|
||||
_authInfos = (ArrayList<Transferable>) intent.getSerializableExtra("authInfos");
|
||||
|
||||
int controlVisibility = _authInfos.size() != 1 ? View.VISIBLE : View.INVISIBLE;
|
||||
_nextButton.setVisibility(controlVisibility);
|
||||
|
@ -103,10 +107,16 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
}
|
||||
|
||||
private void generateQR() {
|
||||
GoogleAuthInfo selectedEntry = _authInfos.get(_currentEntryCount - 1);
|
||||
_issuer.setText(selectedEntry.getIssuer());
|
||||
_accountName.setText(selectedEntry.getAccountName());
|
||||
_entriesCount.setText(getResources().getQuantityString(R.plurals.entries_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
|
||||
Transferable selectedEntry = _authInfos.get(_currentEntryCount - 1);
|
||||
if (selectedEntry instanceof GoogleAuthInfo) {
|
||||
GoogleAuthInfo entry = (GoogleAuthInfo) selectedEntry;
|
||||
_issuer.setText(entry.getIssuer());
|
||||
_accountName.setText(entry.getAccountName());
|
||||
} else if (selectedEntry instanceof GoogleAuthInfo.Export) {
|
||||
_description.setText(R.string.google_auth_compatible_transfer_description);
|
||||
}
|
||||
|
||||
_entriesCount.setText(getResources().getQuantityString(R.plurals.qr_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
|
||||
|
||||
@ColorInt int backgroundColor = Color.WHITE;
|
||||
if (getConfiguredTheme() == Theme.LIGHT) {
|
||||
|
@ -118,7 +128,7 @@ public class TransferEntriesActivity extends AegisActivity {
|
|||
Bitmap bitmap;
|
||||
try {
|
||||
bitmap = QrCodeHelper.encodeToBitmap(selectedEntry.getUri().toString(), 512, 512, backgroundColor);
|
||||
} catch (WriterException e) {
|
||||
} catch (WriterException | GoogleAuthInfoException e) {
|
||||
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -22,11 +22,17 @@ import com.beemdevelopment.aegis.BuildConfig;
|
|||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.DropdownHelper;
|
||||
import com.beemdevelopment.aegis.importers.DatabaseImporter;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.ImportEntriesActivity;
|
||||
import com.beemdevelopment.aegis.ui.TransferEntriesActivity;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ExportTask;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
|
||||
import com.beemdevelopment.aegis.vault.VaultBackupManager;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepository;
|
||||
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
|
||||
|
@ -37,6 +43,10 @@ import java.io.File;
|
|||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
|
@ -78,6 +88,12 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
|||
startExport();
|
||||
return true;
|
||||
});
|
||||
|
||||
Preference googleAuthStyleExportPreference = requirePreference("pref_google_auth_style_export");
|
||||
googleAuthStyleExportPreference.setOnPreferenceClickListener(preference -> {
|
||||
startGoogleAuthenticatorStyleExport();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -231,6 +247,52 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
|
|||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private void startGoogleAuthenticatorStyleExport() {
|
||||
ArrayList<GoogleAuthInfo> toExport = new ArrayList<>();
|
||||
for (VaultEntry entry : _vaultManager.getVault().getEntries()) {
|
||||
String type = entry.getInfo().getType().toLowerCase();
|
||||
String algo = entry.getInfo().getAlgorithm(false);
|
||||
int digits = entry.getInfo().getDigits();
|
||||
|
||||
if ((Objects.equals(type, TotpInfo.ID) || Objects.equals(type, HotpInfo.ID))
|
||||
&& digits == OtpInfo.DEFAULT_DIGITS
|
||||
&& Objects.equals(algo, OtpInfo.DEFAULT_ALGORITHM)) {
|
||||
GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
|
||||
toExport.add(info);
|
||||
}
|
||||
}
|
||||
|
||||
int entriesSkipped = _vaultManager.getVault().getEntries().size() - toExport.size();
|
||||
if (entriesSkipped > 0) {
|
||||
Toast a = new Toast(requireContext());
|
||||
a.setText(requireContext().getResources().getQuantityString(R.plurals.pref_google_auth_export_incompatible_entries, entriesSkipped, entriesSkipped));
|
||||
a.show();
|
||||
}
|
||||
|
||||
int qrSize = 10;
|
||||
int batchId = new Random().nextInt();
|
||||
int batchSize = toExport.size() / qrSize + (toExport.size() % qrSize == 0 ? 0 : 1);
|
||||
List<GoogleAuthInfo> infos = new ArrayList<>();
|
||||
ArrayList<GoogleAuthInfo.Export> exports = new ArrayList<>();
|
||||
for (int i = 0, batchIndex = 0; i < toExport.size(); i++) {
|
||||
infos.add(toExport.get(i));
|
||||
if (infos.size() == qrSize || toExport.size() == i + 1) {
|
||||
exports.add(new GoogleAuthInfo.Export(infos, batchId, batchIndex++, batchSize));
|
||||
infos = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
if (exports.size() == 0) {
|
||||
Toast t = new Toast(requireContext());
|
||||
t.setText(R.string.pref_google_auth_export_no_data);
|
||||
t.show();
|
||||
} else {
|
||||
Intent intent = new Intent(requireContext(), TransferEntriesActivity.class);
|
||||
intent.putExtra("authInfos", exports);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getExportRequestCode(int spinnerPos, boolean encrypt) {
|
||||
if (spinnerPos == 0) {
|
||||
return encrypt ? CODE_EXPORT : CODE_EXPORT_PLAIN;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue