account work

This commit is contained in:
Christien Rioux 2023-07-25 01:04:34 -04:00
parent b7236befd1
commit b502bc20a7
20 changed files with 168 additions and 95 deletions

View File

@ -5,15 +5,23 @@
"app_bar": {
"settings_tooltip": "Settings"
},
"account": {
"form_name": "Name",
"form_title": "Title (optional)",
"form_lock_type": "Lock Type",
"lock_type_none": "none",
"lock_type_pin": "pin",
"lock_type_password": "password"
},
"new_account_page": {
"titlebar": "Create a new account",
"header": "Account Profile",
"form_name": "Name",
"form_title": "Title (optional)",
"create": "Create",
"instructions": "This information will be shared with the people you invite to connect with you on VeilidChat."
"instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.",
"error": "Account creation error"
},
"button": {
"ok": "Ok",
"cancel": "Cancel",
"change_language": "Change Language"
},

View File

@ -15,12 +15,12 @@ class AccountRecordInfo with _$AccountRecordInfo {
required KeyPair owner,
}) = _AccountRecordInfo;
factory AccountRecordInfo.fromJson(Map<String, dynamic> json) =>
_$AccountRecordInfoFromJson(json);
factory AccountRecordInfo.fromJson(dynamic json) =>
_$AccountRecordInfoFromJson(json as Map<String, dynamic>);
}
// Identity Key points to accounts associated with this identity
// accounts field has a map of service name or uuid to account key pairs
// accounts field has a map of bundle id or uuid to account key pairs
// DHT Schema: DFLT(1)
// DHT Key (Private): identityRecordKey
// DHT Owner Key: identityPublicKey
@ -32,8 +32,8 @@ class Identity with _$Identity {
required IMap<String, ISet<AccountRecordInfo>> accountRecords,
}) = _Identity;
factory Identity.fromJson(Map<String, dynamic> json) =>
_$IdentityFromJson(json);
factory Identity.fromJson(dynamic json) =>
_$IdentityFromJson(json as Map<String, dynamic>);
}
// Identity Master key structure for created account
@ -66,8 +66,8 @@ class IdentityMaster with _$IdentityMaster {
// Signature of masterRecordKey and masterPublicKey by identityPublicKey
required Signature masterSignature}) = _IdentityMaster;
factory IdentityMaster.fromJson(Map<String, dynamic> json) =>
_$IdentityMasterFromJson(json);
factory IdentityMaster.fromJson(dynamic json) =>
_$IdentityMasterFromJson(json as Map<String, dynamic>);
}
extension IdentityMasterExtension on IdentityMaster {
@ -79,15 +79,3 @@ extension IdentityMasterExtension on IdentityMaster {
return KeyPair(key: masterPublicKey, secret: secret);
}
}
// Identity Master with secrets
// Not freezed because we never persist this class in its entirety
class IdentityMasterWithSecrets {
IdentityMaster identityMaster;
SecretKey masterSecret;
SecretKey identitySecret;
IdentityMasterWithSecrets(
{required this.identityMaster,
required this.masterSecret,
required this.identitySecret});
}

View File

@ -24,9 +24,7 @@ _$_Identity _$$_IdentityFromJson(Map<String, dynamic> json) => _$_Identity(
json['account_records'] as Map<String, dynamic>,
(value) => value as String,
(value) => ISet<AccountRecordInfo>.fromJson(
value,
(value) =>
AccountRecordInfo.fromJson(value as Map<String, dynamic>))),
value, (value) => AccountRecordInfo.fromJson(value))),
);
Map<String, dynamic> _$$_IdentityToJson(_$_Identity instance) =>

View File

@ -20,8 +20,8 @@ enum EncryptionKeyType {
password;
String toJson() => name.toPascalCase();
factory EncryptionKeyType.fromJson(String j) =>
EncryptionKeyType.values.byName(j.toCamelCase());
factory EncryptionKeyType.fromJson(dynamic j) =>
EncryptionKeyType.values.byName((j as String).toCamelCase());
}
// Local Accounts are stored in a table locally and not backed by a DHT key
@ -49,6 +49,6 @@ class LocalAccount with _$LocalAccount {
required bool hiddenAccount,
}) = _LocalAccount;
factory LocalAccount.fromJson(Map<String, dynamic> json) =>
_$LocalAccountFromJson(json);
factory LocalAccount.fromJson(dynamic json) =>
_$LocalAccountFromJson(json as Map<String, dynamic>);
}

View File

