From 362cf9777c50518a320393e29dd927c7d8b85741 Mon Sep 17 00:00:00 2001 From: Jordan Nelson Date: Mon, 13 Nov 2023 13:25:56 -0500 Subject: [PATCH] feat: migrate device secrets on iOS --- .../legacy_credential_provider_android.dart | 32 +++++ .../legacy_credential_provider_impl.dart | 40 ++++++ .../legacy_credential_provider_ios.dart | 108 +++++++++++++++++ .../credentials/legacy_ios_cognito_keys.dart | 63 ++++++++++ .../legacy_credential_provider.dart | 41 +++++++ .../lib/src/model/cognito_device_secrets.dart | 13 ++ .../credential_store_state_machine.dart | 114 ++++++++++++++---- 7 files changed, 390 insertions(+), 21 deletions(-) diff --git a/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_credential_provider_android.dart b/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_credential_provider_android.dart index 101164b5b4..542d72c7ac 100644 --- a/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_credential_provider_android.dart +++ b/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_credential_provider_android.dart @@ -45,4 +45,36 @@ class LegacyCredentialProviderAndroid implements LegacyCredentialProvider { final bridge = _stateMachine.expect(); return bridge.clearLegacyCredentials(); } + + @override + Future fetchLegacyDeviceSecrets({ + CognitoUserPoolConfig? userPoolConfig, + }) { + // TODO: implement fetchLegacyDeviceSecrets + throw UnimplementedError(); + } + + @override + Future deleteLegacyDeviceSecrets({ + CognitoUserPoolConfig? userPoolConfig, + }) { + // TODO: implement deleteLegacyDeviceSecrets + throw UnimplementedError(); + } + + @override + Future fetchLegacyAsfDeviceId({ + CognitoUserPoolConfig? userPoolConfig, + }) { + // TODO: implement fetchLegacyAsfDeviceId + throw UnimplementedError(); + } + + @override + Future deleteLegacyAsfDeviceID({ + CognitoUserPoolConfig? userPoolConfig, + }) { + // TODO: implement deleteLegacyAsfDeviceID + throw UnimplementedError(); + } } diff --git a/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_credential_provider_impl.dart b/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_credential_provider_impl.dart index 3421989ce2..0be19235fb 100644 --- a/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_credential_provider_impl.dart +++ b/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_credential_provider_impl.dart @@ -61,4 +61,44 @@ class LegacyCredentialProviderImpl implements LegacyCredentialProvider { hostedUiConfig: hostedUiConfig, ); } + + @override + Future fetchLegacyDeviceSecrets({ + CognitoUserPoolConfig? userPoolConfig, + }) async { + if (_instance == null) return null; + return _instance!.fetchLegacyDeviceSecrets( + userPoolConfig: userPoolConfig, + ); + } + + @override + Future deleteLegacyDeviceSecrets({ + CognitoUserPoolConfig? userPoolConfig, + }) async { + if (_instance == null) return; + return _instance!.deleteLegacyDeviceSecrets( + userPoolConfig: userPoolConfig, + ); + } + + @override + Future fetchLegacyAsfDeviceId({ + CognitoUserPoolConfig? userPoolConfig, + }) async { + if (_instance == null) return null; + return _instance!.fetchLegacyAsfDeviceId( + userPoolConfig: userPoolConfig, + ); + } + + @override + Future deleteLegacyAsfDeviceID({ + CognitoUserPoolConfig? userPoolConfig, + }) async { + if (_instance == null) return; + return _instance!.deleteLegacyAsfDeviceID( + userPoolConfig: userPoolConfig, + ); + } } diff --git a/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_credential_provider_ios.dart b/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_credential_provider_ios.dart index df9497d9a0..2ca892718a 100644 --- a/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_credential_provider_ios.dart +++ b/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_credential_provider_ios.dart @@ -160,6 +160,114 @@ class LegacyCredentialProviderIOS implements LegacyCredentialProvider { } } + @override + Future fetchLegacyDeviceSecrets({ + CognitoUserPoolConfig? userPoolConfig, + }) async { + if (userPoolConfig != null) { + final userPoolStorage = await _getUserPoolStorage(); + final cognitoUserKeys = LegacyCognitoUserKeys(userPoolConfig); + final currentUserId = await userPoolStorage.read( + key: cognitoUserKeys[LegacyCognitoKey.currentUser], + ); + if (currentUserId != null) { + final keys = LegacyDeviceSecretKeys( + currentUserId, + userPoolConfig, + ); + final deviceKey = await userPoolStorage.read( + key: keys[LegacyDeviceSecretKey.id], + ); + final devicePassword = await userPoolStorage.read( + key: keys[LegacyDeviceSecretKey.secret], + ); + final deviceGroupKey = await userPoolStorage.read( + key: keys[LegacyDeviceSecretKey.group], + ); + if (deviceKey != null && + devicePassword != null && + deviceGroupKey != null) { + return LegacyDeviceDetails( + deviceKey: deviceKey, + deviceGroupKey: deviceGroupKey, + devicePassword: devicePassword, + ); + } + } + } + + return null; + } + + @override + Future deleteLegacyDeviceSecrets({ + CognitoUserPoolConfig? userPoolConfig, + }) async { + if (userPoolConfig != null) { + final userPoolStorage = await _getUserPoolStorage(); + final cognitoUserKeys = LegacyCognitoUserKeys(userPoolConfig); + final currentUserId = await userPoolStorage.read( + key: cognitoUserKeys[LegacyCognitoKey.currentUser], + ); + if (currentUserId != null) { + final keys = LegacyDeviceSecretKeys( + currentUserId, + userPoolConfig, + ); + await userPoolStorage.deleteMany([ + keys[LegacyDeviceSecretKey.id], + keys[LegacyDeviceSecretKey.secret], + keys[LegacyDeviceSecretKey.group], + ]); + } + } + } + + @override + Future fetchLegacyAsfDeviceId({ + CognitoUserPoolConfig? userPoolConfig, + }) async { + if (userPoolConfig != null) { + final userPoolStorage = await _getUserPoolStorage(); + final cognitoUserKeys = LegacyCognitoUserKeys(userPoolConfig); + final currentUserId = await userPoolStorage.read( + key: cognitoUserKeys[LegacyCognitoKey.currentUser], + ); + if (currentUserId != null) { + final keys = LegacyAsfDeviceKeys( + currentUserId, + userPoolConfig, + ); + final deviceKey = await userPoolStorage.read( + key: keys[LegacyAsfDeviceKey.id], + ); + return deviceKey; + } + } + + return null; + } + + @override + Future deleteLegacyAsfDeviceID({ + CognitoUserPoolConfig? userPoolConfig, + }) async { + if (userPoolConfig != null) { + final userPoolStorage = await _getUserPoolStorage(); + final cognitoUserKeys = LegacyCognitoUserKeys(userPoolConfig); + final currentUserId = await userPoolStorage.read( + key: cognitoUserKeys[LegacyCognitoKey.currentUser], + ); + if (currentUserId != null) { + final keys = LegacyAsfDeviceKeys( + currentUserId, + userPoolConfig, + ); + await userPoolStorage.delete(key: keys[LegacyAsfDeviceKey.id]); + } + } + } + final _bundleIdMemoizer = AsyncMemoizer(); /// Gets the bundle ID. diff --git a/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_ios_cognito_keys.dart b/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_ios_cognito_keys.dart index ab69acba81..a7f74a832d 100644 --- a/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_ios_cognito_keys.dart +++ b/packages/auth/amplify_auth_cognito/lib/src/credentials/legacy_ios_cognito_keys.dart @@ -53,6 +53,24 @@ enum LegacyCognitoIdentityPoolKey { identityId, } +/// Discrete keys stored for Legacy Device Secrets on iOS. +enum LegacyDeviceSecretKey { + /// The device key. + id, + + /// The device password. + secret, + + /// The device group key. + group, +} + +/// Discrete keys stored for Legacy ASF on iOS. +enum LegacyAsfDeviceKey { + /// The advanced security feature (ASF) device identifier. + id; +} + /// {@template amplify_auth_cognito.legacy_cognito_identity_pool_keys} /// Enumerates and iterates over the keys stored in secure storage by /// legacy Cognito Identity Pool operations. @@ -112,6 +130,51 @@ class LegacyCognitoUserPoolKeys String get prefix => '${config.appClientId}.$currentUserId'; } +/// {@template amplify_auth_cognito.cognito_user_pool_keys} +/// Enumerates and iterates over the keys stored in secure storage for +/// Device Secrets. +/// {@endtemplate} +class LegacyDeviceSecretKeys + extends LegacyIOSCognitoKeys { + /// {@macro amplify_auth_cognito.cognito_user_pool_keys} + const LegacyDeviceSecretKeys(this.currentUserId, this.config); + + /// The Cognito identity pool configuration, used to determine the key + /// prefixes. + final CognitoUserPoolConfig config; + + /// The current user ID, used to determine the key prefixes. + final String currentUserId; + + @override + List get _values => LegacyDeviceSecretKey.values; + + @override + String get prefix => '${config.poolId}.$currentUserId.device'; +} + +/// {@template amplify_auth_cognito.cognito_user_pool_keys} +/// Enumerates and iterates over the keys stored in secure storage for +/// ASF Device Secrets. +/// {@endtemplate} +class LegacyAsfDeviceKeys extends LegacyIOSCognitoKeys { + /// {@macro amplify_auth_cognito.cognito_user_pool_keys} + const LegacyAsfDeviceKeys(this.currentUserId, this.config); + + /// The Cognito identity pool configuration, used to determine the key + /// prefixes. + final CognitoUserPoolConfig config; + + /// The current user ID, used to determine the key prefixes. + final String currentUserId; + + @override + List get _values => LegacyAsfDeviceKey.values; + + @override + String get prefix => '${config.poolId}.$currentUserId.asf.device'; +} + /// {@template amplify_auth_cognito.cognito_keys} /// Iterable secure storage keys. /// {@endtemplate} diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/credentials/legacy_credential_provider.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/credentials/legacy_credential_provider.dart index b01e4bcbaa..d75533fb2f 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/credentials/legacy_credential_provider.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/credentials/legacy_credential_provider.dart @@ -18,10 +18,51 @@ abstract interface class LegacyCredentialProvider { CognitoOAuthConfig? hostedUiConfig, }); + /// Fetches legacy device secrets if they are present. + Future fetchLegacyDeviceSecrets({ + CognitoUserPoolConfig? userPoolConfig, + }); + + /// Fetches legacy asf device ID if present. + Future fetchLegacyAsfDeviceId({ + CognitoUserPoolConfig? userPoolConfig, + }); + /// Deletes legacy credentials if they are present. Future deleteLegacyCredentials({ CognitoUserPoolConfig? userPoolConfig, CognitoIdentityCredentialsProvider? identityPoolConfig, CognitoOAuthConfig? hostedUiConfig, }); + + /// Deletes legacy device secrets if they are present. + Future deleteLegacyDeviceSecrets({ + CognitoUserPoolConfig? userPoolConfig, + }); + + /// Deletes legacy asf device ID if present. + Future deleteLegacyAsfDeviceID({ + CognitoUserPoolConfig? userPoolConfig, + }); +} + +/// {@template amplify_auth_cognito_dart.legacy_device_details} +/// The legacy device details. +/// {@endtemplate} +class LegacyDeviceDetails { + /// {@macro amplify_auth_cognito_dart.legacy_device_details} + const LegacyDeviceDetails({ + required this.deviceKey, + required this.deviceGroupKey, + required this.devicePassword, + }); + + /// The device key/ID. + final String deviceKey; + + /// The device group key. + final String deviceGroupKey; + + /// The device password. + final String devicePassword; } diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/model/cognito_device_secrets.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/model/cognito_device_secrets.dart index 0ad52e8aac..777085a900 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/model/cognito_device_secrets.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/model/cognito_device_secrets.dart @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import 'package:amplify_auth_cognito_dart/src/credentials/legacy_credential_provider.dart'; import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart'; import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; @@ -18,6 +19,18 @@ abstract class CognitoDeviceSecrets factory CognitoDeviceSecrets([ void Function(CognitoDeviceSecretsBuilder) updates, ]) = _$CognitoDeviceSecrets; + + /// Creates a [CognitoDeviceSecrets] from a [LegacyDeviceDetails]. + factory CognitoDeviceSecrets.fromLegacyDeviceDetails( + LegacyDeviceDetails details, + ) { + return CognitoDeviceSecrets((b) { + b + ..devicePassword = details.devicePassword + ..deviceGroupKey = details.deviceGroupKey + ..devicePassword = details.devicePassword; + }); + } CognitoDeviceSecrets._(); @BuiltValueHook(finalizeBuilder: true) diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/credential_store_state_machine.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/credential_store_state_machine.dart index 4ca8fd74f8..7589ebef39 100644 --- a/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/credential_store_state_machine.dart +++ b/packages/auth/amplify_auth_cognito_dart/lib/src/state/machines/credential_store_state_machine.dart @@ -7,9 +7,11 @@ import 'dart:convert'; import 'package:amplify_auth_cognito_dart/amplify_auth_cognito_dart.dart'; import 'package:amplify_auth_cognito_dart/src/credentials/cognito_keys.dart'; import 'package:amplify_auth_cognito_dart/src/credentials/credential_store_keys.dart'; +import 'package:amplify_auth_cognito_dart/src/credentials/device_metadata_repository.dart'; import 'package:amplify_auth_cognito_dart/src/credentials/legacy_credential_provider.dart'; import 'package:amplify_auth_cognito_dart/src/credentials/secure_storage_extension.dart'; import 'package:amplify_auth_cognito_dart/src/model/auth_configuration.dart'; +import 'package:amplify_auth_cognito_dart/src/model/cognito_device_secrets.dart'; import 'package:amplify_auth_cognito_dart/src/model/session/cognito_sign_in_details.dart'; import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart'; import 'package:amplify_auth_cognito_dart/src/state/cognito_state_machine.dart'; @@ -45,6 +47,8 @@ final class CredentialStoreStateMachine SecureStorageInterface get _secureStorage => getOrCreate(); + DeviceMetadataRepository get _deviceRepository => getOrCreate(); + @override Future resolve(CredentialStoreEvent event) async { switch (event) { @@ -310,42 +314,110 @@ final class CredentialStoreStateMachine await _secureStorage.deleteMany(deletions); } + /// Migrates legacy credential store data, include AWS Credentials, User Pool + /// tokens, Device Info, and ASF Device ID. Future _migrateLegacyCredentialStore() async { final version = await getVersion(); if (version != CredentialStoreVersion.none) { return; } - emit(const CredentialStoreState.migratingLegacyStore()); - final legacyCredentialProvider = get(); - if (legacyCredentialProvider != null) { - final authConfig = expect(); + final credentialData = await _migrateLegacyCredentials(); + final username = credentialData?.userPoolTokens?.username; + if (username != null) { + await _migrateDeviceSecrets(username); + } + await _migrateAsfDeviceID(); + await _updateVersion(CredentialStoreVersion.v1); + } + + /// Migrates AWS Credentials and User Pool tokens. + Future _migrateLegacyCredentials() async { + final provider = get(); + final authConfig = expect(); + if (provider == null) return null; + CredentialStoreData? legacyData; + try { + legacyData = await provider.fetchLegacyCredentials( + userPoolConfig: authConfig.userPoolConfig, + identityPoolConfig: authConfig.identityPoolConfig, + hostedUiConfig: authConfig.hostedUiConfig, + ); + if (legacyData != null) { + await _storeCredentials(legacyData); + } + } on Object catch (e, s) { + logger.error('Error migrating legacy credentials', e, s); + } finally { try { - final legacyData = - await legacyCredentialProvider.fetchLegacyCredentials( + await provider.deleteLegacyCredentials( userPoolConfig: authConfig.userPoolConfig, identityPoolConfig: authConfig.identityPoolConfig, hostedUiConfig: authConfig.hostedUiConfig, ); - if (legacyData != null) { - await _storeCredentials(legacyData); - } } on Object catch (e, s) { - logger.error('Error migrating legacy credentials', e, s); - } finally { - try { - await legacyCredentialProvider.deleteLegacyCredentials( - userPoolConfig: authConfig.userPoolConfig, - identityPoolConfig: authConfig.identityPoolConfig, - hostedUiConfig: authConfig.hostedUiConfig, - ); - } on Object catch (e, s) { - logger.error('Error clearing legacy credentials', e, s); - } + logger.error('Error clearing legacy credentials', e, s); } } + return legacyData; + } - await _updateVersion(CredentialStoreVersion.v1); + /// Migrates legacy device secrets. + Future _migrateDeviceSecrets(String username) async { + final credentialProvider = get(); + final authConfig = expect(); + if (credentialProvider == null) return; + try { + final legacySecrets = await credentialProvider.fetchLegacyDeviceSecrets( + userPoolConfig: authConfig.userPoolConfig, + ); + if (legacySecrets != null) { + final secrets = CognitoDeviceSecrets.fromLegacyDeviceDetails( + legacySecrets, + ); + await _deviceRepository.put(username, secrets); + } + } on Object catch (e, s) { + logger.error('Error migrating legacy device secrets', e, s); + } finally { + try { + await credentialProvider.deleteLegacyDeviceSecrets( + userPoolConfig: authConfig.userPoolConfig, + ); + } on Object catch (e, s) { + logger.error('Error clearing legacy device secrets', e, s); + } + } + } + + /// Migrates legacy device secrets. + Future _migrateAsfDeviceID() async { + final credentialProvider = get(); + final authConfig = expect(); + final userPoolConfig = authConfig.userPoolConfig; + if (credentialProvider == null || userPoolConfig == null) return; + final userPoolKeys = CognitoUserPoolKeys(userPoolConfig); + try { + final legacyDeviceId = await credentialProvider.fetchLegacyAsfDeviceId( + userPoolConfig: userPoolConfig, + ); + if (legacyDeviceId != null) { + await _secureStorage.write( + key: userPoolKeys[CognitoUserPoolKey.asfDeviceId], + value: legacyDeviceId, + ); + } + } on Object catch (e, s) { + logger.error('Error migrating legacy ASF device ID', e, s); + } finally { + try { + await _secureStorage.delete( + key: userPoolKeys[CognitoUserPoolKey.asfDeviceId], + ); + } on Object catch (e, s) { + logger.error('Error clearing legacy ASF device ID', e, s); + } + } } /// State machine callback for the [CredentialStoreLoadCredentialStore] event.