Skip to content

Commit

Permalink
Add secure storage fallback option
Browse files Browse the repository at this point in the history
  • Loading branch information
bbedward committed Apr 4, 2019
1 parent a158884 commit 7a487aa
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.banano.kaliumwallet;

import android.util.Base64;

public class LegacyStorage {

public String getSecret() {
return generateEncryptionKey();
}

private String generateEncryptionKey() {
if (Vault.getVault().getString(Vault.ENCRYPTION_KEY_NAME, null) == null) {
Vault.getVault()
.edit()
.putString(Vault.ENCRYPTION_KEY_NAME,
Base64.encodeToString(Vault.generateKey(), Base64.DEFAULT))
.apply();
}
return Vault.getVault().getString(Vault.ENCRYPTION_KEY_NAME, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) {
new MigrationStuff().clearLegacyData();
} else if (call.method.equals("getLegacyPin")) {
result.success(new MigrationStuff().getLegacyPin());
} else if (call.method.equals("getSecret")) {
result.success(new LegacyStorage().getSecret());
} else {
result.notImplemented();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ public void onCreate() {

try {
Vault.initializeVault(this);
generateEncryptionKey();
} catch (Exception e) {

}
generateEncryptionKey();
FlutterMain.startInitialization(this);
}

Expand Down
80 changes: 48 additions & 32 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -266,43 +266,59 @@ class SplashState extends State<Splash> with WidgetsBindingObserver {
}

Future checkLoggedIn() async {
// iOS key store is persistent, so if this is first launch then we will clear the keystore
bool firstLaunch = await SharedPrefsUtil.inst.getFirstLaunch();
if (firstLaunch) {
bool migrated = false;
if (Platform.isAndroid) {
migrated = await _doAndroidMigration();
try {
// iOS key store is persistent, so if this is first launch then we will clear the keystore
bool firstLaunch = await SharedPrefsUtil.inst.getFirstLaunch();
if (firstLaunch) {
bool migrated = false;
if (Platform.isAndroid) {
migrated = await _doAndroidMigration();
}
if (!migrated) {
await Vault.inst.deleteAll();
}
}
if (!migrated) {
await Vault.inst.deleteAll();
await SharedPrefsUtil.inst.setFirstLaunch();
await SharedPrefsUtil.inst.setFirstLaunch();
// See if logged in already
bool isLoggedIn = false;
var seed = await Vault.inst.getSeed();
var pin = await Vault.inst.getPin();
// If we have a seed set, but not a pin - or vice versa
// Then delete the seed and pin from device and start over.
// This would mean user did not complete the intro screen completely.
if (seed != null && pin != null) {
isLoggedIn = true;
} else if (seed != null && pin == null) {
await Vault.inst.deleteSeed();
} else if (pin != null && seed == null) {
await Vault.inst.deletePin();
}
}
await SharedPrefsUtil.inst.setFirstLaunch();
await SharedPrefsUtil.inst.setFirstLaunch();
// See if logged in already
bool isLoggedIn = false;
var seed = await Vault.inst.getSeed();
var pin = await Vault.inst.getPin();
// If we have a seed set, but not a pin - or vice versa
// Then delete the seed and pin from device and start over.
// This would mean user did not complete the intro screen completely.
if (seed != null && pin != null) {
isLoggedIn = true;
} else if (seed != null && pin == null) {
await Vault.inst.deleteSeed();
} else if (pin != null && seed == null) {
await Vault.inst.deletePin();
}

if (isLoggedIn) {
if (await SharedPrefsUtil.inst.getLock() || await SharedPrefsUtil.inst.shouldLock()) {
Navigator.of(context).pushReplacementNamed('/lock_screen');
if (isLoggedIn) {
if (await SharedPrefsUtil.inst.getLock() || await SharedPrefsUtil.inst.shouldLock()) {
Navigator.of(context).pushReplacementNamed('/lock_screen');
} else {
await NanoUtil().loginAccount(context);
Navigator.of(context).pushReplacementNamed('/home');
}
} else {
await NanoUtil().loginAccount(context);
Navigator.of(context).pushReplacementNamed('/home');
Navigator.of(context).pushReplacementNamed('/intro_welcome');
}
} catch (e) {
/// Fallback secure storage
/// A very small percentage of users are encountering issues writing to the
/// Android keyStore using the flutter_secure_storage plugin.
///
/// Instead of telling them they are out of luck, this is an automatic "fallback"
/// It will generate a 64-byte secret using the native android "bottlerocketstudios" Vault
/// This secret is used to encrypt sensitive data and save it in SharedPreferences
if (Platform.isAndroid && e.toString().contains("flutter_secure")) {
if (!(await SharedPrefsUtil.inst.useLegacyStorage())) {
await SharedPrefsUtil.inst.setUseLegacyStorage();
checkLoggedIn();
}
}
} else {
Navigator.of(context).pushReplacementNamed('/intro_welcome');
}
}

Expand Down
67 changes: 66 additions & 1 deletion lib/model/vault.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import 'package:flutter/services.dart';

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

import 'package:shared_preferences/shared_preferences.dart';
import 'package:kalium_wallet_flutter/util/encrypt.dart';
import 'package:kalium_wallet_flutter/util/sharedprefsutil.dart';

/**
* Singleton for keystore access methods in android/iOS
*/
Expand All @@ -13,17 +19,35 @@ class Vault {
static const String pinKey = 'fkalium_pin';
final FlutterSecureStorage secureStorage = new FlutterSecureStorage();

Future<bool> legacy() async {
return await SharedPrefsUtil.inst.useLegacyStorage();
}

// Re-usable
Future<String> _write(String key, String value) async {
await secureStorage.write(key:key, value:value);
if (await legacy()) {
await setEncrypted(key, value);
} else {
await secureStorage.write(key:key, value:value);
}
return value;
}

Future<String> _read(String key, {String defaultValue}) async {
if (await legacy()) {
return await getEncrypted(key);
}
return await secureStorage.read(key:key) ?? defaultValue;
}

Future<void> deleteAll() async {
if (await legacy()) {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove(encryptionKey);
await prefs.remove(seedKey);
await prefs.remove(pinKey);
return;
}
return await secureStorage.deleteAll();
}

Expand All @@ -37,6 +61,10 @@ class Vault {
}

Future<void> deleteSeed() async {
if (await legacy()) {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove(seedKey);
}
return await secureStorage.delete(key: seedKey);
}

Expand All @@ -49,6 +77,10 @@ class Vault {
}

Future<void> deleteEncryptionPhrase() async {
if (await legacy()) {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove(encryptionKey);
}
return await secureStorage.delete(key: encryptionKey);
}

Expand All @@ -61,6 +93,39 @@ class Vault {
}

Future<void> deletePin() async {
if (await legacy()) {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove(pinKey);
}
return await secureStorage.delete(key: pinKey);
}

// For encrypted data
Future<void> setEncrypted(String key, String value) async {
String secret = await getSecret();
if (secret == null) return null;
// Decrypt and return
Salsa20Encryptor encrypter = new Salsa20Encryptor(secret.substring(0, secret.length - 8), secret.substring(secret.length - 8));
// Encrypt and save
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString(key, encrypter.encrypt(value));
}

Future<String> getEncrypted(String key) async {
String secret = await getSecret();
if (secret == null) return null;
// Decrypt and return
Salsa20Encryptor encrypter = new Salsa20Encryptor(secret.substring(0, secret.length - 8), secret.substring(secret.length - 8));
SharedPreferences prefs = await SharedPreferences.getInstance();
String encrypted = prefs.get(key);
if (encrypted == null) return null;
return encrypter.decrypt(encrypted);
}

static const _channel = const MethodChannel('fappchannel');

static Future<String> getSecret() async {
return await _channel.invokeMethod('getSecret');
}

}
10 changes: 10 additions & 0 deletions lib/util/sharedprefsutil.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class SharedPrefsUtil {
// For maximum pin attempts
static const String pin_attempts = 'fkalium_pin_attempts';
static const String pin_lock_until = 'fkalium_lock_duraton';
// For certain keystore incompatible androids
static const String use_legacy_storage = 'fkalium_legacy_storage';

// For plain-text data
Future<void> set(String key, value) async {
Expand Down Expand Up @@ -218,6 +220,14 @@ class SharedPrefsUtil {
return false;
}

Future<bool> useLegacyStorage() async {
return await get(use_legacy_storage, defaultValue: false);
}

Future<void> setUseLegacyStorage() async {
await set(use_legacy_storage, true);
}

Future<void> updateLockDate() async {
int attempts = await getLockAttempts();
if (attempts >= 20) {
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: A new Flutter project.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# Read more about versioning at semver.org.
version: 2.0.3+61
version: 2.0.4+62

environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"
Expand Down

0 comments on commit 7a487aa

Please sign in to comment.