diff --git a/client/drift_schemas/defguard/drift_schema_v5.json b/client/drift_schemas/defguard/drift_schema_v5.json new file mode 100644 index 00000000..25da0869 --- /dev/null +++ b/client/drift_schemas/defguard/drift_schema_v5.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"defguard_instances","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"uuid","getter_name":"uuid","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"url","getter_name":"url","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"device_id","getter_name":"deviceId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"proxy_url","getter_name":"proxyUrl","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"username","getter_name":"username","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"client_traffic_policy","getter_name":"clientTrafficPolicy","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const ClientTrafficPolicyConverter()","dart_type_name":"ClientTrafficPolicy"}},{"name":"enterprise_enabled","getter_name":"enterpriseEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"enterprise_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"enterprise_enabled\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"pub_key","getter_name":"pubKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"mfa_keys_stored","getter_name":"mfaKeysStored","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"mfa_keys_stored\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"mfa_keys_stored\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"openid_display_name","getter_name":"openidDisplayName","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[0],"type":"table","data":{"name":"locations","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"instance","getter_name":"instance","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES defguard_instances (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES defguard_instances (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":[{"foreign_key":{"to":{"table":"defguard_instances","column":"id"},"initially_deferred":false,"on_update":null,"on_delete":"cascade"}}]},{"name":"network_id","getter_name":"networkId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"address","getter_name":"address","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"pub_key","getter_name":"pubKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"endpoint","getter_name":"endpoint","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"allowed_ips","getter_name":"allowedIps","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"dns","getter_name":"dns","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"mfa_enabled","getter_name":"mfaEnabled","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"mfa_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"mfa_enabled\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"traffic_method","getter_name":"trafficMethod","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(RoutingMethod.values)","dart_type_name":"RoutingMethod"}},{"name":"mfa_method","getter_name":"mfaMethod","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const MfaMethodConverter()","dart_type_name":"MfaMethod"}},{"name":"keep_alive_interval","getter_name":"keepAliveInterval","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"location_mfa_mode","getter_name":"locationMfaMode","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const LocationMfaModeConverter()","dart_type_name":"LocationMfaMode"}},{"name":"posture_check_required","getter_name":"postureCheckRequired","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"posture_check_required\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"posture_check_required\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}}]} \ No newline at end of file diff --git a/client/ios/Podfile b/client/ios/Podfile index 1b6ef802..57678908 100644 --- a/client/ios/Podfile +++ b/client/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '15.6' +platform :ios, '15.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -39,5 +39,12 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + # Pods declaring their own, older minimum keep it (podhelper only clears + # targets below 13), and Xcode 27 rejects anything below 15.0. + target.build_configurations.each do |config| + if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < 15.0 + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' + end + end end end diff --git a/client/ios/Podfile.lock b/client/ios/Podfile.lock index ce14a2c7..055d86b3 100644 --- a/client/ios/Podfile.lock +++ b/client/ios/Podfile.lock @@ -124,6 +124,6 @@ SPEC CHECKSUMS: url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa wireguard_plugin: 4d2720563b180d23101f7162ecdbf57203e82e8e -PODFILE CHECKSUM: ae9e65fc23486119b8e977fd41d9213f251e537a +PODFILE CHECKSUM: 274710f88d7bc73352a0d86724aba1a04360601c COCOAPODS: 1.16.2 diff --git a/client/ios/Runner.xcodeproj/project.pbxproj b/client/ios/Runner.xcodeproj/project.pbxproj index 39b0488f..2e3c888e 100644 --- a/client/ios/Runner.xcodeproj/project.pbxproj +++ b/client/ios/Runner.xcodeproj/project.pbxproj @@ -576,7 +576,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; REGISTER_APP_GROUPS = YES; SUPPORTED_PLATFORMS = iphoneos; @@ -600,7 +600,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Defguard; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -791,7 +791,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = net.defguard.mobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -811,7 +811,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = net.defguard.mobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -829,7 +829,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = net.defguard.mobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -890,7 +890,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; REGISTER_APP_GROUPS = YES; @@ -944,7 +944,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; REGISTER_APP_GROUPS = YES; SUPPORTED_PLATFORMS = iphoneos; @@ -970,7 +970,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Defguard; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1009,7 +1009,7 @@ INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Defguard; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/client/lib/data/db/database.dart b/client/lib/data/db/database.dart index eb9c7aa2..c818aa31 100644 --- a/client/lib/data/db/database.dart +++ b/client/lib/data/db/database.dart @@ -2,7 +2,10 @@ import "package:drift/drift.dart"; import "package:drift_flutter/drift_flutter.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:mobile/data/db/database.steps.dart"; +import "package:mobile/data/db/db_file.dart"; import "package:mobile/data/db/enums.dart"; +import "package:mobile/utils/instance_secrets.dart"; +import "package:mobile/utils/keychain.dart"; import "package:path_provider/path_provider.dart"; import "package:riverpod_annotation/riverpod_annotation.dart"; @@ -27,8 +30,6 @@ class DefguardInstances extends Table with AutoIncrementingPrimaryKey { TextColumn get username => text()(); - TextColumn get poolingToken => text()(); - @JsonKey('client_traffic_policy') IntColumn get clientTrafficPolicy => integer() .withDefault(const Constant(0)) @@ -40,9 +41,6 @@ class DefguardInstances extends Table with AutoIncrementingPrimaryKey { // user public key TextColumn get pubKey => text()(); - // user private key - TextColumn get privateKey => text()(); - // tells if the secure biometric storage exists for this instance BoolColumn get mfaKeysStored => boolean()(); @@ -103,13 +101,22 @@ class AppDatabase extends _$AppDatabase { AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection()); @override - int get schemaVersion => 4; + int get schemaVersion => 5; @override MigrationStrategy get migration { return MigrationStrategy( beforeOpen: (details) async { await customStatement('PRAGMA foreign_keys = ON'); + // Overwrite deleted content instead of leaving it in freed pages. + await customStatement('PRAGMA secure_delete = ON'); + if (details.hadUpgrade && details.versionBefore! < 5) { + // Rebuild the database file so the pages that held the secrets moved + // to the keychain in the 4 -> 5 migration are gone for good. Cannot + // run inside the migration itself, VACUUM is not allowed in a + // transaction. + await customStatement('VACUUM'); + } }, onUpgrade: stepByStep( from1To2: (m, schema) async { @@ -140,13 +147,34 @@ class AppDatabase extends _$AppDatabase { schema.locations.postureCheckRequired, ); }, + from4To5: (m, schema) async { + // Move the WireGuard private keys and the proxy tokens out of the + // database and into the platform keychain. Any failure here aborts + // the migration so it is retried on the next launch with the columns + // still in place. + await customStatement('PRAGMA secure_delete = ON'); + final instances = await customSelect( + 'SELECT uuid, device_id, private_key, pooling_token ' + 'FROM defguard_instances', + ).get(); + for (final instance in instances) { + await storeInstanceSecrets( + uuid: instance.read('uuid'), + deviceId: instance.read('device_id'), + privateKey: instance.read('private_key'), + poolingToken: instance.read('pooling_token'), + ); + } + await m.dropColumn(schema.defguardInstances, 'private_key'); + await m.dropColumn(schema.defguardInstances, 'pooling_token'); + }, ), ); } static QueryExecutor _openConnection() { return driftDatabase( - name: 'defguard', + name: databaseName, native: const DriftNativeOptions( databaseDirectory: getApplicationSupportDirectory, ), @@ -166,7 +194,24 @@ extension DefguardInstanceLogName on DefguardInstance { } extension DefguardInstanceStorageKey on DefguardInstance { - String get secureStorageKey => 'mfa-$uuid-$deviceId'; + String get secureStorageKey => mfaStorageKey(uuid, deviceId); +} + +/// Secrets of an instance are kept in the platform keychain, not in the +/// database. They can be absent (see [reportMissingSecret]), callers have to +/// handle a `null`. +extension DefguardInstanceSecrets on DefguardInstance { + Future wireguardPrivateKey() => + secureStorage.read(key: wireguardKeyStorageKey(uuid, deviceId)); + + Future poolingToken() => + secureStorage.read(key: tokenStorageKey(uuid, deviceId)); + + Future storeToken(String token) => + secureStorage.write(key: tokenStorageKey(uuid, deviceId), value: token); + + Future removeSecrets() => + removeInstanceSecrets(uuid: uuid, deviceId: deviceId); } extension LocationLogName on Location { diff --git a/client/lib/data/db/database.g.dart b/client/lib/data/db/database.g.dart index 1638e6f8..43b53ad0 100644 --- a/client/lib/data/db/database.g.dart +++ b/client/lib/data/db/database.g.dart @@ -82,17 +82,6 @@ class $DefguardInstancesTable extends DefguardInstances type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _poolingTokenMeta = const VerificationMeta( - 'poolingToken', - ); - @override - late final GeneratedColumn poolingToken = GeneratedColumn( - 'pooling_token', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); @override late final GeneratedColumnWithTypeConverter clientTrafficPolicy = @@ -129,17 +118,6 @@ class $DefguardInstancesTable extends DefguardInstances type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _privateKeyMeta = const VerificationMeta( - 'privateKey', - ); - @override - late final GeneratedColumn privateKey = GeneratedColumn( - 'private_key', - aliasedName, - false, - type: DriftSqlType.string, - requiredDuringInsert: true, - ); static const VerificationMeta _mfaKeysStoredMeta = const VerificationMeta( 'mfaKeysStored', ); @@ -175,11 +153,9 @@ class $DefguardInstancesTable extends DefguardInstances deviceId, proxyUrl, username, - poolingToken, clientTrafficPolicy, enterpriseEnabled, pubKey, - privateKey, mfaKeysStored, openidDisplayName, ]; @@ -246,17 +222,6 @@ class $DefguardInstancesTable extends DefguardInstances } else if (isInserting) { context.missing(_usernameMeta); } - if (data.containsKey('pooling_token')) { - context.handle( - _poolingTokenMeta, - poolingToken.isAcceptableOrUnknown( - data['pooling_token']!, - _poolingTokenMeta, - ), - ); - } else if (isInserting) { - context.missing(_poolingTokenMeta); - } if (data.containsKey('enterprise_enabled')) { context.handle( _enterpriseEnabledMeta, @@ -276,14 +241,6 @@ class $DefguardInstancesTable extends DefguardInstances } else if (isInserting) { context.missing(_pubKeyMeta); } - if (data.containsKey('private_key')) { - context.handle( - _privateKeyMeta, - privateKey.isAcceptableOrUnknown(data['private_key']!, _privateKeyMeta), - ); - } else if (isInserting) { - context.missing(_privateKeyMeta); - } if (data.containsKey('mfa_keys_stored')) { context.handle( _mfaKeysStoredMeta, @@ -341,10 +298,6 @@ class $DefguardInstancesTable extends DefguardInstances DriftSqlType.string, data['${effectivePrefix}username'], )!, - poolingToken: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}pooling_token'], - )!, clientTrafficPolicy: $DefguardInstancesTable.$converterclientTrafficPolicy .fromSql( attachedDatabase.typeMapping.read( @@ -360,10 +313,6 @@ class $DefguardInstancesTable extends DefguardInstances DriftSqlType.string, data['${effectivePrefix}pub_key'], )!, - privateKey: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}private_key'], - )!, mfaKeysStored: attachedDatabase.typeMapping.read( DriftSqlType.bool, data['${effectivePrefix}mfa_keys_stored'], @@ -393,11 +342,9 @@ class DefguardInstance extends DataClass final int deviceId; final String proxyUrl; final String username; - final String poolingToken; final ClientTrafficPolicy clientTrafficPolicy; final bool enterpriseEnabled; final String pubKey; - final String privateKey; final bool mfaKeysStored; final String? openidDisplayName; const DefguardInstance({ @@ -408,11 +355,9 @@ class DefguardInstance extends DataClass required this.deviceId, required this.proxyUrl, required this.username, - required this.poolingToken, required this.clientTrafficPolicy, required this.enterpriseEnabled, required this.pubKey, - required this.privateKey, required this.mfaKeysStored, this.openidDisplayName, }); @@ -426,7 +371,6 @@ class DefguardInstance extends DataClass map['device_id'] = Variable(deviceId); map['proxy_url'] = Variable(proxyUrl); map['username'] = Variable(username); - map['pooling_token'] = Variable(poolingToken); { map['client_traffic_policy'] = Variable( $DefguardInstancesTable.$converterclientTrafficPolicy.toSql( @@ -436,7 +380,6 @@ class DefguardInstance extends DataClass } map['enterprise_enabled'] = Variable(enterpriseEnabled); map['pub_key'] = Variable(pubKey); - map['private_key'] = Variable(privateKey); map['mfa_keys_stored'] = Variable(mfaKeysStored); if (!nullToAbsent || openidDisplayName != null) { map['openid_display_name'] = Variable(openidDisplayName); @@ -453,11 +396,9 @@ class DefguardInstance extends DataClass deviceId: Value(deviceId), proxyUrl: Value(proxyUrl), username: Value(username), - poolingToken: Value(poolingToken), clientTrafficPolicy: Value(clientTrafficPolicy), enterpriseEnabled: Value(enterpriseEnabled), pubKey: Value(pubKey), - privateKey: Value(privateKey), mfaKeysStored: Value(mfaKeysStored), openidDisplayName: openidDisplayName == null && nullToAbsent ? const Value.absent() @@ -478,13 +419,11 @@ class DefguardInstance extends DataClass deviceId: serializer.fromJson(json['deviceId']), proxyUrl: serializer.fromJson(json['proxy_url']), username: serializer.fromJson(json['username']), - poolingToken: serializer.fromJson(json['poolingToken']), clientTrafficPolicy: serializer.fromJson( json['client_traffic_policy'], ), enterpriseEnabled: serializer.fromJson(json['enterprise_enabled']), pubKey: serializer.fromJson(json['pubKey']), - privateKey: serializer.fromJson(json['privateKey']), mfaKeysStored: serializer.fromJson(json['mfaKeysStored']), openidDisplayName: serializer.fromJson( json['openidDisplayName'], @@ -502,13 +441,11 @@ class DefguardInstance extends DataClass 'deviceId': serializer.toJson(deviceId), 'proxy_url': serializer.toJson(proxyUrl), 'username': serializer.toJson(username), - 'poolingToken': serializer.toJson(poolingToken), 'client_traffic_policy': serializer.toJson( clientTrafficPolicy, ), 'enterprise_enabled': serializer.toJson(enterpriseEnabled), 'pubKey': serializer.toJson(pubKey), - 'privateKey': serializer.toJson(privateKey), 'mfaKeysStored': serializer.toJson(mfaKeysStored), 'openidDisplayName': serializer.toJson(openidDisplayName), }; @@ -522,11 +459,9 @@ class DefguardInstance extends DataClass int? deviceId, String? proxyUrl, String? username, - String? poolingToken, ClientTrafficPolicy? clientTrafficPolicy, bool? enterpriseEnabled, String? pubKey, - String? privateKey, bool? mfaKeysStored, Value openidDisplayName = const Value.absent(), }) => DefguardInstance( @@ -537,11 +472,9 @@ class DefguardInstance extends DataClass deviceId: deviceId ?? this.deviceId, proxyUrl: proxyUrl ?? this.proxyUrl, username: username ?? this.username, - poolingToken: poolingToken ?? this.poolingToken, clientTrafficPolicy: clientTrafficPolicy ?? this.clientTrafficPolicy, enterpriseEnabled: enterpriseEnabled ?? this.enterpriseEnabled, pubKey: pubKey ?? this.pubKey, - privateKey: privateKey ?? this.privateKey, mfaKeysStored: mfaKeysStored ?? this.mfaKeysStored, openidDisplayName: openidDisplayName.present ? openidDisplayName.value @@ -556,9 +489,6 @@ class DefguardInstance extends DataClass deviceId: data.deviceId.present ? data.deviceId.value : this.deviceId, proxyUrl: data.proxyUrl.present ? data.proxyUrl.value : this.proxyUrl, username: data.username.present ? data.username.value : this.username, - poolingToken: data.poolingToken.present - ? data.poolingToken.value - : this.poolingToken, clientTrafficPolicy: data.clientTrafficPolicy.present ? data.clientTrafficPolicy.value : this.clientTrafficPolicy, @@ -566,9 +496,6 @@ class DefguardInstance extends DataClass ? data.enterpriseEnabled.value : this.enterpriseEnabled, pubKey: data.pubKey.present ? data.pubKey.value : this.pubKey, - privateKey: data.privateKey.present - ? data.privateKey.value - : this.privateKey, mfaKeysStored: data.mfaKeysStored.present ? data.mfaKeysStored.value : this.mfaKeysStored, @@ -588,11 +515,9 @@ class DefguardInstance extends DataClass ..write('deviceId: $deviceId, ') ..write('proxyUrl: $proxyUrl, ') ..write('username: $username, ') - ..write('poolingToken: $poolingToken, ') ..write('clientTrafficPolicy: $clientTrafficPolicy, ') ..write('enterpriseEnabled: $enterpriseEnabled, ') ..write('pubKey: $pubKey, ') - ..write('privateKey: $privateKey, ') ..write('mfaKeysStored: $mfaKeysStored, ') ..write('openidDisplayName: $openidDisplayName') ..write(')')) @@ -608,11 +533,9 @@ class DefguardInstance extends DataClass deviceId, proxyUrl, username, - poolingToken, clientTrafficPolicy, enterpriseEnabled, pubKey, - privateKey, mfaKeysStored, openidDisplayName, ); @@ -627,11 +550,9 @@ class DefguardInstance extends DataClass other.deviceId == this.deviceId && other.proxyUrl == this.proxyUrl && other.username == this.username && - other.poolingToken == this.poolingToken && other.clientTrafficPolicy == this.clientTrafficPolicy && other.enterpriseEnabled == this.enterpriseEnabled && other.pubKey == this.pubKey && - other.privateKey == this.privateKey && other.mfaKeysStored == this.mfaKeysStored && other.openidDisplayName == this.openidDisplayName); } @@ -644,11 +565,9 @@ class DefguardInstancesCompanion extends UpdateCompanion { final Value deviceId; final Value proxyUrl; final Value username; - final Value poolingToken; final Value clientTrafficPolicy; final Value enterpriseEnabled; final Value pubKey; - final Value privateKey; final Value mfaKeysStored; final Value openidDisplayName; const DefguardInstancesCompanion({ @@ -659,11 +578,9 @@ class DefguardInstancesCompanion extends UpdateCompanion { this.deviceId = const Value.absent(), this.proxyUrl = const Value.absent(), this.username = const Value.absent(), - this.poolingToken = const Value.absent(), this.clientTrafficPolicy = const Value.absent(), this.enterpriseEnabled = const Value.absent(), this.pubKey = const Value.absent(), - this.privateKey = const Value.absent(), this.mfaKeysStored = const Value.absent(), this.openidDisplayName = const Value.absent(), }); @@ -675,11 +592,9 @@ class DefguardInstancesCompanion extends UpdateCompanion { required int deviceId, required String proxyUrl, required String username, - required String poolingToken, this.clientTrafficPolicy = const Value.absent(), required bool enterpriseEnabled, required String pubKey, - required String privateKey, required bool mfaKeysStored, this.openidDisplayName = const Value.absent(), }) : name = Value(name), @@ -688,10 +603,8 @@ class DefguardInstancesCompanion extends UpdateCompanion { deviceId = Value(deviceId), proxyUrl = Value(proxyUrl), username = Value(username), - poolingToken = Value(poolingToken), enterpriseEnabled = Value(enterpriseEnabled), pubKey = Value(pubKey), - privateKey = Value(privateKey), mfaKeysStored = Value(mfaKeysStored); static Insertable custom({ Expression? id, @@ -701,11 +614,9 @@ class DefguardInstancesCompanion extends UpdateCompanion { Expression? deviceId, Expression? proxyUrl, Expression? username, - Expression? poolingToken, Expression? clientTrafficPolicy, Expression? enterpriseEnabled, Expression? pubKey, - Expression? privateKey, Expression? mfaKeysStored, Expression? openidDisplayName, }) { @@ -717,12 +628,10 @@ class DefguardInstancesCompanion extends UpdateCompanion { if (deviceId != null) 'device_id': deviceId, if (proxyUrl != null) 'proxy_url': proxyUrl, if (username != null) 'username': username, - if (poolingToken != null) 'pooling_token': poolingToken, if (clientTrafficPolicy != null) 'client_traffic_policy': clientTrafficPolicy, if (enterpriseEnabled != null) 'enterprise_enabled': enterpriseEnabled, if (pubKey != null) 'pub_key': pubKey, - if (privateKey != null) 'private_key': privateKey, if (mfaKeysStored != null) 'mfa_keys_stored': mfaKeysStored, if (openidDisplayName != null) 'openid_display_name': openidDisplayName, }); @@ -736,11 +645,9 @@ class DefguardInstancesCompanion extends UpdateCompanion { Value? deviceId, Value? proxyUrl, Value? username, - Value? poolingToken, Value? clientTrafficPolicy, Value? enterpriseEnabled, Value? pubKey, - Value? privateKey, Value? mfaKeysStored, Value? openidDisplayName, }) { @@ -752,11 +659,9 @@ class DefguardInstancesCompanion extends UpdateCompanion { deviceId: deviceId ?? this.deviceId, proxyUrl: proxyUrl ?? this.proxyUrl, username: username ?? this.username, - poolingToken: poolingToken ?? this.poolingToken, clientTrafficPolicy: clientTrafficPolicy ?? this.clientTrafficPolicy, enterpriseEnabled: enterpriseEnabled ?? this.enterpriseEnabled, pubKey: pubKey ?? this.pubKey, - privateKey: privateKey ?? this.privateKey, mfaKeysStored: mfaKeysStored ?? this.mfaKeysStored, openidDisplayName: openidDisplayName ?? this.openidDisplayName, ); @@ -786,9 +691,6 @@ class DefguardInstancesCompanion extends UpdateCompanion { if (username.present) { map['username'] = Variable(username.value); } - if (poolingToken.present) { - map['pooling_token'] = Variable(poolingToken.value); - } if (clientTrafficPolicy.present) { map['client_traffic_policy'] = Variable( $DefguardInstancesTable.$converterclientTrafficPolicy.toSql( @@ -802,9 +704,6 @@ class DefguardInstancesCompanion extends UpdateCompanion { if (pubKey.present) { map['pub_key'] = Variable(pubKey.value); } - if (privateKey.present) { - map['private_key'] = Variable(privateKey.value); - } if (mfaKeysStored.present) { map['mfa_keys_stored'] = Variable(mfaKeysStored.value); } @@ -824,11 +723,9 @@ class DefguardInstancesCompanion extends UpdateCompanion { ..write('deviceId: $deviceId, ') ..write('proxyUrl: $proxyUrl, ') ..write('username: $username, ') - ..write('poolingToken: $poolingToken, ') ..write('clientTrafficPolicy: $clientTrafficPolicy, ') ..write('enterpriseEnabled: $enterpriseEnabled, ') ..write('pubKey: $pubKey, ') - ..write('privateKey: $privateKey, ') ..write('mfaKeysStored: $mfaKeysStored, ') ..write('openidDisplayName: $openidDisplayName') ..write(')')) @@ -1761,11 +1658,9 @@ typedef $$DefguardInstancesTableCreateCompanionBuilder = required int deviceId, required String proxyUrl, required String username, - required String poolingToken, Value clientTrafficPolicy, required bool enterpriseEnabled, required String pubKey, - required String privateKey, required bool mfaKeysStored, Value openidDisplayName, }); @@ -1778,11 +1673,9 @@ typedef $$DefguardInstancesTableUpdateCompanionBuilder = Value deviceId, Value proxyUrl, Value username, - Value poolingToken, Value clientTrafficPolicy, Value enterpriseEnabled, Value pubKey, - Value privateKey, Value mfaKeysStored, Value openidDisplayName, }); @@ -1866,11 +1759,6 @@ class $$DefguardInstancesTableFilterComposer builder: (column) => ColumnFilters(column), ); - ColumnFilters get poolingToken => $composableBuilder( - column: $table.poolingToken, - builder: (column) => ColumnFilters(column), - ); - ColumnWithTypeConverterFilters get clientTrafficPolicy => $composableBuilder( column: $table.clientTrafficPolicy, @@ -1887,11 +1775,6 @@ class $$DefguardInstancesTableFilterComposer builder: (column) => ColumnFilters(column), ); - ColumnFilters get privateKey => $composableBuilder( - column: $table.privateKey, - builder: (column) => ColumnFilters(column), - ); - ColumnFilters get mfaKeysStored => $composableBuilder( column: $table.mfaKeysStored, builder: (column) => ColumnFilters(column), @@ -1972,11 +1855,6 @@ class $$DefguardInstancesTableOrderingComposer builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get poolingToken => $composableBuilder( - column: $table.poolingToken, - builder: (column) => ColumnOrderings(column), - ); - ColumnOrderings get clientTrafficPolicy => $composableBuilder( column: $table.clientTrafficPolicy, builder: (column) => ColumnOrderings(column), @@ -1992,11 +1870,6 @@ class $$DefguardInstancesTableOrderingComposer builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get privateKey => $composableBuilder( - column: $table.privateKey, - builder: (column) => ColumnOrderings(column), - ); - ColumnOrderings get mfaKeysStored => $composableBuilder( column: $table.mfaKeysStored, builder: (column) => ColumnOrderings(column), @@ -2038,11 +1911,6 @@ class $$DefguardInstancesTableAnnotationComposer GeneratedColumn get username => $composableBuilder(column: $table.username, builder: (column) => column); - GeneratedColumn get poolingToken => $composableBuilder( - column: $table.poolingToken, - builder: (column) => column, - ); - GeneratedColumnWithTypeConverter get clientTrafficPolicy => $composableBuilder( column: $table.clientTrafficPolicy, @@ -2057,11 +1925,6 @@ class $$DefguardInstancesTableAnnotationComposer GeneratedColumn get pubKey => $composableBuilder(column: $table.pubKey, builder: (column) => column); - GeneratedColumn get privateKey => $composableBuilder( - column: $table.privateKey, - builder: (column) => column, - ); - GeneratedColumn get mfaKeysStored => $composableBuilder( column: $table.mfaKeysStored, builder: (column) => column, @@ -2138,12 +2001,10 @@ class $$DefguardInstancesTableTableManager Value deviceId = const Value.absent(), Value proxyUrl = const Value.absent(), Value username = const Value.absent(), - Value poolingToken = const Value.absent(), Value clientTrafficPolicy = const Value.absent(), Value enterpriseEnabled = const Value.absent(), Value pubKey = const Value.absent(), - Value privateKey = const Value.absent(), Value mfaKeysStored = const Value.absent(), Value openidDisplayName = const Value.absent(), }) => DefguardInstancesCompanion( @@ -2154,11 +2015,9 @@ class $$DefguardInstancesTableTableManager deviceId: deviceId, proxyUrl: proxyUrl, username: username, - poolingToken: poolingToken, clientTrafficPolicy: clientTrafficPolicy, enterpriseEnabled: enterpriseEnabled, pubKey: pubKey, - privateKey: privateKey, mfaKeysStored: mfaKeysStored, openidDisplayName: openidDisplayName, ), @@ -2171,12 +2030,10 @@ class $$DefguardInstancesTableTableManager required int deviceId, required String proxyUrl, required String username, - required String poolingToken, Value clientTrafficPolicy = const Value.absent(), required bool enterpriseEnabled, required String pubKey, - required String privateKey, required bool mfaKeysStored, Value openidDisplayName = const Value.absent(), }) => DefguardInstancesCompanion.insert( @@ -2187,11 +2044,9 @@ class $$DefguardInstancesTableTableManager deviceId: deviceId, proxyUrl: proxyUrl, username: username, - poolingToken: poolingToken, clientTrafficPolicy: clientTrafficPolicy, enterpriseEnabled: enterpriseEnabled, pubKey: pubKey, - privateKey: privateKey, mfaKeysStored: mfaKeysStored, openidDisplayName: openidDisplayName, ), diff --git a/client/lib/data/db/database.steps.dart b/client/lib/data/db/database.steps.dart index 8f97ee00..f51018de 100644 --- a/client/lib/data/db/database.steps.dart +++ b/client/lib/data/db/database.steps.dart @@ -520,10 +520,100 @@ i1.GeneratedColumn _column_25(String aliasedName) => 'CHECK ("posture_check_required" IN (0, 1))', ), ); + +final class Schema5 extends i0.VersionedSchema { + Schema5({required super.database}) : super(version: 5); + @override + late final List entities = [ + defguardInstances, + locations, + ]; + late final Shape4 defguardInstances = Shape4( + source: i0.VersionedTable( + entityName: 'defguard_instances', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + _column_4, + _column_5, + _column_6, + _column_8, + _column_9, + _column_10, + _column_12, + _column_24, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 locations = Shape3( + source: i0.VersionedTable( + entityName: 'locations', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_13, + _column_14, + _column_1, + _column_15, + _column_10, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_22, + _column_23, + _column_25, + ], + attachedDatabase: database, + ), + alias: null, + ); +} + +class Shape4 extends i0.VersionedTable { + Shape4({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get name => + columnsByName['name']! as i1.GeneratedColumn; + i1.GeneratedColumn get uuid => + columnsByName['uuid']! as i1.GeneratedColumn; + i1.GeneratedColumn get url => + columnsByName['url']! as i1.GeneratedColumn; + i1.GeneratedColumn get deviceId => + columnsByName['device_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get proxyUrl => + columnsByName['proxy_url']! as i1.GeneratedColumn; + i1.GeneratedColumn get username => + columnsByName['username']! as i1.GeneratedColumn; + i1.GeneratedColumn get clientTrafficPolicy => + columnsByName['client_traffic_policy']! as i1.GeneratedColumn; + i1.GeneratedColumn get enterpriseEnabled => + columnsByName['enterprise_enabled']! as i1.GeneratedColumn; + i1.GeneratedColumn get pubKey => + columnsByName['pub_key']! as i1.GeneratedColumn; + i1.GeneratedColumn get mfaKeysStored => + columnsByName['mfa_keys_stored']! as i1.GeneratedColumn; + i1.GeneratedColumn get openidDisplayName => + columnsByName['openid_display_name']! as i1.GeneratedColumn; +} + i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, required Future Function(i1.Migrator m, Schema4 schema) from3To4, + required Future Function(i1.Migrator m, Schema5 schema) from4To5, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -542,6 +632,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from3To4(migrator, schema); return 4; + case 4: + final schema = Schema5(database: database); + final migrator = i1.Migrator(database, schema); + await from4To5(migrator, schema); + return 5; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -552,10 +647,12 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, required Future Function(i1.Migrator m, Schema4 schema) from3To4, + required Future Function(i1.Migrator m, Schema5 schema) from4To5, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, from2To3: from2To3, from3To4: from3To4, + from4To5: from4To5, ), ); diff --git a/client/lib/data/db/db_file.dart b/client/lib/data/db/db_file.dart new file mode 100644 index 00000000..ef493e94 --- /dev/null +++ b/client/lib/data/db/db_file.dart @@ -0,0 +1,18 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +/// Name drift builds the database file from, see `AppDatabase._openConnection`. +const databaseName = 'defguard'; + +/// The file drift stores the database in. +/// +/// Everything that has to reason about the database outside of drift itself +/// derives the path from here - the fresh install detection in +/// `initSecureStorage` keys off its existence, so a rename that only landed in +/// one of the copies would silently wipe the stored secrets. +Future databaseFile() async { + final directory = await getApplicationSupportDirectory(); + return File(p.join(directory.path, '$databaseName.sqlite')); +} diff --git a/client/lib/data/proxy/enrollment.dart b/client/lib/data/proxy/enrollment.dart index ffa0990b..af97ffe4 100644 --- a/client/lib/data/proxy/enrollment.dart +++ b/client/lib/data/proxy/enrollment.dart @@ -292,8 +292,6 @@ class InstanceInfo { return DefguardInstancesCompanion( id: d.Value.absentIfNull(instance?.id), pubKey: d.Value.absentIfNull(instance?.pubKey), - privateKey: d.Value.absentIfNull(instance?.privateKey), - poolingToken: d.Value.absentIfNull(instance?.poolingToken), mfaKeysStored: d.Value.absentIfNull(instance?.mfaKeysStored), name: d.Value(name), url: d.Value(url), diff --git a/client/lib/enterprise/config_update.dart b/client/lib/enterprise/config_update.dart index d2360f6c..9ad740d8 100644 --- a/client/lib/enterprise/config_update.dart +++ b/client/lib/enterprise/config_update.dart @@ -5,6 +5,7 @@ import 'package:mobile/data/db/database.dart'; import 'package:mobile/data/db/enums.dart'; import 'package:mobile/open/api.dart'; import 'package:mobile/open/widgets/toaster/toast_manager.dart'; +import 'package:mobile/utils/instance_secrets.dart'; import 'package:mobile/utils/update_instance.dart'; import 'package:pub_semver/pub_semver.dart'; @@ -46,8 +47,17 @@ class ConfigurationUpdater extends HookConsumerWidget { talker.debug( "Auto configuration update started for ${instance.name} (${instance.id})", ); + final token = await instance.poolingToken(); + if (token == null) { + reportMissingSecret( + instance.logName, + "Proxy token", + notifyUser: false, + ); + continue; + } final (responseData, responseStatus, headers) = await proxyApi - .pollConfiguration(instance.proxyUrl, instance.poolingToken); + .pollConfiguration(instance.proxyUrl, token); talker.debug("Headers: $headers"); // Check versions try { diff --git a/client/lib/main.dart b/client/lib/main.dart index 948730ad..efc666f9 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -7,10 +7,12 @@ import 'package:mobile/open/services/snackbar_service.dart'; import 'package:mobile/plugin.dart'; import 'package:mobile/theme.dart'; +import 'package:mobile/utils/keychain.dart'; import 'package:mobile/utils/notifications.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await initSecureStorage(); await initNotifications(); runApp( ProviderScope( diff --git a/client/lib/open/screens/add_instance/screens/name_device_screen.dart b/client/lib/open/screens/add_instance/screens/name_device_screen.dart index 6e25082e..c5ace41b 100644 --- a/client/lib/open/screens/add_instance/screens/name_device_screen.dart +++ b/client/lib/open/screens/add_instance/screens/name_device_screen.dart @@ -11,6 +11,7 @@ import 'package:mobile/open/widgets/dg_single_child_scroll_view.dart'; import 'package:mobile/open/widgets/dg_text_form_field.dart'; import 'package:mobile/router/routes.dart'; import 'package:mobile/theme/spacing.dart'; +import 'package:mobile/utils/instance_secrets.dart'; import 'package:mobile/utils/screen_padding.dart'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; @@ -49,33 +50,46 @@ class NameDeviceScreen extends HookConsumerWidget { screenData.proxyUrl, createDeviceData, ); - final instance = await db.managers.defguardInstances.createReturning( - (o) => o( - id: drift.Value.absent(), - pubKey: keyPair.pubKey, - privateKey: keyPair.privKey, - name: createResponse.instance.name, - uuid: createResponse.instance.id, - deviceId: createResponse.device.id, - enterpriseEnabled: createResponse.instance.enterpriseEnabled, - clientTrafficPolicy: drift.Value(createResponse.instance.getPolicy()), - proxyUrl: createResponse.instance.proxyUrl, - url: screenData.startResponse.instance.url, - username: createResponse.instance.username, - poolingToken: createResponse.token, - mfaKeysStored: false, - openidDisplayName: drift.Value( - createResponse.instance.openidDisplayName, - ), - ), - mode: drift.InsertMode.insertOrFail, - ); - await db.managers.locations.bulkCreate( - (o) => createResponse.configs.map( - (config) => config.toCompanion(instanceId: instance.id), - ), + final uuid = createResponse.instance.id; + final deviceId = createResponse.device.id; + // secrets live in the keychain, not in the database + await storeInstanceSecrets( + uuid: uuid, + deviceId: deviceId, + privateKey: keyPair.privKey, + poolingToken: createResponse.token, ); - return instance; + try { + final instance = await db.managers.defguardInstances.createReturning( + (o) => o( + id: drift.Value.absent(), + pubKey: keyPair.pubKey, + name: createResponse.instance.name, + uuid: uuid, + deviceId: deviceId, + enterpriseEnabled: createResponse.instance.enterpriseEnabled, + clientTrafficPolicy: drift.Value(createResponse.instance.getPolicy()), + proxyUrl: createResponse.instance.proxyUrl, + url: screenData.startResponse.instance.url, + username: createResponse.instance.username, + mfaKeysStored: false, + openidDisplayName: drift.Value( + createResponse.instance.openidDisplayName, + ), + ), + mode: drift.InsertMode.insertOrFail, + ); + await db.managers.locations.bulkCreate( + (o) => createResponse.configs.map( + (config) => config.toCompanion(instanceId: instance.id), + ), + ); + return instance; + } catch (e) { + // do not leave secrets of an instance that was never stored + await removeInstanceSecrets(uuid: uuid, deviceId: deviceId); + rethrow; + } } @override diff --git a/client/lib/open/screens/instance/instance_screen.dart b/client/lib/open/screens/instance/instance_screen.dart index 310acefd..2f80236a 100644 --- a/client/lib/open/screens/instance/instance_screen.dart +++ b/client/lib/open/screens/instance/instance_screen.dart @@ -29,6 +29,7 @@ import 'package:mobile/theme/color.dart'; import 'package:mobile/theme/spacing.dart'; import 'package:mobile/theme/text.dart'; import 'package:mobile/utils/position.dart'; +import 'package:mobile/utils/instance_secrets.dart'; import 'package:mobile/utils/update_instance.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; @@ -181,8 +182,13 @@ class _PullWrapper extends HookConsumerWidget { onRefresh: () async { try { final instance = screenData.instance; + final token = await instance.poolingToken(); + if (token == null) { + reportMissingSecret(instance.logName, "Proxy token"); + return; + } final (responseData, responseStatus, responseHeaders) = await proxyApi - .pollConfiguration(instance.proxyUrl, instance.poolingToken); + .pollConfiguration(instance.proxyUrl, token); if (responseData == null) { SnackbarService.showError( "Failed to get new information for instance.", diff --git a/client/lib/open/screens/instance/services/tunnel_service.dart b/client/lib/open/screens/instance/services/tunnel_service.dart index 62ab24d6..9ec29a6d 100644 --- a/client/lib/open/screens/instance/services/tunnel_service.dart +++ b/client/lib/open/screens/instance/services/tunnel_service.dart @@ -13,6 +13,7 @@ import 'package:mobile/open/screens/instance/widgets/mfa_method_dialog.dart'; import 'package:mobile/open/screens/instance/widgets/routing_method_dialog.dart'; import 'package:mobile/open/widgets/dg_snackbar.dart'; import 'package:mobile/theme/color.dart'; +import 'package:mobile/utils/instance_secrets.dart'; import 'package:mobile/utils/secure_storage.dart'; import 'dart:convert'; @@ -69,10 +70,16 @@ class TunnelService { } // prepare wireguard plugin payload + final privateKey = await instance.wireguardPrivateKey(); + if (privateKey == null) { + reportMissingSecret(instance.logName, "WireGuard private key"); + return; + } PluginConnectPayload payload = _makePayload( instance, location, trafficMethod, + privateKey, ); // handle MFA if configured @@ -121,11 +128,16 @@ class TunnelService { } payload.presharedKey = presharedKey; } else if (payload.postureCheckRequired) { + final pollingToken = await instance.poolingToken(); + if (pollingToken == null) { + reportMissingSecret(instance.logName, "Proxy token"); + return; + } final presharedKey = await _performPostureCheck( navigator: navigator, proxyUrl: instance.proxyUrl, payload: payload, - pollingToken: instance.poolingToken, + pollingToken: pollingToken, ); if (presharedKey == null) { return; @@ -373,11 +385,12 @@ class TunnelService { DefguardInstance instance, Location location, RoutingMethod trafficMethod, + String privateKey, ) { return PluginConnectPayload( publicKey: location.pubKey, devicePublicKey: instance.pubKey, - privateKey: instance.privateKey, + privateKey: privateKey, address: location.address, dns: location.dns, endpoint: location.endpoint, diff --git a/client/lib/open/screens/instance/widgets/delete_instance_dialog.dart b/client/lib/open/screens/instance/widgets/delete_instance_dialog.dart index e60477dc..b7deeb37 100644 --- a/client/lib/open/screens/instance/widgets/delete_instance_dialog.dart +++ b/client/lib/open/screens/instance/widgets/delete_instance_dialog.dart @@ -6,7 +6,6 @@ import 'package:mobile/open/widgets/buttons/dg_button.dart'; import 'package:mobile/open/widgets/dg_dialog.dart'; import 'package:mobile/open/widgets/icons/asset_icons_simple.dart'; import 'package:mobile/theme/text.dart'; -import 'package:mobile/utils/secure_storage.dart'; import '../../../services/snackbar_service.dart'; @@ -24,9 +23,7 @@ class DeleteInstanceDialog extends HookConsumerWidget { Future deleteInstance(BuildContext context) async { try { - if (instance.mfaKeysStored) { - await removeInstanceStorage(instance.secureStorageKey); - } + await instance.removeSecrets(); await db.managers.defguardInstances .filter((row) => row.id.equals(instance.id)) .delete(); diff --git a/client/lib/utils/dev/delete_db.dart b/client/lib/utils/dev/delete_db.dart index ba004088..7f162e44 100644 --- a/client/lib/utils/dev/delete_db.dart +++ b/client/lib/utils/dev/delete_db.dart @@ -1,11 +1,7 @@ -import 'dart:io'; -import 'package:path_provider/path_provider.dart'; -import 'package:path/path.dart' as path; +import 'package:mobile/data/db/db_file.dart'; Future deleteDbForDev() async { - final directory = await getApplicationSupportDirectory(); - final dbPath = path.join(directory.path, 'defguard.sqlite'); - final dbFile = File(dbPath); + final dbFile = await databaseFile(); if (await dbFile.exists()) { await dbFile.delete(); print("Local DB Deleted"); diff --git a/client/lib/utils/instance_secrets.dart b/client/lib/utils/instance_secrets.dart new file mode 100644 index 00000000..a61fe4ec --- /dev/null +++ b/client/lib/utils/instance_secrets.dart @@ -0,0 +1,72 @@ +import 'package:mobile/open/services/snackbar_service.dart'; +import 'package:mobile/utils/keychain.dart'; + +import '../logging.dart'; + +/// Per-instance secrets kept in the platform keychain instead of the database: +/// the device WireGuard private key, the proxy (polling) token and the +/// biometric MFA key pair. +/// +/// Items are keyed by the instance UUID and device id, so they stay addressable +/// without a database row - which is what enrollment (the row does not exist +/// yet) and the 4 -> 5 migration (the row is not mapped yet) need. + +String wireguardKeyStorageKey(String uuid, int deviceId) => + 'wg-key-$uuid-$deviceId'; + +String tokenStorageKey(String uuid, int deviceId) => 'token-$uuid-$deviceId'; + +String mfaStorageKey(String uuid, int deviceId) => 'mfa-$uuid-$deviceId'; + +/// Stores the secrets of a newly enrolled instance. +Future storeInstanceSecrets({ + required String uuid, + required int deviceId, + required String privateKey, + required String poolingToken, +}) async { + await secureStorage.write( + key: wireguardKeyStorageKey(uuid, deviceId), + value: privateKey, + ); + await secureStorage.write( + key: tokenStorageKey(uuid, deviceId), + value: poolingToken, + ); +} + +/// Removes every secret of an instance, including its biometric MFA key pair. +Future removeInstanceSecrets({ + required String uuid, + required int deviceId, +}) async { + await secureStorage.delete(key: wireguardKeyStorageKey(uuid, deviceId)); + await secureStorage.delete(key: tokenStorageKey(uuid, deviceId)); + await secureStorage.delete(key: mfaStorageKey(uuid, deviceId)); +} + +/// Message shown when an instance has a database row but no secrets. +/// +/// Secrets are stored `ThisDeviceOnly`, so a database restored from a backup +/// onto another device (or one restored after the keychain was reset) keeps its +/// instances while the key material is gone. Such an instance cannot connect +/// and has to be enrolled again. +const _missingSecretsMessage = + "This instance is missing its credentials. Please delete it and add it again."; + +/// Reports an instance whose secrets are gone. +/// +/// Background work (configuration polling) passes `notifyUser: false`: it runs +/// without the user asking for anything, so it only logs. +void reportMissingSecret( + String logName, + String what, { + bool notifyUser = true, +}) { + talker.error( + "$what of $logName is not present in the secure storage, the instance has to be re-enrolled", + ); + if (notifyUser) { + SnackbarService.showError(_missingSecretsMessage); + } +} diff --git a/client/lib/utils/keychain.dart b/client/lib/utils/keychain.dart new file mode 100644 index 00000000..b54a7fc5 --- /dev/null +++ b/client/lib/utils/keychain.dart @@ -0,0 +1,67 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:mobile/data/db/db_file.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../logging.dart'; + +/// iOS keychain items are stored with +/// `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`. +/// +/// The client never needs a secret while the device is locked: it declares no +/// `UIBackgroundModes`, sets no on-demand VPN rules, and every read happens in a +/// foreground flow (enrollment, tunnel start, configuration polling driven by +/// the app lifecycle). Requiring an unlocked device therefore costs no +/// functionality, and `ThisDeviceOnly` additionally keeps the secrets out of +/// device backups and off any other device. +const _iosOptions = IOSOptions( + accessibility: KeychainAccessibility.unlocked_this_device, +); + +const _androidOptions = AndroidOptions(encryptedSharedPreferences: true); + +/// Storage for every secret the client keeps at rest: WireGuard private keys, +/// proxy tokens and the biometric MFA key pairs. +const secureStorage = FlutterSecureStorage( + iOptions: _iosOptions, + aOptions: _androidOptions, +); + +const _initializedKey = 'secure_storage_initialized'; + +/// Brings the secure storage in line with this installation, once. +/// +/// Must be awaited during startup, before anything reads a secret. Both +/// branches are idempotent, so a failure leaves the marker unset and is simply +/// retried on the next launch. +Future initSecureStorage() async { + final prefs = SharedPreferencesAsync(); + if (await prefs.getBool(_initializedKey) == true) return; + try { + if (!await (await databaseFile()).exists()) { + // Keychain items outlive the app on iOS: after a reinstall the client + // starts with an empty database but inherits every secret the previous + // installation wrote, and none of them can ever be used again. + // + // A missing marker alone would not prove a fresh install - it is missing + // for clients upgrading from a version that predates it too - but the + // database file is removed on uninstall and exists for every upgrading + // client, so its absence does. + talker.info("Fresh install, purging secrets of a previous installation"); + await secureStorage.deleteAll(); + } else { + // Items written before the accessibility class was pinned use the plugin + // default (`kSecAttrAccessibleWhenUnlocked`, which is included in + // encrypted backups). Rewriting an item applies the current class. + final stored = await secureStorage.readAll(); + for (final entry in stored.entries) { + await secureStorage.write(key: entry.key, value: entry.value); + } + talker.info( + "Applied accessibility class to ${stored.length} stored secret(s)", + ); + } + await prefs.setBool(_initializedKey, true); + } catch (e) { + talker.error("Secure storage setup failed, retrying on the next launch", e); + } +} diff --git a/client/lib/utils/secure_storage.dart b/client/lib/utils/secure_storage.dart index 38ea5331..2091af7b 100644 --- a/client/lib/utils/secure_storage.dart +++ b/client/lib/utils/secure_storage.dart @@ -3,10 +3,10 @@ import 'dart:convert'; import 'package:ed25519_edwards/ed25519_edwards.dart' as ed; import 'package:flutter/services.dart'; import 'package:mobile/data/proxy/mfa.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:local_auth/local_auth.dart'; import 'package:local_auth/error_codes.dart' as auth_error; import 'package:mobile/logging.dart'; +import 'package:mobile/utils/keychain.dart'; class UserCanceledAuth implements Exception { const UserCanceledAuth(); @@ -30,12 +30,6 @@ String getErrorMessageFromBiometricsException(PlatformException e) { return "Unknown error"; } -AndroidOptions _getAndroidOptions() => - const AndroidOptions(encryptedSharedPreferences: true); - -FlutterSecureStorage _getStorage() => - FlutterSecureStorage(aOptions: _getAndroidOptions()); - String signChallenge(String challenge, String privateKey) { final List decodedKey = base64.decode(privateKey).toList(); final private = ed.PrivateKey(decodedKey); @@ -50,11 +44,6 @@ SecureInstanceStorage _generateInstanceStorage() { return SecureInstanceStorage(privateKey: privateKey, publicKey: publicKey); } -Future removeInstanceStorage(String storageKey) async { - final storage = _getStorage(); - await storage.delete(key: storageKey); -} - Future createBiometricStorage( String storageKey, { String? prompt, @@ -67,10 +56,9 @@ Future createBiometricStorage( biometricOnly: true, ), )) { - final storage = _getStorage(); final instanceStorage = _generateInstanceStorage(); final serializedStorage = jsonEncode(instanceStorage.toJson()); - await storage.write(key: storageKey, value: serializedStorage); + await secureStorage.write(key: storageKey, value: serializedStorage); return instanceStorage; } throw UserCanceledAuth(); @@ -89,8 +77,7 @@ Future getBiometricInstanceStorage( biometricOnly: true, ), )) { - final storage = _getStorage(); - final storeRawData = await storage.read(key: storageKey); + final storeRawData = await secureStorage.read(key: storageKey); if (storeRawData != null) { return SecureInstanceStorage.fromJson(jsonDecode(storeRawData)); } diff --git a/client/lib/utils/update_instance.dart b/client/lib/utils/update_instance.dart index 52804943..14699000 100644 --- a/client/lib/utils/update_instance.dart +++ b/client/lib/utils/update_instance.dart @@ -2,7 +2,6 @@ import 'package:collection/collection.dart'; import 'package:mobile/logging.dart'; import '../data/db/database.dart'; import '../data/proxy/enrollment.dart'; -import 'package:drift/drift.dart' as drift; class UpdateInstanceResult { bool instanceChanged; @@ -42,6 +41,12 @@ Future updateInstance({ ); try { + // store the rotated token before anything else, it is the only copy + if (token != null) { + await instance.storeToken(token); + talker.debug("${instance.logName} token updated"); + } + final locations = await db.managers.locations .filter((row) => row.instance.id.equals(instance.id)) .get(); @@ -55,17 +60,6 @@ Future updateInstance({ result.instanceChanged = true; } - // update token if provided - if (token != null) { - await db.managers.defguardInstances - .filter((row) => row.id.equals(instance.id)) - .update( - (_) => - DefguardInstancesCompanion(poolingToken: drift.Value(token)), - ); - talker.debug("${instance.logName} token updated"); - } - // remove locations not included in update (ware deleted or device have no longer granted access) final existingConfigs = configs.map((c) => c.networkId); final List toDelete = locations diff --git a/client/test/drift/defguard/generated/schema.dart b/client/test/drift/defguard/generated/schema.dart index 22131b11..c42542af 100644 --- a/client/test/drift/defguard/generated/schema.dart +++ b/client/test/drift/defguard/generated/schema.dart @@ -7,6 +7,7 @@ import 'schema_v1.dart' as v1; import 'schema_v2.dart' as v2; import 'schema_v3.dart' as v3; import 'schema_v4.dart' as v4; +import 'schema_v5.dart' as v5; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -20,10 +21,12 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v3.DatabaseAtV3(db); case 4: return v4.DatabaseAtV4(db); + case 5: + return v5.DatabaseAtV5(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4]; + static const versions = const [1, 2, 3, 4, 5]; } diff --git a/client/test/drift/defguard/generated/schema_v5.dart b/client/test/drift/defguard/generated/schema_v5.dart new file mode 100644 index 00000000..35dd019b --- /dev/null +++ b/client/test/drift/defguard/generated/schema_v5.dart @@ -0,0 +1,1296 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class DefguardInstances extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + DefguardInstances(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn uuid = GeneratedColumn( + 'uuid', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn url = GeneratedColumn( + 'url', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn deviceId = GeneratedColumn( + 'device_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn proxyUrl = GeneratedColumn( + 'proxy_url', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn username = GeneratedColumn( + 'username', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn clientTrafficPolicy = GeneratedColumn( + 'client_traffic_policy', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn enterpriseEnabled = GeneratedColumn( + 'enterprise_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("enterprise_enabled" IN (0, 1))', + ), + ); + late final GeneratedColumn pubKey = GeneratedColumn( + 'pub_key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn mfaKeysStored = GeneratedColumn( + 'mfa_keys_stored', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("mfa_keys_stored" IN (0, 1))', + ), + ); + late final GeneratedColumn openidDisplayName = + GeneratedColumn( + 'openid_display_name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + uuid, + url, + deviceId, + proxyUrl, + username, + clientTrafficPolicy, + enterpriseEnabled, + pubKey, + mfaKeysStored, + openidDisplayName, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'defguard_instances'; + @override + Set get $primaryKey => {id}; + @override + DefguardInstancesData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return DefguardInstancesData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + uuid: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}uuid'], + )!, + url: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}url'], + )!, + deviceId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}device_id'], + )!, + proxyUrl: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}proxy_url'], + )!, + username: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}username'], + )!, + clientTrafficPolicy: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}client_traffic_policy'], + )!, + enterpriseEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}enterprise_enabled'], + )!, + pubKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pub_key'], + )!, + mfaKeysStored: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}mfa_keys_stored'], + )!, + openidDisplayName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}openid_display_name'], + ), + ); + } + + @override + DefguardInstances createAlias(String alias) { + return DefguardInstances(attachedDatabase, alias); + } +} + +class DefguardInstancesData extends DataClass + implements Insertable { + final int id; + final String name; + final String uuid; + final String url; + final int deviceId; + final String proxyUrl; + final String username; + final int clientTrafficPolicy; + final bool enterpriseEnabled; + final String pubKey; + final bool mfaKeysStored; + final String? openidDisplayName; + const DefguardInstancesData({ + required this.id, + required this.name, + required this.uuid, + required this.url, + required this.deviceId, + required this.proxyUrl, + required this.username, + required this.clientTrafficPolicy, + required this.enterpriseEnabled, + required this.pubKey, + required this.mfaKeysStored, + this.openidDisplayName, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['uuid'] = Variable(uuid); + map['url'] = Variable(url); + map['device_id'] = Variable(deviceId); + map['proxy_url'] = Variable(proxyUrl); + map['username'] = Variable(username); + map['client_traffic_policy'] = Variable(clientTrafficPolicy); + map['enterprise_enabled'] = Variable(enterpriseEnabled); + map['pub_key'] = Variable(pubKey); + map['mfa_keys_stored'] = Variable(mfaKeysStored); + if (!nullToAbsent || openidDisplayName != null) { + map['openid_display_name'] = Variable(openidDisplayName); + } + return map; + } + + DefguardInstancesCompanion toCompanion(bool nullToAbsent) { + return DefguardInstancesCompanion( + id: Value(id), + name: Value(name), + uuid: Value(uuid), + url: Value(url), + deviceId: Value(deviceId), + proxyUrl: Value(proxyUrl), + username: Value(username), + clientTrafficPolicy: Value(clientTrafficPolicy), + enterpriseEnabled: Value(enterpriseEnabled), + pubKey: Value(pubKey), + mfaKeysStored: Value(mfaKeysStored), + openidDisplayName: openidDisplayName == null && nullToAbsent + ? const Value.absent() + : Value(openidDisplayName), + ); + } + + factory DefguardInstancesData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return DefguardInstancesData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + uuid: serializer.fromJson(json['uuid']), + url: serializer.fromJson(json['url']), + deviceId: serializer.fromJson(json['deviceId']), + proxyUrl: serializer.fromJson(json['proxyUrl']), + username: serializer.fromJson(json['username']), + clientTrafficPolicy: serializer.fromJson( + json['clientTrafficPolicy'], + ), + enterpriseEnabled: serializer.fromJson(json['enterpriseEnabled']), + pubKey: serializer.fromJson(json['pubKey']), + mfaKeysStored: serializer.fromJson(json['mfaKeysStored']), + openidDisplayName: serializer.fromJson( + json['openidDisplayName'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'uuid': serializer.toJson(uuid), + 'url': serializer.toJson(url), + 'deviceId': serializer.toJson(deviceId), + 'proxyUrl': serializer.toJson(proxyUrl), + 'username': serializer.toJson(username), + 'clientTrafficPolicy': serializer.toJson(clientTrafficPolicy), + 'enterpriseEnabled': serializer.toJson(enterpriseEnabled), + 'pubKey': serializer.toJson(pubKey), + 'mfaKeysStored': serializer.toJson(mfaKeysStored), + 'openidDisplayName': serializer.toJson(openidDisplayName), + }; + } + + DefguardInstancesData copyWith({ + int? id, + String? name, + String? uuid, + String? url, + int? deviceId, + String? proxyUrl, + String? username, + int? clientTrafficPolicy, + bool? enterpriseEnabled, + String? pubKey, + bool? mfaKeysStored, + Value openidDisplayName = const Value.absent(), + }) => DefguardInstancesData( + id: id ?? this.id, + name: name ?? this.name, + uuid: uuid ?? this.uuid, + url: url ?? this.url, + deviceId: deviceId ?? this.deviceId, + proxyUrl: proxyUrl ?? this.proxyUrl, + username: username ?? this.username, + clientTrafficPolicy: clientTrafficPolicy ?? this.clientTrafficPolicy, + enterpriseEnabled: enterpriseEnabled ?? this.enterpriseEnabled, + pubKey: pubKey ?? this.pubKey, + mfaKeysStored: mfaKeysStored ?? this.mfaKeysStored, + openidDisplayName: openidDisplayName.present + ? openidDisplayName.value + : this.openidDisplayName, + ); + DefguardInstancesData copyWithCompanion(DefguardInstancesCompanion data) { + return DefguardInstancesData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + uuid: data.uuid.present ? data.uuid.value : this.uuid, + url: data.url.present ? data.url.value : this.url, + deviceId: data.deviceId.present ? data.deviceId.value : this.deviceId, + proxyUrl: data.proxyUrl.present ? data.proxyUrl.value : this.proxyUrl, + username: data.username.present ? data.username.value : this.username, + clientTrafficPolicy: data.clientTrafficPolicy.present + ? data.clientTrafficPolicy.value + : this.clientTrafficPolicy, + enterpriseEnabled: data.enterpriseEnabled.present + ? data.enterpriseEnabled.value + : this.enterpriseEnabled, + pubKey: data.pubKey.present ? data.pubKey.value : this.pubKey, + mfaKeysStored: data.mfaKeysStored.present + ? data.mfaKeysStored.value + : this.mfaKeysStored, + openidDisplayName: data.openidDisplayName.present + ? data.openidDisplayName.value + : this.openidDisplayName, + ); + } + + @override + String toString() { + return (StringBuffer('DefguardInstancesData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('uuid: $uuid, ') + ..write('url: $url, ') + ..write('deviceId: $deviceId, ') + ..write('proxyUrl: $proxyUrl, ') + ..write('username: $username, ') + ..write('clientTrafficPolicy: $clientTrafficPolicy, ') + ..write('enterpriseEnabled: $enterpriseEnabled, ') + ..write('pubKey: $pubKey, ') + ..write('mfaKeysStored: $mfaKeysStored, ') + ..write('openidDisplayName: $openidDisplayName') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + uuid, + url, + deviceId, + proxyUrl, + username, + clientTrafficPolicy, + enterpriseEnabled, + pubKey, + mfaKeysStored, + openidDisplayName, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DefguardInstancesData && + other.id == this.id && + other.name == this.name && + other.uuid == this.uuid && + other.url == this.url && + other.deviceId == this.deviceId && + other.proxyUrl == this.proxyUrl && + other.username == this.username && + other.clientTrafficPolicy == this.clientTrafficPolicy && + other.enterpriseEnabled == this.enterpriseEnabled && + other.pubKey == this.pubKey && + other.mfaKeysStored == this.mfaKeysStored && + other.openidDisplayName == this.openidDisplayName); +} + +class DefguardInstancesCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value uuid; + final Value url; + final Value deviceId; + final Value proxyUrl; + final Value username; + final Value clientTrafficPolicy; + final Value enterpriseEnabled; + final Value pubKey; + final Value mfaKeysStored; + final Value openidDisplayName; + const DefguardInstancesCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.uuid = const Value.absent(), + this.url = const Value.absent(), + this.deviceId = const Value.absent(), + this.proxyUrl = const Value.absent(), + this.username = const Value.absent(), + this.clientTrafficPolicy = const Value.absent(), + this.enterpriseEnabled = const Value.absent(), + this.pubKey = const Value.absent(), + this.mfaKeysStored = const Value.absent(), + this.openidDisplayName = const Value.absent(), + }); + DefguardInstancesCompanion.insert({ + this.id = const Value.absent(), + required String name, + required String uuid, + required String url, + required int deviceId, + required String proxyUrl, + required String username, + this.clientTrafficPolicy = const Value.absent(), + required bool enterpriseEnabled, + required String pubKey, + required bool mfaKeysStored, + this.openidDisplayName = const Value.absent(), + }) : name = Value(name), + uuid = Value(uuid), + url = Value(url), + deviceId = Value(deviceId), + proxyUrl = Value(proxyUrl), + username = Value(username), + enterpriseEnabled = Value(enterpriseEnabled), + pubKey = Value(pubKey), + mfaKeysStored = Value(mfaKeysStored); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? uuid, + Expression? url, + Expression? deviceId, + Expression? proxyUrl, + Expression? username, + Expression? clientTrafficPolicy, + Expression? enterpriseEnabled, + Expression? pubKey, + Expression? mfaKeysStored, + Expression? openidDisplayName, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (uuid != null) 'uuid': uuid, + if (url != null) 'url': url, + if (deviceId != null) 'device_id': deviceId, + if (proxyUrl != null) 'proxy_url': proxyUrl, + if (username != null) 'username': username, + if (clientTrafficPolicy != null) + 'client_traffic_policy': clientTrafficPolicy, + if (enterpriseEnabled != null) 'enterprise_enabled': enterpriseEnabled, + if (pubKey != null) 'pub_key': pubKey, + if (mfaKeysStored != null) 'mfa_keys_stored': mfaKeysStored, + if (openidDisplayName != null) 'openid_display_name': openidDisplayName, + }); + } + + DefguardInstancesCompanion copyWith({ + Value? id, + Value? name, + Value? uuid, + Value? url, + Value? deviceId, + Value? proxyUrl, + Value? username, + Value? clientTrafficPolicy, + Value? enterpriseEnabled, + Value? pubKey, + Value? mfaKeysStored, + Value? openidDisplayName, + }) { + return DefguardInstancesCompanion( + id: id ?? this.id, + name: name ?? this.name, + uuid: uuid ?? this.uuid, + url: url ?? this.url, + deviceId: deviceId ?? this.deviceId, + proxyUrl: proxyUrl ?? this.proxyUrl, + username: username ?? this.username, + clientTrafficPolicy: clientTrafficPolicy ?? this.clientTrafficPolicy, + enterpriseEnabled: enterpriseEnabled ?? this.enterpriseEnabled, + pubKey: pubKey ?? this.pubKey, + mfaKeysStored: mfaKeysStored ?? this.mfaKeysStored, + openidDisplayName: openidDisplayName ?? this.openidDisplayName, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (uuid.present) { + map['uuid'] = Variable(uuid.value); + } + if (url.present) { + map['url'] = Variable(url.value); + } + if (deviceId.present) { + map['device_id'] = Variable(deviceId.value); + } + if (proxyUrl.present) { + map['proxy_url'] = Variable(proxyUrl.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (clientTrafficPolicy.present) { + map['client_traffic_policy'] = Variable(clientTrafficPolicy.value); + } + if (enterpriseEnabled.present) { + map['enterprise_enabled'] = Variable(enterpriseEnabled.value); + } + if (pubKey.present) { + map['pub_key'] = Variable(pubKey.value); + } + if (mfaKeysStored.present) { + map['mfa_keys_stored'] = Variable(mfaKeysStored.value); + } + if (openidDisplayName.present) { + map['openid_display_name'] = Variable(openidDisplayName.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('DefguardInstancesCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('uuid: $uuid, ') + ..write('url: $url, ') + ..write('deviceId: $deviceId, ') + ..write('proxyUrl: $proxyUrl, ') + ..write('username: $username, ') + ..write('clientTrafficPolicy: $clientTrafficPolicy, ') + ..write('enterpriseEnabled: $enterpriseEnabled, ') + ..write('pubKey: $pubKey, ') + ..write('mfaKeysStored: $mfaKeysStored, ') + ..write('openidDisplayName: $openidDisplayName') + ..write(')')) + .toString(); + } +} + +class Locations extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Locations(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'PRIMARY KEY AUTOINCREMENT', + ), + ); + late final GeneratedColumn instance = GeneratedColumn( + 'instance', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES defguard_instances (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn networkId = GeneratedColumn( + 'network_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn address = GeneratedColumn( + 'address', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn pubKey = GeneratedColumn( + 'pub_key', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn endpoint = GeneratedColumn( + 'endpoint', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn allowedIps = GeneratedColumn( + 'allowed_ips', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn dns = GeneratedColumn( + 'dns', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn mfaEnabled = GeneratedColumn( + 'mfa_enabled', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("mfa_enabled" IN (0, 1))', + ), + ); + late final GeneratedColumn trafficMethod = GeneratedColumn( + 'traffic_method', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn mfaMethod = GeneratedColumn( + 'mfa_method', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn keepAliveInterval = GeneratedColumn( + 'keep_alive_interval', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn locationMfaMode = GeneratedColumn( + 'location_mfa_mode', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn postureCheckRequired = GeneratedColumn( + 'posture_check_required', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("posture_check_required" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + instance, + networkId, + name, + address, + pubKey, + endpoint, + allowedIps, + dns, + mfaEnabled, + trafficMethod, + mfaMethod, + keepAliveInterval, + locationMfaMode, + postureCheckRequired, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'locations'; + @override + Set get $primaryKey => {id}; + @override + LocationsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocationsData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + instance: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}instance'], + )!, + networkId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}network_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + address: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}address'], + )!, + pubKey: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pub_key'], + )!, + endpoint: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}endpoint'], + )!, + allowedIps: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}allowed_ips'], + )!, + dns: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}dns'], + ), + mfaEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}mfa_enabled'], + ), + trafficMethod: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}traffic_method'], + ), + mfaMethod: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}mfa_method'], + ), + keepAliveInterval: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}keep_alive_interval'], + )!, + locationMfaMode: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}location_mfa_mode'], + ), + postureCheckRequired: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}posture_check_required'], + ), + ); + } + + @override + Locations createAlias(String alias) { + return Locations(attachedDatabase, alias); + } +} + +class LocationsData extends DataClass implements Insertable { + final int id; + final int instance; + final int networkId; + final String name; + final String address; + final String pubKey; + final String endpoint; + final String allowedIps; + final String? dns; + final bool? mfaEnabled; + final String? trafficMethod; + final int? mfaMethod; + final int keepAliveInterval; + final int? locationMfaMode; + final bool? postureCheckRequired; + const LocationsData({ + required this.id, + required this.instance, + required this.networkId, + required this.name, + required this.address, + required this.pubKey, + required this.endpoint, + required this.allowedIps, + this.dns, + this.mfaEnabled, + this.trafficMethod, + this.mfaMethod, + required this.keepAliveInterval, + this.locationMfaMode, + this.postureCheckRequired, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['instance'] = Variable(instance); + map['network_id'] = Variable(networkId); + map['name'] = Variable(name); + map['address'] = Variable(address); + map['pub_key'] = Variable(pubKey); + map['endpoint'] = Variable(endpoint); + map['allowed_ips'] = Variable(allowedIps); + if (!nullToAbsent || dns != null) { + map['dns'] = Variable(dns); + } + if (!nullToAbsent || mfaEnabled != null) { + map['mfa_enabled'] = Variable(mfaEnabled); + } + if (!nullToAbsent || trafficMethod != null) { + map['traffic_method'] = Variable(trafficMethod); + } + if (!nullToAbsent || mfaMethod != null) { + map['mfa_method'] = Variable(mfaMethod); + } + map['keep_alive_interval'] = Variable(keepAliveInterval); + if (!nullToAbsent || locationMfaMode != null) { + map['location_mfa_mode'] = Variable(locationMfaMode); + } + if (!nullToAbsent || postureCheckRequired != null) { + map['posture_check_required'] = Variable(postureCheckRequired); + } + return map; + } + + LocationsCompanion toCompanion(bool nullToAbsent) { + return LocationsCompanion( + id: Value(id), + instance: Value(instance), + networkId: Value(networkId), + name: Value(name), + address: Value(address), + pubKey: Value(pubKey), + endpoint: Value(endpoint), + allowedIps: Value(allowedIps), + dns: dns == null && nullToAbsent ? const Value.absent() : Value(dns), + mfaEnabled: mfaEnabled == null && nullToAbsent + ? const Value.absent() + : Value(mfaEnabled), + trafficMethod: trafficMethod == null && nullToAbsent + ? const Value.absent() + : Value(trafficMethod), + mfaMethod: mfaMethod == null && nullToAbsent + ? const Value.absent() + : Value(mfaMethod), + keepAliveInterval: Value(keepAliveInterval), + locationMfaMode: locationMfaMode == null && nullToAbsent + ? const Value.absent() + : Value(locationMfaMode), + postureCheckRequired: postureCheckRequired == null && nullToAbsent + ? const Value.absent() + : Value(postureCheckRequired), + ); + } + + factory LocationsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocationsData( + id: serializer.fromJson(json['id']), + instance: serializer.fromJson(json['instance']), + networkId: serializer.fromJson(json['networkId']), + name: serializer.fromJson(json['name']), + address: serializer.fromJson(json['address']), + pubKey: serializer.fromJson(json['pubKey']), + endpoint: serializer.fromJson(json['endpoint']), + allowedIps: serializer.fromJson(json['allowedIps']), + dns: serializer.fromJson(json['dns']), + mfaEnabled: serializer.fromJson(json['mfaEnabled']), + trafficMethod: serializer.fromJson(json['trafficMethod']), + mfaMethod: serializer.fromJson(json['mfaMethod']), + keepAliveInterval: serializer.fromJson(json['keepAliveInterval']), + locationMfaMode: serializer.fromJson(json['locationMfaMode']), + postureCheckRequired: serializer.fromJson( + json['postureCheckRequired'], + ), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'instance': serializer.toJson(instance), + 'networkId': serializer.toJson(networkId), + 'name': serializer.toJson(name), + 'address': serializer.toJson(address), + 'pubKey': serializer.toJson(pubKey), + 'endpoint': serializer.toJson(endpoint), + 'allowedIps': serializer.toJson(allowedIps), + 'dns': serializer.toJson(dns), + 'mfaEnabled': serializer.toJson(mfaEnabled), + 'trafficMethod': serializer.toJson(trafficMethod), + 'mfaMethod': serializer.toJson(mfaMethod), + 'keepAliveInterval': serializer.toJson(keepAliveInterval), + 'locationMfaMode': serializer.toJson(locationMfaMode), + 'postureCheckRequired': serializer.toJson(postureCheckRequired), + }; + } + + LocationsData copyWith({ + int? id, + int? instance, + int? networkId, + String? name, + String? address, + String? pubKey, + String? endpoint, + String? allowedIps, + Value dns = const Value.absent(), + Value mfaEnabled = const Value.absent(), + Value trafficMethod = const Value.absent(), + Value mfaMethod = const Value.absent(), + int? keepAliveInterval, + Value locationMfaMode = const Value.absent(), + Value postureCheckRequired = const Value.absent(), + }) => LocationsData( + id: id ?? this.id, + instance: instance ?? this.instance, + networkId: networkId ?? this.networkId, + name: name ?? this.name, + address: address ?? this.address, + pubKey: pubKey ?? this.pubKey, + endpoint: endpoint ?? this.endpoint, + allowedIps: allowedIps ?? this.allowedIps, + dns: dns.present ? dns.value : this.dns, + mfaEnabled: mfaEnabled.present ? mfaEnabled.value : this.mfaEnabled, + trafficMethod: trafficMethod.present + ? trafficMethod.value + : this.trafficMethod, + mfaMethod: mfaMethod.present ? mfaMethod.value : this.mfaMethod, + keepAliveInterval: keepAliveInterval ?? this.keepAliveInterval, + locationMfaMode: locationMfaMode.present + ? locationMfaMode.value + : this.locationMfaMode, + postureCheckRequired: postureCheckRequired.present + ? postureCheckRequired.value + : this.postureCheckRequired, + ); + LocationsData copyWithCompanion(LocationsCompanion data) { + return LocationsData( + id: data.id.present ? data.id.value : this.id, + instance: data.instance.present ? data.instance.value : this.instance, + networkId: data.networkId.present ? data.networkId.value : this.networkId, + name: data.name.present ? data.name.value : this.name, + address: data.address.present ? data.address.value : this.address, + pubKey: data.pubKey.present ? data.pubKey.value : this.pubKey, + endpoint: data.endpoint.present ? data.endpoint.value : this.endpoint, + allowedIps: data.allowedIps.present + ? data.allowedIps.value + : this.allowedIps, + dns: data.dns.present ? data.dns.value : this.dns, + mfaEnabled: data.mfaEnabled.present + ? data.mfaEnabled.value + : this.mfaEnabled, + trafficMethod: data.trafficMethod.present + ? data.trafficMethod.value + : this.trafficMethod, + mfaMethod: data.mfaMethod.present ? data.mfaMethod.value : this.mfaMethod, + keepAliveInterval: data.keepAliveInterval.present + ? data.keepAliveInterval.value + : this.keepAliveInterval, + locationMfaMode: data.locationMfaMode.present + ? data.locationMfaMode.value + : this.locationMfaMode, + postureCheckRequired: data.postureCheckRequired.present + ? data.postureCheckRequired.value + : this.postureCheckRequired, + ); + } + + @override + String toString() { + return (StringBuffer('LocationsData(') + ..write('id: $id, ') + ..write('instance: $instance, ') + ..write('networkId: $networkId, ') + ..write('name: $name, ') + ..write('address: $address, ') + ..write('pubKey: $pubKey, ') + ..write('endpoint: $endpoint, ') + ..write('allowedIps: $allowedIps, ') + ..write('dns: $dns, ') + ..write('mfaEnabled: $mfaEnabled, ') + ..write('trafficMethod: $trafficMethod, ') + ..write('mfaMethod: $mfaMethod, ') + ..write('keepAliveInterval: $keepAliveInterval, ') + ..write('locationMfaMode: $locationMfaMode, ') + ..write('postureCheckRequired: $postureCheckRequired') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + instance, + networkId, + name, + address, + pubKey, + endpoint, + allowedIps, + dns, + mfaEnabled, + trafficMethod, + mfaMethod, + keepAliveInterval, + locationMfaMode, + postureCheckRequired, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocationsData && + other.id == this.id && + other.instance == this.instance && + other.networkId == this.networkId && + other.name == this.name && + other.address == this.address && + other.pubKey == this.pubKey && + other.endpoint == this.endpoint && + other.allowedIps == this.allowedIps && + other.dns == this.dns && + other.mfaEnabled == this.mfaEnabled && + other.trafficMethod == this.trafficMethod && + other.mfaMethod == this.mfaMethod && + other.keepAliveInterval == this.keepAliveInterval && + other.locationMfaMode == this.locationMfaMode && + other.postureCheckRequired == this.postureCheckRequired); +} + +class LocationsCompanion extends UpdateCompanion { + final Value id; + final Value instance; + final Value networkId; + final Value name; + final Value address; + final Value pubKey; + final Value endpoint; + final Value allowedIps; + final Value dns; + final Value mfaEnabled; + final Value trafficMethod; + final Value mfaMethod; + final Value keepAliveInterval; + final Value locationMfaMode; + final Value postureCheckRequired; + const LocationsCompanion({ + this.id = const Value.absent(), + this.instance = const Value.absent(), + this.networkId = const Value.absent(), + this.name = const Value.absent(), + this.address = const Value.absent(), + this.pubKey = const Value.absent(), + this.endpoint = const Value.absent(), + this.allowedIps = const Value.absent(), + this.dns = const Value.absent(), + this.mfaEnabled = const Value.absent(), + this.trafficMethod = const Value.absent(), + this.mfaMethod = const Value.absent(), + this.keepAliveInterval = const Value.absent(), + this.locationMfaMode = const Value.absent(), + this.postureCheckRequired = const Value.absent(), + }); + LocationsCompanion.insert({ + this.id = const Value.absent(), + required int instance, + required int networkId, + required String name, + required String address, + required String pubKey, + required String endpoint, + required String allowedIps, + this.dns = const Value.absent(), + this.mfaEnabled = const Value.absent(), + this.trafficMethod = const Value.absent(), + this.mfaMethod = const Value.absent(), + required int keepAliveInterval, + this.locationMfaMode = const Value.absent(), + this.postureCheckRequired = const Value.absent(), + }) : instance = Value(instance), + networkId = Value(networkId), + name = Value(name), + address = Value(address), + pubKey = Value(pubKey), + endpoint = Value(endpoint), + allowedIps = Value(allowedIps), + keepAliveInterval = Value(keepAliveInterval); + static Insertable custom({ + Expression? id, + Expression? instance, + Expression? networkId, + Expression? name, + Expression? address, + Expression? pubKey, + Expression? endpoint, + Expression? allowedIps, + Expression? dns, + Expression? mfaEnabled, + Expression? trafficMethod, + Expression? mfaMethod, + Expression? keepAliveInterval, + Expression? locationMfaMode, + Expression? postureCheckRequired, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (instance != null) 'instance': instance, + if (networkId != null) 'network_id': networkId, + if (name != null) 'name': name, + if (address != null) 'address': address, + if (pubKey != null) 'pub_key': pubKey, + if (endpoint != null) 'endpoint': endpoint, + if (allowedIps != null) 'allowed_ips': allowedIps, + if (dns != null) 'dns': dns, + if (mfaEnabled != null) 'mfa_enabled': mfaEnabled, + if (trafficMethod != null) 'traffic_method': trafficMethod, + if (mfaMethod != null) 'mfa_method': mfaMethod, + if (keepAliveInterval != null) 'keep_alive_interval': keepAliveInterval, + if (locationMfaMode != null) 'location_mfa_mode': locationMfaMode, + if (postureCheckRequired != null) + 'posture_check_required': postureCheckRequired, + }); + } + + LocationsCompanion copyWith({ + Value? id, + Value? instance, + Value? networkId, + Value? name, + Value? address, + Value? pubKey, + Value? endpoint, + Value? allowedIps, + Value? dns, + Value? mfaEnabled, + Value? trafficMethod, + Value? mfaMethod, + Value? keepAliveInterval, + Value? locationMfaMode, + Value? postureCheckRequired, + }) { + return LocationsCompanion( + id: id ?? this.id, + instance: instance ?? this.instance, + networkId: networkId ?? this.networkId, + name: name ?? this.name, + address: address ?? this.address, + pubKey: pubKey ?? this.pubKey, + endpoint: endpoint ?? this.endpoint, + allowedIps: allowedIps ?? this.allowedIps, + dns: dns ?? this.dns, + mfaEnabled: mfaEnabled ?? this.mfaEnabled, + trafficMethod: trafficMethod ?? this.trafficMethod, + mfaMethod: mfaMethod ?? this.mfaMethod, + keepAliveInterval: keepAliveInterval ?? this.keepAliveInterval, + locationMfaMode: locationMfaMode ?? this.locationMfaMode, + postureCheckRequired: postureCheckRequired ?? this.postureCheckRequired, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (instance.present) { + map['instance'] = Variable(instance.value); + } + if (networkId.present) { + map['network_id'] = Variable(networkId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (address.present) { + map['address'] = Variable(address.value); + } + if (pubKey.present) { + map['pub_key'] = Variable(pubKey.value); + } + if (endpoint.present) { + map['endpoint'] = Variable(endpoint.value); + } + if (allowedIps.present) { + map['allowed_ips'] = Variable(allowedIps.value); + } + if (dns.present) { + map['dns'] = Variable(dns.value); + } + if (mfaEnabled.present) { + map['mfa_enabled'] = Variable(mfaEnabled.value); + } + if (trafficMethod.present) { + map['traffic_method'] = Variable(trafficMethod.value); + } + if (mfaMethod.present) { + map['mfa_method'] = Variable(mfaMethod.value); + } + if (keepAliveInterval.present) { + map['keep_alive_interval'] = Variable(keepAliveInterval.value); + } + if (locationMfaMode.present) { + map['location_mfa_mode'] = Variable(locationMfaMode.value); + } + if (postureCheckRequired.present) { + map['posture_check_required'] = Variable( + postureCheckRequired.value, + ); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocationsCompanion(') + ..write('id: $id, ') + ..write('instance: $instance, ') + ..write('networkId: $networkId, ') + ..write('name: $name, ') + ..write('address: $address, ') + ..write('pubKey: $pubKey, ') + ..write('endpoint: $endpoint, ') + ..write('allowedIps: $allowedIps, ') + ..write('dns: $dns, ') + ..write('mfaEnabled: $mfaEnabled, ') + ..write('trafficMethod: $trafficMethod, ') + ..write('mfaMethod: $mfaMethod, ') + ..write('keepAliveInterval: $keepAliveInterval, ') + ..write('locationMfaMode: $locationMfaMode, ') + ..write('postureCheckRequired: $postureCheckRequired') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV5 extends GeneratedDatabase { + DatabaseAtV5(QueryExecutor e) : super(e); + late final DefguardInstances defguardInstances = DefguardInstances(this); + late final Locations locations = Locations(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + defguardInstances, + locations, + ]; + @override + int get schemaVersion => 5; +} diff --git a/client/test/drift/defguard/migration_test.dart b/client/test/drift/defguard/migration_test.dart index 6ab73b27..94af3f97 100644 --- a/client/test/drift/defguard/migration_test.dart +++ b/client/test/drift/defguard/migration_test.dart @@ -1,6 +1,7 @@ // dart format width=80 // ignore_for_file: unused_local_variable, unused_import import 'package:drift/drift.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:drift_dev/api/migrations_native.dart'; import 'package:mobile/data/db/database.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -8,6 +9,8 @@ import 'generated/schema.dart'; import 'generated/schema_v1.dart' as v1; import 'generated/schema_v2.dart' as v2; +import 'generated/schema_v4.dart' as v4; +import 'generated/schema_v5.dart' as v5; void main() { driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; @@ -76,4 +79,55 @@ void main() { }, ); }); + + test( + 'migration from v4 to v5 moves the secrets to the secure storage', + () async { + final storedSecrets = {}; + FlutterSecureStorage.setMockInitialValues(storedSecrets); + + final oldDefguardInstancesData = [ + const v4.DefguardInstancesData( + id: 1, + name: 'instance', + uuid: 'instance-uuid', + url: 'https://defguard.example', + deviceId: 7, + proxyUrl: 'https://proxy.defguard.example', + username: 'user', + poolingToken: 'polling-token', + clientTrafficPolicy: 0, + enterpriseEnabled: true, + pubKey: 'public-key', + privateKey: 'private-key', + mfaKeysStored: false, + ), + ]; + + await verifier.testWithDataIntegrity( + oldVersion: 4, + newVersion: 5, + createOld: v4.DatabaseAtV4.new, + createNew: v5.DatabaseAtV5.new, + openTestedDatabase: AppDatabase.new, + createItems: (batch, oldDb) { + batch.insertAll(oldDb.defguardInstances, oldDefguardInstancesData); + }, + validateItems: (newDb) async { + // the instance is kept, only its secrets moved + final instance = await newDb + .select(newDb.defguardInstances) + .getSingle(); + expect(instance.uuid, 'instance-uuid'); + expect(instance.deviceId, 7); + expect(instance.pubKey, 'public-key'); + + expect(storedSecrets, { + 'wg-key-instance-uuid-7': 'private-key', + 'token-instance-uuid-7': 'polling-token', + }); + }, + ); + }, + ); }