Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Commit

Permalink
Implemented biometric authentication based on FaceID or TouchID, pass…
Browse files Browse the repository at this point in the history
…word is stored in iOS encrypted keychain
  • Loading branch information
aeoliux committed Apr 19, 2024
1 parent 972ebea commit 663f582
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 18 deletions.
16 changes: 14 additions & 2 deletions LibrePass.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
B26C2F082BC6C988008CA547 /* LibrePassEncryptedCipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B26C2EF32BC6C988008CA547 /* LibrePassEncryptedCipher.swift */; };
B26C2F092BC6C988008CA547 /* OATH.swift in Sources */ = {isa = PBXBuildFile; fileRef = B26C2EF42BC6C988008CA547 /* OATH.swift */; };
B26C2F0A2BC6C988008CA547 /* PasswordGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B26C2EF52BC6C988008CA547 /* PasswordGenerator.swift */; };
B291B9D62BD274A1002901E3 /* Biometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B291B9D52BD274A1002901E3 /* Biometry.swift */; };
B29496522BC6C90200128B33 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B29496512BC6C90200128B33 /* Assets.xcassets */; };
B29496552BC6C90200128B33 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B29496542BC6C90200128B33 /* Preview Assets.xcassets */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -56,6 +57,8 @@
B26C2EF32BC6C988008CA547 /* LibrePassEncryptedCipher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibrePassEncryptedCipher.swift; sourceTree = "<group>"; };
B26C2EF42BC6C988008CA547 /* OATH.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OATH.swift; sourceTree = "<group>"; };
B26C2EF52BC6C988008CA547 /* PasswordGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordGenerator.swift; sourceTree = "<group>"; };
B291B9D52BD274A1002901E3 /* Biometry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Biometry.swift; sourceTree = "<group>"; };
B291B9D72BD2824A002901E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
B29496482BC6C90200128B33 /* LibrePass.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LibrePass.app; sourceTree = BUILT_PRODUCTS_DIR; };
B29496512BC6C90200128B33 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
B29496542BC6C90200128B33 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -87,6 +90,7 @@
B26C2EE02BC6C988008CA547 /* Context.swift */,
B26C2EE12BC6C988008CA547 /* NetworkMonitor.swift */,
B26C2EE22BC6C988008CA547 /* Storage.swift */,
B291B9D52BD274A1002901E3 /* Biometry.swift */,
);
path = Context;
sourceTree = "<group>";
Expand Down Expand Up @@ -144,6 +148,7 @@
B294964A2BC6C90200128B33 /* LibrePass */ = {
isa = PBXGroup;
children = (
B291B9D72BD2824A002901E3 /* Info.plist */,
B26C2EF62BC6C988008CA547 /* API */,
B26C2EEC2BC6C988008CA547 /* App */,
B29496512BC6C90200128B33 /* Assets.xcassets */,
Expand Down Expand Up @@ -285,6 +290,7 @@
files = (
B26C2F072BC6C988008CA547 /* LibrePassCipher.swift in Sources */,
B26C2EFA2BC6C988008CA547 /* LibrePassAccountSettings.swift in Sources */,
B291B9D62BD274A1002901E3 /* Biometry.swift in Sources */,
B26C2F042BC6C988008CA547 /* CredentialsUpdate.swift in Sources */,
B26C2EF72BC6C988008CA547 /* Context.swift in Sources */,
B26C2F012BC6C988008CA547 /* Utils.swift in Sources */,
Expand Down Expand Up @@ -437,10 +443,13 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"LibrePass/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = LibrePass/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = LibrePass;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSFaceIDUsageDescription = "Save password in keychain";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
Expand All @@ -450,7 +459,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.3.0;
MARKETING_VERSION = 1.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.github.aeoliux.LibrePass;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
Expand All @@ -472,10 +481,13 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"LibrePass/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = LibrePass/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = LibrePass;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSFaceIDUsageDescription = "Save password in keychain";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
Expand All @@ -485,7 +497,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.3.0;
MARKETING_VERSION = 1.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.github.aeoliux.LibrePass;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
Expand Down
10 changes: 10 additions & 0 deletions LibrePass/API/LibrePassAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,16 @@ struct LibrePassClient {
return nil
}

func validatePassword(credentialsDatabase: LibrePassCredentialsDatabase, password: String) throws -> Bool {
let (_, oldPublicData, oldSharedData) = try self.getKeys(email: credentialsDatabase.email, password: password, argon2options: credentialsDatabase.argon2idParams)

if dataToHexString(data: oldPublicData) != credentialsDatabase.publicKey {
return false
}

return true
}

mutating func updateCredentials(credentialsDatabase: LibrePassCredentialsDatabase, oldPassword: String, newEmail: String, newPassword: String?, newPasswordHint: String?, vault: [LibrePassEncryptedCipher]) throws {
struct CompactCipher: Codable {
var id: String
Expand Down
101 changes: 101 additions & 0 deletions LibrePass/App/Context/Biometry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// Biometry.swift
// LibrePass
//
// Created by Zapomnij on 19/04/2024.
//

import Foundation
import LocalAuthentication

func setUpBiometricalAuthentication(password: String) async -> Bool {
let context = LAContext()

if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) {
do {
try await context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "Save password in keychain")

guard let access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, .userPresence, nil) else {
return false
}

guard let passwordData = password.data(using: .utf8) else {
return false
}

let query: [String: Any] =
[kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "LibrePass",
kSecAttrAccessControl as String: access,
kSecUseAuthenticationContext as String: context,
kSecValueData as String: passwordData as Data]
SecItemDelete(query as CFDictionary)

let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecSuccess {
return true
}
} catch {

}
}

return false
}

func accessKeychain() async -> String? {
let context = LAContext()

if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) {
do {
try await context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "Save password in keychain")

let query: [String: Any] =
[kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "LibrePass",
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnAttributes as String: true,
kSecUseAuthenticationContext as String: context,
kSecReturnData as String: true]

var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
print(status)
if status == errSecSuccess {
if let decoded = item as? [String: Any], let passwordData = decoded[kSecValueData as String] as? Data, let password = String(data: passwordData, encoding: .utf8) {
return password
}
}
} catch {

}
}

return nil
}

func disableBiometricAuthentication() async -> Bool {
let context = LAContext()

if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) {
do {
try await context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "Save password in keychain")

let query: [String: Any] =
[kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "LibrePass",
kSecReturnAttributes as String: true,
kSecUseAuthenticationContext as String: context,
kSecReturnData as String: true]

let status = SecItemDelete(query as CFDictionary)
if status == errSecSuccess {
return true
}
} catch {

}
}

return false
}
1 change: 1 addition & 0 deletions LibrePass/App/Context/Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class LibrePassContext: ObservableObject {
@Published var lClient: LibrePassClient?
@Published var loggedIn: Bool
@Published var locallyLoggedIn: Bool
@Published var wasLogged: Bool = false
@Published var credentialsDatabase: LibrePassCredentialsDatabase?

init(lClient: LibrePassClient? = nil) {
Expand Down
1 change: 1 addition & 0 deletions LibrePass/App/Context/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ extension [EncryptedCipherStorageItem] {
@Model
class CredentialsDatabaseStorageItem {
var credentialsDatabase: LibrePassCredentialsDatabase
var biometric: Bool?

init(credentialsDatabase: LibrePassCredentialsDatabase) {
self.credentialsDatabase = credentialsDatabase
Expand Down
40 changes: 30 additions & 10 deletions LibrePass/App/LibrePassAccountSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ struct LibrePassAccountSettings: View {
}

Section {
if self.credentialsDatabaseStorage.count > 0 && self.credentialsDatabaseStorage[0].biometric ?? false {
Button("Disable biometric authentication") {
if let success = try? self.context.lClient!.validatePassword(credentialsDatabase: self.credentialsDatabaseStorage[0].credentialsDatabase, password: self.password), success {
self.clearKeyChain()
}
}
} else {
Button("Enable biometric authentication") {
Task {
if let success = try? self.context.lClient!.validatePassword(credentialsDatabase: self.credentialsDatabaseStorage[0].credentialsDatabase, password: self.password), success {
self.credentialsDatabaseStorage[0].biometric = await setUpBiometricalAuthentication(password: self.password)
}
}
}
}

Button("Log out", role: .destructive) {
self.logOut()
}
Expand All @@ -69,17 +85,21 @@ struct LibrePassAccountSettings: View {
}

func logOut() {
do {
try modelContext.delete(model: CredentialsDatabaseStorageItem.self)
try modelContext.delete(model: EncryptedCipherStorageItem.self)
try modelContext.delete(model: SyncQueueItem.self)
try modelContext.delete(model: LastSyncStorage.self)

self.context.loggedIn = false
self.context.locallyLoggedIn = false
self.context.lClient = nil
} catch {
self.clearKeyChain()

try? modelContext.delete(model: CredentialsDatabaseStorageItem.self)
try? modelContext.delete(model: EncryptedCipherStorageItem.self)
try? modelContext.delete(model: SyncQueueItem.self)
try? modelContext.delete(model: LastSyncStorage.self)

self.context.loggedIn = false
self.context.locallyLoggedIn = false
self.context.lClient = nil
}

func clearKeyChain() {
Task {
self.credentialsDatabaseStorage[0].biometric = !(await disableBiometricAuthentication())
}
}

Expand Down
29 changes: 28 additions & 1 deletion LibrePass/App/LibrePassLocalLogin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,21 @@ struct LibrePassLocalLogin: View {
Section(header: Text("Login")) {
SecureField("Password", text: $password)
.autocapitalization(.none)
ButtonWithSpinningWheel(text: "Unlock vault", task: { try self.context.localLogIn(password: self.password, credentialsDatabase: credentials[0].credentialsDatabase) })
ButtonWithSpinningWheel(text: "Unlock vault", task: self.logIn)
}

if self.credentials.count > 0 && self.credentials[0].biometric ?? false {
Section(header: Text("Biometry")) {
Button("Face ID/Touch ID") {
self.biometricalLogin()
}
}
}
}

.onAppear {
if self.credentials[0].biometric ?? false && !self.context.wasLogged {
self.biometricalLogin()
}
}

Expand All @@ -48,4 +62,17 @@ struct LibrePassLocalLogin: View {
}
}
}

func logIn() throws {
try self.context.localLogIn(password: self.password, credentialsDatabase: credentials[0].credentialsDatabase)
self.context.wasLogged = true
}

func biometricalLogin() {
Task {
guard let password = await accessKeychain() else { return }
self.password = password
try? self.logIn()
}
}
}
11 changes: 11 additions & 0 deletions LibrePass/App/LibrePassLoginWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
//

import SwiftUI
import SwiftData

struct LibrePassLoginWindow: View {
@EnvironmentObject var context: LibrePassContext

@Environment(\.modelContext) var modelContext
@Query var credentialsDatabaseStorage: [CredentialsDatabaseStorageItem]

@State private var email = String()
@State private var password = String()
Expand All @@ -27,6 +30,14 @@ struct LibrePassLoginWindow: View {
ButtonWithSpinningWheel(text: "Log in", task: {
modelContext.insert(try self.context.logIn(email: self.email, password: self.password, apiUrl: self.apiServer))
modelContext.insert(LastSyncStorage(lastSync: 0))

Task {
if await setUpBiometricalAuthentication(password: self.password) {
self.credentialsDatabaseStorage[0].biometric = true
}
}

self.context.wasLogged = true
})
}

Expand Down
13 changes: 8 additions & 5 deletions LibrePass/App/LibrePassManagerWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct LibrePassManagerWindow: View {
@State private var showAlert = false
@State private var new = false

@State var refreshIndicator: Bool = true
@State var refreshIndicator: Bool = false
@State var deletionIndicator: Bool = false

@State var toDelete: IndexSet = []
Expand Down Expand Up @@ -89,10 +89,9 @@ struct LibrePassManagerWindow: View {
}

Button(action: {
if let _ = try? modelContext.delete(model: EncryptedCipherStorageItem.self) {
self.lastStorageItem[0].lastSync = 0
self.refreshIndicator = true
}
try? modelContext.delete(model: EncryptedCipherStorageItem.self)
self.lastStorageItem[0].lastSync = 0
self.refreshIndicator = true
}) {
Image(systemName: "arrow.clockwise")
Text("Refetch ciphers")
Expand Down Expand Up @@ -130,6 +129,10 @@ struct LibrePassManagerWindow: View {
.sheet(isPresented: self.$accountSettings) {
LibrePassAccountSettings()
}

.onAppear {
self.refreshIndicator = true
}
}

func deleteCiphers() throws {
Expand Down
5 changes: 5 additions & 0 deletions LibrePass/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

0 comments on commit 663f582

Please sign in to comment.