Files
smart-city-digital-twin-mar…/smart-app-city/frontend/node_modules/expo-secure-store/ios/SecureStoreModule.swift
Eric FELIXINE e30ae8ed09 feat(smart-app): implement complete mobile app MVP
- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
2026-06-01 18:00:35 -04:00

208 lines
7.5 KiB
Swift

import ExpoModulesCore
import LocalAuthentication
import Security
public final class SecureStoreModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoSecureStore")
Constants([
"AFTER_FIRST_UNLOCK": SecureStoreAccessible.afterFirstUnlock.rawValue,
"AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY": SecureStoreAccessible.afterFirstUnlockThisDeviceOnly.rawValue,
"ALWAYS": SecureStoreAccessible.always.rawValue,
"WHEN_PASSCODE_SET_THIS_DEVICE_ONLY": SecureStoreAccessible.whenPasscodeSetThisDeviceOnly.rawValue,
"ALWAYS_THIS_DEVICE_ONLY": SecureStoreAccessible.alwaysThisDeviceOnly.rawValue,
"WHEN_UNLOCKED": SecureStoreAccessible.whenUnlocked.rawValue,
"WHEN_UNLOCKED_THIS_DEVICE_ONLY": SecureStoreAccessible.whenUnlockedThisDeviceOnly.rawValue
])
AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) -> String? in
return try get(with: key, options: options)
}
Function("getValueWithKeySync") { (key: String, options: SecureStoreOptions) -> String? in
return try get(with: key, options: options)
}
AsyncFunction("setValueWithKeyAsync") { (value: String, key: String, options: SecureStoreOptions) -> Bool in
guard let key = validate(for: key) else {
throw InvalidKeyException()
}
return try set(value: value, with: key, options: options)
}
Function("setValueWithKeySync") {(value: String, key: String, options: SecureStoreOptions) -> Bool in
guard let key = validate(for: key) else {
throw InvalidKeyException()
}
return try set(value: value, with: key, options: options)
}
AsyncFunction("deleteValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in
let noAuthSearchDictionary = query(with: key, options: options, requireAuthentication: false)
let authSearchDictionary = query(with: key, options: options, requireAuthentication: true)
let legacySearchDictionary = query(with: key, options: options)
SecItemDelete(legacySearchDictionary as CFDictionary)
SecItemDelete(authSearchDictionary as CFDictionary)
SecItemDelete(noAuthSearchDictionary as CFDictionary)
}
Function("canUseBiometricAuthentication") {() -> Bool in
let context = LAContext()
var error: NSError?
let isBiometricsSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error)
if error != nil {
return false
}
return isBiometricsSupported
}
}
private func get(with key: String, options: SecureStoreOptions) throws -> String? {
guard let key = validate(for: key) else {
throw InvalidKeyException()
}
if let unauthenticatedItem = try searchKeyChain(with: key, options: options, requireAuthentication: false) {
return String(data: unauthenticatedItem, encoding: .utf8)
}
if let authenticatedItem = try searchKeyChain(with: key, options: options, requireAuthentication: true) {
return String(data: authenticatedItem, encoding: .utf8)
}
if let legacyItem = try searchKeyChain(with: key, options: options) {
return String(data: legacyItem, encoding: .utf8)
}
return nil
}
private func set(value: String, with key: String, options: SecureStoreOptions) throws -> Bool {
var setItemQuery = query(with: key, options: options, requireAuthentication: options.requireAuthentication)
let valueData = value.data(using: .utf8)
setItemQuery[kSecValueData as String] = valueData
let accessibility = attributeWith(options: options)
if !options.requireAuthentication {
setItemQuery[kSecAttrAccessible as String] = accessibility
} else {
guard let _ = Bundle.main.infoDictionary?["NSFaceIDUsageDescription"] as? String else {
throw MissingPlistKeyException()
}
let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, SecAccessControlCreateFlags.biometryCurrentSet, nil)
setItemQuery[kSecAttrAccessControl as String] = accessOptions
}
let status = SecItemAdd(setItemQuery as CFDictionary, nil)
switch status {
case errSecSuccess:
// On success we want to remove the other key alias and legacy key (if they exist) to avoid conflicts during reads
SecItemDelete(query(with: key, options: options) as CFDictionary)
SecItemDelete(query(with: key, options: options, requireAuthentication: !options.requireAuthentication) as CFDictionary)
return true
case errSecDuplicateItem:
return try update(value: value, with: key, options: options)
default:
throw KeyChainException(status)
}
}
private func update(value: String, with key: String, options: SecureStoreOptions) throws -> Bool {
var query = query(with: key, options: options, requireAuthentication: options.requireAuthentication)
let valueData = value.data(using: .utf8)
let updateDictionary = [kSecValueData as String: valueData]
if let authPrompt = options.authenticationPrompt {
query[kSecUseOperationPrompt as String] = authPrompt
}
let status = SecItemUpdate(query as CFDictionary, updateDictionary as CFDictionary)
if status == errSecSuccess {
return true
} else {
throw KeyChainException(status)
}
}
private func searchKeyChain(with key: String, options: SecureStoreOptions, requireAuthentication: Bool? = nil) throws -> Data? {
var query = query(with: key, options: options, requireAuthentication: requireAuthentication)
query[kSecMatchLimit as String] = kSecMatchLimitOne
query[kSecReturnData as String] = kCFBooleanTrue
if let authPrompt = options.authenticationPrompt {
query[kSecUseOperationPrompt as String] = authPrompt
}
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
switch status {
case errSecSuccess:
guard let item = item as? Data else {
return nil
}
return item
case errSecItemNotFound:
return nil
default:
throw KeyChainException(status)
}
}
private func query(with key: String, options: SecureStoreOptions, requireAuthentication: Bool? = nil) -> [String: Any] {
var service = options.keychainService ?? "app"
if let requireAuthentication {
service.append(":\(requireAuthentication ? "auth" : "no-auth")")
}
let encodedKey = Data(key.utf8)
return [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrGeneric as String: encodedKey,
kSecAttrAccount as String: encodedKey
]
}
private func attributeWith(options: SecureStoreOptions) -> CFString {
switch options.keychainAccessible {
case .afterFirstUnlock:
return kSecAttrAccessibleAfterFirstUnlock
case .afterFirstUnlockThisDeviceOnly:
return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
case .always:
return kSecAttrAccessibleAlways
case .whenPasscodeSetThisDeviceOnly:
return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
case .whenUnlocked:
return kSecAttrAccessibleWhenUnlocked
case .alwaysThisDeviceOnly:
return kSecAttrAccessibleAlwaysThisDeviceOnly
case .whenUnlockedThisDeviceOnly:
return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
default:
return kSecAttrAccessibleWhenUnlocked
}
}
private func validate(for key: String) -> String? {
let trimmedKey = key.trimmingCharacters(in: .whitespaces)
if trimmedKey.isEmpty {
return nil
}
return key
}
}