Add aes-cbc encryption; add config repo based on shared_preferences

This commit is contained in:
mitchell 2019-07-16 02:14:54 -04:00
parent 80f9705b19
commit 67744527cc
15 changed files with 254 additions and 154 deletions

View file

@ -2,24 +2,26 @@ import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'repositories/grpc_credentials_client.dart';
import 'repositories/secure_storage_config.dart';
import 'repositories/encrypted_shared_preferences.dart';
import 'screens/authentication.dart';
import 'screens/config.dart';
import 'screens/credential.dart';
import 'screens/credentials.dart';
import 'screens/config.dart';
import 'screens/home.dart';
import 'types/abstracts.dart';
import 'types/screen_arguments.dart';
void main() => runApp(Selfpass());
void main() {
runApp(Selfpass());
}
class Selfpass extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider<ConfigRepo>(
builder: (BuildContext context) => SecureStorageConfig(),
builder: (BuildContext context) => EncryptedSharedPreferences(),
child: CupertinoApp(
title: 'Selfpass',
onGenerateRoute: (RouteSettings settings) {
@ -36,7 +38,8 @@ class Selfpass extends StatelessWidget {
title = 'Hosts';
builder = (BuildContext context) => Provider<CredentialsRepo>(
builder: (BuildContext context) =>
GRPCCredentialsClient.getInstance(config: settings.arguments),
GRPCCredentialsClient.getInstance(
config: settings.arguments),
child: Home(),
);
break;

View file

@ -0,0 +1,25 @@
class ConfigBase {
static const keyPrivateKey = "private_key";
static const keyConnectionConfig = "connection_config";
static const keyPassword = "password";
bool passwordMatched = false;
String _password;
String get password {
checkIfPasswordMatched();
return _password;
}
set password(String password) => _password = password;
void checkIfPasswordMatched() {
if (passwordMatched) return;
throw Exception('password not matched yet');
}
void reset() {
passwordMatched = false;
_password = null;
}
}

View file

@ -0,0 +1,102 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'config_base.dart';
import '../types/abstracts.dart';
import '../types/connection_config.dart';
import '../utils/crypto.dart' as crypto;
class EncryptedSharedPreferences extends ConfigBase implements ConfigRepo {
@override
Future<ConnectionConfig> get connectionConfig async {
checkIfPasswordMatched();
final prefs = await SharedPreferences.getInstance();
final cipherText = prefs.getString(ConfigBase.keyConnectionConfig);
if (cipherText == null) return null;
final configJson = crypto.decrypt(cipherText, password);
return ConnectionConfig.fromJson(json.decode(configJson));
}
@override
Future<void> deleteAll() async {
checkIfPasswordMatched();
final prefs = await SharedPreferences.getInstance();
prefs.remove(ConfigBase.keyConnectionConfig);
prefs.remove(ConfigBase.keyPassword);
prefs.remove(ConfigBase.keyPrivateKey);
}
@override
Future<bool> matchesPasswordHash(String password) async {
final prefs = await SharedPreferences.getInstance();
passwordMatched = crypto.matchHashedPassword(
prefs.getString(ConfigBase.keyPassword),
password,
);
if (passwordMatched) this.password = password;
return passwordMatched;
}
@override
Future<bool> get passwordIsSet async {
final prefs = await SharedPreferences.getInstance();
final isSet = prefs.containsKey(ConfigBase.keyPassword);
passwordMatched = !isSet;
return isSet;
}
@override
Future<String> get privateKey async {
checkIfPasswordMatched();
final prefs = await SharedPreferences.getInstance();
final cipherText = prefs.getString(ConfigBase.keyPrivateKey);
return crypto.decrypt(cipherText, password);
}
@override
Future<void> setConnectionConfig(ConnectionConfig config) async {
checkIfPasswordMatched();
final prefs = await SharedPreferences.getInstance();
final configJson = json.encode(config);
prefs.setString(
ConfigBase.keyConnectionConfig,
crypto.encrypt(configJson, password),
);
}
@override
Future<void> setPassword(String password) async {
final prefs = await SharedPreferences.getInstance();
this.password = password;
passwordMatched = true;
prefs.setString(ConfigBase.keyPassword, crypto.hashPassword(password));
}
@override
Future<void> setPrivateKey(String key) async {
final prefs = await SharedPreferences.getInstance();
prefs.setString(ConfigBase.keyPrivateKey, crypto.encrypt(key, password));
}
}

View file

@ -1,91 +0,0 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../types/abstracts.dart';
import '../types/connection_config.dart';
import '../utils/crypto.dart' as crypto;
class SecureStorageConfig implements ConfigRepo {
static const _keyPrivateKey = "private_key";
static const _keyConnectionConfig = "connection_config";
static const _keyPassword = "password";
final _storage = FlutterSecureStorage();
bool _passwordMatched = false;
String _password;
String get password {
_checkIfPasswordMatched();
return _password;
}
Future<void> setPrivateKey(String key) {
_checkIfPasswordMatched();
return _storage.write(key: _keyPrivateKey, value: key.replaceAll('-', ''));
}
Future<String> get privateKey {
_checkIfPasswordMatched();
return _storage.read(key: _keyPrivateKey);
}
Future<void> setPassword(String password) {
_checkIfPasswordMatched();
_password = password;
return _storage.write(
key: _keyPassword, value: crypto.hashPassword(password));
}
Future<bool> get passwordIsSet async {
final passHash = await _storage.read(key: _keyPassword);
if (passHash != null) {
return true;
}
_passwordMatched = true;
return false;
}
Future<bool> matchesPasswordHash(String password) async {
_passwordMatched = crypto.matchHashedPassword(
await _storage.read(key: _keyPassword), password);
if (_passwordMatched) _password = password;
return _passwordMatched;
}
Future<void> setConnectionConfig(ConnectionConfig config) {
_checkIfPasswordMatched();
return _storage.write(
key: _keyConnectionConfig, value: json.encode(config));
}
Future<ConnectionConfig> get connectionConfig async {
_checkIfPasswordMatched();
final connConfig = await _storage.read(key: _keyConnectionConfig);
if (connConfig == null) {
return null;
}
return ConnectionConfig.fromJson(json.decode(connConfig));
}
Future<void> deleteAll() {
_checkIfPasswordMatched();
return _storage.deleteAll();
}
void _checkIfPasswordMatched() {
if (_passwordMatched) return;
throw Exception('password not matched yet');
}
}

View file

@ -58,8 +58,10 @@ class _ConfigState extends State<Config> {
: CupertinoNavigationBar(
trailing: GestureDetector(
onTap: _buildResetAllHandler(context),
child: Text('Reset App',
style: TextStyle(color: CupertinoColors.destructiveRed)),
child: Text(
'Reset',
style: TextStyle(color: CupertinoColors.destructiveRed),
),
),
),
child: Container(

View file

@ -30,8 +30,9 @@ class Credentials extends StatelessWidget {
children: [
Text('Decrypting credential...'),
Container(
margin: EdgeInsets.only(top: 10),
child: CupertinoActivityIndicator()),
margin: EdgeInsets.only(top: 10),
child: CupertinoActivityIndicator(),
),
],
)),
);
@ -44,12 +45,18 @@ class Credentials extends StatelessWidget {
final credential = await client.get(id);
credential.password = crypto.decryptPassword(
password, await privateKey, credential.password);
credential.password = crypto.decrypt(
credential.password,
password,
await privateKey,
);
if (credential.otpSecret != null && credential.otpSecret != '') {
credential.otpSecret = crypto.decryptPassword(
password, await privateKey, credential.otpSecret);
if (credential.otpSecret.isNotEmpty) {
credential.otpSecret = crypto.decrypt(
credential.otpSecret,
password,
await privateKey,
);
}
Navigator.of(context)

View file

@ -92,6 +92,7 @@ class _HomeState extends State<Home> with WidgetsBindingObserver {
const checkPeriod = 30;
return Timer(Duration(seconds: checkPeriod), () {
_config.reset();
Navigator.of(context)
.pushNamedAndRemoveUntil('/', ModalRoute.withName('/home'));
});
@ -127,7 +128,11 @@ class _HomeState extends State<Home> with WidgetsBindingObserver {
}
GestureTapCallback _makeLockOnTapHandler(BuildContext context) {
return () => Navigator.of(context).pushReplacementNamed('/');
return () {
_config.reset();
Navigator.of(context)
.pushNamedAndRemoveUntil('/', ModalRoute.withName('/home'));
};
}
GestureTapCallback _makeConfigOnTapHandler(BuildContext context) {

View file

@ -24,4 +24,6 @@ abstract class ConfigRepo {
Future<ConnectionConfig> get connectionConfig;
Future<void> deleteAll();
void reset();
}

View file

@ -1,31 +1,37 @@
import 'dart:math';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypt/crypt.dart';
import 'package:encrypt/encrypt.dart';
import 'package:password_hash/password_hash.dart';
String hashPassword(String password) {
const saltSize = 16;
const saltIntMax = 256;
final random = Random.secure();
final saltInts =
List<int>.generate(saltSize, (_) => random.nextInt(saltIntMax));
final salt = base64.encode(saltInts);
final salt = Salt.generateAsBase64String(saltSize);
return Crypt.sha256(password, salt: salt).toString();
}
bool matchHashedPassword(String hashedPassword, String password) =>
Crypt(hashedPassword).match(password);
String decryptPassword(String masterpass, privateKey, cipherText) {
String decrypt(String cipherText, String masterpass, [String privateKey]) {
var cipherBytes = base64.decode(cipherText);
if (privateKey == null) {
final saltLength = cipherBytes[0];
cipherBytes = cipherBytes.sublist(1);
privateKey = base64.encode(cipherBytes.sublist(0, saltLength));
cipherBytes = cipherBytes.sublist(saltLength);
}
final key = PBKDF2().generateKey(
masterpass, privateKey, pbkdf2Rounds, keySize,
masterpass,
privateKey,
pbkdf2Rounds,
keySize,
);
var cipherBytes = base64.decode(cipherText);
final ivBytes = cipherBytes.sublist(0, aesBlockSize);
cipherBytes = cipherBytes.sublist(aesBlockSize);
@ -34,6 +40,40 @@ String decryptPassword(String masterpass, privateKey, cipherText) {
return encrypter.decrypt(Encrypted(cipherBytes), iv: iv);
}
String encrypt(String plainText, String masterpass, [String privateKey]) {
bool privateKeyWasEmpty = false;
if (privateKey == null) {
privateKey = Salt.generateAsBase64String(saltSize);
privateKeyWasEmpty = true;
}
final key = PBKDF2().generateKey(
masterpass,
privateKey,
pbkdf2Rounds,
keySize,
);
final random = Random.secure();
final ivBytes = List<int>.generate(aesBlockSize, (_) => random.nextInt(byteIntMax));
final iv = IV(Uint8List.fromList(ivBytes));
final encrypter = Encrypter(AES(Key(key), mode: AESMode.cbc));
final cipherBytes = List<int>.from(encrypter.encrypt(plainText, iv: iv).bytes);
cipherBytes.insertAll(0, ivBytes);
if (privateKeyWasEmpty) {
final base64PrivKey = base64.decode(privateKey);
cipherBytes.insertAll(0, base64PrivKey);
cipherBytes.insert(0, base64PrivKey.length);
}
return base64.encode(cipherBytes);
}
const saltSize = 16;
const pbkdf2Rounds = 4096;
const keySize = 32;
const aesBlockSize = 16;
const byteIntMax = 256;