mirror of https://github.com/mitchell/selfpass.git
Add aes-cbc encryption; add config repo based on shared_preferences
This commit is contained in:
parent
80f9705b19
commit
67744527cc
30
README.md
30
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -24,4 +24,6 @@ abstract class ConfigRepo {
|
|||
Future<ConnectionConfig> get connectionConfig;
|
||||
|
||||
Future<void> deleteAll();
|
||||
|
||||
void reset();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue