Skip to content

Commit

Permalink
Add support for importing from the new Google Authenticator export QR…
Browse files Browse the repository at this point in the history
… codes
  • Loading branch information
alexbakker committed May 11, 2020
1 parent 6b650e7 commit 56bde0e
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 14 deletions.
17 changes: 17 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'com.google.protobuf'

def getCmdOutput = { cmd ->
def stdout = new ByteArrayOutputStream()
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@

-keep class com.beemdevelopment.aegis.importers.** { *; }
-keep class net.sqlcipher.** { *; }

-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
109 changes: 107 additions & 2 deletions app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@

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;
Expand All @@ -22,7 +31,7 @@ public OtpInfo getOtpInfo() {

public Uri getUri() {
Uri.Builder builder = new Uri.Builder();
builder.scheme("otpauth");
builder.scheme(SCHEME);

if (_info instanceof TotpInfo) {
if (_info instanceof SteamInfo) {
Expand Down Expand Up @@ -62,7 +71,7 @@ public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {

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");
}

Expand Down Expand Up @@ -164,11 +173,107 @@ public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
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;
}

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;
}
}
}
13 changes: 11 additions & 2 deletions app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,17 @@ private void startEditEntryActivity(int requestCode, VaultEntry entry, boolean i
}

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@
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;
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;
Expand All @@ -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) {
Expand Down Expand Up @@ -107,13 +115,14 @@ public void onPause() {
@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) -> {
Expand All @@ -122,6 +131,47 @@ public void handleResult(Result rawResult) {
}
}

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);
Expand Down
33 changes: 33 additions & 0 deletions app/src/main/proto/google_auth.proto
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 56bde0e

Please sign in to comment.