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

@ -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 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. using GUIs, with all the same safety and encryption as the CLI.
| Goal | Progress | Comment | | Goal | Progress | Comment |
| --- | :---: | --- | | --- | :---: | --- |
| Support mutual TLS. | 100% | | | Support mutual TLS. | 100% | |
| Support credentials CRUD via gRPC. | 25% | TODO: CUD | | Support credentials CRUD via gRPC. | 25% | TODO: CUD |
| Support storage of certs, PK, and host in shared preferences, encrypted. | 100% | | | 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 | | Support AES-CBC encryption of passes and OTP secrets, using MP and PK. | 100% | |
## Other Info ## 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)** **Architectural 3rd-party Technologies in Use (and where)**
- Golang (services & protobuf) - Golang: services, sp, & protobuf
- Dart (client & protobuf) - Dart: client & protobuf
- Flutter (client) - Flutter: client
- Go-Kit (services) - Go-Kit: services
- gRPC & Protobuf (all) - gRPC/Protobuf: all
- Cobra Commander & Viper Config (spc) - Cobra Commander & Viper Config: sp
- Redis (services) - Redis: services
- Docker (services) - Docker: services
- Debian (docker images and machines) - Debian: docker images & machines

View File

@ -1,21 +1,21 @@
PODS: PODS:
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_secure_storage (3.2.0): - shared_preferences (0.0.1):
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- Flutter (from `.symlinks/flutter/ios`) - 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: EXTERNAL SOURCES:
Flutter: Flutter:
:path: ".symlinks/flutter/ios" :path: ".symlinks/flutter/ios"
flutter_secure_storage: shared_preferences:
:path: ".symlinks/plugins/flutter_secure_storage/ios" :path: ".symlinks/plugins/shared_preferences/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
Flutter: 58dd7d1b27887414a370fcccb9e645c08ffd7a6a Flutter: 58dd7d1b27887414a370fcccb9e645c08ffd7a6a
flutter_secure_storage: 0c5779648ff644110e507909b77a57e620cbbf8b shared_preferences: 1feebfa37bb57264736e16865e7ffae7fc99b523
PODFILE CHECKSUM: aff02bfeed411c636180d6812254b2daeea14d09 PODFILE CHECKSUM: aff02bfeed411c636180d6812254b2daeea14d09

View File

