diff --git a/lib/kdbx.dart b/lib/kdbx.dart index abddd6d..45ef7fe 100644 --- a/lib/kdbx.dart +++ b/lib/kdbx.dart @@ -1,6 +1,9 @@ /// dart library for reading keepass file format (kdbx). library kdbx; +import 'package:kdbx/src/kee_vault_model/enums.dart'; +import 'package:kdbx/src/kee_vault_model/form_field_type.dart'; + export 'src/credentials/credentials.dart' show Credentials, CredentialsPart, HashCredentials, PasswordCredentials; export 'src/credentials/keyfile.dart' show KeyFileComposite, KeyFileCredentials; @@ -8,7 +11,8 @@ export 'src/crypto/key_encrypter_kdf.dart' show KeyEncrypterKdf, KdfType, KdfField; export 'src/crypto/protected_value.dart' show ProtectedValue, StringValue, PlainValue; -export 'src/field.dart' show BrowserFieldModel, FormFieldType, FieldStorage; +export 'src/kee_vault_model/kee_vault_model.dart' + show BrowserFieldModel, FormFieldType, FieldStorage; export 'src/internal/kdf_cache.dart' show KdfCache; export 'src/kdbx_binary.dart' show KdbxBinary; export 'src/kdbx_consts.dart'; diff --git a/lib/src/kdbx_entry.dart b/lib/src/kdbx_entry.dart index 429c0c9..66f30e5 100644 --- a/lib/src/kdbx_entry.dart +++ b/lib/src/kdbx_entry.dart @@ -2,6 +2,8 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:typed_data'; import 'package:collection/collection.dart'; +import 'package:kdbx/src/kee_vault_model/browser_entry_settings_v1.dart'; +import 'package:kdbx/src/kee_vault_model/kee_vault_model.dart'; import 'package:kdbx/src/internal/extension_utils.dart'; import 'package:kdbx/src/kdbx_format.dart'; import 'package:kdbx/src/kdbx_object.dart'; @@ -72,302 +74,6 @@ class KdbxKey { } } -class BrowserEntrySettings { - BrowserEntrySettings({ - this.version = 1, - this.behaviour = BrowserAutoFillBehaviour.Default, - required this.minimumMatchAccuracy, - this.priority = 0, - this.hide = false, - this.realm = '', - List? includeUrls, - List? excludeUrls, - List? fields, - }) : includeUrls = includeUrls ?? [], - excludeUrls = excludeUrls ?? [], - fields = fields ?? []; - - factory BrowserEntrySettings.fromMap(Map? map, - {required MatchAccuracy minimumMatchAccuracy}) { - if (map == null) { - return BrowserEntrySettings(minimumMatchAccuracy: minimumMatchAccuracy); - } - - return BrowserEntrySettings( - version: map['version'] as int? ?? 1, - behaviour: getBehaviour(map), - minimumMatchAccuracy: getMam(map), - priority: map['priority'] as int? ?? 0, - hide: map['hide'] as bool? ?? false, - realm: map['hTTPRealm'] as String?, - includeUrls: getIncludeUrls(map), - excludeUrls: getExcludeUrls(map), - fields: List.from((map['formFieldList'] - as List?) - ?.cast>() - .map((x) => BrowserFieldModel.fromMap(x)) ?? - []), - ); - } - - factory BrowserEntrySettings.fromJson(String source, - {required MatchAccuracy minimumMatchAccuracy}) => - BrowserEntrySettings.fromMap(json.decode(source) as Map?, - minimumMatchAccuracy: minimumMatchAccuracy); - - int version; - // enum - BrowserAutoFillBehaviour behaviour; - // enum - MatchAccuracy minimumMatchAccuracy; - int priority; // always 0 - bool hide; - String? realm; - List includeUrls; - List excludeUrls; - List fields; - - BrowserEntrySettings copyWith({ - int? version, - BrowserAutoFillBehaviour? behaviour, - MatchAccuracy? minimumMatchAccuracy, - int? priority, - bool? hide, - String? realm, - List? includeUrls, - List? excludeUrls, - List? fields, - }) { - return BrowserEntrySettings( - version: version ?? this.version, - behaviour: behaviour ?? this.behaviour, - minimumMatchAccuracy: minimumMatchAccuracy ?? this.minimumMatchAccuracy, - priority: priority ?? this.priority, - hide: hide ?? this.hide, - realm: realm ?? this.realm, - includeUrls: includeUrls ?? this.includeUrls, - excludeUrls: excludeUrls ?? this.excludeUrls, - fields: fields ?? this.fields, - ); - } - - static BrowserAutoFillBehaviour getBehaviour(Map map) { - if (map['neverAutoFill'] as bool? ?? false) { - return BrowserAutoFillBehaviour.NeverAutoFillNeverAutoSubmit; - } else if (map['alwaysAutoSubmit'] as bool? ?? false) { - return BrowserAutoFillBehaviour.AlwaysAutoFillAlwaysAutoSubmit; - } else if ((map['alwaysAutoFill'] as bool? ?? false) && - (map['neverAutoSubmit'] as bool? ?? false)) { - return BrowserAutoFillBehaviour.AlwaysAutoFillNeverAutoSubmit; - } else if (map['neverAutoSubmit'] as bool? ?? false) { - return BrowserAutoFillBehaviour.NeverAutoSubmit; - } else if (map['alwaysAutoFill'] as bool? ?? false) { - return BrowserAutoFillBehaviour.AlwaysAutoFill; - } else { - return BrowserAutoFillBehaviour.Default; - } - } - - static MatchAccuracy getMam(Map map) { - if (map['blockHostnameOnlyMatch'] as bool? ?? false) { - return MatchAccuracy.Exact; - } else if (map['blockDomainOnlyMatch'] as bool? ?? false) { - return MatchAccuracy.Hostname; - } else { - return MatchAccuracy.Domain; - } - } - - static Map parseBehaviour(BrowserAutoFillBehaviour behaviour) { - switch (behaviour) { - case BrowserAutoFillBehaviour.AlwaysAutoFill: - return { - 'alwaysAutoFill': true, - 'alwaysAutoSubmit': false, - 'neverAutoFill': false, - 'neverAutoSubmit': false, - }; - case BrowserAutoFillBehaviour.NeverAutoSubmit: - return { - 'alwaysAutoFill': false, - 'alwaysAutoSubmit': false, - 'neverAutoFill': false, - 'neverAutoSubmit': true, - }; - case BrowserAutoFillBehaviour.AlwaysAutoFillAlwaysAutoSubmit: - return { - 'alwaysAutoFill': true, - 'alwaysAutoSubmit': true, - 'neverAutoFill': false, - 'neverAutoSubmit': false, - }; - case BrowserAutoFillBehaviour.NeverAutoFillNeverAutoSubmit: - return { - 'alwaysAutoFill': false, - 'alwaysAutoSubmit': false, - 'neverAutoFill': true, - 'neverAutoSubmit': true, - }; - case BrowserAutoFillBehaviour.AlwaysAutoFillNeverAutoSubmit: - return { - 'alwaysAutoFill': true, - 'alwaysAutoSubmit': false, - 'neverAutoFill': false, - 'neverAutoSubmit': true, - }; - case BrowserAutoFillBehaviour.Default: - return { - 'alwaysAutoFill': false, - 'alwaysAutoSubmit': false, - 'neverAutoFill': false, - 'neverAutoSubmit': false, - }; - } - } - - static Map parseMam(MatchAccuracy mam) { - switch (mam) { - case MatchAccuracy.Domain: - return { - 'blockDomainOnlyMatch': false, - 'blockHostnameOnlyMatch': false, - }; - case MatchAccuracy.Hostname: - return { - 'blockDomainOnlyMatch': true, - 'blockHostnameOnlyMatch': false, - }; - default: - return { - 'blockDomainOnlyMatch': false, - 'blockHostnameOnlyMatch': true, - }; - } - } - - static Map> parseUrls( - List includeUrls, List excludeUrls) { - final altURLs = []; - final regExURLs = []; - final blockedURLs = []; - final regExBlockedURLs = []; - for (final p in includeUrls) { - if (p is RegExp) { - regExURLs.add(p.pattern); - } else if (p is String) { - altURLs.add(p); - } - } - for (final p in excludeUrls) { - if (p is RegExp) { - regExBlockedURLs.add(p.pattern); - } else if (p is String) { - blockedURLs.add(p); - } - } - return >{ - 'altURLs': altURLs, - 'regExURLs': regExURLs, - 'blockedURLs': blockedURLs, - 'regExBlockedURLs': regExBlockedURLs, - }; - } - - static List getIncludeUrls(Map map) { - final includeUrls = []; - final altUrls = (map['altURLs'] as List?)?.cast(); - final regExURLs = (map['regExURLs'] as List?)?.cast(); - if (altUrls != null) { - altUrls.forEach(includeUrls.add); - } - if (regExURLs != null) { - for (final url in regExURLs) { - includeUrls.add(RegExp(url)); - } - } - return includeUrls; - } - - static List getExcludeUrls(Map map) { - final excludeUrls = []; - final blockedURLs = (map['blockedURLs'] as List?)?.cast(); - final regExBlockedURLs = - (map['regExBlockedURLs'] as List?)?.cast(); - if (blockedURLs != null) { - blockedURLs.forEach(excludeUrls.add); - } - if (regExBlockedURLs != null) { - for (final url in regExBlockedURLs) { - excludeUrls.add(RegExp(url)); - } - } - return excludeUrls; - } - - Map toMap() { - return { - 'version': version, - 'priority': priority, - 'hide': hide, - 'hTTPRealm': realm, - 'formFieldList': fields.map((x) => x.toMap()).toList(), - ...parseBehaviour(behaviour), - ...parseMam(minimumMatchAccuracy), - ...parseUrls(includeUrls, excludeUrls), - }; - } - - String toJson() => json.encode(toMap()); - - @override - String toString() { - return 'BrowserSettingsModel(version: $version, behaviour: $behaviour, minimumMatchAccuracy: $minimumMatchAccuracy, priority: $priority, hide: $hide, realm: $realm, includeUrls: $includeUrls, excludeUrls: $excludeUrls, fields: $fields)'; - } - - @override - // ignore: avoid_renaming_method_parameters - bool operator ==(Object o) { - if (identical(this, o)) { - return true; - } - final unOrdDeepEq = const DeepCollectionEquality.unordered().equals; - return o is BrowserEntrySettings && - o.version == version && - o.behaviour == behaviour && - o.minimumMatchAccuracy == minimumMatchAccuracy && - o.priority == priority && - o.hide == hide && - o.realm == realm && - unOrdDeepEq(o.includeUrls, includeUrls) && - unOrdDeepEq(o.excludeUrls, excludeUrls) && - unOrdDeepEq(o.fields, fields); - } - - @override - int get hashCode { - return version.hashCode ^ - behaviour.hashCode ^ - minimumMatchAccuracy.hashCode ^ - priority.hashCode ^ - hide.hashCode ^ - realm.hashCode ^ - includeUrls.hashCode ^ - excludeUrls.hashCode ^ - fields.hashCode; - } -} - -enum BrowserAutoFillBehaviour { - Default, - AlwaysAutoFill, - NeverAutoSubmit, - AlwaysAutoFillNeverAutoSubmit, - AlwaysAutoFillAlwaysAutoSubmit, - NeverAutoFillNeverAutoSubmit -} - -enum MatchAccuracy { Exact, Hostname, Domain } - extension KdbxEntryInternal on KdbxEntry { KdbxEntry cloneInto(KdbxGroup otherGroup, {bool toHistoryEntry = false, bool withNewUuid = false}) => @@ -461,7 +167,7 @@ class KdbxEntry extends KdbxObject { }) : history = [], super.create(file.ctx, file, 'Entry', parent) { icon.set(KdbxIcon.Key); - _browserSettings = BrowserEntrySettings( + _browserSettings = BrowserEntrySettingsV1( minimumMatchAccuracy: file.body.meta.browserSettings.defaultMatchAccuracy); } @@ -523,19 +229,19 @@ class KdbxEntry extends KdbxObject { customData['KeeVault.AndroidPackageNames'] = json.encode(names); } - BrowserEntrySettings? _browserSettings; - BrowserEntrySettings get browserSettings { + BrowserEntrySettingsV1? _browserSettings; + BrowserEntrySettingsV1 get browserSettings { if (_browserSettings == null) { final tempJson = stringEntries .firstWhereOrNull((s) => s.key.key == 'KPRPC JSON') ?.value; if (tempJson != null) { - _browserSettings = BrowserEntrySettings.fromJson(tempJson.getText(), + _browserSettings = BrowserEntrySettingsV1.fromJson(tempJson.getText(), minimumMatchAccuracy: file!.body.meta.browserSettings.defaultMatchAccuracy); } else { - _browserSettings = BrowserEntrySettings( + _browserSettings = BrowserEntrySettingsV1( minimumMatchAccuracy: file!.body.meta.browserSettings.defaultMatchAccuracy); } @@ -543,7 +249,7 @@ class KdbxEntry extends KdbxObject { return _browserSettings!; } - set browserSettings(BrowserEntrySettings settings) { + set browserSettings(BrowserEntrySettingsV1 settings) { setString( KdbxKey('KPRPC JSON'), ProtectedValue.fromString(settings.toJson())); _browserSettings = null; diff --git a/lib/src/kdbx_meta.dart b/lib/src/kdbx_meta.dart index a7ee9f4..0ab59ef 100644 --- a/lib/src/kdbx_meta.dart +++ b/lib/src/kdbx_meta.dart @@ -12,12 +12,15 @@ import 'package:kdbx/src/kdbx_format.dart'; import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_xml.dart'; +import 'package:kdbx/src/kee_vault_model/enums.dart'; import 'package:logging/logging.dart'; import 'package:quiver/iterables.dart'; import 'package:uuid/uuid.dart'; import 'package:xml/xml.dart' as xml; import 'package:xml/xml.dart'; +import 'kee_vault_model/kee_vault_model.dart'; + final _logger = Logger('kdbx_meta'); class KdbxMeta extends KdbxNode implements KdbxNodeContext { diff --git a/lib/src/kee_vault_model/browser_entry_settings_v1.dart b/lib/src/kee_vault_model/browser_entry_settings_v1.dart new file mode 100644 index 0000000..64b1dae --- /dev/null +++ b/lib/src/kee_vault_model/browser_entry_settings_v1.dart @@ -0,0 +1,293 @@ +import 'dart:convert'; +import 'package:collection/collection.dart'; +import 'package:kdbx/src/kee_vault_model/enums.dart'; +import 'package:kdbx/src/kee_vault_model/kee_vault_model.dart'; + +//TODO: EntryConfigV2 + +class BrowserEntrySettingsV1 { + BrowserEntrySettingsV1({ + this.version = 1, + this.behaviour = BrowserAutoFillBehaviour.Default, + required this.minimumMatchAccuracy, + this.priority = 0, + this.hide = false, + this.realm = '', + List? includeUrls, + List? excludeUrls, + List? fields, + }) : includeUrls = includeUrls ?? [], + excludeUrls = excludeUrls ?? [], + fields = fields ?? []; + + factory BrowserEntrySettingsV1.fromMap(Map? map, + {required MatchAccuracy minimumMatchAccuracy}) { + if (map == null) { + return BrowserEntrySettingsV1(minimumMatchAccuracy: minimumMatchAccuracy); + } + + return BrowserEntrySettingsV1( + version: map['version'] as int? ?? 1, + behaviour: getBehaviour(map), + minimumMatchAccuracy: getMam(map), + priority: map['priority'] as int? ?? 0, + hide: map['hide'] as bool? ?? false, + realm: map['hTTPRealm'] as String?, + includeUrls: getIncludeUrls(map), + excludeUrls: getExcludeUrls(map), + fields: List.from( + (map['formFieldList'] as List?) + ?.cast>() + .map( + (x) => BrowserFieldModelV1.fromMap(x)) ?? + []), + ); + } + + factory BrowserEntrySettingsV1.fromJson(String source, + {required MatchAccuracy minimumMatchAccuracy}) => + BrowserEntrySettingsV1.fromMap( + json.decode(source) as Map?, + minimumMatchAccuracy: minimumMatchAccuracy); + + int version; + // enum + BrowserAutoFillBehaviour behaviour; + // enum + MatchAccuracy minimumMatchAccuracy; + int priority; // always 0 + bool hide; + String? realm; + List includeUrls; + List excludeUrls; + List fields; + + BrowserEntrySettingsV1 copyWith({ + int? version, + BrowserAutoFillBehaviour? behaviour, + MatchAccuracy? minimumMatchAccuracy, + int? priority, + bool? hide, + String? realm, + List? includeUrls, + List? excludeUrls, + List? fields, + }) { + return BrowserEntrySettingsV1( + version: version ?? this.version, + behaviour: behaviour ?? this.behaviour, + minimumMatchAccuracy: minimumMatchAccuracy ?? this.minimumMatchAccuracy, + priority: priority ?? this.priority, + hide: hide ?? this.hide, + realm: realm ?? this.realm, + includeUrls: includeUrls ?? this.includeUrls, + excludeUrls: excludeUrls ?? this.excludeUrls, + fields: fields ?? this.fields, + ); + } + + static BrowserAutoFillBehaviour getBehaviour(Map map) { + if (map['neverAutoFill'] as bool? ?? false) { + return BrowserAutoFillBehaviour.NeverAutoFillNeverAutoSubmit; + } else if (map['alwaysAutoSubmit'] as bool? ?? false) { + return BrowserAutoFillBehaviour.AlwaysAutoFillAlwaysAutoSubmit; + } else if ((map['alwaysAutoFill'] as bool? ?? false) && + (map['neverAutoSubmit'] as bool? ?? false)) { + return BrowserAutoFillBehaviour.AlwaysAutoFillNeverAutoSubmit; + } else if (map['neverAutoSubmit'] as bool? ?? false) { + return BrowserAutoFillBehaviour.NeverAutoSubmit; + } else if (map['alwaysAutoFill'] as bool? ?? false) { + return BrowserAutoFillBehaviour.AlwaysAutoFill; + } else { + return BrowserAutoFillBehaviour.Default; + } + } + + static MatchAccuracy getMam(Map map) { + if (map['blockHostnameOnlyMatch'] as bool? ?? false) { + return MatchAccuracy.Exact; + } else if (map['blockDomainOnlyMatch'] as bool? ?? false) { + return MatchAccuracy.Hostname; + } else { + return MatchAccuracy.Domain; + } + } + + static Map parseBehaviour(BrowserAutoFillBehaviour behaviour) { + switch (behaviour) { + case BrowserAutoFillBehaviour.AlwaysAutoFill: + return { + 'alwaysAutoFill': true, + 'alwaysAutoSubmit': false, + 'neverAutoFill': false, + 'neverAutoSubmit': false, + }; + case BrowserAutoFillBehaviour.NeverAutoSubmit: + return { + 'alwaysAutoFill': false, + 'alwaysAutoSubmit': false, + 'neverAutoFill': false, + 'neverAutoSubmit': true, + }; + case BrowserAutoFillBehaviour.AlwaysAutoFillAlwaysAutoSubmit: + return { + 'alwaysAutoFill': true, + 'alwaysAutoSubmit': true, + 'neverAutoFill': false, + 'neverAutoSubmit': false, + }; + case BrowserAutoFillBehaviour.NeverAutoFillNeverAutoSubmit: + return { + 'alwaysAutoFill': false, + 'alwaysAutoSubmit': false, + 'neverAutoFill': true, + 'neverAutoSubmit': true, + }; + case BrowserAutoFillBehaviour.AlwaysAutoFillNeverAutoSubmit: + return { + 'alwaysAutoFill': true, + 'alwaysAutoSubmit': false, + 'neverAutoFill': false, + 'neverAutoSubmit': true, + }; + case BrowserAutoFillBehaviour.Default: + return { + 'alwaysAutoFill': false, + 'alwaysAutoSubmit': false, + 'neverAutoFill': false, + 'neverAutoSubmit': false, + }; + } + } + + static Map parseMam(MatchAccuracy mam) { + switch (mam) { + case MatchAccuracy.Domain: + return { + 'blockDomainOnlyMatch': false, + 'blockHostnameOnlyMatch': false, + }; + case MatchAccuracy.Hostname: + return { + 'blockDomainOnlyMatch': true, + 'blockHostnameOnlyMatch': false, + }; + default: + return { + 'blockDomainOnlyMatch': false, + 'blockHostnameOnlyMatch': true, + }; + } + } + + static Map> parseUrls( + List includeUrls, List excludeUrls) { + final altURLs = []; + final regExURLs = []; + final blockedURLs = []; + final regExBlockedURLs = []; + for (final p in includeUrls) { + if (p is RegExp) { + regExURLs.add(p.pattern); + } else if (p is String) { + altURLs.add(p); + } + } + for (final p in excludeUrls) { + if (p is RegExp) { + regExBlockedURLs.add(p.pattern); + } else if (p is String) { + blockedURLs.add(p); + } + } + return >{ + 'altURLs': altURLs, + 'regExURLs': regExURLs, + 'blockedURLs': blockedURLs, + 'regExBlockedURLs': regExBlockedURLs, + }; + } + + static List getIncludeUrls(Map map) { + final includeUrls = []; + final altUrls = (map['altURLs'] as List?)?.cast(); + final regExURLs = (map['regExURLs'] as List?)?.cast(); + if (altUrls != null) { + altUrls.forEach(includeUrls.add); + } + if (regExURLs != null) { + for (final url in regExURLs) { + includeUrls.add(RegExp(url)); + } + } + return includeUrls; + } + + static List getExcludeUrls(Map map) { + final excludeUrls = []; + final blockedURLs = (map['blockedURLs'] as List?)?.cast(); + final regExBlockedURLs = + (map['regExBlockedURLs'] as List?)?.cast(); + if (blockedURLs != null) { + blockedURLs.forEach(excludeUrls.add); + } + if (regExBlockedURLs != null) { + for (final url in regExBlockedURLs) { + excludeUrls.add(RegExp(url)); + } + } + return excludeUrls; + } + + Map toMap() { + return { + 'version': version, + 'priority': priority, + 'hide': hide, + 'hTTPRealm': realm, + 'formFieldList': fields.map((x) => x.toMap()).toList(), + ...parseBehaviour(behaviour), + ...parseMam(minimumMatchAccuracy), + ...parseUrls(includeUrls, excludeUrls), + }; + } + + String toJson() => json.encode(toMap()); + + @override + String toString() { + return 'BrowserSettingsModel(version: $version, behaviour: $behaviour, minimumMatchAccuracy: $minimumMatchAccuracy, priority: $priority, hide: $hide, realm: $realm, includeUrls: $includeUrls, excludeUrls: $excludeUrls, fields: $fields)'; + } + + @override + // ignore: avoid_renaming_method_parameters + bool operator ==(Object o) { + if (identical(this, o)) { + return true; + } + final unOrdDeepEq = const DeepCollectionEquality.unordered().equals; + return o is BrowserEntrySettingsV1 && + o.version == version && + o.behaviour == behaviour && + o.minimumMatchAccuracy == minimumMatchAccuracy && + o.priority == priority && + o.hide == hide && + o.realm == realm && + unOrdDeepEq(o.includeUrls, includeUrls) && + unOrdDeepEq(o.excludeUrls, excludeUrls) && + unOrdDeepEq(o.fields, fields); + } + + @override + int get hashCode { + return version.hashCode ^ + behaviour.hashCode ^ + minimumMatchAccuracy.hashCode ^ + priority.hashCode ^ + hide.hashCode ^ + realm.hashCode ^ + includeUrls.hashCode ^ + excludeUrls.hashCode ^ + fields.hashCode; + } +} diff --git a/lib/src/kee_vault_model/entry_matcher.dart b/lib/src/kee_vault_model/entry_matcher.dart new file mode 100644 index 0000000..9a73d0e --- /dev/null +++ b/lib/src/kee_vault_model/entry_matcher.dart @@ -0,0 +1,69 @@ +import 'dart:convert'; +import 'package:collection/collection.dart'; +import 'package:kdbx/src/kee_vault_model/enums.dart'; + +class EntryMatcher { + EntryMatcher({ + this.matchLogic, + this.queries = const [], + this.pageTitles = const [], + }); + + EntryMatcher copyWith({ + MatcherLogic? matchLogic, + List? queries, + List? pageTitles, + }) { + return EntryMatcher( + matchLogic: matchLogic ?? this.matchLogic, + queries: queries ?? this.queries, + pageTitles: pageTitles ?? this.pageTitles, + ); + } + + Map toMap() { + return { + 'matchLogic': matchLogic?.name, + 'queries': queries, + 'pageTitles': pageTitles, + }; + } + + factory EntryMatcher.fromMap(Map? map) { + if (map == null) { + return EntryMatcher(); + } + + return EntryMatcher( + matchLogic: + MatcherLogic.values.firstWhereOrNull((v) => v == map['matchLogic']), + queries: (map['queries'] as List?)?.cast() ?? [], + pageTitles: (map['pageTitles'] as List?)?.cast() ?? [], + ); + } + + String toJson() => json.encode(toMap()); + + factory EntryMatcher.fromJson(String source) => + EntryMatcher.fromMap(json.decode(source) as Map?); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + final listEquals = const DeepCollectionEquality().equals; + + return other is EntryMatcher && + other.matchLogic == matchLogic && + listEquals(other.queries, queries) && + listEquals(other.pageTitles, pageTitles); + } + + @override + int get hashCode { + return matchLogic.hashCode ^ queries.hashCode ^ pageTitles.hashCode; + } + + MatcherLogic? matchLogic; // default to Client initially + List queries; // HTML DOM select query + List pageTitles; // HTML Page title contains +} diff --git a/lib/src/kee_vault_model/entry_matcher_config.dart b/lib/src/kee_vault_model/entry_matcher_config.dart new file mode 100644 index 0000000..221f73a --- /dev/null +++ b/lib/src/kee_vault_model/entry_matcher_config.dart @@ -0,0 +1,21 @@ +import 'package:kdbx/src/kee_vault_model/entry_matcher.dart'; +import 'package:kdbx/src/kee_vault_model/enums.dart'; + +class EntryMatcherConfig { + EntryMatcherConfig({ + this.matcherType, + this.customMatcher, + this.urlMatchMethod, + this.weight, // 0 = client decides or ignores locator + this.actionOnMatch, + this.actionOnNoMatch, + }); + + EntryMatcherType? matcherType; + EntryMatcher? customMatcher; + MatchAccuracy? urlMatchMethod; + num? weight; // 0 = client decides or ignores locator + MatchAction? actionOnMatch; + MatchAction? + actionOnNoMatch; // critical to use TotalBlock here for Url match type +} diff --git a/lib/src/kee_vault_model/enums.dart b/lib/src/kee_vault_model/enums.dart new file mode 100644 index 0000000..0cb8d90 --- /dev/null +++ b/lib/src/kee_vault_model/enums.dart @@ -0,0 +1,32 @@ +enum BrowserAutoFillBehaviour { + Default, + AlwaysAutoFill, + NeverAutoSubmit, + AlwaysAutoFillNeverAutoSubmit, + AlwaysAutoFillAlwaysAutoSubmit, + NeverAutoFillNeverAutoSubmit +} + +enum MatchAccuracy { Exact, Hostname, Domain } + +enum FieldStorage { CUSTOM, JSON, BOTH } + +enum FieldType { Text, Password, Existing, Toggle, Otp, SomeChars } + +enum FieldMatcherType { + Custom, + UsernameDefaultHeuristic, + PasswordDefaultHeuristic, +} + +enum EntryMatcherType { + Custom, + Hide, + Url, // magic type that uses primary URL + the 4 URL data arrays and current urlmatchconfig to determine a match +} + +enum MatchAction { TotalMatch, TotalBlock, WeightedMatch, WeightedBlock } + +enum MatcherLogic { Client, All, Any } + +enum PlaceholderHandling { Default, Enabled, Disabled } diff --git a/lib/src/kee_vault_model/field.dart b/lib/src/kee_vault_model/field.dart new file mode 100644 index 0000000..eac068b --- /dev/null +++ b/lib/src/kee_vault_model/field.dart @@ -0,0 +1,114 @@ +import 'dart:convert'; +import 'package:collection/collection.dart'; +import 'package:kdbx/src/kee_vault_model/enums.dart'; +import 'package:kdbx/src/kee_vault_model/field_matcher_config.dart'; + +class Field { + Field({ + this.uuid, + this.name, + this.valuePath, + this.value, + this.page = 1, + this.type, + this.placeholderHandling, + this.matcherConfigs, + }); + + Field copyWith({ + String? uuid, + String? name, + String? valuePath, + String? value, + int? page, + PlaceholderHandling? placeholderHandling, + List? matcherConfigs, + }) { + return Field( + uuid: uuid ?? this.uuid, + name: name ?? this.name, + valuePath: valuePath ?? this.valuePath, + value: value ?? this.value, + page: page ?? this.page, + placeholderHandling: placeholderHandling ?? this.placeholderHandling, + matcherConfigs: matcherConfigs ?? this.matcherConfigs, + ); + } + + Map toMap() { + return { + 'uuid': uuid, + 'name': name, + 'valuePath': valuePath, + 'value': value, + 'page': page, + 'placeholderHandling': placeholderHandling?.name, + 'matcherConfigs': matcherConfigs?.map((x) => x?.toMap())?.toList(), + }; + } + + factory Field.fromMap(Map? map) { + if (map == null) { + return Field(); + } + return Field( + uuid: map['uuid'] as String?, + name: map['name'] as String?, + valuePath: map['valuePath'] as String?, + value: map['value'] as String?, + page: map['page'] as int? ?? 1, + placeholderHandling: PlaceholderHandling.values + .firstWhereOrNull((v) => v == map['placeholderHandling']), + matcherConfigs: List.from((map['matcherConfigs'] + as List?) + ?.cast>() + .map((x) => FieldMatcherConfig.fromMap(x)) ?? + []), + ); + } + + String toJson() => json.encode(toMap()); + + factory Field.fromJson(String source) => + Field.fromMap(json.decode(source) as Map?); + + @override + String toString() { + return 'Field(uuid: $uuid, name: $name, valuePath: $valuePath, value: $value, page: $page, placeholderHandling: $placeholderHandling, matcherConfigs: $matcherConfigs)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + final listEquals = const DeepCollectionEquality().equals; + + return other is Field && + other.uuid == uuid && + other.name == name && + other.valuePath == valuePath && + other.value == value && + other.page == page && + other.placeholderHandling == placeholderHandling && + listEquals(other.matcherConfigs, matcherConfigs); + } + + @override + int get hashCode { + return uuid.hashCode ^ + name.hashCode ^ + valuePath.hashCode ^ + value.hashCode ^ + page.hashCode ^ + placeholderHandling.hashCode ^ + matcherConfigs.hashCode; + } + + String? uuid; + String? name; + String? valuePath; + String? value; + int page = 1; + FieldType? type; + PlaceholderHandling? placeholderHandling; + List? matcherConfigs; +} diff --git a/lib/src/kee_vault_model/field_matcher.dart b/lib/src/kee_vault_model/field_matcher.dart new file mode 100644 index 0000000..8420a7c --- /dev/null +++ b/lib/src/kee_vault_model/field_matcher.dart @@ -0,0 +1,125 @@ +import 'dart:convert'; +import 'package:collection/collection.dart'; +import 'package:kdbx/src/kee_vault_model/enums.dart'; + +class FieldMatcher { + FieldMatcher({ + this.matchLogic, + this.ids = const [], + this.names = const [], + this.types = const [], + this.queries = const [], + this.labels = const [], + this.autocompleteValues = const [], + this.maxLength, + this.minLength, + }); + + FieldMatcher copyWith({ + MatcherLogic? matchLogic, + List? ids, + List? names, + List? types, + List? queries, + List? labels, + List? autocompleteValues, + int? maxLength, + int? minLength, + }) { + return FieldMatcher( + matchLogic: matchLogic ?? this.matchLogic, + ids: ids ?? this.ids, + names: names ?? this.names, + types: types ?? this.types, + queries: queries ?? this.queries, + labels: labels ?? this.labels, + autocompleteValues: autocompleteValues ?? this.autocompleteValues, + maxLength: maxLength ?? this.maxLength, + minLength: minLength ?? this.minLength, + ); + } + + Map toMap() { + return { + 'matchLogic': matchLogic?.name, + 'ids': ids, + 'names': names, + 'types': types, + 'queries': queries, + 'labels': labels, + 'autocompleteValues': autocompleteValues, + 'maxLength': maxLength, + 'minLength': minLength, + }; + } + + factory FieldMatcher.fromMap(Map? map) { + if (map == null) { + return FieldMatcher(); + } + + return FieldMatcher( + matchLogic: + MatcherLogic.values.firstWhereOrNull((v) => v == map['matchLogic']), + ids: (map['ids'] as List?)?.cast() ?? [], + names: (map['names'] as List?)?.cast() ?? [], + types: (map['types'] as List?)?.cast() ?? [], + queries: (map['queries'] as List?)?.cast() ?? [], + labels: (map['labels'] as List?)?.cast() ?? [], + autocompleteValues: + (map['autocompleteValues'] as List?)?.cast() ?? [], + maxLength: map['maxLength'] as int?, + minLength: map['minLength'] as int?, + ); + } + + String toJson() => json.encode(toMap()); + + factory FieldMatcher.fromJson(String source) => + FieldMatcher.fromMap(json.decode(source) as Map?); + + @override + String toString() { + return 'FieldMatcher(matchLogic: $matchLogic, ids: $ids, names: $names, types: $types, queries: $queries, labels: $labels, autocompleteValues: $autocompleteValues, maxLength: $maxLength, minLength: $minLength)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + final listEquals = const DeepCollectionEquality().equals; + + return other is FieldMatcher && + other.matchLogic == matchLogic && + listEquals(other.ids, ids) && + listEquals(other.names, names) && + listEquals(other.types, types) && + listEquals(other.queries, queries) && + listEquals(other.labels, labels) && + listEquals(other.autocompleteValues, autocompleteValues) && + other.maxLength == maxLength && + other.minLength == minLength; + } + + @override + int get hashCode { + return matchLogic.hashCode ^ + ids.hashCode ^ + names.hashCode ^ + types.hashCode ^ + queries.hashCode ^ + labels.hashCode ^ + autocompleteValues.hashCode ^ + maxLength.hashCode ^ + minLength.hashCode; + } + + MatcherLogic? matchLogic; // default to Client initially + List ids; // HTML id attribute + List names; // HTML name attribute + List types; // HTML input type + List queries; // HTML DOM select query + List labels; // HTML Label or otherwise visible UI label + List autocompleteValues; // HTML autocomplete attribute values + int? maxLength; // max chars allowed in a candidate field for this to match + int? minLength; // min chars allowed in a candidate field for this to match +} diff --git a/lib/src/kee_vault_model/field_matcher_config.dart b/lib/src/kee_vault_model/field_matcher_config.dart new file mode 100644 index 0000000..85d4975 --- /dev/null +++ b/lib/src/kee_vault_model/field_matcher_config.dart @@ -0,0 +1,110 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:kdbx/src/kee_vault_model/enums.dart'; +import 'package:kdbx/src/kee_vault_model/field_matcher.dart'; +import 'package:kdbx/src/utils/field_type_utils.dart'; + +class FieldMatcherConfig { + FieldMatcherConfig({ + this.matcherType, + this.customMatcher, + this.weight, // 0 = client decides or ignores locator + this.actionOnMatch, + }); + + FieldMatcherConfig.forSingleClientMatch(String? id, String? name, String fft) + : this( + customMatcher: FieldMatcher( + ids: id == null ? [] : [id], + names: name == null ? [] : [name], + types: [Utilities.formFieldTypeToHtmlType(fft)], + queries: [], + ), + ); + + FieldMatcherConfig.forSingleClientMatchHtmlType( + String? id, String? name, String? htmlType, String? domSelector) + : this( + customMatcher: FieldMatcher( + ids: id == null ? [] : [id], + names: name == null ? [] : [name], + types: htmlType == null ? [] : [htmlType], + queries: domSelector == null ? [] : [domSelector], + ), + ); + + FieldMatcherConfig copyWith({ + FieldMatcherType? matcherType, + FieldMatcher? customMatcher, + num? weight, + MatchAction? actionOnMatch, + }) { + return FieldMatcherConfig( + matcherType: matcherType ?? this.matcherType, + customMatcher: customMatcher ?? this.customMatcher, + weight: weight ?? this.weight, + actionOnMatch: actionOnMatch ?? this.actionOnMatch, + ); + } + + Map toMap() { + return { + 'matcherType': matcherType?.name, + 'customMatcher': customMatcher?.toMap(), + 'weight': weight, + 'actionOnMatch': actionOnMatch?.name, + }; + } + + factory FieldMatcherConfig.fromMap(Map? map) { + if (map == null) { + return FieldMatcherConfig(); + } + + return FieldMatcherConfig( + matcherType: FieldMatcherType.values + .firstWhereOrNull((v) => v == map['matchLogic']), + customMatcher: map['customMatcher'] != null + ? FieldMatcher.fromMap(map['customMatcher'] as Map) + : null, + weight: map['weight'] as int?, + actionOnMatch: + MatchAction.values.firstWhereOrNull((v) => v == map['actionOnMatch']), + ); + } + + String toJson() => json.encode(toMap()); + + factory FieldMatcherConfig.fromJson(String source) => + FieldMatcherConfig.fromMap(json.decode(source) as Map?); + + @override + String toString() { + return 'FieldMatcherConfig(matcherType: $matcherType, customMatcher: $customMatcher, weight: $weight, actionOnMatch: $actionOnMatch)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is FieldMatcherConfig && + other.matcherType == matcherType && + other.customMatcher == customMatcher && + other.weight == weight && + other.actionOnMatch == actionOnMatch; + } + + @override + int get hashCode { + return matcherType.hashCode ^ + customMatcher.hashCode ^ + weight.hashCode ^ + actionOnMatch.hashCode; + } + + FieldMatcherType? matcherType; + FieldMatcher? customMatcher; + num? weight; // 0 = client decides or ignores locator + MatchAction? actionOnMatch; +} diff --git a/lib/src/kee_vault_model/form_field_type.dart b/lib/src/kee_vault_model/form_field_type.dart new file mode 100644 index 0000000..b1e9bc1 --- /dev/null +++ b/lib/src/kee_vault_model/form_field_type.dart @@ -0,0 +1,8 @@ +class FormFieldType { + static const String USERNAME = 'FFTusername'; + static const String PASSWORD = 'FFTpassword'; + static const String TEXT = 'FFTtext'; + static const String RADIO = 'FFTradio'; + static const String CHECKBOX = 'FFTcheckbox'; + static const String SELECT = 'FFTselect'; +} diff --git a/lib/src/field.dart b/lib/src/kee_vault_model/kee_vault_model.dart similarity index 82% rename from lib/src/field.dart rename to lib/src/kee_vault_model/kee_vault_model.dart index 9b89737..4d2f84b 100644 --- a/lib/src/field.dart +++ b/lib/src/kee_vault_model/kee_vault_model.dart @@ -1,18 +1,15 @@ import 'dart:convert'; -enum FieldStorage { CUSTOM, JSON, BOTH } - -class FormFieldType { - static const String USERNAME = 'FFTusername'; - static const String PASSWORD = 'FFTpassword'; - static const String TEXT = 'FFTtext'; - static const String RADIO = 'FFTradio'; - static const String CHECKBOX = 'FFTcheckbox'; - static const String SELECT = 'FFTselect'; -} +import 'package:collection/collection.dart'; + +import 'package:kdbx/src/kee_vault_model/entry_matcher.dart'; +import 'package:kdbx/src/kee_vault_model/enums.dart'; +import 'package:kdbx/src/kee_vault_model/form_field_type.dart'; -class BrowserFieldModel { - BrowserFieldModel({ +import 'field_matcher_config.dart'; + +class BrowserFieldModelV1 { + BrowserFieldModelV1({ this.displayName, this.name = '', this.type = FormFieldType.TEXT, @@ -22,12 +19,12 @@ class BrowserFieldModel { this.value = '', }); - factory BrowserFieldModel.fromMap(Map? map) { + factory BrowserFieldModelV1.fromMap(Map? map) { if (map == null) { - return BrowserFieldModel(); + return BrowserFieldModelV1(); } - return BrowserFieldModel( + return BrowserFieldModelV1( displayName: map['displayName'] as String?, name: map['name'] as String?, type: map['type'] as String?, @@ -39,8 +36,8 @@ class BrowserFieldModel { value: map['value'] as String?, ); } - factory BrowserFieldModel.fromJson(String source) => - BrowserFieldModel.fromMap(json.decode(source) as Map?); + factory BrowserFieldModelV1.fromJson(String source) => + BrowserFieldModelV1.fromMap(json.decode(source) as Map?); String? displayName; String? name; @@ -57,7 +54,7 @@ class BrowserFieldModel { return true; } - return o is BrowserFieldModel && + return o is BrowserFieldModelV1 && o.displayName == displayName && o.name == name && o.type == type && @@ -78,7 +75,7 @@ class BrowserFieldModel { value.hashCode; } - BrowserFieldModel copyWith({ + BrowserFieldModelV1 copyWith({ String? displayName, String? name, String? type, @@ -87,7 +84,7 @@ class BrowserFieldModel { String? placeholderHandling, String? value, }) { - return BrowserFieldModel( + return BrowserFieldModelV1( displayName: displayName ?? this.displayName, name: name ?? this.name, type: type ?? this.type, @@ -118,6 +115,8 @@ class BrowserFieldModel { } } +//TODO: delete all below when configv2 is working + // defaults... // class BrowserFieldModel( // String displayName: this.getBrowserFieldDisplayNameDefault(), diff --git a/lib/src/utils/field_type_utils.dart b/lib/src/utils/field_type_utils.dart new file mode 100644 index 0000000..a571063 --- /dev/null +++ b/lib/src/utils/field_type_utils.dart @@ -0,0 +1,96 @@ +import 'package:kdbx/src/kee_vault_model/enums.dart'; +import 'package:kdbx/src/kee_vault_model/form_field_type.dart'; +import 'package:kdbx/src/kee_vault_model/kee_vault_model.dart'; + +class Utilities { + static String formFieldTypeToHtmlType(String fft) { + if (fft == FormFieldType.PASSWORD) { + return 'password'; + } + if (fft == FormFieldType.SELECT) { + return 'select-one'; + } + if (fft == FormFieldType.RADIO) { + return 'radio'; + } + if (fft == FormFieldType.CHECKBOX) { + return 'checkbox'; + } + return 'text'; + } + + static FieldType formFieldTypeToFieldType(String fft) { + FieldType type = FieldType.Text; + if (fft == FormFieldType.PASSWORD) { + type = FieldType.Password; + } else if (fft == FormFieldType.SELECT) { + type = FieldType.Existing; + } else if (fft == FormFieldType.RADIO) { + type = FieldType.Existing; + } else if (fft == FormFieldType.USERNAME) { + type = FieldType.Text; + } else if (fft == FormFieldType.CHECKBOX) { + type = FieldType.Toggle; + } + return type; + } + + static String fieldTypeToDisplay(FieldType type, bool titleCase) { + String typeD = 'Text'; + if (type == FieldType.Password) { + typeD = 'Password'; + } else if (type == FieldType.Existing) { + typeD = 'Existing'; + } else if (type == FieldType.Text) { + typeD = 'Text'; + } else if (type == FieldType.Toggle) { + typeD = 'Toggle'; + } + if (!titleCase) { + return typeD.toLowerCase(); + } + return typeD; + } + + static String fieldTypeToHtmlType(FieldType ft) { + switch (ft) { + case FieldType.Password: + return 'password'; + case FieldType.Existing: + return 'radio'; + case FieldType.Toggle: + return 'checkbox'; + default: + return 'text'; + } + } + + static String fieldTypeToFormFieldType(FieldType ft) { + switch (ft) { + case FieldType.Password: + return FormFieldType.PASSWORD; + case FieldType.Existing: + return FormFieldType.RADIO; + case FieldType.Toggle: + return FormFieldType.CHECKBOX; + default: + return FormFieldType.TEXT; + } + } + + // Assumes funky Username type has already been determined so all textual stuff is type text by now + static String formFieldTypeFromHtmlTypeOrFieldType(String t, FieldType ft) { + switch (t) { + case 'password': + return FormFieldType.PASSWORD; + case 'radio': + return FormFieldType.RADIO; + case 'checkbox': + return FormFieldType.CHECKBOX; + case 'select-one': + return FormFieldType.SELECT; + default: + return Utilities.fieldTypeToFormFieldType(ft); + } + } +} diff --git a/test/internal/test_utils.dart b/test/internal/test_utils.dart index c993dba..5efd6ee 100644 --- a/test/internal/test_utils.dart +++ b/test/internal/test_utils.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:argon2_ffi_base/argon2_ffi_base.dart'; import 'package:kdbx/kdbx.dart'; +import 'package:kdbx/src/kee_vault_model/kee_vault_model.dart'; import 'package:logging/logging.dart'; import 'package:logging_appenders/logging_appenders.dart'; @@ -105,7 +106,7 @@ class TestUtil { Function proceedSeconds) async { final file = TestUtil.createEmptyFile(); final entry = createEntry(file, file.body.rootGroup, 'test1', 'test1'); - entry.browserSettings.fields.add(BrowserFieldModel( + entry.browserSettings.fields.add(BrowserFieldModelV1( displayName: 'test name', fieldId: 'id', name: 'form field name',