Add support for importing from the new Google Authenticator export QR codes

This commit is contained in:
Alexander Bakker 2020-05-09 15:32:57 +02:00
parent 6b650e777f
commit 56bde0e19b
9 changed files with 234 additions and 14 deletions

View file

@ -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;
}
}
}

View file

@ -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) {

View file

@ -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);