Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 6 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -13,7 +16,11 @@ let package = Package(
targets: [
.target(
name: "KeychainLibrary",
dependencies: [])
]
dependencies: [],
swiftSettings: [
.swiftLanguageMode(.v5)
])
],
swiftLanguageModes: [.v5]
)

100 changes: 59 additions & 41 deletions Sources/KeychainLibrary.swift
Original file line number Diff line number Diff line change
@@ -1,38 +1,43 @@
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<Int8>, cStringService: UnsafePointer<Int8>) -> Bool {
public func addToKeychain(cStringData: UnsafePointer<Int8>, cStringService: UnsafePointer<Int8>)
-> Bool
{
let data = String(cString: cStringData).data(using: .utf8)!
let service = String(cString: cStringService)

if #available(macOS 10.13.4, *) {
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
}

let query: [String: Any] = [
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
}

Expand All @@ -41,36 +46,29 @@ public func addToKeychain(cStringData: UnsafePointer<Int8>, cStringService: Unsa
public func getFromKeychain(
cStringService: UnsafePointer<Int8>,
requireBiometrics: Bool,
callback: @escaping @convention(c) (UnsafePointer<Int8>?, UnsafePointer<Int8>?
) -> Void) {
callback: @escaping @convention(c) (
UnsafePointer<Int8>?, UnsafePointer<Int8>?
) -> 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 {
Expand All @@ -80,26 +78,40 @@ enum BiometricAuthenticationError: Error {
case unknown(String)
}

func _getFromKeychain(service: String, requireBiometrics: Bool, completion: @escaping (Result<String, BiometricAuthenticationError>) -> Void) throws {
func _getFromKeychain(
service: String, requireBiometrics: Bool,
completion: @escaping @Sendable (Result<String, BiometricAuthenticationError>) -> 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
}

// Check biometric authentication
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")))
}
Expand All @@ -110,17 +122,20 @@ func _getFromKeychain(service: String, requireBiometrics: Bool, completion: @esc
}
}

func _getPassword(context: LAContext, service: String, completion: @escaping (Result<String, BiometricAuthenticationError>) -> Void) {
func _getPassword(
context: LAContext, service: String,
completion: @escaping @Sendable (Result<String, BiometricAuthenticationError>) -> 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))
Expand All @@ -135,12 +150,12 @@ func _getPassword(context: LAContext, service: String, completion: @escaping (Re
@_cdecl("deleteFromKeychain")
public func deleteFromKeychain(cStringService: UnsafePointer<Int8>) -> 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
}
Expand All @@ -152,19 +167,22 @@ public func isBiometricsSupported() -> Bool {
}

@_cdecl("requestBiometricsVerification")
public func requestBiometricsVerification(cStringReason: UnsafePointer<Int8>, callback: @escaping @convention(c) (Bool) -> Void) {
public func requestBiometricsVerification(
cStringReason: UnsafePointer<Int8>, 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 {
Expand Down
Loading