mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-22 23:09:13 +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
|
@ -1,4 +1,5 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
|
||||
def getCmdOutput = { cmd ->
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
|
@ -74,9 +75,25 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.8.0'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
task.builtins {
|
||||
java {
|
||||
option "lite"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def libsuVersion = '2.5.1'
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.8.0'
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation "androidx.biometric:biometric:1.0.1"
|
||||
|
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
@ -18,3 +18,5 @@
|
|||
|
||||
-keep class com.beemdevelopment.aegis.importers.** { *; }
|
||||
-keep class net.sqlcipher.** { *; }
|
||||
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
|
|
|
@ -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);
|
||||
|
|
33
app/src/main/proto/google_auth.proto
Normal file
33
app/src/main/proto/google_auth.proto
Normal file
|
@ -0,0 +1,33 @@
|
|||
syntax = "proto3";
|
||||
|
||||
option java_package = "com.beemdevelopment.aegis";
|
||||
option java_outer_classname = "GoogleAuthProtos";
|
||||
|
||||
message MigrationPayload {
|
||||
enum Algorithm {
|
||||
ALGO_INVALID = 0;
|
||||
ALGO_SHA1 = 1;
|
||||
}
|
||||
|
||||
enum OtpType {
|
||||
OTP_INVALID = 0;
|
||||
OTP_HOTP = 1;
|
||||
OTP_TOTP = 2;
|
||||
}
|
||||
|
||||
message OtpParameters {
|
||||
bytes secret = 1;
|
||||
string name = 2;
|
||||
string issuer = 3;
|
||||
Algorithm algorithm = 4;
|
||||
int32 digits = 5;
|
||||
OtpType type = 6;
|
||||
int64 counter = 7;
|
||||
}
|
||||
|
||||
repeated OtpParameters otp_parameters = 1;
|
||||
int32 version = 2;
|
||||
int32 batch_size = 3;
|
||||
int32 batch_index = 4;
|
||||
int32 batch_id = 5;
|
||||
}
|
|
@ -234,6 +234,9 @@
|
|||
<string name="time_sync_warning_title">Automatic time synchronization</string>
|
||||
<string name="time_sync_warning_message">Aegis relies on the system time to be in sync to generate correct codes. A deviation of only a few seconds could result in incorrect codes. It looks like your device is not configured to automatically synchronize the time. Would you like to do so now?</string>
|
||||
<string name="time_sync_warning_disable">Stop warning me. I know what I\'m doing.</string>
|
||||
<string name="google_qr_export_unrelated">Unrelated QR code found. Try restarting the scanner.</string>
|
||||
<string name="google_qr_export_scanned">Scanned %d/%d QR codes</string>
|
||||
<string name="google_qr_export_unexpected">Expected QR code #%d, but scanned #%d instead</string>
|
||||
|
||||
<string name="custom_notices_format_style" translatable="false" >
|
||||
body {
|
||||
|
|
|
@ -6,7 +6,8 @@ buildscript {
|
|||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
|||
#Sun Sep 01 21:12:20 CEST 2019
|
||||
#Fri May 08 13:48:01 GMT 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
||||
|
|
Loading…
Add table
Reference in a new issue