@ -8,14 +8,13 @@ part of 'local_account.dart';
_$_LocalAccount _$$_LocalAccountFromJson(Map<String, dynamic> json) =>
_$_LocalAccount(
identityMaster: IdentityMaster.fromJson(
json['identity_master'] as Map<String, dynamic>),
identityMaster: IdentityMaster.fromJson(json['identity_master']),
identitySecretKeyBytes: const Uint8ListJsonConverter()
.fromJson(json['identity_secret_key_bytes'] as String),
identitySecretSaltBytes: const Uint8ListJsonConverter()
.fromJson(json['identity_secret_salt_bytes'] as String),
encryptionKeyType:
EncryptionKeyType.fromJson(json['encryption_key_type'] as String),
EncryptionKeyType.fromJson(json['encryption_key_type']),
biometricsEnabled: json['biometrics_enabled'] as bool,
hiddenAccount: json['hidden_account'] as bool,
);

View File

@ -12,8 +12,8 @@ enum DarkModePreference {
dark;
String toJson() => name.toPascalCase();
factory DarkModePreference.fromJson(String j) =>
DarkModePreference.values.byName(j.toCamelCase());
factory DarkModePreference.fromJson(dynamic j) =>
DarkModePreference.values.byName((j as String).toCamelCase());
}
// Lock preference changes how frequently the messenger locks its
@ -26,8 +26,8 @@ class LockPreference with _$LockPreference {
required bool lockWithSystemLock,
}) = _LockPreference;
factory LockPreference.fromJson(Map<String, dynamic> json) =>
_$LockPreferenceFromJson(json);
factory LockPreference.fromJson(dynamic json) =>
_$LockPreferenceFromJson(json as Map<String, dynamic>);
}
// Theme supports multiple color variants based on 'Radix'
@ -62,8 +62,8 @@ enum ColorPreference {
yellow;
String toJson() => name.toPascalCase();
factory ColorPreference.fromJson(String j) =>
ColorPreference.values.byName(j.toCamelCase());
factory ColorPreference.fromJson(dynamic j) =>
ColorPreference.values.byName((j as String).toCamelCase());
}
// Theme supports multiple translations
@ -71,8 +71,8 @@ enum LanguagePreference {
englishUS;
String toJson() => name.toPascalCase();
factory LanguagePreference.fromJson(String j) =>
LanguagePreference.values.byName(j.toCamelCase());
factory LanguagePreference.fromJson(dynamic j) =>
LanguagePreference.values.byName((j as String).toCamelCase());
}
// Preferences are stored in a table locally and globally affect all
@ -87,6 +87,6 @@ class Preferences with _$Preferences {
required LockPreference locking,
}) = _Preferences;
factory Preferences.fromJson(Map<String, dynamic> json) =>
_$PreferencesFromJson(json);
factory Preferences.fromJson(dynamic json) =>
_$PreferencesFromJson(json as Map<String, dynamic>);
}

View File

@ -22,11 +22,11 @@ Map<String, dynamic> _$$_LockPreferenceToJson(_$_LockPreference instance) =>
_$_Preferences _$$_PreferencesFromJson(Map<String, dynamic> json) =>
_$_Preferences(
darkMode: DarkModePreference.fromJson(json['dark_mode'] as String),
themeColor: ColorPreference.fromJson(json['theme_color'] as String),
language: LanguagePreference.fromJson(json['language'] as String),
darkMode: DarkModePreference.fromJson(json['dark_mode']),
themeColor: ColorPreference.fromJson(json['theme_color']),
language: LanguagePreference.fromJson(json['language']),
displayScale: json['display_scale'] as int,
locking: LockPreference.fromJson(json['locking'] as Map<String, dynamic>),
locking: LockPreference.fromJson(json['locking']),
);
Map<String, dynamic> _$$_PreferencesToJson(_$_Preferences instance) =>

View File

