Add decryption of credential encrypted fields; refactored config;
add app icon
|
@ -33,7 +33,7 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.example.selfpass_mobile"
|
||||
applicationId "com.mjfs.selfpass"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 28
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.selfpass_mobile">
|
||||
package="com.mjfs.selfpass">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.selfpass_mobile">
|
||||
package="com.mjfs.selfpass">
|
||||
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
|
@ -8,7 +8,7 @@
|
|||
FlutterApplication and put your custom class here. -->
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:label="selfpass_mobile"
|
||||
android:label="selfpass_client"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.selfpass_mobile">
|
||||
package="com.mjfs.selfpass">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
|
|
|
@ -4,12 +4,12 @@ PODS:
|
|||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- Flutter (from `.symlinks/flutter/ios`)
|
||||
- Flutter (from `.symlinks/flutter/ios-release`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Flutter:
|
||||
:path: ".symlinks/flutter/ios"
|
||||
:path: ".symlinks/flutter/ios-release"
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
|
||||
|
|
|
@ -253,7 +253,7 @@
|
|||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
|
||||
"${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework",
|
||||
"${PODS_ROOT}/../.symlinks/flutter/ios-release/Flutter.framework",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
|
|
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 163 B |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 233 B |
After Width: | Height: | Size: 276 B |
After Width: | Height: | Size: 394 B |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 549 B |
After Width: | Height: | Size: 600 B |
After Width: | Height: | Size: 601 B |
After Width: | Height: | Size: 695 B |
After Width: | Height: | Size: 893 B |
After Width: | Height: | Size: 1.0 KiB |
|
@ -3,116 +3,134 @@
|
|||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"filename" : "40.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"filename" : "60.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"filename" : "29.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"filename" : "58.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"filename" : "87.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"filename" : "80.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"filename" : "120.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"size" : "57x57",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"filename" : "57.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "57x57",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "114.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"filename" : "120.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "180.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"filename" : "1024.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "16.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "32.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "32.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "64.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "128.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "256.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "256.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "512.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "512.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "1024-1.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 564 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.5 KiB |
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>selfpass_mobile</string>
|
||||
<string>selfpass_client</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
|
|
@ -60,7 +60,9 @@ class Selfpass extends StatelessWidget {
|
|||
break;
|
||||
|
||||
case '/config':
|
||||
final ConfigScreenArguments arguments = settings.arguments;
|
||||
final ConfigScreenArguments arguments = settings.arguments == null
|
||||
? ConfigScreenArguments()
|
||||
: settings.arguments;
|
||||
title = 'Configuration';
|
||||
builder = (BuildContext context) =>
|
||||
Config(arguments.connectionConfig, arguments.privateKey);
|
||||
|
|
|
@ -4,25 +4,32 @@
|
|||
///
|
||||
// ignore_for_file: camel_case_types,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name
|
||||
|
||||
import 'dart:core' as $core show bool, Deprecated, double, int, List, Map, override, pragma, String;
|
||||
import 'dart:core' as $core
|
||||
show bool, Deprecated, double, int, List, Map, override, pragma, String;
|
||||
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
import 'dart:core' as $core show DateTime, Duration;
|
||||
|
||||
class Timestamp extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo('Timestamp', package: const $pb.PackageName('google.protobuf'))
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo('Timestamp',
|
||||
package: const $pb.PackageName('google.protobuf'))
|
||||
..aInt64(1, 'seconds')
|
||||
..a<$core.int>(2, 'nanos', $pb.PbFieldType.O3)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
..hasRequiredFields = false;
|
||||
|
||||
Timestamp._() : super();
|
||||
factory Timestamp() => create();
|
||||
factory Timestamp.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory Timestamp.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
factory Timestamp.fromBuffer($core.List<$core.int> i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromBuffer(i, r);
|
||||
factory Timestamp.fromJson($core.String i,
|
||||
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
|
||||
create()..mergeFromJson(i, r);
|
||||
Timestamp clone() => Timestamp()..mergeFromMessage(this);
|
||||
Timestamp copyWith(void Function(Timestamp) updates) => super.copyWith((message) => updates(message as Timestamp));
|
||||
Timestamp copyWith(void Function(Timestamp) updates) =>
|
||||
super.copyWith((message) => updates(message as Timestamp));
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static Timestamp create() => Timestamp._();
|
||||
|
@ -32,30 +39,36 @@ class Timestamp extends $pb.GeneratedMessage {
|
|||
static Timestamp _defaultInstance;
|
||||
|
||||
Int64 get seconds => $_getI64(0);
|
||||
set seconds(Int64 v) { $_setInt64(0, v); }
|
||||
set seconds(Int64 v) {
|
||||
$_setInt64(0, v);
|
||||
}
|
||||
|
||||
$core.bool hasSeconds() => $_has(0);
|
||||
void clearSeconds() => clearField(1);
|
||||
|
||||
$core.int get nanos => $_get(1, 0);
|
||||
set nanos($core.int v) { $_setSignedInt32(1, v); }
|
||||
set nanos($core.int v) {
|
||||
$_setSignedInt32(1, v);
|
||||
}
|
||||
|
||||
$core.bool hasNanos() => $_has(1);
|
||||
void clearNanos() => clearField(2);
|
||||
/// Converts an instance to [DateTime].
|
||||
///
|
||||
/// The result is in UTC time zone and has microsecond precision, as
|
||||
/// [DateTime] does not support nanosecond precision.
|
||||
$core.DateTime toDateTime() => $core.DateTime.fromMicrosecondsSinceEpoch(
|
||||
seconds.toInt() * $core.Duration.microsecondsPerSecond + nanos ~/ 1000,
|
||||
isUtc: true);
|
||||
|
||||
/// Creates a new instance from [dateTime].
|
||||
///
|
||||
/// Time zone information will not be preserved.
|
||||
static Timestamp fromDateTime($core.DateTime dateTime) {
|
||||
$core.int micros = dateTime.microsecondsSinceEpoch;
|
||||
return Timestamp()
|
||||
..seconds = Int64(micros ~/ $core.Duration.microsecondsPerSecond)
|
||||
..nanos = (micros % $core.Duration.microsecondsPerSecond).toInt() * 1000;
|
||||
}
|
||||
/// Converts an instance to [DateTime].
|
||||
///
|
||||
/// The result is in UTC time zone and has microsecond precision, as
|
||||
/// [DateTime] does not support nanosecond precision.
|
||||
$core.DateTime toDateTime() => $core.DateTime.fromMicrosecondsSinceEpoch(
|
||||
seconds.toInt() * $core.Duration.microsecondsPerSecond + nanos ~/ 1000,
|
||||
isUtc: true);
|
||||
|
||||
/// Creates a new instance from [dateTime].
|
||||
///
|
||||
/// Time zone information will not be preserved.
|
||||
static Timestamp fromDateTime($core.DateTime dateTime) {
|
||||
$core.int micros = dateTime.microsecondsSinceEpoch;
|
||||
return Timestamp()
|
||||
..seconds = Int64(micros ~/ $core.Duration.microsecondsPerSecond)
|
||||
..nanos = (micros % $core.Duration.microsecondsPerSecond).toInt() * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,4 +11,3 @@ const Timestamp$json = const {
|
|||
const {'1': 'nanos', '3': 2, '4': 1, '5': 5, '10': 'nanos'},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -29,7 +29,14 @@ const UpdateRequest$json = const {
|
|||
'1': 'UpdateRequest',
|
||||
'2': const [
|
||||
const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
|
||||
const {'1': 'credential', '3': 2, '4': 1, '5': 11, '6': '.selfpass.credentials.CredentialRequest', '10': 'credential'},
|
||||
const {
|
||||
'1': 'credential',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.selfpass.credentials.CredentialRequest',
|
||||
'10': 'credential'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -48,8 +55,22 @@ const Metadata$json = const {
|
|||
'1': 'Metadata',
|
||||
'2': const [
|
||||
const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
|
||||
const {'1': 'created_at', '3': 2, '4': 1, '5': 11, '6': '.google.protobuf.Timestamp', '10': 'createdAt'},
|
||||
const {'1': 'updated_at', '3': 3, '4': 1, '5': 11, '6': '.google.protobuf.Timestamp', '10': 'updatedAt'},
|
||||
const {
|
||||
'1': 'created_at',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.google.protobuf.Timestamp',
|
||||
'10': 'createdAt'
|
||||
},
|
||||
const {
|
||||
'1': 'updated_at',
|
||||
'3': 3,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.google.protobuf.Timestamp',
|
||||
'10': 'updatedAt'
|
||||
},
|
||||
const {'1': 'primary', '3': 4, '4': 1, '5': 9, '10': 'primary'},
|
||||
const {'1': 'source_host', '3': 5, '4': 1, '5': 9, '10': 'sourceHost'},
|
||||
const {'1': 'login_url', '3': 6, '4': 1, '5': 9, '10': 'loginUrl'},
|
||||
|
@ -61,8 +82,22 @@ const Credential$json = const {
|
|||
'1': 'Credential',
|
||||
'2': const [
|
||||
const {'1': 'id', '3': 1, '4': 1, '5': 9, '10': 'id'},
|
||||
const {'1': 'created_at', '3': 2, '4': 1, '5': 11, '6': '.google.protobuf.Timestamp', '10': 'createdAt'},
|
||||
const {'1': 'updated_at', '3': 3, '4': 1, '5': 11, '6': '.google.protobuf.Timestamp', '10': 'updatedAt'},
|
||||
const {
|
||||
'1': 'created_at',
|
||||
'3': 2,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.google.protobuf.Timestamp',
|
||||
'10': 'createdAt'
|
||||
},
|
||||
const {
|
||||
'1': 'updated_at',
|
||||
'3': 3,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.google.protobuf.Timestamp',
|
||||
'10': 'updatedAt'
|
||||
},
|
||||
const {'1': 'primary', '3': 4, '4': 1, '5': 9, '10': 'primary'},
|
||||
const {'1': 'username', '3': 5, '4': 1, '5': 9, '10': 'username'},
|
||||
const {'1': 'email', '3': 6, '4': 1, '5': 9, '10': 'email'},
|
||||
|
@ -87,4 +122,3 @@ const CredentialRequest$json = const {
|
|||
const {'1': 'otp_secret', '3': 8, '4': 1, '5': 9, '10': 'otpSecret'},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -12,11 +12,17 @@ class Config implements ConfigRepo {
|
|||
static const _keyConnectionConfig = "connection_config";
|
||||
static const _keyPassword = "password";
|
||||
final FlutterSecureStorage _storage = FlutterSecureStorage();
|
||||
var _passwordMatched = false;
|
||||
bool _passwordMatched = false;
|
||||
String _password;
|
||||
|
||||
String get password {
|
||||
_checkIfPasswordMatched();
|
||||
return _password;
|
||||
}
|
||||
|
||||
Future<void> setPrivateKey(String key) {
|
||||
_checkIfPasswordMatched();
|
||||
return _storage.write(key: _keyPrivateKey, value: key);
|
||||
return _storage.write(key: _keyPrivateKey, value: key.replaceAll('-', ''));
|
||||
}
|
||||
|
||||
Future<String> get privateKey {
|
||||
|
@ -26,6 +32,7 @@ class Config implements ConfigRepo {
|
|||
|
||||
Future<void> setPassword(String password) {
|
||||
_checkIfPasswordMatched();
|
||||
_password = password;
|
||||
return _storage.write(
|
||||
key: _keyPassword, value: crypto.hashPassword(password));
|
||||
}
|
||||
|
@ -42,9 +49,16 @@ class Config implements ConfigRepo {
|
|||
return false;
|
||||
}
|
||||
|
||||
Future<bool> matchesPasswordHash(String password) async =>
|
||||
_passwordMatched = crypto.matchHashedPassword(
|
||||
await _storage.read(key: _keyPassword), password);
|
||||
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();
|
||||
|
|
|
@ -105,10 +105,8 @@ class _AuthenticationState extends State<Authentication> {
|
|||
return () async {
|
||||
if (await _passwordIsSet) {
|
||||
if (await _config.matchesPasswordHash(_passwordController.text)) {
|
||||
Navigator.of(context).pushReplacementNamed(
|
||||
'/home',
|
||||
arguments: await _config.connectionConfig,
|
||||
);
|
||||
Navigator.of(context).pushReplacementNamed('/home',
|
||||
arguments: await _config.connectionConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:otp/otp.dart';
|
||||
|
||||
import '../types/credential.dart' as types;
|
||||
|
||||
|
@ -19,17 +20,14 @@ class _CredentialState extends State<Credential> {
|
|||
Map<String, _FieldBuildConfig> _fieldMap;
|
||||
types.Credential _credential;
|
||||
|
||||
_CredentialState(this._credential) : super() {
|
||||
_controllers = _CredentialControllers.fromCredential(_credential);
|
||||
_fieldMap = _buildFieldMap(_controllers, _credential);
|
||||
}
|
||||
_CredentialState(this._credential) : super();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 30, left: 30),
|
||||
padding: const EdgeInsets.only(top: 15, bottom: 30, left: 30),
|
||||
child: ListView(
|
||||
children: _buildFieldRows(context),
|
||||
),
|
||||
|
@ -43,12 +41,12 @@ class _CredentialState extends State<Credential> {
|
|||
) {
|
||||
final fieldMap = {
|
||||
'Id:': _FieldBuildConfig(mutable: false, text: credential.meta.id),
|
||||
'Created: ': _FieldBuildConfig(
|
||||
'Created:': _FieldBuildConfig(
|
||||
mutable: false,
|
||||
copyable: false,
|
||||
text: credential.meta.createdAt.toString(),
|
||||
),
|
||||
'Updated: ': _FieldBuildConfig(
|
||||
'Updated:': _FieldBuildConfig(
|
||||
mutable: false,
|
||||
copyable: false,
|
||||
text: credential.meta.updatedAt.toString(),
|
||||
|
@ -69,15 +67,26 @@ class _CredentialState extends State<Credential> {
|
|||
fieldMap['Email:'] = _FieldBuildConfig(controller: controllers.email);
|
||||
}
|
||||
|
||||
fieldMap['Password:'] =
|
||||
_FieldBuildConfig(controller: controllers.password, obscured: true);
|
||||
|
||||
if (credential.otpSecret != null && credential.otpSecret != '') {
|
||||
fieldMap['OTP Secret:'] = _FieldBuildConfig(
|
||||
controller: controllers.otpSecret, obscured: true, otp: true);
|
||||
}
|
||||
|
||||
return fieldMap;
|
||||
}
|
||||
|
||||
List<Widget> _buildFieldRows(BuildContext context) {
|
||||
List<Widget> rows = [];
|
||||
|
||||
_controllers = _CredentialControllers.fromCredential(_credential);
|
||||
_fieldMap = _buildFieldMap(_controllers, _credential);
|
||||
|
||||
_fieldMap.forEach((key, value) {
|
||||
rows.add(Container(
|
||||
margin: EdgeInsets.only(top: 10),
|
||||
margin: EdgeInsets.only(top: 2.5),
|
||||
child: Text(key, style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
));
|
||||
|
||||
|
@ -85,20 +94,27 @@ class _CredentialState extends State<Credential> {
|
|||
Expanded(
|
||||
flex: 3,
|
||||
child: value.mutable
|
||||
? TextField(maxLines: 1, controller: value.controller)
|
||||
? TextField(
|
||||
maxLines: 1,
|
||||
controller: value.controller,
|
||||
obscure: value.obscured)
|
||||
: Container(
|
||||
margin: EdgeInsets.symmetric(vertical: 10),
|
||||
child: Text(value.text),
|
||||
),
|
||||
child: Text(value.text)),
|
||||
),
|
||||
];
|
||||
|
||||
if (value.copyable) {
|
||||
widgets.add(Flexible(
|
||||
widgets.add(Expanded(
|
||||
child: CupertinoButton(
|
||||
child: Text('Copy'),
|
||||
child: Text(value.otp ? 'OTP' : 'Copy'),
|
||||
onPressed: () => Clipboard.setData(ClipboardData(
|
||||
text: value.mutable ? value.controller.text : value.text,
|
||||
text: value.otp
|
||||
? OTP
|
||||
.generateTOTPCode(value.controller.text,
|
||||
DateTime.now().millisecondsSinceEpoch)
|
||||
.toString()
|
||||
: value.mutable ? value.controller.text : value.text,
|
||||
)),
|
||||
),
|
||||
));
|
||||
|
@ -116,10 +132,14 @@ class _FieldBuildConfig {
|
|||
final String text;
|
||||
final bool mutable;
|
||||
final bool copyable;
|
||||
final bool obscured;
|
||||
final bool otp;
|
||||
|
||||
const _FieldBuildConfig({
|
||||
this.mutable = true,
|
||||
this.copyable = true,
|
||||
this.obscured = false,
|
||||
this.otp = false,
|
||||
this.controller,
|
||||
this.text,
|
||||
});
|
||||
|
@ -151,5 +171,7 @@ class _CredentialControllers {
|
|||
tag: TextEditingController(text: credential.meta.tag),
|
||||
username: TextEditingController(text: credential.username),
|
||||
email: TextEditingController(text: credential.email),
|
||||
password: TextEditingController(text: credential.password),
|
||||
otpSecret: TextEditingController(text: credential.otpSecret),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import 'package:provider/provider.dart';
|
|||
import '../types/abstracts.dart';
|
||||
import '../types/credential.dart';
|
||||
|
||||
import '../utils/crypto.dart' as crypto;
|
||||
|
||||
import '../widgets/tappable_text_list.dart';
|
||||
|
||||
class Credentials extends StatelessWidget {
|
||||
|
@ -20,10 +22,39 @@ class Credentials extends StatelessWidget {
|
|||
}
|
||||
|
||||
Map<String, GestureTapCallback> _buildTappableText(BuildContext context) {
|
||||
var makeOnTapHandler = (String id) => () async {
|
||||
final credential =
|
||||
await Provider.of<CredentialsRepo>(context).get(id);
|
||||
Navigator.of(context).pushNamed('/credential', arguments: credential);
|
||||
final makeOnTapHandler = (String id) => () async {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => CupertinoAlertDialog(
|
||||
content: Column(
|
||||
children: [
|
||||
Text('Decrypting credential...'),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 10),
|
||||
child: CupertinoActivityIndicator()),
|
||||
],
|
||||
)),
|
||||
);
|
||||
|
||||
final config = Provider.of<ConfigRepo>(context);
|
||||
final client = Provider.of<CredentialsRepo>(context);
|
||||
|
||||
final Future<String> privateKey = config.privateKey;
|
||||
final String password = config.password;
|
||||
|
||||
final credential = await client.get(id);
|
||||
|
||||
credential.password = crypto.decryptPassword(
|
||||
password, await privateKey, credential.password);
|
||||
|
||||
if (credential.otpSecret != null && credential.otpSecret != '') {
|
||||
credential.otpSecret = crypto.decryptPassword(
|
||||
password, await privateKey, credential.otpSecret);
|
||||
}
|
||||
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pushNamed('/credential', arguments: credential);
|
||||
};
|
||||
|
||||
Map<String, GestureTapCallback> tappableText = {};
|
||||
|
|
|
@ -91,6 +91,7 @@ class _HomeState extends State<Home> {
|
|||
GestureTapCallback _makeConfigOnTapHandler(BuildContext context) {
|
||||
return () async => Navigator.of(context).pushNamed('/config',
|
||||
arguments: ConfigScreenArguments(
|
||||
await _config.connectionConfig, await _config.privateKey));
|
||||
connectionConfig: await _config.connectionConfig,
|
||||
privateKey: await _config.privateKey));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ abstract class ConfigRepo {
|
|||
Future<void> setPrivateKey(String key);
|
||||
Future<String> get privateKey;
|
||||
|
||||
String get password;
|
||||
Future<void> setPassword(String password);
|
||||
Future<bool> get passwordSet;
|
||||
Future<bool> matchesPasswordHash(String password);
|
||||
|
|
|
@ -4,5 +4,5 @@ class ConfigScreenArguments {
|
|||
final ConnectionConfig connectionConfig;
|
||||
final String privateKey;
|
||||
|
||||
const ConfigScreenArguments(this.connectionConfig, this.privateKey);
|
||||
const ConfigScreenArguments({this.connectionConfig, this.privateKey});
|
||||
}
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
import 'dart:math';
|
||||
import 'dart:convert';
|
||||
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(16, (_) => random.nextInt(256));
|
||||
final saltInts =
|
||||
List<int>.generate(saltSize, (_) => random.nextInt(saltIntMax));
|
||||
final salt = base64.encode(saltInts);
|
||||
|
||||
return Crypt.sha256(password, salt: salt).toString();
|
||||
|
@ -13,3 +20,23 @@ String hashPassword(String password) {
|
|||
|
||||
bool matchHashedPassword(String hashedPassword, String password) =>
|
||||
Crypt(hashedPassword).match(password);
|
||||
|
||||
String decryptPassword(String masterpass, privateKey, ciphertext) {
|
||||
final key =
|
||||
PBKDF2().generateKey(masterpass, privateKey, pbkdf2Rounds, keySize);
|
||||
|
||||
var cipherbytes = base64.decode(ciphertext);
|
||||
final iv =
|
||||
IV(Uint8List.fromList(cipherbytes.getRange(0, aesBlockSize).toList()));
|
||||
|
||||
cipherbytes = Uint8List.fromList(
|
||||
cipherbytes.getRange(aesBlockSize, cipherbytes.length).toList());
|
||||
|
||||
final encrypter = Encrypter(AES(Key(key), mode: AESMode.cbc));
|
||||
|
||||
return encrypter.decrypt(Encrypted(cipherbytes), iv: iv);
|
||||
}
|
||||
|
||||
const pbkdf2Rounds = 4096;
|
||||
const keySize = 32;
|
||||
const aesBlockSize = 16;
|
||||
|
|
|
@ -7,6 +7,7 @@ typedef OnSubmittedBuilder = ValueChanged<String> Function(
|
|||
class TextField extends StatelessWidget {
|
||||
final OnSubmittedBuilder onSubmittedBuilder;
|
||||
final TextEditingController controller;
|
||||
final OverlayVisibilityMode clearButtonMode;
|
||||
final Widget prefix;
|
||||
final Widget suffix;
|
||||
final bool obscure;
|
||||
|
@ -25,6 +26,7 @@ class TextField extends StatelessWidget {
|
|||
this.autocorrect = false,
|
||||
this.prefix,
|
||||
this.suffix,
|
||||
this.clearButtonMode = OverlayVisibilityMode.editing,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -36,7 +38,7 @@ class TextField extends StatelessWidget {
|
|||
border: Border.all(color: CupertinoColors.black),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5.0)),
|
||||
),
|
||||
clearButtonMode: OverlayVisibilityMode.editing,
|
||||
clearButtonMode: clearButtonMode,
|
||||
textAlign: TextAlign.start,
|
||||
onSubmitted: this.onSubmittedBuilder != null
|
||||
? onSubmittedBuilder(context)
|
||||
|
|
61
pubspec.lock
|
@ -1,5 +1,5 @@
|
|||
# Generated by pub
|
||||
# See https://www.dartlang.org/tools/pub/glossary#lockfile
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_discoveryapis_commons:
|
||||
dependency: transitive
|
||||
|
@ -8,13 +8,34 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.8+1"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
asn1lib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: asn1lib
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.8"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.2.0"
|
||||
base32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: base32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -64,6 +85,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
encrypt:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: encrypt
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -144,6 +172,20 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.6"
|
||||
otp:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: otp
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
password_hash:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: password_hash
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -157,7 +199,14 @@ packages:
|
|||
name: pedantic
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
version: "1.7.0"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
protobuf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -178,7 +227,7 @@ packages:
|
|||
name: quiver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.0.3"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -225,7 +274,7 @@ packages:
|
|||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.4"
|
||||
version: "0.2.5"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -241,4 +290,4 @@ packages:
|
|||
source: hosted
|
||||
version: "2.0.8"
|
||||
sdks:
|
||||
dart: ">=2.2.0 <3.0.0"
|
||||
dart: ">=2.2.2 <3.0.0"
|
||||
|
|
|
@ -30,15 +30,17 @@ dependencies:
|
|||
|
||||
crypt: ^1.0.7
|
||||
flutter_secure_storage: ^3.2.1
|
||||
password_hash: ^2.0.0
|
||||
encrypt: ^3.2.0
|
||||
|
||||
otp: ^1.0.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://www.dartlang.org/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter.
|
||||
flutter:
|
||||
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:selfpass_mobile/main.dart';
|
||||
import 'package:selfpass_client/main.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(MyApp());
|
||||
await tester.pumpWidget(Selfpass());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
|
|