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
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

View File

@ -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

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;

View File

@ -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"

View File

@ -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:

View File

@ -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

View File

@ -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) {