From 67744527cc32d2a86af4b1bce166463fbda62126 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 16 Jul 2019 02:14:54 -0400 Subject: [PATCH] Add aes-cbc encryption; add config repo based on shared_preferences --- README.md | 30 +++--- client/ios/Podfile.lock | 10 +- client/lib/main.dart | 13 ++- client/lib/repositories/config_base.dart | 25 +++++ .../encrypted_shared_preferences.dart | 102 ++++++++++++++++++ .../repositories/secure_storage_config.dart | 91 ---------------- client/lib/screens/config.dart | 6 +- client/lib/screens/credentials.dart | 21 ++-- client/lib/screens/home.dart | 7 +- client/lib/types/abstracts.dart | 2 + client/lib/utils/crypto.dart | 64 ++++++++--- client/pubspec.lock | 19 ++-- client/pubspec.yaml | 4 +- .../credentials/repositories/grpc_client.go | 5 +- services/credentials/transport/encoding.go | 9 +- 15 files changed, 254 insertions(+), 154 deletions(-) create mode 100644 client/lib/repositories/config_base.dart create mode 100644 client/lib/repositories/encrypted_shared_preferences.dart delete mode 100644 client/lib/repositories/secure_storage_config.dart diff --git a/README.md b/README.md index 1ec0846..7e4b682 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,12 @@ The newest addition to the *selfpass* project is the client built using Flutter, capable of targeting to iOS, Android, and Desktop. It supports all the same features as the CLI tool using GUIs, with all the same safety and encryption as the CLI. -| Goal | Progress | Comment | -| --- | :---: | --- | -| Support mutual TLS. | 100% | | -| Support credentials CRUD via gRPC. | 25% | TODO: CUD | -| Support storage of certs, PK, and host in shared preferences, encrypted. | 100% | | -| Support AES-CBC encryption of passes and OTP secrets, using MP and PK. | 50% | TODO: decryption | +| Goal | Progress | Comment | +| --- | :---: | --- | +| Support mutual TLS. | 100% | | +| Support credentials CRUD via gRPC. | 25% | TODO: CUD | +| Support storage of certs, PK, and host in shared preferences, encrypted. | 100% | | +| Support AES-CBC encryption of passes and OTP secrets, using MP and PK. | 100% | | ## Other Info @@ -58,12 +58,12 @@ using GUIs, with all the same safety and encryption as the CLI. **Architectural 3rd-party Technologies in Use (and where)** -- Golang (services & protobuf) -- Dart (client & protobuf) -- Flutter (client) -- Go-Kit (services) -- gRPC & Protobuf (all) -- Cobra Commander & Viper Config (spc) -- Redis (services) -- Docker (services) -- Debian (docker images and machines) +- Golang: services, sp, & protobuf +- Dart: client & protobuf +- Flutter: client +- Go-Kit: services +- gRPC/Protobuf: all +- Cobra Commander & Viper Config: sp +- Redis: services +- Docker: services +- Debian: docker images & machines diff --git a/client/ios/Podfile.lock b/client/ios/Podfile.lock index 557bbc7..bdb6cd3 100644 --- a/client/ios/Podfile.lock +++ b/client/ios/Podfile.lock @@ -1,21 +1,21 @@ PODS: - Flutter (1.0.0) - - flutter_secure_storage (3.2.0): + - shared_preferences (0.0.1): - Flutter DEPENDENCIES: - Flutter (from `.symlinks/flutter/ios`) - - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) EXTERNAL SOURCES: Flutter: :path: ".symlinks/flutter/ios" - flutter_secure_storage: - :path: ".symlinks/plugins/flutter_secure_storage/ios" + shared_preferences: + :path: ".symlinks/plugins/shared_preferences/ios" SPEC CHECKSUMS: Flutter: 58dd7d1b27887414a370fcccb9e645c08ffd7a6a - flutter_secure_storage: 0c5779648ff644110e507909b77a57e620cbbf8b + shared_preferences: 1feebfa37bb57264736e16865e7ffae7fc99b523 PODFILE CHECKSUM: aff02bfeed411c636180d6812254b2daeea14d09 diff --git a/client/lib/main.dart b/client/lib/main.dart index 89f7c4b..ab60db0 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -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( - 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( builder: (BuildContext context) => - GRPCCredentialsClient.getInstance(config: settings.arguments), + GRPCCredentialsClient.getInstance( + config: settings.arguments), child: Home(), ); break; diff --git a/client/lib/repositories/config_base.dart b/client/lib/repositories/config_base.dart new file mode 100644 index 0000000..674432b --- /dev/null +++ b/client/lib/repositories/config_base.dart @@ -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; + } +} diff --git a/client/lib/repositories/encrypted_shared_preferences.dart b/client/lib/repositories/encrypted_shared_preferences.dart new file mode 100644 index 0000000..a739b73 --- /dev/null +++ b/client/lib/repositories/encrypted_shared_preferences.dart @@ -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 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 deleteAll() async { + checkIfPasswordMatched(); + + final prefs = await SharedPreferences.getInstance(); + + prefs.remove(ConfigBase.keyConnectionConfig); + prefs.remove(ConfigBase.keyPassword); + prefs.remove(ConfigBase.keyPrivateKey); + } + + @override + Future 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 get passwordIsSet async { + final prefs = await SharedPreferences.getInstance(); + + final isSet = prefs.containsKey(ConfigBase.keyPassword); + passwordMatched = !isSet; + + return isSet; + } + + @override + Future get privateKey async { + checkIfPasswordMatched(); + + final prefs = await SharedPreferences.getInstance(); + final cipherText = prefs.getString(ConfigBase.keyPrivateKey); + + return crypto.decrypt(cipherText, password); + } + + @override + Future 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 setPassword(String password) async { + final prefs = await SharedPreferences.getInstance(); + + this.password = password; + passwordMatched = true; + + prefs.setString(ConfigBase.keyPassword, crypto.hashPassword(password)); + } + + @override + Future setPrivateKey(String key) async { + final prefs = await SharedPreferences.getInstance(); + + prefs.setString(ConfigBase.keyPrivateKey, crypto.encrypt(key, password)); + } +} diff --git a/client/lib/repositories/secure_storage_config.dart b/client/lib/repositories/secure_storage_config.dart deleted file mode 100644 index 9d9c4cf..0000000 --- a/client/lib/repositories/secure_storage_config.dart +++ /dev/null @@ -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 setPrivateKey(String key) { - _checkIfPasswordMatched(); - return _storage.write(key: _keyPrivateKey, value: key.replaceAll('-', '')); - } - - Future get privateKey { - _checkIfPasswordMatched(); - return _storage.read(key: _keyPrivateKey); - } - - Future setPassword(String password) { - _checkIfPasswordMatched(); - - _password = password; - - return _storage.write( - key: _keyPassword, value: crypto.hashPassword(password)); - } - - Future get passwordIsSet async { - final passHash = await _storage.read(key: _keyPassword); - - if (passHash != null) { - return true; - } - - _passwordMatched = true; - - return false; - } - - Future matchesPasswordHash(String password) async { - _passwordMatched = crypto.matchHashedPassword( - await _storage.read(key: _keyPassword), password); - - if (_passwordMatched) _password = password; - - return _passwordMatched; - } - - Future setConnectionConfig(ConnectionConfig config) { - _checkIfPasswordMatched(); - return _storage.write( - key: _keyConnectionConfig, value: json.encode(config)); - } - - Future get connectionConfig async { - _checkIfPasswordMatched(); - final connConfig = await _storage.read(key: _keyConnectionConfig); - - if (connConfig == null) { - return null; - } - - return ConnectionConfig.fromJson(json.decode(connConfig)); - } - - Future deleteAll() { - _checkIfPasswordMatched(); - return _storage.deleteAll(); - } - - void _checkIfPasswordMatched() { - if (_passwordMatched) return; - throw Exception('password not matched yet'); - } -} diff --git a/client/lib/screens/config.dart b/client/lib/screens/config.dart index 58b7929..a8f8f70 100644 --- a/client/lib/screens/config.dart +++ b/client/lib/screens/config.dart @@ -58,8 +58,10 @@ class _ConfigState extends State { : 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( diff --git a/client/lib/screens/credentials.dart b/client/lib/screens/credentials.dart index ab102ab..501cfc5 100644 --- a/client/lib/screens/credentials.dart +++ b/client/lib/screens/credentials.dart @@ -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) diff --git a/client/lib/screens/home.dart b/client/lib/screens/home.dart index ce528c3..62d28c8 100644 --- a/client/lib/screens/home.dart +++ b/client/lib/screens/home.dart @@ -92,6 +92,7 @@ class _HomeState extends State 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 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) { diff --git a/client/lib/types/abstracts.dart b/client/lib/types/abstracts.dart index b399efe..04ddef6 100644 --- a/client/lib/types/abstracts.dart +++ b/client/lib/types/abstracts.dart @@ -24,4 +24,6 @@ abstract class ConfigRepo { Future get connectionConfig; Future deleteAll(); + + void reset(); } diff --git a/client/lib/utils/crypto.dart b/client/lib/utils/crypto.dart index 833503b..036ee51 100644 --- a/client/lib/utils/crypto.dart +++ b/client/lib/utils/crypto.dart @@ -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.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.generate(aesBlockSize, (_) => random.nextInt(byteIntMax)); + final iv = IV(Uint8List.fromList(ivBytes)); + + final encrypter = Encrypter(AES(Key(key), mode: AESMode.cbc)); + final cipherBytes = List.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; diff --git a/client/pubspec.lock b/client/pubspec.lock index 0cd7052..9e3abf9 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -35,7 +35,7 @@ packages: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.5" charcode: dependency: transitive description: @@ -97,13 +97,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_secure_storage: - dependency: "direct main" - description: - name: flutter_secure_storage - url: "https://pub.dartlang.org" - source: hosted - version: "3.2.1+1" flutter_test: dependency: "direct dev" description: flutter @@ -185,7 +178,7 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0+1" pointycastle: dependency: transitive description: @@ -221,6 +214,13 @@ packages: relative: true source: path version: "0.0.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.3" sky_engine: dependency: transitive description: flutter @@ -284,3 +284,4 @@ packages: version: "2.0.8" sdks: dart: ">=2.2.2 <3.0.0" + flutter: ">=0.1.4 <2.0.0" diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 8f636d8..9335610 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -28,10 +28,12 @@ dependencies: provider: ^3.0.0 crypt: ^1.0.7 - flutter_secure_storage: ^3.2.1 password_hash: ^2.0.0 encrypt: ^3.2.0 + # flutter_secure_storage: ^3.2.1 + shared_preferences: 0.5.3 + otp: ^1.0.3 selfpass_protobuf: diff --git a/services/credentials/repositories/grpc_client.go b/services/credentials/repositories/grpc_client.go index 8165e5a..9cc3bb8 100644 --- a/services/credentials/repositories/grpc_client.go +++ b/services/credentials/repositories/grpc_client.go @@ -57,7 +57,10 @@ func (c credentialsClient) GetAllMetadata(ctx context.Context, sourceHost string Errors: errch, }) - srv, err := c.client.GetAllMetadata(ctx, &protobuf.SourceHostRequest{SourceHost: sourceHost}) + srv, err := c.client.GetAllMetadata( + ctx, + transport.EncodeSourceHostRequest(endpoints.SourceHostRequest{SourceHost: sourceHost}), + ) if err != nil { errch <- err return nil, errch diff --git a/services/credentials/transport/encoding.go b/services/credentials/transport/encoding.go index ad84bd4..8ff28c6 100644 --- a/services/credentials/transport/encoding.go +++ b/services/credentials/transport/encoding.go @@ -17,11 +17,10 @@ func decodeSourceHostRequest(ctx context.Context, request interface{}) (interfac }, nil } -func EncodeSourceHostRequest(ctx context.Context, request interface{}) (interface{}, error) { - r := request.(endpoints.SourceHostRequest) - return protobuf.SourceHostRequest{ - SourceHost: r.SourceHost, - }, nil +func EncodeSourceHostRequest(request endpoints.SourceHostRequest) *protobuf.SourceHostRequest { + return &protobuf.SourceHostRequest{ + SourceHost: request.SourceHost, + } } func encodeMetadataStreamResponse(ctx context.Context, response interface{}) (interface{}, error) {