@ -1038,7 +1038,7 @@ class Profile extends $pb.GeneratedMessage {
..aOS(2, _omitFieldNames ? '' : 'title')
..aOS(3, _omitFieldNames ? '' : 'status')
..e<Availability>(4, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values)
..aOM<TypedKey>(5, _omitFieldNames ? '' : 'icon', subBuilder: TypedKey.create)
..aOM<TypedKey>(5, _omitFieldNames ? '' : 'avatar', subBuilder: TypedKey.create)
..hasRequiredFields = false
;
@ -1100,15 +1100,15 @@ class Profile extends $pb.GeneratedMessage {
void clearAvailability() => clearField(4);
@$pb.TagNumber(5)
TypedKey get icon => $_getN(4);
TypedKey get avatar => $_getN(4);
@$pb.TagNumber(5)
set icon(TypedKey v) { setField(5, v); }
set avatar(TypedKey v) { setField(5, v); }
@$pb.TagNumber(5)
$core.bool hasIcon() => $_has(4);
$core.bool hasAvatar() => $_has(4);
@$pb.TagNumber(5)
void clearIcon() => clearField(5);
void clearAvatar() => clearField(5);
@$pb.TagNumber(5)
TypedKey ensureIcon() => $_ensure(4);
TypedKey ensureAvatar() => $_ensure(4);
}
class Account extends $pb.GeneratedMessage {

View File

@ -279,7 +279,10 @@ const Profile$json = {
{'1': 'title', '3': 2, '4': 1, '5': 9, '10': 'title'},
{'1': 'status', '3': 3, '4': 1, '5': 9, '10': 'status'},
{'1': 'availability', '3': 4, '4': 1, '5': 14, '6': '.Availability', '10': 'availability'},
{'1': 'icon', '3': 5, '4': 1, '5': 11, '6': '.TypedKey', '10': 'icon'},
{'1': 'avatar', '3': 5, '4': 1, '5': 11, '6': '.TypedKey', '9': 0, '10': 'avatar', '17': true},
],
'8': [
{'1': '_avatar'},
],
};
@ -287,7 +290,8 @@ const Profile$json = {
final $typed_data.Uint8List profileDescriptor = $convert.base64Decode(
'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSFAoFdGl0bGUYAiABKAlSBXRpdGxlEhYKBn'
'N0YXR1cxgDIAEoCVIGc3RhdHVzEjEKDGF2YWlsYWJpbGl0eRgEIAEoDjINLkF2YWlsYWJpbGl0'
'eVIMYXZhaWxhYmlsaXR5Eh0KBGljb24YBSABKAsyCS5UeXBlZEtleVIEaWNvbg==');
'eVIMYXZhaWxhYmlsaXR5EiYKBmF2YXRhchgFIAEoCzIJLlR5cGVkS2V5SABSBmF2YXRhcogBAU'
'IJCgdfYXZhdGFy');
@$core.Deprecated('Use accountDescriptor instead')
const Account$json = {

View File

@ -19,8 +19,8 @@ class UserLogin with _$UserLogin {
required Timestamp lastActive,
}) = _UserLogin;
factory UserLogin.fromJson(Map<String, dynamic> json) =>
_$UserLoginFromJson(json);
factory UserLogin.fromJson(dynamic json) =>
_$UserLoginFromJson(json as Map<String, dynamic>);
}
// Represents a set of user logins
@ -37,6 +37,6 @@ class ActiveLogins with _$ActiveLogins {
factory ActiveLogins.empty() =>
const ActiveLogins(userLogins: IListConst([]));
factory ActiveLogins.fromJson(Map<String, dynamic> json) =>
_$ActiveLoginsFromJson(json);
factory ActiveLogins.fromJson(dynamic json) =>
_$ActiveLoginsFromJson(json as Map<String, dynamic>);
}

View File

