Add 'client/' from commit '92f38b5810'

git-subtree-dir: client
git-subtree-mainline: 024017338e
git-subtree-split: 92f38b5810
This commit is contained in:
mitchell 2019-07-11 02:41:03 -04:00
commit 66ec035ee0
90 changed files with 3817 additions and 0 deletions

View file

@ -0,0 +1,129 @@
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import '../types/abstracts.dart';
import '../widgets/text_field.dart';
class Authentication extends StatefulWidget {
@override
_AuthenticationState createState() => _AuthenticationState();
}
class _AuthenticationState extends State<Authentication> {
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _confirmController = TextEditingController();
bool _invalid = false;
bool _passesDontMatch = false;
ConfigRepo _config;
Future<bool> _passwordIsSet;
@override
didChangeDependencies() {
super.didChangeDependencies();
_config = Provider.of<ConfigRepo>(context);
_passwordIsSet = _config.passwordIsSet;
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 50.0),
child: FutureBuilder<bool>(
future: _passwordIsSet,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) =>
snapshot.connectionState == ConnectionState.done
? Column(
children: _buildColumnChildren(context, snapshot.data))
: Center(child: CupertinoActivityIndicator()),
),
),
);
}
List<Widget> _buildColumnChildren(BuildContext context, bool passwordIsSet) {
List<Widget> children = [
const Spacer(flex: 4),
const Flexible(child: Text('Master password:')),
Flexible(
child: TextField(
maxLines: 1,
obscure: true,
autofocus: true,
controller: _passwordController,
),
),
];
if (!passwordIsSet) {
children.add(const Flexible(child: Text('Re-enter password:')));
children.add(Flexible(
child: TextField(
maxLines: 1,
obscure: true,
controller: _confirmController,
),
));
}
if (_invalid) {
children.add(const Flexible(
child: Text(
'invalid masterpass',
style: TextStyle(color: CupertinoColors.destructiveRed),
),
));
}
if (_passesDontMatch) {
children.add(const Flexible(
child: Text(
'passwords don\'t match',
style: TextStyle(color: CupertinoColors.destructiveRed),
),
));
}
children.add(CupertinoButton(
child: Text('Enter'),
onPressed: _buildEnterPressedBuilder(context),
));
if (_passesDontMatch) {
children.add(const Spacer(flex: 1));
} else if (_invalid || !passwordIsSet) {
children.add(const Spacer(flex: 2));
} else {
children.add(const Spacer(flex: 3));
}
return children;
}
VoidCallback _buildEnterPressedBuilder(BuildContext context) {
return () async {
if (await _passwordIsSet) {
if (await _config.matchesPasswordHash(_passwordController.text)) {
Navigator.of(context).pushReplacementNamed('/home',
arguments: await _config.connectionConfig);
return;
}
this.setState(() => _invalid = true);
return;
}
if (_passwordController.text != _confirmController.text) {
this.setState(() {
_passesDontMatch = true;
});
return;
}
_config.setPassword(_passwordController.text);
_passwordIsSet = Future<bool>.value(true);
Navigator.of(context).pushReplacementNamed('/config');
};
}
}

View file

@ -0,0 +1,135 @@
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import '../types/abstracts.dart';
import '../types/connection_config.dart';
import '../widgets/text_field.dart';
class Config extends StatefulWidget {
final ConnectionConfig connectionConfig;
final String privateKey;
const Config(this.connectionConfig, this.privateKey, {Key key})
: super(key: key);
@override
State createState() => _ConfigState(this.connectionConfig, this.privateKey);
}
class _ConfigState extends State<Config> {
TextEditingController _hostController;
TextEditingController _caCertController;
TextEditingController _certController;
TextEditingController _privateCertController;
TextEditingController _privateKeyController;
ConnectionConfig _connectionConfig;
String _privateKey;
ConfigRepo _config;
_ConfigState(this._connectionConfig, this._privateKey) {
if (_connectionConfig == null) {
_connectionConfig = ConnectionConfig();
}
_hostController = TextEditingController(text: _connectionConfig.host);
_certController =
TextEditingController(text: _connectionConfig.certificate);
_caCertController =
TextEditingController(text: _connectionConfig.caCertificate);
_privateCertController =
TextEditingController(text: _connectionConfig.privateCertificate);
_privateKeyController = TextEditingController(text: _privateKey);
}
@override
didChangeDependencies() async {
super.didChangeDependencies();
_config = Provider.of<ConfigRepo>(context);
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: _connectionConfig.host == null
? null
: CupertinoNavigationBar(
trailing: GestureDetector(
onTap: _buildResetAllHandler(context),
child: Text('Reset App',
style: TextStyle(color: CupertinoColors.destructiveRed)),
),
),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 30),
child: ListView(children: [
Container(margin: EdgeInsets.only(top: 10), child: Text('Host:')),
TextField(maxLines: 1, controller: _hostController),
Container(
margin: EdgeInsets.only(top: 5), child: Text('Private key:')),
TextField(maxLines: 1, controller: _privateKeyController),
Container(
margin: EdgeInsets.only(top: 5), child: Text('CA certificate:')),
TextField(maxLines: 5, controller: _caCertController),
Container(
margin: EdgeInsets.only(top: 5),
child: Text('Client certificate:')),
TextField(maxLines: 5, controller: _certController),
Container(
margin: EdgeInsets.only(top: 5),
child: Text('Private certificate:')),
TextField(maxLines: 5, controller: _privateCertController),
CupertinoButton(
child: Text('Save'), onPressed: _makeSaveOnPressed(context)),
]),
),
);
}
GestureTapCallback _buildResetAllHandler(BuildContext context) {
return () {
showCupertinoDialog(
context: context,
builder: (BuildContext context) => CupertinoAlertDialog(
content: Text('Are you sure?'),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
child: Text('Cancel'),
onPressed: () => Navigator.of(context).pop(),
),
CupertinoDialogAction(
isDestructiveAction: true,
child: Text('Confirm'),
onPressed: () async {
_connectionConfig = null;
await _config.deleteAll();
Navigator.of(context)
.pushNamedAndRemoveUntil('/', ModalRoute.withName('/'));
},
),
],
),
);
};
}
VoidCallback _makeSaveOnPressed(BuildContext context) {
return () async {
final connConfig = ConnectionConfig(
host: _hostController.text,
certificate: _certController.text,
caCertificate: _caCertController.text,
privateCertificate: _privateCertController.text,
);
await _config.setConnectionConfig(connConfig);
await _config.setPrivateKey(_privateKeyController.text);
Navigator.of(context)
.pushReplacementNamed('/home', arguments: connConfig);
};
}
}

View file

@ -0,0 +1,177 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:otp/otp.dart';
import '../types/credential.dart' as types;
import '../widgets/text_field.dart';
class Credential extends StatefulWidget {
final types.Credential credential;
const Credential(this.credential, {Key key}) : super(key: key);
@override
State createState() => _CredentialState(credential);
}
class _CredentialState extends State<Credential> {
_CredentialControllers _controllers;
Map<String, _FieldBuildConfig> _fieldMap;
types.Credential _credential;
_CredentialState(this._credential) : super();
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),
child: Container(
padding: const EdgeInsets.only(top: 15, bottom: 30, left: 30),
child: ListView(
children: _buildFieldRows(context),
),
),
);
}
Map<String, _FieldBuildConfig> _buildFieldMap(
_CredentialControllers controllers,
types.Credential credential,
) {
final fieldMap = {
'Id:': _FieldBuildConfig(mutable: false, text: credential.meta.id),
'Created:': _FieldBuildConfig(
mutable: false,
copyable: false,
text: credential.meta.createdAt.toString(),
),
'Updated:': _FieldBuildConfig(
mutable: false,
copyable: false,
text: credential.meta.updatedAt.toString(),
),
'Host:': _FieldBuildConfig(controller: controllers.sourceHost),
'Primary:': _FieldBuildConfig(controller: controllers.primary),
};
if (credential.meta.tag != null && credential.meta.tag != '') {
fieldMap['Tag'] = _FieldBuildConfig(controller: controllers.tag);
}
if (credential.username != null && credential.username != '') {
fieldMap['User:'] = _FieldBuildConfig(controller: controllers.username);
}
if (credential.email != null && credential.email != '') {
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: 2.5),
child: Text(key, style: TextStyle(fontWeight: FontWeight.w600)),
));
final List<Widget> widgets = [
Expanded(
flex: 3,
child: value.mutable
? TextField(
maxLines: 1,
controller: value.controller,
obscure: value.obscured)
: Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: Text(value.text)),
),
];
if (value.copyable) {
widgets.add(Expanded(
child: CupertinoButton(
child: Text(value.otp ? 'OTP' : 'Copy'),
onPressed: () => Clipboard.setData(ClipboardData(
text: value.otp
? OTP
.generateTOTPCode(value.controller.text,
DateTime.now().millisecondsSinceEpoch)
.toString()
: value.mutable ? value.controller.text : value.text,
)),
),
));
}
rows.add(Row(children: widgets));
});
return rows;
}
}
class _FieldBuildConfig {
final TextEditingController controller;
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,
});
}
class _CredentialControllers {
final TextEditingController sourceHost;
final TextEditingController primary;
final TextEditingController tag;
final TextEditingController username;
final TextEditingController email;
final TextEditingController password;
final TextEditingController otpSecret;
const _CredentialControllers({
this.sourceHost,
this.primary,
this.tag,
this.username,
this.email,
this.password,
this.otpSecret,
});
factory _CredentialControllers.fromCredential(types.Credential credential) =>
_CredentialControllers(
sourceHost: TextEditingController(text: credential.meta.sourceHost),
primary: TextEditingController(text: credential.meta.primary),
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),
);
}

View file

@ -0,0 +1,72 @@
import 'package:flutter/cupertino.dart';
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 {
final List<Metadata> metadatas;
const Credentials(this.metadatas);
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: TappableTextList(tappableText: _buildTappableText(context)),
navigationBar: CupertinoNavigationBar(),
);
}
Map<String, GestureTapCallback> _buildTappableText(BuildContext context) {
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 = {};
metadatas.forEach((Metadata metadata) {
var primary = metadata.primary;
if (metadata.tag != null && metadata.tag != '') {
primary += "-" + metadata.tag;
}
tappableText[primary] = makeOnTapHandler(metadata.id);
});
return tappableText;
}
}

View file

@ -0,0 +1,139 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import '../types/abstracts.dart';
import '../types/credential.dart';
import '../types/screen_arguments.dart';
import '../widgets/tappable_text_list.dart';
class Home extends StatefulWidget {
const Home({Key key}) : super(key: key);
@override
State createState() => _HomeState();
}
class _HomeState extends State<Home> with WidgetsBindingObserver {
CredentialsRepo _client;
ConfigRepo _config;
Future<List<Metadata>> _metadatas;
bool _stateIsPaused = false;
Timer _pausedStateTimer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_config = Provider.of<ConfigRepo>(context);
_client = Provider.of<CredentialsRepo>(context);
_metadatas = _client.getAllMetadata('').toList();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
_stateIsPaused = state == AppLifecycleState.paused;
if (_stateIsPaused) {
_pausedStateTimer = _newPausedStateTimer();
return;
}
if (_pausedStateTimer != null) _pausedStateTimer.cancel();
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: FutureBuilder<List<Metadata>>(
future: _metadatas,
builder: (
BuildContext context,
AsyncSnapshot<List<Metadata>> snapshot,
) =>
(snapshot.connectionState == ConnectionState.done)
? TappableTextList(
tappableText: _buildTappableText(context, snapshot.data))
: Center(child: CupertinoActivityIndicator()),
),
navigationBar: CupertinoNavigationBar(
leading: GestureDetector(
child: Align(
child: Text('Lock',
style: TextStyle(color: CupertinoColors.destructiveRed)),
alignment: Alignment(-0.9, 0)),
onTap: _makeLockOnTapHandler(context),
),
trailing: GestureDetector(
child: Icon(CupertinoIcons.gear),
onTap: _makeConfigOnTapHandler(context),
),
),
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
if (_pausedStateTimer != null) _pausedStateTimer.cancel();
super.dispose();
}
Timer _newPausedStateTimer() {
const checkPeriod = 30;
return Timer(Duration(seconds: checkPeriod), () {
Navigator.of(context)
.pushNamedAndRemoveUntil('/', ModalRoute.withName('/home'));
});
}
Map<String, GestureTapCallback> _buildTappableText(
BuildContext context,
List<Metadata> metadatas,
) {
final Map<String, List<Metadata>> metaMap = {};
metadatas.sort((a, b) => a.id.compareTo(b.id));
for (var metadata in metadatas) {
final source = metadata.sourceHost;
if (metaMap[source] == null) {
metaMap[source] = [metadata];
} else {
metaMap[source].add(metadata);
}
}
final handleOnTap = (List<Metadata> metadatas) => () async =>
Navigator.of(context).pushNamed('/credentials', arguments: metadatas);
final Map<String, GestureTapCallback> tappableText = {};
metaMap.forEach((String key, List<Metadata> value) =>
tappableText[key] = handleOnTap(value));
return tappableText;
}
GestureTapCallback _makeLockOnTapHandler(BuildContext context) {
return () => Navigator.of(context).pushReplacementNamed('/');
}
GestureTapCallback _makeConfigOnTapHandler(BuildContext context) {
return () async => Navigator.of(context).pushNamed('/config',
arguments: ConfigScreenArguments(
connectionConfig: await _config.connectionConfig,
privateKey: await _config.privateKey));
}
}