mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-29 14:47:21 +02:00
Add SecureStorage to iOS and implement refresh token methods
This commit is contained in:
@@ -25,6 +25,7 @@
|
|||||||
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */; };
|
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */; };
|
||||||
4D91EEC62A40F28D004807ED /* EBookFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D91EEC52A40F28D004807ED /* EBookFile.swift */; };
|
4D91EEC62A40F28D004807ED /* EBookFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D91EEC52A40F28D004807ED /* EBookFile.swift */; };
|
||||||
4DABC04F2B0139CA000F6264 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DABC04E2B0139CA000F6264 /* User.swift */; };
|
4DABC04F2B0139CA000F6264 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DABC04E2B0139CA000F6264 /* User.swift */; };
|
||||||
|
4DB441E12E19B8BF0056C8F1 /* SecureStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DB441E02E19B8BF0056C8F1 /* SecureStorage.swift */; };
|
||||||
4DF6C7172DB58ABF004059F1 /* AbsLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF6C7162DB58ABF004059F1 /* AbsLogger.swift */; };
|
4DF6C7172DB58ABF004059F1 /* AbsLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF6C7162DB58ABF004059F1 /* AbsLogger.swift */; };
|
||||||
4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF74911287105C600AC7814 /* DeviceSettings.swift */; };
|
4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF74911287105C600AC7814 /* DeviceSettings.swift */; };
|
||||||
4DFE2DA32D345C390000B204 /* MyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DFE2DA22D345C390000B204 /* MyViewController.swift */; };
|
4DFE2DA32D345C390000B204 /* MyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DFE2DA22D345C390000B204 /* MyViewController.swift */; };
|
||||||
@@ -98,6 +99,7 @@
|
|||||||
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
|
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
4D91EEC52A40F28D004807ED /* EBookFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EBookFile.swift; sourceTree = "<group>"; };
|
4D91EEC52A40F28D004807ED /* EBookFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EBookFile.swift; sourceTree = "<group>"; };
|
||||||
4DABC04E2B0139CA000F6264 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
|
4DABC04E2B0139CA000F6264 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
|
||||||
|
4DB441E02E19B8BF0056C8F1 /* SecureStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStorage.swift; sourceTree = "<group>"; };
|
||||||
4DF6C7162DB58ABF004059F1 /* AbsLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsLogger.swift; sourceTree = "<group>"; };
|
4DF6C7162DB58ABF004059F1 /* AbsLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsLogger.swift; sourceTree = "<group>"; };
|
||||||
4DF74911287105C600AC7814 /* DeviceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSettings.swift; sourceTree = "<group>"; };
|
4DF74911287105C600AC7814 /* DeviceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSettings.swift; sourceTree = "<group>"; };
|
||||||
4DFE2DA22D345C390000B204 /* MyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyViewController.swift; sourceTree = "<group>"; };
|
4DFE2DA22D345C390000B204 /* MyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyViewController.swift; sourceTree = "<group>"; };
|
||||||
@@ -239,6 +241,7 @@
|
|||||||
3AFCB5E627EA23F700ECCC05 /* util */ = {
|
3AFCB5E627EA23F700ECCC05 /* util */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
4DB441E02E19B8BF0056C8F1 /* SecureStorage.swift */,
|
||||||
E9FA07E228C82848005520B0 /* Logger.swift */,
|
E9FA07E228C82848005520B0 /* Logger.swift */,
|
||||||
3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */,
|
3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */,
|
||||||
3AD4FCEA280443DD006DB301 /* Database.swift */,
|
3AD4FCEA280443DD006DB301 /* Database.swift */,
|
||||||
@@ -550,6 +553,7 @@
|
|||||||
E9D5505828AC1C1A00C746DD /* Library.swift in Sources */,
|
E9D5505828AC1C1A00C746DD /* Library.swift in Sources */,
|
||||||
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */,
|
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */,
|
||||||
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
|
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
|
||||||
|
4DB441E12E19B8BF0056C8F1 /* SecureStorage.swift in Sources */,
|
||||||
4DABC04F2B0139CA000F6264 /* User.swift in Sources */,
|
4DABC04F2B0139CA000F6264 /* User.swift in Sources */,
|
||||||
E9D5506828AC1DC300C746DD /* LocalPodcastEpisode.swift in Sources */,
|
E9D5506828AC1DC300C746DD /* LocalPodcastEpisode.swift in Sources */,
|
||||||
EACB38162BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift in Sources */,
|
EACB38162BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift in Sources */,
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
|||||||
public let pluginMethods: [CAPPluginMethod] = [
|
public let pluginMethods: [CAPPluginMethod] = [
|
||||||
CAPPluginMethod(name: "setCurrentServerConnectionConfig", returnType: CAPPluginReturnPromise),
|
CAPPluginMethod(name: "setCurrentServerConnectionConfig", returnType: CAPPluginReturnPromise),
|
||||||
CAPPluginMethod(name: "removeServerConnectionConfig", returnType: CAPPluginReturnPromise),
|
CAPPluginMethod(name: "removeServerConnectionConfig", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getRefreshToken", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "clearRefreshToken", returnType: CAPPluginReturnPromise),
|
||||||
CAPPluginMethod(name: "logout", returnType: CAPPluginReturnPromise),
|
CAPPluginMethod(name: "logout", returnType: CAPPluginReturnPromise),
|
||||||
CAPPluginMethod(name: "getDeviceData", returnType: CAPPluginReturnPromise),
|
CAPPluginMethod(name: "getDeviceData", returnType: CAPPluginReturnPromise),
|
||||||
CAPPluginMethod(name: "getLocalLibraryItems", returnType: CAPPluginReturnPromise),
|
CAPPluginMethod(name: "getLocalLibraryItems", returnType: CAPPluginReturnPromise),
|
||||||
@@ -49,6 +51,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
|||||||
]
|
]
|
||||||
|
|
||||||
private let logger = AppLogger(category: "AbsDatabase")
|
private let logger = AppLogger(category: "AbsDatabase")
|
||||||
|
private let secureStorage = SecureStorage()
|
||||||
|
|
||||||
@objc func setCurrentServerConnectionConfig(_ call: CAPPluginCall) {
|
@objc func setCurrentServerConnectionConfig(_ call: CAPPluginCall) {
|
||||||
var id = call.getString("id")
|
var id = call.getString("id")
|
||||||
@@ -62,7 +65,9 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
|||||||
let name = "\(address) (\(username))"
|
let name = "\(address) (\(username))"
|
||||||
|
|
||||||
if (refreshToken != "") {
|
if (refreshToken != "") {
|
||||||
// TODO: Implement secure storage
|
// Store refresh token securely if provided
|
||||||
|
let hasRefreshToken = secureStorage.storeRefreshToken(serverConnectionConfigId: id ?? "", refreshToken: refreshToken)
|
||||||
|
logger.log("Refresh token secured = \(hasRefreshToken)")
|
||||||
}
|
}
|
||||||
|
|
||||||
if id == nil {
|
if id == nil {
|
||||||
@@ -83,22 +88,35 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
|
|||||||
let savedConfig = Store.serverConfig // Fetch the latest value
|
let savedConfig = Store.serverConfig // Fetch the latest value
|
||||||
call.resolve(convertServerConnectionConfigToJSON(config: savedConfig!))
|
call.resolve(convertServerConnectionConfigToJSON(config: savedConfig!))
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func removeServerConnectionConfig(_ call: CAPPluginCall) {
|
@objc func removeServerConnectionConfig(_ call: CAPPluginCall) {
|
||||||
let id = call.getString("serverConnectionConfigId", "")
|
let id = call.getString("serverConnectionConfigId", "")
|
||||||
|
|
||||||
|
// Remove refresh token if it exists
|
||||||
|
secureStorage.removeRefreshToken(serverConnectionConfigId: id)
|
||||||
|
|
||||||
Database.shared.deleteServerConnectionConfig(id: id)
|
Database.shared.deleteServerConnectionConfig(id: id)
|
||||||
|
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func getRefreshToken(_ call: CAPPluginCall) {
|
@objc func getRefreshToken(_ call: CAPPluginCall) {
|
||||||
let serverConnectionConfigId = call.getString("serverConnectionConfigId", "")
|
let serverConnectionConfigId = call.getString("serverConnectionConfigId", "")
|
||||||
// TODO: Implement secure storage
|
|
||||||
call.resolve()
|
let refreshToken = secureStorage.getRefreshToken(serverConnectionConfigId: serverConnectionConfigId)
|
||||||
|
if let refreshToken = refreshToken {
|
||||||
|
call.resolve(["refreshToken": refreshToken])
|
||||||
|
} else {
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func clearRefreshToken(_ call: CAPPluginCall) {
|
@objc func clearRefreshToken(_ call: CAPPluginCall) {
|
||||||
let serverConnectionConfigId = call.getString("serverConnectionConfigId", "")
|
let serverConnectionConfigId = call.getString("serverConnectionConfigId", "")
|
||||||
// TODO: Implement secure storage
|
|
||||||
call.resolve()
|
let success = secureStorage.removeRefreshToken(serverConnectionConfigId: serverConnectionConfigId)
|
||||||
|
call.resolve(["success": success])
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func logout(_ call: CAPPluginCall) {
|
@objc func logout(_ call: CAPPluginCall) {
|
||||||
Store.serverConfig = nil
|
Store.serverConfig = nil
|
||||||
call.resolve()
|
call.resolve()
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
//
|
||||||
|
// SecureStorage.swift
|
||||||
|
// App
|
||||||
|
//
|
||||||
|
// Created by advplyr on 2025-07-05
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Security
|
||||||
|
|
||||||
|
class SecureStorage {
|
||||||
|
private static let tag = "SecureStorage"
|
||||||
|
private static let serviceName = "AudiobookshelfRefreshTokens"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts and stores a refresh token for a specific server connection
|
||||||
|
*/
|
||||||
|
func storeRefreshToken(serverConnectionConfigId: String, refreshToken: String) -> Bool {
|
||||||
|
let key = "refresh_token_\(serverConnectionConfigId)"
|
||||||
|
|
||||||
|
guard let data = refreshToken.data(using: .utf8) else {
|
||||||
|
print("\(Self.tag): Failed to convert refresh token to data")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: Self.serviceName,
|
||||||
|
kSecAttrAccount as String: key,
|
||||||
|
kSecValueData as String: data,
|
||||||
|
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
|
||||||
|
]
|
||||||
|
|
||||||
|
// First, try to delete any existing item
|
||||||
|
SecItemDelete(query as CFDictionary)
|
||||||
|
|
||||||
|
// Then add the new item
|
||||||
|
let status = SecItemAdd(query as CFDictionary, nil)
|
||||||
|
|
||||||
|
if status == errSecSuccess {
|
||||||
|
print("\(Self.tag): Successfully stored encrypted refresh token for server: \(serverConnectionConfigId)")
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
print("\(Self.tag): Failed to store refresh token for server: \(serverConnectionConfigId), status: \(status)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves and decrypts a refresh token for a specific server connection
|
||||||
|
*/
|
||||||
|
func getRefreshToken(serverConnectionConfigId: String) -> String? {
|
||||||
|
let key = "refresh_token_\(serverConnectionConfigId)"
|
||||||
|
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: Self.serviceName,
|
||||||
|
kSecAttrAccount as String: key,
|
||||||
|
kSecReturnData as String: true,
|
||||||
|
kSecMatchLimit as String: kSecMatchLimitOne
|
||||||
|
]
|
||||||
|
|
||||||
|
var result: AnyObject?
|
||||||
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||||
|
|
||||||
|
if status == errSecSuccess,
|
||||||
|
let data = result as? Data,
|
||||||
|
let refreshToken = String(data: data, encoding: .utf8) {
|
||||||
|
return refreshToken
|
||||||
|
} else {
|
||||||
|
print("\(Self.tag): Failed to retrieve refresh token for server: \(serverConnectionConfigId), status: \(status)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a refresh token for a specific server connection
|
||||||
|
*/
|
||||||
|
func removeRefreshToken(serverConnectionConfigId: String) -> Bool {
|
||||||
|
let key = "refresh_token_\(serverConnectionConfigId)"
|
||||||
|
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: Self.serviceName,
|
||||||
|
kSecAttrAccount as String: key
|
||||||
|
]
|
||||||
|
|
||||||
|
let status = SecItemDelete(query as CFDictionary)
|
||||||
|
|
||||||
|
if status == errSecSuccess || status == errSecItemNotFound {
|
||||||
|
print("\(Self.tag): Successfully removed refresh token for server: \(serverConnectionConfigId)")
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
print("\(Self.tag): Failed to remove refresh token for server: \(serverConnectionConfigId), status: \(status)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a refresh token exists for a specific server connection
|
||||||
|
*/
|
||||||
|
func hasRefreshToken(serverConnectionConfigId: String) -> Bool {
|
||||||
|
let key = "refresh_token_\(serverConnectionConfigId)"
|
||||||
|
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: Self.serviceName,
|
||||||
|
kSecAttrAccount as String: key,
|
||||||
|
kSecReturnData as String: false,
|
||||||
|
kSecMatchLimit as String: kSecMatchLimitOne
|
||||||
|
]
|
||||||
|
|
||||||
|
let status = SecItemCopyMatching(query as CFDictionary, nil)
|
||||||
|
return status == errSecSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user