@ -23,8 +23,8 @@ Map<String, dynamic> _$$_UserLoginToJson(_$_UserLogin instance) =>
_$_ActiveLogins _$$_ActiveLoginsFromJson(Map<String, dynamic> json) =>
_$_ActiveLogins(
userLogins: IList<UserLogin>.fromJson(json['user_logins'],
(value) => UserLogin.fromJson(value as Map<String, dynamic>)),
userLogins: IList<UserLogin>.fromJson(
json['user_logins'], (value) => UserLogin.fromJson(value)),
activeUserLogin: json['active_user_login'] == null
? null
: Typed<FixedEncodedString43>.fromJson(json['active_user_login']),

View File

@ -196,7 +196,7 @@ message Contact {
bool show_availability = 6;
}
// Contact availability as specified by the
// Contact availability
enum Availability {
AVAILABILITY_UNSPECIFIED = 0;
AVAILABILITY_OFFLINE = 1;
@ -222,8 +222,8 @@ message Profile {
string status = 3;
// Availability
Availability availability = 4;
// Icon DHTData
TypedKey icon = 5;
// Avatar DHTData
optional TypedKey avatar = 5;
}
// A record of an individual account

View File

@ -1,15 +1,17 @@
import 'dart:io';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:quickalert/quickalert.dart';
import '../components/default_app_bar.dart';
import '../entities/proto.dart' as proto;
import '../providers/local_accounts.dart';
import '../providers/logins.dart';
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
class NewAccountPage extends ConsumerStatefulWidget {
const NewAccountPage({super.key});
@ -24,6 +26,34 @@ class NewAccountPage extends ConsumerStatefulWidget {
class NewAccountPageState extends ConsumerState<NewAccountPage> {
final _formKey = GlobalKey<FormBuilderState>();
late bool isInAsyncCall = false;
static const String formFieldName = "name";
static const String formFieldTitle = "title";
Future<void> createAccount() async {
final imws = await newIdentityMaster();
try {
final localAccounts = ref.read(localAccountsProvider.notifier);
final logins = ref.read(loginsProvider.notifier);
final profile = proto.Profile();
profile.name = _formKey.currentState!.fields[formFieldName]!.value;
profile.title = _formKey.currentState!.fields[formFieldTitle]!.value;
final account = proto.Account();
account.profile = profile;
final localAccount = await localAccounts.newAccount(
identityMaster: imws.identityMaster,
identitySecret: imws.identitySecret,
account: account);
// Log in the new account by default with no pin
final ok = await logins
.loginWithNone(localAccount.identityMaster.masterRecordKey);
assert(ok == true);
} catch (e) {
await imws.delete();
rethrow;
}
}
Widget _newAccountForm(BuildContext context,
{required Future<void> Function(GlobalKey<FormBuilderState>) onSubmit}) {
@ -36,9 +66,9 @@ class NewAccountPageState extends ConsumerState<NewAccountPage> {
.paddingSymmetric(vertical: 16),
FormBuilderTextField(
autofocus: true,
name: 'name',
decoration: InputDecoration(
hintText: translate("new_account_page.form_name")),
name: formFieldName,
decoration:
InputDecoration(hintText: translate("account.form_name")),
maxLength: 64,
// The validator receives the text that the user has entered.
validator: FormBuilderValidators.compose([
@ -46,10 +76,10 @@ class NewAccountPageState extends ConsumerState<NewAccountPage> {
]),
),
FormBuilderTextField(
name: 'title',
name: formFieldTitle,
maxLength: 64,
decoration: InputDecoration(
hintText: translate("new_account_page.form_title")),
decoration:
InputDecoration(hintText: translate("account.form_title")),
),
Row(children: [
const Spacer(),
@ -85,7 +115,8 @@ class NewAccountPageState extends ConsumerState<NewAccountPage> {
enableTitleBar(true);
portraitOnly();
final localAccounts = ref.watch(localAccountsProvider);
// final localAccountsData = ref.watch(localAccountsProvider);
final displayModalHUD = isInAsyncCall; // || !localAccountsData.hasValue;
return Scaffold(
// resizeToAvoidBottomInset: false,
@ -96,9 +127,21 @@ class NewAccountPageState extends ConsumerState<NewAccountPage> {
onSubmit: (formKey) async {
debugPrint(_formKey.currentState?.value.toString());
FocusScope.of(context).unfocus();
await Future.delayed(Duration(seconds: 5));
try {
await createAccount();
} catch (e) {
QuickAlert.show(
context: context,
type: QuickAlertType.error,
title: translate("new_account_page.error"),
text: 'Exception: ${e.toString()}',
//backgroundColor: Colors.black,
//titleColor: Colors.white,
//textColor: Colors.white,
);
}
},
).paddingSymmetric(horizontal: 24, vertical: 8),
).withModalHUD(context, isInAsyncCall);
).withModalHUD(context, displayModalHUD);
}
}

View File

@ -52,11 +52,11 @@ class LocalAccounts extends _$LocalAccounts
/// Creates a new account associated with master identity
Future<LocalAccount> newAccount(
IdentityMaster identityMaster,
SecretKey identitySecret,
EncryptionKeyType encryptionKeyType,
String encryptionKey,
proto.Account account) async {
{required IdentityMaster identityMaster,
required SecretKey identitySecret,
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
String encryptionKey = "",
required proto.Account account}) async {
final veilid = await eventualVeilid.future;
final localAccounts = state.requireValue;
@ -114,7 +114,7 @@ class LocalAccounts extends _$LocalAccounts
await identityRec.eventualUpdateJson(Identity.fromJson,
(oldIdentity) async {
final accountRecords = IMapOfSets.from(oldIdentity.accountRecords)
.add("VeilidChat", newAccountRecordInfo)
.add("com.veilid.veilidchat", newAccountRecordInfo)
.asIMap();
return oldIdentity.copyWith(accountRecords: accountRecords);
});

View File

@ -6,7 +6,7 @@ part of 'local_accounts.dart';
// RiverpodGenerator
// **************************************************************************
String _$localAccountsHash() => r'1faa6b22284a402e4f47b2629e54a39ffda9a4ad';
String _$localAccountsHash() => r'37ed2ab40b6ed9063c7d1d00f067b7006c9a7670';
/// See also [LocalAccounts].
@ProviderFor(LocalAccounts)

View File

@ -152,11 +152,6 @@ class DHTRecord {
// Get existing identity key
ValueData? valueData;
do {
// Ensure it exists already
if (valueData == null) {
throw const FormatException("value does not exist");
}
// Set the new data
valueData =
await _dhtctx.setDHTValue(_recordDescriptor.key, subkey, newValue);

View File

@ -41,8 +41,10 @@ class DHTRecordCryptoPrivate implements DHTRecordCrypto {
// generate nonce
final nonce = await _cryptoSystem.randomNonce();
// crypt and append nonce
return (await _cryptoSystem.cryptNoAuth(data, nonce, _secretKey))
..addAll(nonce.decode());
var b = BytesBuilder();
b.add(await _cryptoSystem.cryptNoAuth(data, nonce, _secretKey));
b.add(nonce.decode());
return b.toBytes();
}
@override

View File

@ -1,13 +1,35 @@
import 'dart:typed_data';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:veilid/veilid.dart';
import '../entities/identity.dart';
import 'veilid_support.dart';
// Identity Master with secrets
// Not freezed because we never persist this class in its entirety
class IdentityMasterWithSecrets {
IdentityMaster identityMaster;
SecretKey masterSecret;
SecretKey identitySecret;
IdentityMasterWithSecrets(
{required this.identityMaster,
required this.masterSecret,
required this.identitySecret});
Future<void> delete() async {
final veilid = await eventualVeilid.future;
final dhtctx = (await veilid.routingContext())
.withPrivacy()
.withSequencing(Sequencing.ensureOrdered);
await dhtctx.deleteDHTRecord(identityMaster.masterRecordKey);
await dhtctx.deleteDHTRecord(identityMaster.identityRecordKey);
}
}
/// Creates a new master identity and returns it with its secrets
Future<IdentityMasterWithSecrets> newIdentityMaster() async {
final veilid = await eventualVeilid.future;
final crypto = await veilid.bestCryptoSystem();
final dhtctx = (await veilid.routingContext())
.withPrivacy()
.withSequencing(Sequencing.ensureOrdered);
@ -20,18 +42,23 @@ Future<IdentityMasterWithSecrets> newIdentityMaster() async {
// Make IdentityMaster
final masterRecordKey = masterRec.key();
final masterOwner = masterRec.ownerKeyPair()!;
final masterSigBuf = masterRecordKey.decode()
..addAll(masterOwner.key.decode());
final masterSigBuf = BytesBuilder();
masterSigBuf.add(masterRecordKey.decode());
masterSigBuf.add(masterOwner.key.decode());
final identityRecordKey = identityRec.key();
final identityOwner = identityRec.ownerKeyPair()!;
final identitySigBuf = identityRecordKey.decode()
..addAll(identityOwner.key.decode());
final identitySigBuf = BytesBuilder();
identitySigBuf.add(identityRecordKey.decode());
identitySigBuf.add(identityOwner.key.decode());
assert(masterRecordKey.kind == identityRecordKey.kind);
final crypto = await veilid.getCryptoSystem(masterRecordKey.kind);
final identitySignature =
await crypto.signWithKeyPair(masterOwner, identitySigBuf);
await crypto.signWithKeyPair(masterOwner, identitySigBuf.toBytes());
final masterSignature =
await crypto.signWithKeyPair(identityOwner, masterSigBuf);
await crypto.signWithKeyPair(identityOwner, masterSigBuf.toBytes());
final identityMaster = IdentityMaster(
identityRecordKey: identityRecordKey,

View File

@ -813,6 +813,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.3"
quickalert:
dependency: "direct main"
description:
name: quickalert
sha256: "33a52870b2a87c55d0649d0cd228efaa2368d5df39231fdecebb71f349a9b221"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
radix_colors:
dependency: "direct main"
description:

View File

@ -49,6 +49,7 @@ dependencies:
form_builder_validators: ^9.0.0
blurry_modal_progress_hud: ^1.1.0
flutter_spinkit: ^5.2.0
quickalert: ^1.0.1
dev_dependencies:
flutter_test: