mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-04 20:30:36 +00:00
Add support for importing from the new Google Authenticator export QR codes
This commit is contained in:
parent
6b650e777f
commit
56bde0e19b
9 changed files with 234 additions and 14 deletions
|
@ -2,10 +2,19 @@ package com.beemdevelopment.aegis.otp;
|
|||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.beemdevelopment.aegis.GoogleAuthProtos;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GoogleAuthInfo {
|
||||
public static final String SCHEME = "otpauth";
|
||||
public static final String SCHEME_EXPORT = "otpauth-migration";
|
||||
|
||||
private OtpInfo _info;
|
||||
private String _accountName;
|
||||
private String _issuer;
|
||||
|
@ -22,7 +31,7 @@ public class GoogleAuthInfo {
|
|||
|
||||
public Uri getUri() {
|
||||
Uri.Builder builder = new Uri.Builder();
|
||||
builder.scheme("otpauth");
|
||||
builder.scheme(SCHEME);
|
||||
|
||||
if (_info instanceof TotpInfo) {
|
||||
if (_info instanceof SteamInfo) {
|
||||
|
@ -62,7 +71,7 @@ public class GoogleAuthInfo {
|
|||
|
||||
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || !scheme.equals("otpauth")) {
|
||||
if (scheme == null || !scheme.equals(SCHEME)) {
|
||||
throw new GoogleAuthInfoException("Unsupported protocol");
|
||||
}
|
||||
|
||||
|
@ -164,6 +173,72 @@ public class GoogleAuthInfo {
|
|||
return new GoogleAuthInfo(info, accountName, issuer);
|
||||
}
|
||||
|
||||
public static Export parseExportUri(String s) throws GoogleAuthInfoException {
|
||||
Uri uri = Uri.parse(s);
|
||||
if (uri == null) {
|
||||
throw new GoogleAuthInfoException("Bad URI format");
|
||||
}
|
||||
return GoogleAuthInfo.parseExportUri(uri);
|
||||
}
|
||||
|
||||
public static Export parseExportUri(Uri uri) throws GoogleAuthInfoException {
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || !scheme.equals(SCHEME_EXPORT)) {
|
||||
throw new GoogleAuthInfoException("Unsupported protocol");
|
||||
}
|
||||
|
||||
String host = uri.getHost();
|
||||
if (host == null || !host.equals("offline")) {
|
||||
throw new GoogleAuthInfoException("Unsupported host");
|
||||
}
|
||||
|
||||
String data = uri.getQueryParameter("data");
|
||||
if (data == null) {
|
||||
throw new GoogleAuthInfoException("Parameter 'data' is not set");
|
||||
}
|
||||
|
||||
GoogleAuthProtos.MigrationPayload payload;
|
||||
try {
|
||||
byte[] bytes = Base64.decode(data);
|
||||
payload = GoogleAuthProtos.MigrationPayload.parseFrom(bytes);
|
||||
} catch (EncodingException | InvalidProtocolBufferException e) {
|
||||
throw new GoogleAuthInfoException(e);
|
||||
}
|
||||
|
||||
List<GoogleAuthInfo> infos = new ArrayList<>();
|
||||
for (GoogleAuthProtos.MigrationPayload.OtpParameters params : payload.getOtpParametersList()) {
|
||||
OtpInfo otp;
|
||||
try {
|
||||
byte[] secret = params.getSecret().toByteArray();
|
||||
switch (params.getType()) {
|
||||
case OTP_HOTP:
|
||||
otp = new HotpInfo(secret, params.getCounter());
|
||||
break;
|
||||
case OTP_TOTP:
|
||||
otp = new TotpInfo(secret);
|
||||
break;
|
||||
default:
|
||||
throw new GoogleAuthInfoException(String.format("Unsupported algorithm: %d", params.getType().ordinal()));
|
||||
}
|
||||
} catch (OtpInfoException e){
|
||||
throw new GoogleAuthInfoException(e);
|
||||
}
|
||||
|
||||
String name = params.getName();
|
||||
String issuer = params.getIssuer();
|
||||
int colonI = name.indexOf(':');
|
||||
if (issuer.isEmpty() && colonI != -1) {
|
||||
issuer = name.substring(0, colonI);
|
||||
name = name.substring(colonI + 1);
|
||||
}
|
||||
|
||||
GoogleAuthInfo info = new GoogleAuthInfo(otp, name, issuer);
|
||||
infos.add(info);
|
||||
}
|
||||
|
||||
return new Export(infos, payload.getBatchId(), payload.getBatchIndex(), payload.getBatchSize());
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return _issuer;
|
||||
}
|
||||
|
@ -171,4 +246,34 @@ public class GoogleAuthInfo {
|
|||
public String getAccountName() {
|
||||
return _accountName;
|
||||
}
|
||||
|
||||
public static class Export {
|
||||
private int _batchId;
|
||||
private int _batchIndex;
|
||||
private int _batchSize;
|
||||
private List<GoogleAuthInfo> _entries;
|
||||
|
||||
public Export(List<GoogleAuthInfo> entries, int batchId, int batchIndex, int batchSize) {
|
||||
_batchId = batchId;
|
||||
_batchIndex = batchIndex;
|
||||
_batchSize = batchSize;
|
||||
_entries = entries;
|
||||
}
|
||||
|
||||
public List<GoogleAuthInfo> getEntries() {
|
||||
return _entries;
|
||||
}
|
||||
|
||||
public int getBatchSize() {
|
||||
return _batchSize;
|
||||
}
|
||||
|
||||
public int getBatchIndex() {
|
||||
return _batchIndex;
|
||||
}
|
||||
|
||||
public int getBatchId() {
|
||||
return _batchId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -248,8 +248,17 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
|||
}
|
||||
|
||||
private void onScanResult(Intent data) {
|
||||
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
|
||||
List<VaultEntry> entries = (ArrayList<VaultEntry>) data.getSerializableExtra("entries");
|
||||
if (entries.size() == 1) {
|
||||
startEditEntryActivity(CODE_ADD_ENTRY, entries.get(0), true);
|
||||
} else {
|
||||
for (VaultEntry entry : entries) {
|
||||
_vault.addEntry(entry);
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
|
||||
saveVault();
|
||||
}
|
||||
}
|
||||
|
||||
private void onAddEntryResult(Intent data) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.beemdevelopment.aegis.ui;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.hardware.Camera;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
@ -10,14 +11,16 @@ import android.widget.Toast;
|
|||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.helpers.SquareFinderView;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.Result;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import me.dm7.barcodescanner.core.IViewFinder;
|
||||
import me.dm7.barcodescanner.zxing.ZXingScannerView;
|
||||
|
@ -30,10 +33,15 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
|
|||
private Menu _menu;
|
||||
private int _facing = CAMERA_FACING_BACK;
|
||||
|
||||
private int _batchId = 0;
|
||||
private int _batchIndex = -1;
|
||||
private List<VaultEntry> _entries;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
_entries = new ArrayList<>();
|
||||
_scannerView = new ZXingScannerView(this) {
|
||||
@Override
|
||||
protected IViewFinder createViewFinderView(Context context) {
|
||||
|
@ -107,13 +115,14 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
|
|||
@Override
|
||||
public void handleResult(Result rawResult) {
|
||||
try {
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(rawResult.getText().trim());
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
Uri uri = Uri.parse(rawResult.getText().trim());
|
||||
if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) {
|
||||
handleExportUri(uri);
|
||||
} else {
|
||||
handleUri(uri);
|
||||
}
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entry", entry);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
_scannerView.resumeCameraPreview(this);
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(this, R.string.read_qr_error, e, (dialog, which) -> {
|
||||
|
@ -122,6 +131,47 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
|
|||
}
|
||||
}
|
||||
|
||||
private void handleUri(Uri uri) throws GoogleAuthInfoException {
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
|
||||
List<VaultEntry> entries = new ArrayList<>();
|
||||
entries.add(new VaultEntry(info));
|
||||
finish(entries);
|
||||
}
|
||||
|
||||
private void handleExportUri(Uri uri) throws GoogleAuthInfoException {
|
||||
GoogleAuthInfo.Export export = GoogleAuthInfo.parseExportUri(uri);
|
||||
|
||||
if (_batchId == 0) {
|
||||
_batchId = export.getBatchId();
|
||||
}
|
||||
|
||||
int batchIndex = export.getBatchIndex();
|
||||
if (_batchId != export.getBatchId()) {
|
||||
Toast.makeText(this, R.string.google_qr_export_unrelated, Toast.LENGTH_SHORT).show();
|
||||
} else if (_batchIndex == -1 || _batchIndex == batchIndex - 1) {
|
||||
for (GoogleAuthInfo info : export.getEntries()) {
|
||||
VaultEntry entry = new VaultEntry(info);
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
_batchIndex = batchIndex;
|
||||
if (_batchIndex + 1 == export.getBatchSize()) {
|
||||
finish(_entries);
|
||||
}
|
||||
|
||||
Toast.makeText(this, getString(R.string.google_qr_export_scanned, _batchIndex + 1, export.getBatchSize()), Toast.LENGTH_SHORT).show();
|
||||
} else if (_batchIndex != batchIndex) {
|
||||
Toast.makeText(this, getString(R.string.google_qr_export_unexpected, _batchIndex + 1, batchIndex + 1), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void finish(List<VaultEntry> entries) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entries", (ArrayList<VaultEntry>) entries);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void updateCameraIcon() {
|
||||
if (_menu != null) {
|
||||
MenuItem item = _menu.findItem(R.id.action_camera);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue