diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 84a2468..cc745f6 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -8,18 +8,18 @@ on: jobs: build: - runs-on: macos-14 # ARM64 + runs-on: macos-latest # ARM64 steps: - - uses: actions/checkout@v2 - - name: Use Node.js 18.20.x - uses: actions/setup-node@v1 + - uses: actions/checkout@v4 + - name: Use Node.js 22.11.x + uses: actions/setup-node@v4 with: - node-version: 18.20.x - - name: Use Swift 5.9 + node-version: 22.11.x + - name: Use Swift 6.1 uses: swift-actions/setup-swift@v2 with: - swift-version: 5.9 + swift-version: 6.1.0 - run: npm ci - run: npm run lint - run: npm run build diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cb81ba0..3aa02de 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,17 +6,17 @@ on: jobs: build: - runs-on: macos-14 + runs-on: macos-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 18.20.x + node-version: 22.11.x registry-url: 'https://registry.npmjs.org' - - name: Use Swift 5.9 + - name: Use Swift 6.1 uses: swift-actions/setup-swift@v2 with: - swift-version: 5.9 + swift-version: 6.1.0 - run: npm ci && npm run build - name: Publish package on NPM 📦 run: npm publish diff --git a/Package.swift b/Package.swift index 1bdde7e..18fb384 100644 --- a/Package.swift +++ b/Package.swift @@ -1,9 +1,12 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.1 import PackageDescription let package = Package( name: "KeychainLibrary", + platforms: [ + .macOS(.v10_15) + ], products: [ .library( name: "KeychainLibrary", @@ -13,7 +16,11 @@ let package = Package( targets: [ .target( name: "KeychainLibrary", - dependencies: []) - ] + dependencies: [], + swiftSettings: [ + .swiftLanguageMode(.v5) + ]) + ], + swiftLanguageModes: [.v5] ) diff --git a/Sources/KeychainLibrary.swift b/Sources/KeychainLibrary.swift index e3a69c6..36c995e 100644 --- a/Sources/KeychainLibrary.swift +++ b/Sources/KeychainLibrary.swift @@ -1,10 +1,12 @@ import Foundation -import LocalAuthentication +@preconcurrency import LocalAuthentication import Security // Function to add data to keychain with biometrics protection @_cdecl("addToKeychain") -public func addToKeychain(cStringData: UnsafePointer, cStringService: UnsafePointer) -> Bool { +public func addToKeychain(cStringData: UnsafePointer, cStringService: UnsafePointer) + -> Bool +{ let data = String(cString: cStringData).data(using: .utf8)! let service = String(cString: cStringService) @@ -12,8 +14,11 @@ public func addToKeychain(cStringData: UnsafePointer, cStringService: Unsa let context = LAContext() var error: NSError? - guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { - print("Biometric authentication not available: \(error?.localizedDescription ?? "Unknown error")") + guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) + else { + print( + "Biometric authentication not available: \(error?.localizedDescription ?? "Unknown error")" + ) return false } @@ -21,18 +26,18 @@ public func addToKeychain(cStringData: UnsafePointer, cStringService: Unsa kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecValueData as String: data, - kSecUseAuthenticationContext as String: context + kSecUseAuthenticationContext as String: context, ] // Delete existing item if exists SecItemDelete(query as CFDictionary) - + let status = SecItemAdd(query as CFDictionary, nil) // print(SecCopyErrorMessageString(status, nil)!) - + return status == errSecSuccess } - + return false } @@ -41,36 +46,29 @@ public func addToKeychain(cStringData: UnsafePointer, cStringService: Unsa public func getFromKeychain( cStringService: UnsafePointer, requireBiometrics: Bool, - callback: @escaping @convention(c) (UnsafePointer?, UnsafePointer? -) -> Void) { + callback: @escaping @convention(c) ( + UnsafePointer?, UnsafePointer? + ) -> Void +) { let semaphore = DispatchSemaphore(value: 0) - - var resultData: String? - var resultError: Error? do { let service = String(cString: cStringService) try _getFromKeychain(service: service, requireBiometrics: requireBiometrics) { result in switch result { - case .success(let data): - resultData = data - case .failure(let error): - resultError = error + case .success(let data): + callback(nil, data) + case .failure(let error): + callback(error.localizedDescription, nil) } semaphore.signal() } } catch { - resultError = error + callback(error.localizedDescription, nil) semaphore.signal() } - - semaphore.wait() - - if let data = resultData { - return callback(nil, data) - } - callback(resultError?.localizedDescription, nil) + semaphore.wait() } enum BiometricAuthenticationError: Error { @@ -80,13 +78,23 @@ enum BiometricAuthenticationError: Error { case unknown(String) } -func _getFromKeychain(service: String, requireBiometrics: Bool, completion: @escaping (Result) -> Void) throws { +func _getFromKeychain( + service: String, requireBiometrics: Bool, + completion: @escaping @Sendable (Result) -> Void +) throws { let context = LAContext() - + // Check if biometric authentication is available var error: NSError? - guard !requireBiometrics || context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { - completion(.failure(.notAvailable("Biometric authentication not available: \(error?.localizedDescription ?? "Unknown error")"))) + guard + !requireBiometrics + || context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) + else { + completion( + .failure( + .notAvailable( + "Biometric authentication not available: \(error?.localizedDescription ?? "Unknown error")" + ))) return } @@ -94,12 +102,16 @@ func _getFromKeychain(service: String, requireBiometrics: Bool, completion: @esc let policy: LAPolicy = .deviceOwnerAuthenticationWithBiometrics if requireBiometrics { - context.evaluatePolicy(policy, localizedReason: "Access to your secret") { success, evaluateError in + context.evaluatePolicy(policy, localizedReason: "Access to your secret") { + success, evaluateError in if success { _getPassword(context: context, service: service, completion: completion) } else { if let error = evaluateError { - completion(.failure(.failed("Biometric authentication failed: \(error.localizedDescription)"))) + completion( + .failure( + .failed( + "Biometric authentication failed: \(error.localizedDescription)"))) } else { completion(.failure(.failed("Biometric authentication failed"))) } @@ -110,17 +122,20 @@ func _getFromKeychain(service: String, requireBiometrics: Bool, completion: @esc } } -func _getPassword(context: LAContext, service: String, completion: @escaping (Result) -> Void) { +func _getPassword( + context: LAContext, service: String, + completion: @escaping @Sendable (Result) -> Void +) { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecUseAuthenticationContext as String: context, - kSecReturnData as String: true + kSecReturnData as String: true, ] - + var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) - + if status == errSecSuccess, let data = result as? Data { if let dataString = String(data: data, encoding: .utf8) { completion(.success(dataString)) @@ -135,12 +150,12 @@ func _getPassword(context: LAContext, service: String, completion: @escaping (Re @_cdecl("deleteFromKeychain") public func deleteFromKeychain(cStringService: UnsafePointer) -> Bool { let service = String(cString: cStringService) - + let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: service + kSecAttrService as String: service, ] - + let status = SecItemDelete(query as CFDictionary) return status == errSecSuccess } @@ -152,19 +167,22 @@ public func isBiometricsSupported() -> Bool { } @_cdecl("requestBiometricsVerification") -public func requestBiometricsVerification(cStringReason: UnsafePointer, callback: @escaping @convention(c) (Bool) -> Void) { +public func requestBiometricsVerification( + cStringReason: UnsafePointer, callback: @escaping @convention(c) (Bool) -> Void +) { let semaphore = DispatchSemaphore(value: 0) let reason = String(cString: cStringReason) let context = LAContext() - // Check if biometric authentication is available + // Check if biometric authentication is available var error: NSError? guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { callback(false) return } - context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, evaluateError in + context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { + success, evaluateError in if success { callback(true) } else { diff --git a/package-lock.json b/package-lock.json index f4c6c34..a7e3b0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "parcel": "^2.12.0", "parcel-reporter-static-files-copy": "^1.5.3", "prettier": "^3.2.5", + "ts-node": "^10.9.2", "typescript": "^5.4.3" }, "engines": { @@ -147,6 +148,19 @@ "node": ">=4" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -280,6 +294,34 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@lezer/common": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", @@ -2320,6 +2362,34 @@ "node": ">=10.13.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2564,6 +2634,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2604,6 +2687,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2815,6 +2905,13 @@ } } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3011,6 +3108,16 @@ "node": ">=0.10" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4140,6 +4247,13 @@ "node": ">=10" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -4915,6 +5029,50 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -5012,6 +5170,13 @@ "node": ">= 4" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/weak-lru-cache": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", @@ -5045,6 +5210,16 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 06f634f..e4c25c0 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "parcel": "^2.12.0", "parcel-reporter-static-files-copy": "^1.5.3", "prettier": "^3.2.5", + "ts-node": "^10.9.2", "typescript": "^5.4.3" }, "repository": { @@ -59,6 +60,6 @@ } ], "engines": { - "node": ">=18.20.0" + "node": ">=22.11.0" } }