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

@ -46,7 +46,7 @@ using GUIs, with all the same safety and encryption as the CLI.
| 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 |
| 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

@ -31,7 +31,8 @@ class Credentials extends StatelessWidget {
Text('Decrypting credential...'),
Container(
margin: EdgeInsets.only(top: 10),
child: CupertinoActivityIndicator()),
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) {