Add decryption of credential encrypted fields; refactored config;

add app icon
This commit is contained in:
mitchell 2019-07-08 22:03:44 -04:00
parent 910bdeae12
commit 27215e6596
60 changed files with 370 additions and 151 deletions

View file

@ -60,7 +60,9 @@ class Selfpass extends StatelessWidget {
break;
case '/config':
final ConfigScreenArguments arguments = settings.arguments;
final ConfigScreenArguments arguments = settings.arguments == null
? ConfigScreenArguments()
: settings.arguments;
title = 'Configuration';
builder = (BuildContext context) =>
Config(arguments.connectionConfig, arguments.privateKey);

View file

@ -4,25 +4,32 @@
///
// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name
import 'dart:core' as $core show bool, Deprecated, double, int, List, Map, override, pragma, String;
import 'dart:core' as $core
show bool, Deprecated, double, int, List, Map, override, pragma, String;
import 'package:fixnum/fixnum.dart';
import 'package:protobuf/protobuf.dart' as $pb;
import 'dart:core' as $core show DateTime, Duration;
class Timestamp extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo('Timestamp', package: const $pb.PackageName('google.protobuf'))
static final $pb.BuilderInfo _i = $pb.BuilderInfo('Timestamp',
package: const $pb.PackageName('google.protobuf'))
..aInt64(1, 'seconds')
..a<$core.int>(2, 'nanos', $pb.PbFieldType.O3)
..hasRequiredFields = false
;
..hasRequiredFields = false;
Timestamp._() : super();
factory Timestamp() => create();
factory Timestamp.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory Timestamp.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
factory Timestamp.fromBuffer($core.List<$core.int> i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(i, r);
factory Timestamp.fromJson($core.String i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(i, r);
Timestamp clone() => Timestamp()..mergeFromMessage(this);
Timestamp copyWith(void Function(Timestamp) updates) => super.copyWith((message) => updates(message as Timestamp));
Timestamp copyWith(void Function(Timestamp) updates) =>
super.copyWith((message) => updates(message as Timestamp));
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Timestamp create() => Timestamp._();
@ -32,30 +39,36 @@ class Timestamp extends $pb.GeneratedMessage {
static Timestamp _defaultInstance;
Int64 get seconds => $_getI64(0);
set seconds(Int64 v) { $_setInt64(0, v); }
set seconds(Int64 v) {
$_setInt64(0, v);
}
$core.bool hasSeconds() => $_has(0);
void clearSeconds() => clearField(1);
$core.int get nanos => $_get(1, 0);
set nanos($core.int v) { $_setSignedInt32(1, v); }
set nanos($core.int v) {
$_setSignedInt32(1, v);
}
$core.bool hasNanos() => $_has(1);
void clearNanos() => clearField(2);
/// Converts an instance to [DateTime].
///
/// The result is in UTC time zone and has microsecond precision, as
/// [DateTime] does not support nanosecond precision.
$core.DateTime toDateTime() => $core.DateTime.fromMicrosecondsSinceEpoch(
seconds.toInt() * $core.Duration.microsecondsPerSecond + nanos ~/ 1000,
isUtc: true);
/// Creates a new instance from [dateTime].
///
/// Time zone information will not be preserved.
static Timestamp fromDateTime($core.DateTime dateTime) {
$core.int micros = dateTime.microsecondsSinceEpoch;
return Timestamp()
..seconds = Int64(micros ~/ $core.Duration.microsecondsPerSecond)
..nanos = (micros % $core.Duration.microsecondsPerSecond).toInt() * 1000;
}
/// Converts an instance to [DateTime].
///
/// The result is in UTC time zone and has microsecond precision, as
/// [DateTime] does not support nanosecond precision.
$core.DateTime toDateTime() => $core.DateTime.fromMicrosecondsSinceEpoch(
seconds.toInt() * $core.Duration.microsecondsPerSecond + nanos ~/ 1000,
isUtc: true);
/// Creates a new instance from [dateTime].
///
/// Time zone information will not be preserved.
static Timestamp fromDateTime($core.DateTime dateTime) {
$core.int micros = dateTime.microsecondsSinceEpoch;
return Timestamp()
..seconds = Int64(micros ~/ $core.Duration.microsecondsPerSecond)
..nanos = (micros % $core.Duration.microsecondsPerSecond).toInt() * 1000;
}
}

View file

@ -11,4 +11,3 @@ const Timestamp$json = const {
const {'1': 'nanos', '3': 2, '4': 1, '5': 5, '10': 'nanos'},
],
};

View file

@ -29,7 +29,14 @@ const UpdateRequest$json = const {
'1': 'UpdateRequest',
'2': const [
const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
const {'1': 'credential', '3': 2, '4': 1, '5': 11, '6': '.selfpass.credentials.CredentialRequest', '10': 'credential'},
const {
'1': 'credential',
'3': 2,
'4': 1,
'5': 11,
'6': '.selfpass.credentials.CredentialRequest',
'10': 'credential'
},
],
};
@ -48,8 +55,22 @@ const Metadata$json = const {
'1': 'Metadata',
'2': const [
const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
const {'1': 'created_at', '3': 2, '4': 1, '5': 11, '6': '.google.protobuf.Timestamp', '10': 'createdAt'},
const {'1': 'updated_at', '3': 3, '4': 1, '5': 11, '6': '.google.protobuf.Timestamp', '10': 'updatedAt'},
const {
'1': 'created_at',
'3': 2,
'4': 1,
'5': 11,
'6': '.google.protobuf.Timestamp',
'10': 'createdAt'
},
const {
'1': 'updated_at',
'3': 3,
'4': 1,
'5': 11,
'6': '.google.protobuf.Timestamp',
'10': 'updatedAt'
},
const {'1': 'primary', '3': 4, '4': 1, '5': 9, '10': 'primary'},
const {'1': 'source_host', '3': 5, '4': 1, '5': 9, '10': 'sourceHost'},
const {'1': 'login_url', '3': 6, '4': 1, '5': 9, '10': 'loginUrl'},
@ -61,8 +82,22 @@ const Credential$json = const {
'1': 'Credential',
'2': const [
const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
const {'1': 'created_at', '3': 2, '4': 1, '5': 11, '6': '.google.protobuf.Timestamp', '10': 'createdAt'},
const {'1': 'updated_at', '3': 3, '4': 1, '5': 11, '6': '.google.protobuf.Timestamp', '10': 'updatedAt'},
const {
'1': 'created_at',
'3': 2,
'4': 1,
'5': 11,
'6': '.google.protobuf.Timestamp',
'10': 'createdAt'
},
const {
'1': 'updated_at',
'3': 3,
'4': 1,
'5': 11,
'6': '.google.protobuf.Timestamp',
'10': 'updatedAt'
},
const {'1': 'primary', '3': 4, '4': 1, '5': 9, '10': 'primary'},
const {'1': 'username', '3': 5, '4': 1, '5': 9, '10': 'username'},
const {'1': 'email', '3': 6, '4': 1, '5': 9, '10': 'email'},
@ -87,4 +122,3 @@ const CredentialRequest$json = const {
const {'1': 'otp_secret', '3': 8, '4': 1, '5': 9, '10': 'otpSecret'},
],
};

View file

@ -12,11 +12,17 @@ class Config implements ConfigRepo {
static const _keyConnectionConfig = "connection_config";
static const _keyPassword = "password";
final FlutterSecureStorage _storage = FlutterSecureStorage();
var _passwordMatched = false;
bool _passwordMatched = false;
String _password;
String get password {
_checkIfPasswordMatched();
return _password;
}
Future<void> setPrivateKey(String key) {
_checkIfPasswordMatched();
return _storage.write(key: _keyPrivateKey, value: key);
return _storage.write(key: _keyPrivateKey, value: key.replaceAll('-', ''));
}
Future<String> get privateKey {
@ -26,6 +32,7 @@ class Config implements ConfigRepo {
Future<void> setPassword(String password) {
_checkIfPasswordMatched();
_password = password;
return _storage.write(
key: _keyPassword, value: crypto.hashPassword(password));
}
@ -42,9 +49,16 @@ class Config implements ConfigRepo {
return false;
}
Future<bool> matchesPasswordHash(String password) async =>
_passwordMatched = crypto.matchHashedPassword(
await _storage.read(key: _keyPassword), password);
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();

View file

@ -105,10 +105,8 @@ class _AuthenticationState extends State<Authentication> {
return () async {
if (await _passwordIsSet) {
if (await _config.matchesPasswordHash(_passwordController.text)) {
Navigator.of(context).pushReplacementNamed(
'/home',
arguments: await _config.connectionConfig,
);
Navigator.of(context).pushReplacementNamed('/home',
arguments: await _config.connectionConfig);
return;
}

View file

@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:otp/otp.dart';
import '../types/credential.dart' as types;
@ -19,17 +20,14 @@ class _CredentialState extends State<Credential> {
Map<String, _FieldBuildConfig> _fieldMap;
types.Credential _credential;
_CredentialState(this._credential) : super() {
_controllers = _CredentialControllers.fromCredential(_credential);
_fieldMap = _buildFieldMap(_controllers, _credential);
}
_CredentialState(this._credential) : super();
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),
child: Container(
margin: const EdgeInsets.only(top: 30, left: 30),
padding: const EdgeInsets.only(top: 15, bottom: 30, left: 30),
child: ListView(
children: _buildFieldRows(context),
),
@ -43,12 +41,12 @@ class _CredentialState extends State<Credential> {
) {
final fieldMap = {
'Id:': _FieldBuildConfig(mutable: false, text: credential.meta.id),
'Created: ': _FieldBuildConfig(
'Created:': _FieldBuildConfig(
mutable: false,
copyable: false,
text: credential.meta.createdAt.toString(),
),
'Updated: ': _FieldBuildConfig(
'Updated:': _FieldBuildConfig(
mutable: false,
copyable: false,
text: credential.meta.updatedAt.toString(),
@ -69,15 +67,26 @@ class _CredentialState extends State<Credential> {
fieldMap['Email:'] = _FieldBuildConfig(controller: controllers.email);
}
fieldMap['Password:'] =
_FieldBuildConfig(controller: controllers.password, obscured: true);
if (credential.otpSecret != null && credential.otpSecret != '') {
fieldMap['OTP Secret:'] = _FieldBuildConfig(
controller: controllers.otpSecret, obscured: true, otp: true);
}
return fieldMap;
}
List<Widget> _buildFieldRows(BuildContext context) {
List<Widget> rows = [];
_controllers = _CredentialControllers.fromCredential(_credential);
_fieldMap = _buildFieldMap(_controllers, _credential);
_fieldMap.forEach((key, value) {
rows.add(Container(
margin: EdgeInsets.only(top: 10),
margin: EdgeInsets.only(top: 2.5),
child: Text(key, style: TextStyle(fontWeight: FontWeight.w600)),
));
@ -85,20 +94,27 @@ class _CredentialState extends State<Credential> {
Expanded(
flex: 3,
child: value.mutable
? TextField(maxLines: 1, controller: value.controller)
? TextField(
maxLines: 1,
controller: value.controller,
obscure: value.obscured)
: Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: Text(value.text),
),
child: Text(value.text)),
),
];
if (value.copyable) {
widgets.add(Flexible(
widgets.add(Expanded(
child: CupertinoButton(
child: Text('Copy'),
child: Text(value.otp ? 'OTP' : 'Copy'),
onPressed: () => Clipboard.setData(ClipboardData(
text: value.mutable ? value.controller.text : value.text,
text: value.otp
? OTP
.generateTOTPCode(value.controller.text,
DateTime.now().millisecondsSinceEpoch)
.toString()
: value.mutable ? value.controller.text : value.text,
)),
),
));
@ -116,10 +132,14 @@ class _FieldBuildConfig {
final String text;
final bool mutable;
final bool copyable;
final bool obscured;
final bool otp;
const _FieldBuildConfig({
this.mutable = true,
this.copyable = true,
this.obscured = false,
this.otp = false,
this.controller,
this.text,
});
@ -151,5 +171,7 @@ class _CredentialControllers {
tag: TextEditingController(text: credential.meta.tag),
username: TextEditingController(text: credential.username),
email: TextEditingController(text: credential.email),
password: TextEditingController(text: credential.password),
otpSecret: TextEditingController(text: credential.otpSecret),
);
}

View file

@ -4,6 +4,8 @@ import 'package:provider/provider.dart';
import '../types/abstracts.dart';
import '../types/credential.dart';
import '../utils/crypto.dart' as crypto;
import '../widgets/tappable_text_list.dart';
class Credentials extends StatelessWidget {
@ -20,10 +22,39 @@ class Credentials extends StatelessWidget {
}
Map<String, GestureTapCallback> _buildTappableText(BuildContext context) {
var makeOnTapHandler = (String id) => () async {
final credential =
await Provider.of<CredentialsRepo>(context).get(id);
Navigator.of(context).pushNamed('/credential', arguments: credential);
final makeOnTapHandler = (String id) => () async {
showCupertinoDialog(
context: context,
builder: (BuildContext context) => CupertinoAlertDialog(
content: Column(
children: [
Text('Decrypting credential...'),
Container(
margin: EdgeInsets.only(top: 10),
child: CupertinoActivityIndicator()),
],
)),
);
final config = Provider.of<ConfigRepo>(context);
final client = Provider.of<CredentialsRepo>(context);
final Future<String> privateKey = config.privateKey;
final String password = config.password;
final credential = await client.get(id);
credential.password = crypto.decryptPassword(
password, await privateKey, credential.password);
if (credential.otpSecret != null && credential.otpSecret != '') {
credential.otpSecret = crypto.decryptPassword(
password, await privateKey, credential.otpSecret);
}
Navigator.of(context)
..pop()
..pushNamed('/credential', arguments: credential);
};
Map<String, GestureTapCallback> tappableText = {};

View file

@ -91,6 +91,7 @@ class _HomeState extends State<Home> {
GestureTapCallback _makeConfigOnTapHandler(BuildContext context) {
return () async => Navigator.of(context).pushNamed('/config',
arguments: ConfigScreenArguments(
await _config.connectionConfig, await _config.privateKey));
connectionConfig: await _config.connectionConfig,
privateKey: await _config.privateKey));
}
}

View file

@ -15,6 +15,7 @@ abstract class ConfigRepo {
Future<void> setPrivateKey(String key);
Future<String> get privateKey;
String get password;
Future<void> setPassword(String password);
Future<bool> get passwordSet;
Future<bool> matchesPasswordHash(String password);

View file

@ -4,5 +4,5 @@ class ConfigScreenArguments {
final ConnectionConfig connectionConfig;
final String privateKey;
const ConfigScreenArguments(this.connectionConfig, this.privateKey);
const ConfigScreenArguments({this.connectionConfig, this.privateKey});
}

View file

@ -1,11 +1,18 @@
import 'dart:math';
import 'dart:convert';
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(16, (_) => random.nextInt(256));
final saltInts =
List<int>.generate(saltSize, (_) => random.nextInt(saltIntMax));
final salt = base64.encode(saltInts);
return Crypt.sha256(password, salt: salt).toString();
@ -13,3 +20,23 @@ String hashPassword(String password) {
bool matchHashedPassword(String hashedPassword, String password) =>
Crypt(hashedPassword).match(password);
String decryptPassword(String masterpass, privateKey, ciphertext) {
final key =
PBKDF2().generateKey(masterpass, privateKey, pbkdf2Rounds, keySize);
var cipherbytes = base64.decode(ciphertext);
final iv =
IV(Uint8List.fromList(cipherbytes.getRange(0, aesBlockSize).toList()));
cipherbytes = Uint8List.fromList(
cipherbytes.getRange(aesBlockSize, cipherbytes.length).toList());
final encrypter = Encrypter(AES(Key(key), mode: AESMode.cbc));
return encrypter.decrypt(Encrypted(cipherbytes), iv: iv);
}
const pbkdf2Rounds = 4096;
const keySize = 32;
const aesBlockSize = 16;

View file

@ -7,6 +7,7 @@ typedef OnSubmittedBuilder = ValueChanged<String> Function(
class TextField extends StatelessWidget {
final OnSubmittedBuilder onSubmittedBuilder;
final TextEditingController controller;
final OverlayVisibilityMode clearButtonMode;
final Widget prefix;
final Widget suffix;
final bool obscure;
@ -25,6 +26,7 @@ class TextField extends StatelessWidget {
this.autocorrect = false,
this.prefix,
this.suffix,
this.clearButtonMode = OverlayVisibilityMode.editing,
});
@override
@ -36,7 +38,7 @@ class TextField extends StatelessWidget {
border: Border.all(color: CupertinoColors.black),
borderRadius: const BorderRadius.all(Radius.circular(5.0)),
),
clearButtonMode: OverlayVisibilityMode.editing,
clearButtonMode: clearButtonMode,
textAlign: TextAlign.start,
onSubmitted: this.onSubmittedBuilder != null
? onSubmittedBuilder(context)