Skip to content

Commit

Permalink
Merge pull request #210 from tangem/IOS-1943-biometry-expose-lacontext
Browse files Browse the repository at this point in the history
IOS-1943 Exposing LAContext in biometry code
  • Loading branch information
megakoko authored Nov 8, 2022
2 parents 79db9d8 + c840509 commit 3b75358
Show file tree
Hide file tree
Showing 14 changed files with 158 additions and 105 deletions.
2 changes: 1 addition & 1 deletion Example/TangemSdkExample copy-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<key>NFCReaderUsageDescription</key>
<string>Some reason</string>
<key>NSFaceIDUsageDescription</key>
<string>Use Face ID to save access codes in the app </string>
<string>Use Face ID to save access codes in the app</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
Expand Down
15 changes: 12 additions & 3 deletions TangemSdk/TangemSdk/Common/Core/CardSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,11 @@ public class CardSession {
Log.session("Prepare card session")
preflightReadMode = runnable.preflightReadMode

guard runnable.allowsAccessCodeFromRepository else {
runnable.prepare(self, completion: completion)
return
}

let requestAccessCodeAction = {
self.environment.accessCode = UserCode(.accessCode, value: nil)
self.requestUserCodeIfNeeded(.accessCode) { result in
Expand All @@ -355,7 +360,8 @@ public class CardSession {
switch environment.config.accessCodeRequestPolicy {
case .alwaysWithBiometrics:
if shouldRequestBiometrics {
accessCodeRepository?.unlock { result in
let reason = environment.config.biometricsLocalizedReason
accessCodeRepository?.unlock(localizedReason: reason) { result in
switch result {
case .success:
runnable.prepare(self, completion: completion)
Expand Down Expand Up @@ -518,8 +524,11 @@ public class CardSession {
return
}

accessCodeRepository?.save(code, for: card.cardId) {[weak self] result in
self?.accessCodeRepository?.lock()
do {
try accessCodeRepository?.save(code, for: card.cardId)
accessCodeRepository?.lock()
} catch {
Log.error(error)
}
}

Expand Down
5 changes: 5 additions & 0 deletions TangemSdk/TangemSdk/Common/Core/CardSessionRunnable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public protocol CardSessionRunnable {
/// Mode for preflight read. Change this property only if you understand what to do
var preflightReadMode: PreflightReadMode { get }

/// Allows SDK to fetch access code from the local encrypted repository when running the command
var allowsAccessCodeFromRepository: Bool { get }

/// Simple interface for responses received after sending commands to Tangem cards.
associatedtype Response

Expand All @@ -34,6 +37,8 @@ public protocol CardSessionRunnable {
extension CardSessionRunnable {
public var preflightReadMode: PreflightReadMode { .fullCardRead }

public var allowsAccessCodeFromRepository: Bool { true }

public func prepare(_ session: CardSession, completion: @escaping CompletionResult<Void>) {
completion(.success(()))
}
Expand Down
3 changes: 3 additions & 0 deletions TangemSdk/TangemSdk/Common/Core/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ public struct Config {

/// Access codes request policy
public var accessCodeRequestPolicy: AccessCodeRequestPolicy = .`default`

/// Localized reason for Touch ID. DO NOT leave it empty.
public var biometricsLocalizedReason: String = "touch_id_localized_reason".localized
}

public enum CardIdDisplayFormat {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@
"reset_codes_message_body_backup" = "Tap the linked card";
"reset_codes_message_body_restore_final" = "Tap the restore card again";
"reset_codes_success_message" = "Code was reset successfully";
"touch_id_localized_reason" = "Use Touch ID to save access codes in the app";
"sign_multiple_chunks_part" = "Signing part %d of %d";
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@
"reset_codes_message_body_backup" = "Tap the linked card";
"reset_codes_message_body_restore_final" = "Tap the restore card again";
"reset_codes_success_message" = "Code was reset successfully";
"touch_id_localized_reason" = "Use Touch ID to save access codes in the app";
"sign_multiple_chunks_part" = "Signing part %d of %d";
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@
"reset_codes_message_body_backup" = "Tap the linked card";
"reset_codes_message_body_restore_final" = "Tap the restore card again";
"reset_codes_success_message" = "Code was reset successfully";
"touch_id_localized_reason" = "Use Touch ID to save access codes in the app";
"sign_multiple_chunks_part" = "Signing part %d of %d";
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@
"reset_codes_message_body_backup" = "Tap the linked card";
"reset_codes_message_body_restore_final" = "Tap the restore card again";
"reset_codes_success_message" = "Code was reset successfully";
"touch_id_localized_reason" = "Use Touch ID to save access codes in the app";
"sign_multiple_chunks_part" = "Signing part %d of %d";
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@
"reset_codes_message_body_backup" = "Приложите связанную карту";
"reset_codes_message_body_restore_final" = "Приложите еще раз карту, которую хотите восстановить";
"reset_codes_success_message" = "Код успешно сброшен";
"touch_id_localized_reason" = "Используйте Touch ID для сохранения кодов доступа в приложении";
"sign_multiple_chunks_part" = "Подпись части %d из %d";
Original file line number Diff line number Diff line change
Expand Up @@ -12,53 +12,73 @@ public class AccessCodeRepository {
getCards().isEmpty
}

private let storage: Storage = .init()
private let secureStorage: SecureStorage = .init()
private let biometricsStorage: BiometricsStorage = .init()
private var accessCodes: [String: Data] = .init()

public init() {}
public init() {
if !storage.bool(forKey: .hasClearedAccessCodeRepoOnFirstLaunch) {
clear()
storage.set(boolValue: true, forKey: .hasClearedAccessCodeRepoOnFirstLaunch)
}
}

deinit {
Log.debug("AccessCodeRepository deinit")
}

public func save(_ accessCode: Data, for cardIds: [String], completion: @escaping (Result<Void, TangemSdkError>) -> Void) {
public func save(_ accessCode: Data, for cardIds: [String]) throws {
guard BiometricsUtil.isAvailable else {
completion(.failure(.biometricsUnavailable))
return
throw TangemSdkError.biometricsUnavailable
}

guard updateCodesIfNeeded(with: accessCode, for: cardIds) else {
completion(.success(())) //Nothing changed. Return
return
return //Nothing changed. Return
}

do {
let data = try JSONEncoder().encode(accessCodes)
var savedCardIds = getCards()

for cardId in cardIds {
let storageKey = SecureStorageKey.accessCode(for: cardId)

biometricsStorage.store(data, forKey: .accessCodes) { result in
switch result {
case .success:
self.saveCards()
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
if savedCardIds.contains(cardId) {
try biometricsStorage.delete(storageKey)
}
} catch {
Log.error(error)
completion(.failure(error.toTangemSdkError()))

try biometricsStorage.store(accessCode, forKey: storageKey)

savedCardIds.insert(cardId)
}

saveCards(cardIds: savedCardIds)
}

public func save(_ accessCode: Data, for cardId: String, completion: @escaping (Result<Void, TangemSdkError>) -> Void) {
save(accessCode, for: [cardId], completion: completion)
public func save(_ accessCode: Data, for cardId: String) throws {
try save(accessCode, for: [cardId])
}

public func deleteAccessCode(for cardIds: [String]) throws {
if cardIds.isEmpty {
return
}

var savedCardIds = getCards()
for cardId in cardIds {
guard savedCardIds.contains(cardId) else {
continue
}

try biometricsStorage.delete(SecureStorageKey.accessCode(for: cardId))
savedCardIds.remove(cardId)
}
saveCards(cardIds: savedCardIds)
}

public func clear() {
do {
try biometricsStorage.delete(.accessCodes)
try secureStorage.delete(.cardsWithSavedCodes)
let cardIds = getCards()
try deleteAccessCode(for: Array(cardIds))
} catch {
Log.error(error)
}
Expand All @@ -69,25 +89,36 @@ public class AccessCodeRepository {
return savedCards.contains(cardId)
}

func unlock(completion: @escaping (Result<Void, TangemSdkError>) -> Void) {
func unlock(localizedReason: String, completion: @escaping (Result<Void, TangemSdkError>) -> Void) {
guard BiometricsUtil.isAvailable else {
completion(.failure(.biometricsUnavailable))
return
}

accessCodes = .init()
self.accessCodes = [:]

biometricsStorage.get(.accessCodes) { result in
BiometricsUtil.requestAccess(localizedReason: localizedReason) { [weak self] result in
guard let self = self else { return }

switch result {
case .success(let data):
if let data = data,
let codes = try? JSONDecoder().decode([String: Data].self, from: data) {
self.accessCodes = codes
}
completion(.success(()))
case .failure(let error):
Log.error(error)
completion(.failure(error))
case .success(let context):
do {
var fetchedAccessCodes: [String: Data] = [:]

for cardId in self.getCards() {
let accessCode = try self.biometricsStorage.get(SecureStorageKey.accessCode(for: cardId), context: context)
fetchedAccessCodes[cardId] = accessCode
}

self.accessCodes = fetchedAccessCodes
completion(.success(()))
} catch {
Log.error(error)
completion(.failure(error.toTangemSdkError()))
}
}
}
}
Expand Down Expand Up @@ -135,8 +166,8 @@ public class AccessCodeRepository {
return []
}

private func saveCards() {
if let data = try? JSONEncoder().encode(Set(accessCodes.keys)) {
private func saveCards(cardIds: Set<String>) {
if let data = try? JSONEncoder().encode(cardIds) {
try? secureStorage.store(data, forKey: .cardsWithSavedCodes)
}
}
Expand Down
Loading

0 comments on commit 3b75358

Please sign in to comment.