@ -2,24 +2,26 @@ import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'repositories/grpc_credentials_client.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/authentication.dart';
import 'screens/config.dart';
import 'screens/credential.dart'; import 'screens/credential.dart';
import 'screens/credentials.dart'; import 'screens/credentials.dart';
import 'screens/config.dart';
import 'screens/home.dart'; import 'screens/home.dart';
import 'types/abstracts.dart'; import 'types/abstracts.dart';
import 'types/screen_arguments.dart'; import 'types/screen_arguments.dart';
void main() => runApp(Selfpass()); void main() {
runApp(Selfpass());
}
class Selfpass extends StatelessWidget { class Selfpass extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Provider<ConfigRepo>( return Provider<ConfigRepo>(
builder: (BuildContext context) => SecureStorageConfig(), builder: (BuildContext context) => EncryptedSharedPreferences(),
child: CupertinoApp( child: CupertinoApp(
title: 'Selfpass', title: 'Selfpass',
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
@ -36,7 +38,8 @@ class Selfpass extends StatelessWidget {
title = 'Hosts'; title = 'Hosts';
builder = (BuildContext context) => Provider<CredentialsRepo>( builder = (BuildContext context) => Provider<CredentialsRepo>(
builder: (BuildContext context) => builder: (BuildContext context) =>
GRPCCredentialsClient.getInstance(config: settings.arguments), GRPCCredentialsClient.getInstance(
config: settings.arguments),
child: Home(), child: Home(),
); );
break; 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( : CupertinoNavigationBar(
trailing: GestureDetector( trailing: GestureDetector(
onTap: _buildResetAllHandler(context), onTap: _buildResetAllHandler(context),
child: Text('Reset App', child: Text(
style: TextStyle(color: CupertinoColors.destructiveRed)), 'Reset',
style: TextStyle(color: CupertinoColors.destructiveRed),
),
), ),
), ),
child: Container( child: Container(

View File

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

View File

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

View File

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

View File

@ -1,31 +1,37 @@
import 'dart:math';
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypt/crypt.dart'; import 'package:crypt/crypt.dart';
import 'package:encrypt/encrypt.dart'; import 'package:encrypt/encrypt.dart';
import 'package:password_hash/password_hash.dart'; import 'package:password_hash/password_hash.dart';
String hashPassword(String password) { String hashPassword(String password) {
const saltSize = 16; final salt = Salt.generateAsBase64String(saltSize);
const saltIntMax = 256;
final random = Random.secure();
final saltInts =
List<int>.generate(saltSize, (_) => random.nextInt(saltIntMax));
final salt = base64.encode(saltInts);
return Crypt.sha256(password, salt: salt).toString(); return Crypt.sha256(password, salt: salt).toString();
} }
bool matchHashedPassword(String hashedPassword, String password) => bool matchHashedPassword(String hashedPassword, String password) =>
Crypt(hashedPassword).match(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( final key = PBKDF2().generateKey(
masterpass, privateKey, pbkdf2Rounds, keySize, masterpass,
privateKey,
pbkdf2Rounds,
keySize,
); );
var cipherBytes = base64.decode(cipherText);
final ivBytes = cipherBytes.sublist(0, aesBlockSize); final ivBytes = cipherBytes.sublist(0, aesBlockSize);
cipherBytes = cipherBytes.sublist(aesBlockSize); cipherBytes = cipherBytes.sublist(aesBlockSize);
@ -34,6 +40,40 @@ String decryptPassword(String masterpass, privateKey, cipherText) {
return encrypter.decrypt(Encrypted(cipherBytes), iv: iv); 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 pbkdf2Rounds = 4096;
const keySize = 32; const keySize = 32;
const aesBlockSize = 16; const aesBlockSize = 16;
const byteIntMax = 256;

View File

@ -35,7 +35,7 @@ packages:
name: boolean_selector name: boolean_selector
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.4" version: "1.0.5"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -97,13 +97,6 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -185,7 +178,7 @@ packages:
name: pedantic name: pedantic
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.7.0" version: "1.8.0+1"
pointycastle: pointycastle:
dependency: transitive dependency: transitive
description: description:
@ -221,6 +214,13 @@ packages:
relative: true relative: true
source: path source: path
version: "0.0.0" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -284,3 +284,4 @@ packages:
version: "2.0.8" version: "2.0.8"
sdks: sdks:
dart: ">=2.2.2 <3.0.0" dart: ">=2.2.2 <3.0.0"
flutter: ">=0.1.4 <2.0.0"

View File

@ -28,10 +28,12 @@ dependencies:
provider: ^3.0.0 provider: ^3.0.0
crypt: ^1.0.7 crypt: ^1.0.7
flutter_secure_storage: ^3.2.1
password_hash: ^2.0.0 password_hash: ^2.0.0
encrypt: ^3.2.0 encrypt: ^3.2.0
# flutter_secure_storage: ^3.2.1
shared_preferences: 0.5.3
otp: ^1.0.3 otp: ^1.0.3
selfpass_protobuf: selfpass_protobuf:

View File

@ -57,7 +57,10 @@ func (c credentialsClient) GetAllMetadata(ctx context.Context, sourceHost string
Errors: errch, 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 { if err != nil {
errch <- err errch <- err
return nil, errch return nil, errch

View File

@ -17,11 +17,10 @@ func decodeSourceHostRequest(ctx context.Context, request interface{}) (interfac
}, nil }, nil
} }
func EncodeSourceHostRequest(ctx context.Context, request interface{}) (interface{}, error) { func EncodeSourceHostRequest(request endpoints.SourceHostRequest) *protobuf.SourceHostRequest {
r := request.(endpoints.SourceHostRequest) return &protobuf.SourceHostRequest{
return protobuf.SourceHostRequest{ SourceHost: request.SourceHost,
SourceHost: r.SourceHost, }
}, nil
} }
func encodeMetadataStreamResponse(ctx context.Context, response interface{}) (interface{}, error) { func encodeMetadataStreamResponse(ctx context.Context, response interface{}) (interface{}, error) {