Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial support for importing Google Authenticator export QR codes #406

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
michaelschattgen marked this conversation as resolved.
Show resolved Hide resolved
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