Implement iPhone and Apple Watch preferences (#53)
This commit is contained in:
@@ -5,6 +5,12 @@
|
|||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"size" : "1024x1024"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
@@ -629,10 +629,16 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func manualLock() throws
|
func manualLock() throws
|
||||||
|
|
||||||
|
func mobileAppearance() throws -> MobileAppearance
|
||||||
|
|
||||||
func performEntryMutation(request: MobileMutationRequest) throws -> MobileMutationOutcome
|
func performEntryMutation(request: MobileMutationRequest) throws -> MobileMutationOutcome
|
||||||
|
|
||||||
|
func preferences(watchSupported: Bool, watchPaired: Bool, watchAppInstalled: Bool) throws -> MobilePreferences
|
||||||
|
|
||||||
func prepareEntryMutation(path: String, action: MobileMutationAction) throws -> MobileMutationPlan
|
func prepareEntryMutation(path: String, action: MobileMutationAction) throws -> MobileMutationPlan
|
||||||
|
|
||||||
|
func removeApplicationToken() throws
|
||||||
|
|
||||||
func removeEntryEditorField(editor: UInt64, field: UInt64) throws -> MobileEntryEditorPage
|
func removeEntryEditorField(editor: UInt64, field: UInt64) throws -> MobileEntryEditorPage
|
||||||
|
|
||||||
func reorderEntryEditorField(editor: UInt64, field: UInt64, index: UInt32) throws -> MobileEntryEditorPage
|
func reorderEntryEditorField(editor: UInt64, field: UInt64, index: UInt32) throws -> MobileEntryEditorPage
|
||||||
@@ -645,10 +651,14 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func searchCachedTotpPage(query: String) throws -> MobileTotpPage?
|
func searchCachedTotpPage(query: String) throws -> MobileTotpPage?
|
||||||
|
|
||||||
|
func setAuthenticationTimeout(seconds: UInt64) throws
|
||||||
|
|
||||||
func setBiometricUnlock(enabled: Bool) throws -> MobileAuthenticationState
|
func setBiometricUnlock(enabled: Bool) throws -> MobileAuthenticationState
|
||||||
|
|
||||||
func setGitIdentity(name: String, email: String) throws -> MobileGitIdentity
|
func setGitIdentity(name: String, email: String) throws -> MobileGitIdentity
|
||||||
|
|
||||||
|
func setMobileAppearance(appearance: MobileAppearance) throws
|
||||||
|
|
||||||
func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64) throws -> MobileTotpDetail
|
func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64) throws -> MobileTotpDetail
|
||||||
|
|
||||||
func state() throws -> MobileAuthenticationState
|
func state() throws -> MobileAuthenticationState
|
||||||
@@ -852,6 +862,15 @@ open func manualLock()throws {try rustCallWithError(FfiConverterTypeMobileAuth
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func mobileAppearance()throws -> MobileAppearance {
|
||||||
|
return try FfiConverterTypeMobileAppearance_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobileauthentication_mobile_appearance(
|
||||||
|
self.uniffiCloneHandle(),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
open func performEntryMutation(request: MobileMutationRequest)throws -> MobileMutationOutcome {
|
open func performEntryMutation(request: MobileMutationRequest)throws -> MobileMutationOutcome {
|
||||||
return try FfiConverterTypeMobileMutationOutcome_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
return try FfiConverterTypeMobileMutationOutcome_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
@@ -862,6 +881,18 @@ open func performEntryMutation(request: MobileMutationRequest)throws -> MobileM
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func preferences(watchSupported: Bool, watchPaired: Bool, watchAppInstalled: Bool)throws -> MobilePreferences {
|
||||||
|
return try FfiConverterTypeMobilePreferences_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobileauthentication_preferences(
|
||||||
|
self.uniffiCloneHandle(),
|
||||||
|
FfiConverterBool.lower(watchSupported),
|
||||||
|
FfiConverterBool.lower(watchPaired),
|
||||||
|
FfiConverterBool.lower(watchAppInstalled),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
open func prepareEntryMutation(path: String, action: MobileMutationAction)throws -> MobileMutationPlan {
|
open func prepareEntryMutation(path: String, action: MobileMutationAction)throws -> MobileMutationPlan {
|
||||||
return try FfiConverterTypeMobileMutationPlan_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
return try FfiConverterTypeMobileMutationPlan_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
@@ -873,6 +904,14 @@ open func prepareEntryMutation(path: String, action: MobileMutationAction)throws
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func removeApplicationToken()throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobileauthentication_remove_application_token(
|
||||||
|
self.uniffiCloneHandle(),uniffiCallStatus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open func removeEntryEditorField(editor: UInt64, field: UInt64)throws -> MobileEntryEditorPage {
|
open func removeEntryEditorField(editor: UInt64, field: UInt64)throws -> MobileEntryEditorPage {
|
||||||
return try FfiConverterTypeMobileEntryEditorPage_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
return try FfiConverterTypeMobileEntryEditorPage_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
@@ -940,6 +979,15 @@ open func searchCachedTotpPage(query: String)throws -> MobileTotpPage? {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func setAuthenticationTimeout(seconds: UInt64)throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobileauthentication_set_authentication_timeout(
|
||||||
|
self.uniffiCloneHandle(),
|
||||||
|
FfiConverterUInt64.lower(seconds),uniffiCallStatus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open func setBiometricUnlock(enabled: Bool)throws -> MobileAuthenticationState {
|
open func setBiometricUnlock(enabled: Bool)throws -> MobileAuthenticationState {
|
||||||
return try FfiConverterTypeMobileAuthenticationState_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
return try FfiConverterTypeMobileAuthenticationState_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
@@ -961,6 +1009,15 @@ open func setGitIdentity(name: String, email: String)throws -> MobileGitIdentit
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func setMobileAppearance(appearance: MobileAppearance)throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobileauthentication_set_mobile_appearance(
|
||||||
|
self.uniffiCloneHandle(),
|
||||||
|
FfiConverterTypeMobileAppearance_lower(appearance),uniffiCallStatus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64)throws -> MobileTotpDetail {
|
open func setTotpWatchShared(path: String, shared: Bool, unixSeconds: UInt64)throws -> MobileTotpDetail {
|
||||||
return try FfiConverterTypeMobileTotpDetail_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
return try FfiConverterTypeMobileTotpDetail_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
@@ -3962,6 +4019,104 @@ public func FfiConverterTypeMobilePasswordRow_lower(_ value: MobilePasswordRow)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct MobilePreferences: Equatable, Hashable {
|
||||||
|
public var repositoryTitle: String
|
||||||
|
public var repositoryUrl: String
|
||||||
|
public var serverTitle: String
|
||||||
|
public var serverIdentity: String
|
||||||
|
public var applicationAccount: String?
|
||||||
|
public var defaultKeyTitle: String
|
||||||
|
public var defaultKeyFingerprint: String
|
||||||
|
public var authenticationTimeoutSeconds: UInt64
|
||||||
|
public var biometricUnlockEnabled: Bool
|
||||||
|
public var appearance: MobileAppearance
|
||||||
|
public var watchState: MobileWatchPreferenceState
|
||||||
|
public var watchTitle: String
|
||||||
|
public var watchDetail: String
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(repositoryTitle: String, repositoryUrl: String, serverTitle: String, serverIdentity: String, applicationAccount: String?, defaultKeyTitle: String, defaultKeyFingerprint: String, authenticationTimeoutSeconds: UInt64, biometricUnlockEnabled: Bool, appearance: MobileAppearance, watchState: MobileWatchPreferenceState, watchTitle: String, watchDetail: String) {
|
||||||
|
self.repositoryTitle = repositoryTitle
|
||||||
|
self.repositoryUrl = repositoryUrl
|
||||||
|
self.serverTitle = serverTitle
|
||||||
|
self.serverIdentity = serverIdentity
|
||||||
|
self.applicationAccount = applicationAccount
|
||||||
|
self.defaultKeyTitle = defaultKeyTitle
|
||||||
|
self.defaultKeyFingerprint = defaultKeyFingerprint
|
||||||
|
self.authenticationTimeoutSeconds = authenticationTimeoutSeconds
|
||||||
|
self.biometricUnlockEnabled = biometricUnlockEnabled
|
||||||
|
self.appearance = appearance
|
||||||
|
self.watchState = watchState
|
||||||
|
self.watchTitle = watchTitle
|
||||||
|
self.watchDetail = watchDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobilePreferences: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobilePreferences: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePreferences {
|
||||||
|
return
|
||||||
|
try MobilePreferences(
|
||||||
|
repositoryTitle: FfiConverterString.read(from: &buf),
|
||||||
|
repositoryUrl: FfiConverterString.read(from: &buf),
|
||||||
|
serverTitle: FfiConverterString.read(from: &buf),
|
||||||
|
serverIdentity: FfiConverterString.read(from: &buf),
|
||||||
|
applicationAccount: FfiConverterOptionString.read(from: &buf),
|
||||||
|
defaultKeyTitle: FfiConverterString.read(from: &buf),
|
||||||
|
defaultKeyFingerprint: FfiConverterString.read(from: &buf),
|
||||||
|
authenticationTimeoutSeconds: FfiConverterUInt64.read(from: &buf),
|
||||||
|
biometricUnlockEnabled: FfiConverterBool.read(from: &buf),
|
||||||
|
appearance: FfiConverterTypeMobileAppearance.read(from: &buf),
|
||||||
|
watchState: FfiConverterTypeMobileWatchPreferenceState.read(from: &buf),
|
||||||
|
watchTitle: FfiConverterString.read(from: &buf),
|
||||||
|
watchDetail: FfiConverterString.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobilePreferences, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterString.write(value.repositoryTitle, into: &buf)
|
||||||
|
FfiConverterString.write(value.repositoryUrl, into: &buf)
|
||||||
|
FfiConverterString.write(value.serverTitle, into: &buf)
|
||||||
|
FfiConverterString.write(value.serverIdentity, into: &buf)
|
||||||
|
FfiConverterOptionString.write(value.applicationAccount, into: &buf)
|
||||||
|
FfiConverterString.write(value.defaultKeyTitle, into: &buf)
|
||||||
|
FfiConverterString.write(value.defaultKeyFingerprint, into: &buf)
|
||||||
|
FfiConverterUInt64.write(value.authenticationTimeoutSeconds, into: &buf)
|
||||||
|
FfiConverterBool.write(value.biometricUnlockEnabled, into: &buf)
|
||||||
|
FfiConverterTypeMobileAppearance.write(value.appearance, into: &buf)
|
||||||
|
FfiConverterTypeMobileWatchPreferenceState.write(value.watchState, into: &buf)
|
||||||
|
FfiConverterString.write(value.watchTitle, into: &buf)
|
||||||
|
FfiConverterString.write(value.watchDetail, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePreferences_lift(_ buf: RustBuffer) throws -> MobilePreferences {
|
||||||
|
return try FfiConverterTypeMobilePreferences.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobilePreferences_lower(_ value: MobilePreferences) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobilePreferences.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public struct MobileShell: Equatable, Hashable {
|
public struct MobileShell: Equatable, Hashable {
|
||||||
public var selectedTab: MobileTab
|
public var selectedTab: MobileTab
|
||||||
public var pages: [MobilePage]
|
public var pages: [MobilePage]
|
||||||
@@ -4351,6 +4506,79 @@ public func FfiConverterTypeMobileWatchSnapshotStatus_lower(_ value: MobileWatch
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public enum MobileAppearance: Equatable, Hashable {
|
||||||
|
|
||||||
|
case system
|
||||||
|
case light
|
||||||
|
case dark
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobileAppearance: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileAppearance: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = MobileAppearance
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileAppearance {
|
||||||
|
let variant: Int32 = try readInt(&buf)
|
||||||
|
switch variant {
|
||||||
|
|
||||||
|
case 1: return .system
|
||||||
|
|
||||||
|
case 2: return .light
|
||||||
|
|
||||||
|
case 3: return .dark
|
||||||
|
|
||||||
|
default: throw UniffiInternalError.unexpectedEnumCase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileAppearance, into buf: inout [UInt8]) {
|
||||||
|
switch value {
|
||||||
|
|
||||||
|
|
||||||
|
case .system:
|
||||||
|
writeInt(&buf, Int32(1))
|
||||||
|
|
||||||
|
|
||||||
|
case .light:
|
||||||
|
writeInt(&buf, Int32(2))
|
||||||
|
|
||||||
|
|
||||||
|
case .dark:
|
||||||
|
writeInt(&buf, Int32(3))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileAppearance_lift(_ buf: RustBuffer) throws -> MobileAppearance {
|
||||||
|
return try FfiConverterTypeMobileAppearance.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileAppearance_lower(_ value: MobileAppearance) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobileAppearance.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public enum MobileAuthenticationErrorKind: Equatable, Hashable {
|
public enum MobileAuthenticationErrorKind: Equatable, Hashable {
|
||||||
|
|
||||||
case passphraseRequired
|
case passphraseRequired
|
||||||
@@ -6383,6 +6611,93 @@ public func FfiConverterTypeMobileTotpDiscoveryPhase_lower(_ value: MobileTotpDi
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public enum MobileWatchPreferenceState: Equatable, Hashable {
|
||||||
|
|
||||||
|
case unsupported
|
||||||
|
case notPaired
|
||||||
|
case appNotInstalled
|
||||||
|
case ready
|
||||||
|
case pending
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobileWatchPreferenceState: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileWatchPreferenceState: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = MobileWatchPreferenceState
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileWatchPreferenceState {
|
||||||
|
let variant: Int32 = try readInt(&buf)
|
||||||
|
switch variant {
|
||||||
|
|
||||||
|
case 1: return .unsupported
|
||||||
|
|
||||||
|
case 2: return .notPaired
|
||||||
|
|
||||||
|
case 3: return .appNotInstalled
|
||||||
|
|
||||||
|
case 4: return .ready
|
||||||
|
|
||||||
|
case 5: return .pending
|
||||||
|
|
||||||
|
default: throw UniffiInternalError.unexpectedEnumCase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileWatchPreferenceState, into buf: inout [UInt8]) {
|
||||||
|
switch value {
|
||||||
|
|
||||||
|
|
||||||
|
case .unsupported:
|
||||||
|
writeInt(&buf, Int32(1))
|
||||||
|
|
||||||
|
|
||||||
|
case .notPaired:
|
||||||
|
writeInt(&buf, Int32(2))
|
||||||
|
|
||||||
|
|
||||||
|
case .appNotInstalled:
|
||||||
|
writeInt(&buf, Int32(3))
|
||||||
|
|
||||||
|
|
||||||
|
case .ready:
|
||||||
|
writeInt(&buf, Int32(4))
|
||||||
|
|
||||||
|
|
||||||
|
case .pending:
|
||||||
|
writeInt(&buf, Int32(5))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileWatchPreferenceState_lift(_ buf: RustBuffer) throws -> MobileWatchPreferenceState {
|
||||||
|
return try FfiConverterTypeMobileWatchPreferenceState.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileWatchPreferenceState_lower(_ value: MobileWatchPreferenceState) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobileWatchPreferenceState.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public enum MobileWatchSnapshotState: Equatable, Hashable {
|
public enum MobileWatchSnapshotState: Equatable, Hashable {
|
||||||
|
|
||||||
case unavailable
|
case unavailable
|
||||||
@@ -7221,12 +7536,21 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_manual_lock() != 57220) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_manual_lock() != 57220) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_mobile_appearance() != 3564) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_perform_entry_mutation() != 7053) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_perform_entry_mutation() != 7053) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_preferences() != 16444) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_prepare_entry_mutation() != 6234) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_prepare_entry_mutation() != 6234) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_remove_application_token() != 11452) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_remove_entry_editor_field() != 12238) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_remove_entry_editor_field() != 12238) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
@@ -7245,12 +7569,18 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_search_cached_totp_page() != 13608) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_search_cached_totp_page() != 13608) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_authentication_timeout() != 48083) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_biometric_unlock() != 9486) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_biometric_unlock() != 9486) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_git_identity() != 62371) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_git_identity() != 62371) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_mobile_appearance() != 12133) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_totp_watch_shared() != 57472) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_totp_watch_shared() != 57472) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,16 +318,31 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_git_identity(
|
|||||||
void uniffi_ironstorage_apple_fn_method_mobileauthentication_manual_lock(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
void uniffi_ironstorage_apple_fn_method_mobileauthentication_manual_lock(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_MOBILE_APPEARANCE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_MOBILE_APPEARANCE
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_mobile_appearance(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PERFORM_ENTRY_MUTATION
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PERFORM_ENTRY_MUTATION
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PERFORM_ENTRY_MUTATION
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PERFORM_ENTRY_MUTATION
|
||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_perform_entry_mutation(uint64_t ptr, RustBuffer request, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_perform_entry_mutation(uint64_t ptr, RustBuffer request, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PREFERENCES
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PREFERENCES
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_preferences(uint64_t ptr, int8_t watch_supported, int8_t watch_paired, int8_t watch_app_installed, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PREPARE_ENTRY_MUTATION
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PREPARE_ENTRY_MUTATION
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PREPARE_ENTRY_MUTATION
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_PREPARE_ENTRY_MUTATION
|
||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_prepare_entry_mutation(uint64_t ptr, RustBuffer path, RustBuffer action, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_prepare_entry_mutation(uint64_t ptr, RustBuffer path, RustBuffer action, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_REMOVE_APPLICATION_TOKEN
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_REMOVE_APPLICATION_TOKEN
|
||||||
|
void uniffi_ironstorage_apple_fn_method_mobileauthentication_remove_application_token(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_REMOVE_ENTRY_EDITOR_FIELD
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_REMOVE_ENTRY_EDITOR_FIELD
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_REMOVE_ENTRY_EDITOR_FIELD
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_REMOVE_ENTRY_EDITOR_FIELD
|
||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_remove_entry_editor_field(uint64_t ptr, uint64_t editor, uint64_t field, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_remove_entry_editor_field(uint64_t ptr, uint64_t editor, uint64_t field, RustCallStatus *_Nonnull out_status
|
||||||
@@ -358,6 +373,11 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_save_entry_ed
|
|||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_search_cached_totp_page(uint64_t ptr, RustBuffer query, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_search_cached_totp_page(uint64_t ptr, RustBuffer query, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_AUTHENTICATION_TIMEOUT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_AUTHENTICATION_TIMEOUT
|
||||||
|
void uniffi_ironstorage_apple_fn_method_mobileauthentication_set_authentication_timeout(uint64_t ptr, uint64_t seconds, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
|
||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_biometric_unlock(uint64_t ptr, int8_t enabled, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_biometric_unlock(uint64_t ptr, int8_t enabled, RustCallStatus *_Nonnull out_status
|
||||||
@@ -368,6 +388,11 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_biometric
|
|||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_git_identity(uint64_t ptr, RustBuffer name, RustBuffer email, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_git_identity(uint64_t ptr, RustBuffer name, RustBuffer email, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_MOBILE_APPEARANCE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_MOBILE_APPEARANCE
|
||||||
|
void uniffi_ironstorage_apple_fn_method_mobileauthentication_set_mobile_appearance(uint64_t ptr, RustBuffer appearance, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
|
||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_totp_watch_shared(uint64_t ptr, RustBuffer path, int8_t shared, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_set_totp_watch_shared(uint64_t ptr, RustBuffer path, int8_t shared, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status
|
||||||
@@ -1026,18 +1051,36 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_git_ident
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_MANUAL_LOCK
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_MANUAL_LOCK
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_manual_lock(void
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_manual_lock(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_MOBILE_APPEARANCE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_MOBILE_APPEARANCE
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_mobile_appearance(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PERFORM_ENTRY_MUTATION
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PERFORM_ENTRY_MUTATION
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PERFORM_ENTRY_MUTATION
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PERFORM_ENTRY_MUTATION
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_perform_entry_mutation(void
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_perform_entry_mutation(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PREFERENCES
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PREFERENCES
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_preferences(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PREPARE_ENTRY_MUTATION
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PREPARE_ENTRY_MUTATION
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PREPARE_ENTRY_MUTATION
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_PREPARE_ENTRY_MUTATION
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_prepare_entry_mutation(void
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_prepare_entry_mutation(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_REMOVE_APPLICATION_TOKEN
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_REMOVE_APPLICATION_TOKEN
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_remove_application_token(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_REMOVE_ENTRY_EDITOR_FIELD
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_REMOVE_ENTRY_EDITOR_FIELD
|
||||||
@@ -1074,6 +1117,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_save_entr
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SEARCH_CACHED_TOTP_PAGE
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SEARCH_CACHED_TOTP_PAGE
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_search_cached_totp_page(void
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_search_cached_totp_page(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_AUTHENTICATION_TIMEOUT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_AUTHENTICATION_TIMEOUT
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_authentication_timeout(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_BIOMETRIC_UNLOCK
|
||||||
@@ -1086,6 +1135,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_biome
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_GIT_IDENTITY
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_GIT_IDENTITY
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_git_identity(void
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_git_identity(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_MOBILE_APPEARANCE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_MOBILE_APPEARANCE
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_set_mobile_appearance(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_SET_TOTP_WATCH_SHARED
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import UIKit
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
import Vision
|
import Vision
|
||||||
import VisionKit
|
import VisionKit
|
||||||
|
import WatchConnectivity
|
||||||
|
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let ironStorageLocalStoreDidChange = Notification.Name(
|
static let ironStorageLocalStoreDidChange = Notification.Name(
|
||||||
@@ -107,6 +108,13 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
|
|||||||
}
|
}
|
||||||
tabs.viewControllers = navigationControllers
|
tabs.viewControllers = navigationControllers
|
||||||
tabs.delegate = self
|
tabs.delegate = self
|
||||||
|
if let appearance = try? authentication?.mobileAppearance() {
|
||||||
|
tabs.overrideUserInterfaceStyle = switch appearance {
|
||||||
|
case .system: .unspecified
|
||||||
|
case .light: .light
|
||||||
|
case .dark: .dark
|
||||||
|
}
|
||||||
|
}
|
||||||
restoreSelectedTab()
|
restoreSelectedTab()
|
||||||
monitorAuthentication()
|
monitorAuthentication()
|
||||||
return tabs
|
return tabs
|
||||||
@@ -844,13 +852,17 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private final class PreferencesViewController: UITableViewController, MobileTabRoot {
|
private final class PreferencesViewController: UITableViewController, MobileTabRoot,
|
||||||
|
WCSessionDelegate
|
||||||
|
{
|
||||||
fileprivate let shellTab = MobileTab.preferences
|
fileprivate let shellTab = MobileTab.preferences
|
||||||
private var page: MobilePage
|
private var page: MobilePage
|
||||||
private let authentication: MobileAuthentication?
|
private let authentication: MobileAuthentication?
|
||||||
private let keyTransfer = try? mobileKeyTransfer()
|
private let keyTransfer = try? mobileKeyTransfer()
|
||||||
private var state: MobileAuthenticationState?
|
private var state: MobileAuthenticationState?
|
||||||
|
private var preferences: MobilePreferences?
|
||||||
private var gitIdentity: MobileGitIdentity?
|
private var gitIdentity: MobileGitIdentity?
|
||||||
|
private var watchSession: WCSession?
|
||||||
private var preferenceTask: Task<Void, Never>?
|
private var preferenceTask: Task<Void, Never>?
|
||||||
private var loadTask: Task<Void, Never>?
|
private var loadTask: Task<Void, Never>?
|
||||||
private var loadGeneration = 0
|
private var loadGeneration = 0
|
||||||
@@ -859,6 +871,12 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
self.page = page
|
self.page = page
|
||||||
self.authentication = authentication
|
self.authentication = authentication
|
||||||
super.init(style: .insetGrouped)
|
super.init(style: .insetGrouped)
|
||||||
|
if WCSession.isSupported() {
|
||||||
|
let session = WCSession.default
|
||||||
|
watchSession = session
|
||||||
|
session.delegate = self
|
||||||
|
session.activate()
|
||||||
|
}
|
||||||
title = page.title
|
title = page.title
|
||||||
navigationItem.largeTitleDisplayMode = .always
|
navigationItem.largeTitleDisplayMode = .always
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||||
@@ -881,6 +899,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
watchSession?.delegate = nil
|
||||||
preferenceTask?.cancel()
|
preferenceTask?.cancel()
|
||||||
loadTask?.cancel()
|
loadTask?.cancel()
|
||||||
NotificationCenter.default.removeObserver(self)
|
NotificationCenter.default.removeObserver(self)
|
||||||
@@ -892,14 +911,18 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
page.state == .ready ? 4 : 0
|
page.state == .ready ? 7 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(
|
override func tableView(
|
||||||
_ tableView: UITableView,
|
_ tableView: UITableView,
|
||||||
numberOfRowsInSection section: Int
|
numberOfRowsInSection section: Int
|
||||||
) -> Int {
|
) -> Int {
|
||||||
section == 1 ? 2 : (section == 3 ? 2 : 1)
|
switch section {
|
||||||
|
case 0: 2
|
||||||
|
case 1, 2, 3: 3
|
||||||
|
default: 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(
|
override func tableView(
|
||||||
@@ -907,10 +930,13 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
titleForHeaderInSection section: Int
|
titleForHeaderInSection section: Int
|
||||||
) -> String? {
|
) -> String? {
|
||||||
switch section {
|
switch section {
|
||||||
case 0: "Git Commit Identity"
|
case 0: "Password Store"
|
||||||
case 1: "GPG Key Transfer"
|
case 1: "Application Token"
|
||||||
case 2: "Secure Unlock"
|
case 2: "GPG Key & Recovery"
|
||||||
default: "Authentication Session"
|
case 3: "Authentication"
|
||||||
|
case 4: "Appearance"
|
||||||
|
case 5: "Apple Watch"
|
||||||
|
default: "Git Commit Identity"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -918,16 +944,22 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
_ tableView: UITableView,
|
_ tableView: UITableView,
|
||||||
titleForFooterInSection section: Int
|
titleForFooterInSection section: Int
|
||||||
) -> String? {
|
) -> String? {
|
||||||
if section == 0 {
|
switch section {
|
||||||
return "Used as the author for commits created by the iPhone app."
|
case 0:
|
||||||
|
"Non-secret repository and server identities from the shared configuration."
|
||||||
|
case 1:
|
||||||
|
"The application token is stored only in protected system storage and is never displayed."
|
||||||
|
case 2:
|
||||||
|
"Import provides initial or recovery key setup. Private-key transfers require explicit confirmation and passphrase validation."
|
||||||
|
case 3:
|
||||||
|
"Manual lock and inactivity expiry immediately revoke the shared Rust authentication lease."
|
||||||
|
case 5:
|
||||||
|
"Pairing is read from the device; TOTP selection and synchronization state come from Rust."
|
||||||
|
case 6:
|
||||||
|
"Used as the author for commits created by the iPhone app."
|
||||||
|
default:
|
||||||
|
nil
|
||||||
}
|
}
|
||||||
if section == 1 {
|
|
||||||
return "Scan or display ASCII-armored GPG keys. Private-key transfers require explicit confirmation and passphrase validation."
|
|
||||||
}
|
|
||||||
if section == 2 {
|
|
||||||
return "When enabled, the GPG passphrase is device-only, requires a device passcode, and is invalidated when enrolled biometrics change."
|
|
||||||
}
|
|
||||||
return "Manual lock and inactivity expiry immediately revoke the shared Rust authentication lease."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(
|
override func tableView(
|
||||||
@@ -936,24 +968,58 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
) -> UITableViewCell {
|
) -> UITableViewCell {
|
||||||
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
||||||
var content = cell.defaultContentConfiguration()
|
var content = cell.defaultContentConfiguration()
|
||||||
if indexPath.section == 0 {
|
switch (indexPath.section, indexPath.row) {
|
||||||
content.image = UIImage(systemName: "person.crop.circle")
|
case (0, 0):
|
||||||
content.text = gitIdentity?.name ?? "Git Commit Identity"
|
content.image = UIImage(systemName: "externaldrive.fill")
|
||||||
content.secondaryText = gitIdentity?.email ?? "Set the commit author name and email"
|
content.text = preferences?.repositoryTitle ?? "Password Store"
|
||||||
|
content.secondaryText = preferences?.repositoryUrl ?? "Repository unavailable"
|
||||||
|
cell.selectionStyle = .none
|
||||||
|
case (0, 1):
|
||||||
|
content.image = UIImage(systemName: "server.rack")
|
||||||
|
content.text = preferences?.serverTitle ?? "HTTPS Server"
|
||||||
|
content.secondaryText = preferences?.serverIdentity ?? "Server identity unavailable"
|
||||||
|
cell.selectionStyle = .none
|
||||||
|
case (1, 0):
|
||||||
|
content.image = UIImage(systemName: "key.fill")
|
||||||
|
content.text = preferences?.applicationAccount ?? "No Application Token"
|
||||||
|
content.secondaryText = preferences?.applicationAccount == nil
|
||||||
|
? "Git network operations require a replacement token"
|
||||||
|
: "Token stored in protected system storage"
|
||||||
|
cell.selectionStyle = .none
|
||||||
|
case (1, 1):
|
||||||
|
content.image = UIImage(systemName: "arrow.triangle.2.circlepath")
|
||||||
|
content.text = "Replace Token"
|
||||||
|
content.secondaryText = "Store a new HTTPS application token"
|
||||||
cell.accessoryType = .disclosureIndicator
|
cell.accessoryType = .disclosureIndicator
|
||||||
cell.isUserInteractionEnabled = authentication != nil
|
case (1, 2):
|
||||||
cell.contentView.alpha = authentication == nil ? 0.45 : 1
|
content.image = UIImage(systemName: "trash")
|
||||||
} else if indexPath.section == 1 {
|
content.text = "Remove Token"
|
||||||
let importing = indexPath.row == 0
|
content.textProperties.color = .systemRed
|
||||||
|
content.secondaryText = "Disable authenticated Git network operations"
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
cell.isUserInteractionEnabled = preferences?.applicationAccount != nil
|
||||||
|
cell.contentView.alpha = preferences?.applicationAccount == nil ? 0.45 : 1
|
||||||
|
case (2, 0):
|
||||||
|
content.image = UIImage(systemName: "person.badge.key.fill")
|
||||||
|
content.text = preferences?.defaultKeyTitle ?? "Default GPG Key"
|
||||||
|
content.secondaryText = preferences?.defaultKeyFingerprint ?? "Key unavailable"
|
||||||
|
cell.selectionStyle = .none
|
||||||
|
case (2, 1), (2, 2):
|
||||||
|
let importing = indexPath.row == 1
|
||||||
content.image = UIImage(systemName: importing ? "qrcode.viewfinder" : "qrcode")
|
content.image = UIImage(systemName: importing ? "qrcode.viewfinder" : "qrcode")
|
||||||
content.text = importing ? "Import GPG Key" : "Export GPG Key"
|
content.text = importing ? "Import GPG Key" : "Export GPG Key"
|
||||||
content.secondaryText = importing
|
content.secondaryText = importing
|
||||||
? "Scan one or more transfer QR codes"
|
? "Scan an initial or recovery key transfer"
|
||||||
: "Display public or private key armor"
|
: "Display public or private key armor"
|
||||||
cell.accessoryType = .disclosureIndicator
|
cell.accessoryType = .disclosureIndicator
|
||||||
cell.isUserInteractionEnabled = keyTransfer != nil
|
cell.isUserInteractionEnabled = keyTransfer != nil
|
||||||
cell.contentView.alpha = keyTransfer == nil ? 0.45 : 1
|
cell.contentView.alpha = keyTransfer == nil ? 0.45 : 1
|
||||||
} else if indexPath.section == 2 {
|
case (3, 0):
|
||||||
|
content.image = UIImage(systemName: "timer")
|
||||||
|
content.text = "Inactivity Timeout"
|
||||||
|
content.secondaryText = "\(preferences?.authenticationTimeoutSeconds ?? 0) seconds"
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
case (3, 1):
|
||||||
content.image = UIImage(systemName: "faceid")
|
content.image = UIImage(systemName: "faceid")
|
||||||
content.text = "Biometric Unlock"
|
content.text = "Biometric Unlock"
|
||||||
content.secondaryText = state?.biometricUnlockEnabled == true
|
content.secondaryText = state?.biometricUnlockEnabled == true
|
||||||
@@ -966,37 +1032,51 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
toggle.accessibilityLabel = "Biometric Unlock"
|
toggle.accessibilityLabel = "Biometric Unlock"
|
||||||
cell.accessoryView = toggle
|
cell.accessoryView = toggle
|
||||||
cell.selectionStyle = .none
|
cell.selectionStyle = .none
|
||||||
} else if indexPath.row == 0 {
|
case (3, 2):
|
||||||
let unlocked = state?.unlocked == true
|
let unlocked = state?.unlocked == true
|
||||||
content.image = UIImage(systemName: unlocked ? "lock.open.fill" : "lock.fill")
|
content.image = UIImage(systemName: unlocked ? "lock.open.fill" : "lock.fill")
|
||||||
content.text = unlocked ? "Unlocked" : "Locked"
|
|
||||||
content.secondaryText = unlocked
|
|
||||||
? "Locks in \(state?.remainingSeconds ?? 0) seconds without activity"
|
|
||||||
: "Protected content is masked"
|
|
||||||
cell.selectionStyle = .none
|
|
||||||
} else {
|
|
||||||
content.image = UIImage(systemName: "lock.fill")
|
|
||||||
content.text = "Lock Now"
|
content.text = "Lock Now"
|
||||||
content.textProperties.color = .systemRed
|
content.textProperties.color = .systemRed
|
||||||
content.secondaryText = "Revoke all active authentication handles"
|
content.secondaryText = unlocked
|
||||||
|
? "Revoke all active authentication handles"
|
||||||
|
: "IronStorage is already locked"
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
cell.isUserInteractionEnabled = unlocked
|
||||||
|
cell.contentView.alpha = unlocked ? 1 : 0.45
|
||||||
|
case (4, 0):
|
||||||
|
content.image = UIImage(systemName: "circle.lefthalf.filled")
|
||||||
|
content.text = "App Appearance"
|
||||||
|
content.secondaryText = appearanceTitle(preferences?.appearance ?? .system)
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
case (5, 0):
|
||||||
|
content.image = UIImage(systemName: watchImage(preferences?.watchState))
|
||||||
|
content.text = preferences?.watchTitle ?? "Checking Apple Watch"
|
||||||
|
content.secondaryText = preferences?.watchDetail ?? "Reading pairing state"
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
default:
|
||||||
|
content.image = UIImage(systemName: "person.crop.circle")
|
||||||
|
content.text = gitIdentity?.name ?? "Git Commit Identity"
|
||||||
|
content.secondaryText = gitIdentity?.email ?? "Set the commit author name and email"
|
||||||
cell.accessoryType = .disclosureIndicator
|
cell.accessoryType = .disclosureIndicator
|
||||||
cell.isUserInteractionEnabled = state?.unlocked == true
|
|
||||||
cell.contentView.alpha = state?.unlocked == true ? 1 : 0.45
|
|
||||||
}
|
}
|
||||||
content.secondaryTextProperties.numberOfLines = 0
|
content.secondaryTextProperties.numberOfLines = 0
|
||||||
cell.contentConfiguration = content
|
cell.contentConfiguration = content
|
||||||
|
cell.accessibilityLabel = [content.text, content.secondaryText]
|
||||||
|
.compactMap { $0 }
|
||||||
|
.joined(separator: ", ")
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
if indexPath.section == 0 {
|
switch (indexPath.section, indexPath.row) {
|
||||||
editGitIdentity()
|
case (1, 1):
|
||||||
return
|
tokenUpdateRequested()
|
||||||
}
|
case (1, 2):
|
||||||
if indexPath.section == 1 {
|
removeTokenRequested()
|
||||||
|
case (2, 1), (2, 2):
|
||||||
guard let keyTransfer else { return }
|
guard let keyTransfer else { return }
|
||||||
if indexPath.row == 0 {
|
if indexPath.row == 1 {
|
||||||
requestKeyScanner(keyTransfer)
|
requestKeyScanner(keyTransfer)
|
||||||
} else {
|
} else {
|
||||||
navigationController?.pushViewController(
|
navigationController?.pushViewController(
|
||||||
@@ -1004,9 +1084,23 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
animated: true
|
animated: true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
case (3, 0):
|
||||||
|
editAuthenticationTimeout()
|
||||||
|
case (3, 2):
|
||||||
|
lockNow()
|
||||||
|
case (4, 0):
|
||||||
|
chooseAppearance()
|
||||||
|
case (5, 0):
|
||||||
|
openWatchPreference()
|
||||||
|
case (6, 0):
|
||||||
|
editGitIdentity()
|
||||||
|
default:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
guard indexPath.section == 3, indexPath.row == 1, let authentication else { return }
|
}
|
||||||
|
|
||||||
|
private func lockNow() {
|
||||||
|
guard let authentication else { return }
|
||||||
do {
|
do {
|
||||||
try authentication.manualLock()
|
try authentication.manualLock()
|
||||||
refreshState()
|
refreshState()
|
||||||
@@ -1188,7 +1282,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
switch result {
|
switch result {
|
||||||
case let .success(identity):
|
case let .success(identity):
|
||||||
gitIdentity = identity
|
gitIdentity = identity
|
||||||
tableView.reloadSections(IndexSet(integer: 0), with: .automatic)
|
tableView.reloadSections(IndexSet(integer: 6), with: .automatic)
|
||||||
UIAccessibility.post(
|
UIAccessibility.post(
|
||||||
notification: .announcement,
|
notification: .announcement,
|
||||||
argument: "Git commit identity updated"
|
argument: "Git commit identity updated"
|
||||||
@@ -1199,6 +1293,147 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func removeTokenRequested() {
|
||||||
|
guard preferences?.applicationAccount != nil else { return }
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: "Remove Application Token?",
|
||||||
|
message: "Local password browsing remains available, but Fetch, Pull, and Push will fail until a replacement token is stored.",
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
alert.addAction(UIAlertAction(title: "Remove Token", style: .destructive) {
|
||||||
|
[weak self] _ in
|
||||||
|
self?.runPreferenceUpdate(announcement: "Application token removed") {
|
||||||
|
try self?.authentication?.removeApplicationToken()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func editAuthenticationTimeout() {
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: "Inactivity Timeout",
|
||||||
|
message: "Enter a shared timeout from 1 to 86400 seconds.",
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
alert.addTextField { [preferences] field in
|
||||||
|
field.keyboardType = .numberPad
|
||||||
|
field.text = String(preferences?.authenticationTimeoutSeconds ?? 120)
|
||||||
|
field.accessibilityLabel = "Timeout in seconds"
|
||||||
|
}
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
alert.addAction(UIAlertAction(title: "Save", style: .default) {
|
||||||
|
[weak self, weak alert] _ in
|
||||||
|
let seconds = UInt64(alert?.textFields?.first?.text ?? "") ?? 0
|
||||||
|
self?.runPreferenceUpdate(announcement: "Inactivity timeout updated") {
|
||||||
|
try self?.authentication?.setAuthenticationTimeout(seconds: seconds)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func chooseAppearance() {
|
||||||
|
let alert = UIAlertController(title: "App Appearance", message: nil, preferredStyle: .actionSheet)
|
||||||
|
for appearance in [MobileAppearance.system, .light, .dark] {
|
||||||
|
let selected = appearance == preferences?.appearance
|
||||||
|
alert.addAction(UIAlertAction(
|
||||||
|
title: "\(selected ? "✓ " : "")\(appearanceTitle(appearance))",
|
||||||
|
style: .default
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.runPreferenceUpdate(announcement: "Appearance updated") {
|
||||||
|
try self?.authentication?.setMobileAppearance(appearance: appearance)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
alert.popoverPresentationController?.sourceView = tableView
|
||||||
|
alert.popoverPresentationController?.sourceRect = tableView.bounds
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func runPreferenceUpdate(
|
||||||
|
announcement: String,
|
||||||
|
operation: @escaping @Sendable () throws -> Void
|
||||||
|
) {
|
||||||
|
preferenceTask?.cancel()
|
||||||
|
preferenceTask = Task { [weak self] in
|
||||||
|
let result = await Task.detached(priority: .userInitiated) {
|
||||||
|
do {
|
||||||
|
try operation()
|
||||||
|
return Result<Void, AuthenticationFailure>.success(())
|
||||||
|
} catch let error as MobileAuthenticationFfiError {
|
||||||
|
return .failure(AuthenticationFailure(error))
|
||||||
|
} catch {
|
||||||
|
return .failure(.unexpected)
|
||||||
|
}
|
||||||
|
}.value
|
||||||
|
guard !Task.isCancelled, let self else { return }
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
refreshState()
|
||||||
|
UIAccessibility.post(notification: .announcement, argument: announcement)
|
||||||
|
case let .failure(failure):
|
||||||
|
presentAuthenticationFailure(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openWatchPreference() {
|
||||||
|
switch preferences?.watchState {
|
||||||
|
case .ready, .pending:
|
||||||
|
selectTotpTab()
|
||||||
|
case .notPaired:
|
||||||
|
presentWatchGuidance(
|
||||||
|
title: "Pair an Apple Watch",
|
||||||
|
message: "Open the Watch app on this iPhone and pair an Apple Watch before configuring IronStorage synchronization."
|
||||||
|
)
|
||||||
|
case .appNotInstalled:
|
||||||
|
presentWatchGuidance(
|
||||||
|
title: "Install IronStorage on Apple Watch",
|
||||||
|
message: "Open the Watch app on this iPhone and install the IronStorage companion on the paired watch."
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
presentWatchGuidance(
|
||||||
|
title: "Apple Watch Unavailable",
|
||||||
|
message: "Apple Watch connectivity is unavailable on this device."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentWatchGuidance(title: String, message: String) {
|
||||||
|
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func selectTotpTab() {
|
||||||
|
guard let controllers = tabBarController?.viewControllers else { return }
|
||||||
|
tabBarController?.selectedIndex = controllers.firstIndex { controller in
|
||||||
|
guard
|
||||||
|
let navigation = controller as? UINavigationController,
|
||||||
|
let root = navigation.viewControllers.first as? MobileTabRoot
|
||||||
|
else { return false }
|
||||||
|
return root.shellTab == .totp
|
||||||
|
} ?? tabBarController?.selectedIndex ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private func appearanceTitle(_ appearance: MobileAppearance) -> String {
|
||||||
|
switch appearance {
|
||||||
|
case .system: "System"
|
||||||
|
case .light: "Light"
|
||||||
|
case .dark: "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func watchImage(_ state: MobileWatchPreferenceState?) -> String {
|
||||||
|
switch state {
|
||||||
|
case .ready: "applewatch.radiowaves.left.and.right"
|
||||||
|
case .pending: "arrow.triangle.2.circlepath"
|
||||||
|
case .notPaired, .appNotInstalled: "applewatch.slash"
|
||||||
|
default: "applewatch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func authenticationDidChange() {
|
@objc private func authenticationDidChange() {
|
||||||
refreshState()
|
refreshState()
|
||||||
}
|
}
|
||||||
@@ -1244,8 +1479,38 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
private func refreshState() {
|
private func refreshState() {
|
||||||
state = try? authentication?.state()
|
state = try? authentication?.state()
|
||||||
gitIdentity = try? authentication?.gitIdentity()
|
gitIdentity = try? authentication?.gitIdentity()
|
||||||
|
preferences = try? authentication?.preferences(
|
||||||
|
watchSupported: WCSession.isSupported(),
|
||||||
|
watchPaired: watchSession?.isPaired ?? false,
|
||||||
|
watchAppInstalled: watchSession?.isWatchAppInstalled ?? false
|
||||||
|
)
|
||||||
|
if let appearance = preferences?.appearance {
|
||||||
|
tabBarController?.overrideUserInterfaceStyle = switch appearance {
|
||||||
|
case .system: .unspecified
|
||||||
|
case .light: .light
|
||||||
|
case .dark: .dark
|
||||||
|
}
|
||||||
|
}
|
||||||
tableView.reloadData()
|
tableView.reloadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated func session(
|
||||||
|
_ session: WCSession,
|
||||||
|
activationDidCompleteWith activationState: WCSessionActivationState,
|
||||||
|
error: Error?
|
||||||
|
) {
|
||||||
|
Task { @MainActor [weak self] in self?.refreshState() }
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func sessionDidBecomeInactive(_ session: WCSession) {}
|
||||||
|
|
||||||
|
nonisolated func sessionDidDeactivate(_ session: WCSession) {
|
||||||
|
session.activate()
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated func sessionWatchStateDidChange(_ session: WCSession) {
|
||||||
|
Task { @MainActor [weak self] in self?.refreshState() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
|
<key>WKApplication</key>
|
||||||
|
<true/>
|
||||||
<key>WKCompanionAppBundleIdentifier</key>
|
<key>WKCompanionAppBundleIdentifier</key>
|
||||||
<string>de.rfc1437.ironstorage</string>
|
<string>de.rfc1437.ironstorage</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ targets:
|
|||||||
- Assets.xcassets
|
- Assets.xcassets
|
||||||
- Sources/App
|
- Sources/App
|
||||||
- Generated/ironstorage_apple.swift
|
- Generated/ironstorage_apple.swift
|
||||||
|
dependencies:
|
||||||
|
- target: IronStorageWatch
|
||||||
|
embed: true
|
||||||
preBuildScripts:
|
preBuildScripts:
|
||||||
- name: Build Rust core
|
- name: Build Rust core
|
||||||
basedOnDependencyAnalysis: false
|
basedOnDependencyAnalysis: false
|
||||||
@@ -78,6 +81,8 @@ targets:
|
|||||||
path: Watch-Info.plist
|
path: Watch-Info.plist
|
||||||
properties:
|
properties:
|
||||||
CFBundleDisplayName: IronStorage
|
CFBundleDisplayName: IronStorage
|
||||||
|
WKApplication: true
|
||||||
WKCompanionAppBundleIdentifier: de.rfc1437.ironstorage
|
WKCompanionAppBundleIdentifier: de.rfc1437.ironstorage
|
||||||
sources:
|
sources:
|
||||||
|
- Assets.xcassets
|
||||||
- Sources/Watch
|
- Sources/Watch
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use ironstorage::{
|
use ironstorage::{
|
||||||
config::ConfigError,
|
config::{ConfigError, MobileAppearance as StorageMobileAppearance},
|
||||||
mobile::{self, MobileShellState as StorageShellState, MobileTab as StorageTab},
|
mobile::{self, MobileShellState as StorageShellState, MobileTab as StorageTab},
|
||||||
mobile_authentication::{
|
mobile_authentication::{
|
||||||
MobileAuthenticationError as StorageAuthenticationError,
|
MobileAuthenticationError as StorageAuthenticationError,
|
||||||
MobileAuthenticationErrorKind as StorageAuthenticationErrorKind,
|
MobileAuthenticationErrorKind as StorageAuthenticationErrorKind,
|
||||||
MobileAuthenticationState as StorageAuthenticationState,
|
MobileAuthenticationState as StorageAuthenticationState,
|
||||||
MobileEntryCopy as StorageEntryCopy, MobileEntryPresentation as StorageEntryPresentation,
|
MobileEntryCopy as StorageEntryCopy, MobileEntryPresentation as StorageEntryPresentation,
|
||||||
|
MobilePreferences as StorageMobilePreferences,
|
||||||
|
MobileWatchPreferenceState as StorageWatchPreferenceState,
|
||||||
},
|
},
|
||||||
mobile_entry::{
|
mobile_entry::{
|
||||||
MobileEntryEditorFieldKind as StorageEntryEditorFieldKind,
|
MobileEntryEditorFieldKind as StorageEntryEditorFieldKind,
|
||||||
@@ -623,6 +625,91 @@ pub struct MobileAuthenticationState {
|
|||||||
pub remaining_seconds: u64,
|
pub remaining_seconds: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
||||||
|
pub enum MobileAppearance {
|
||||||
|
System,
|
||||||
|
Light,
|
||||||
|
Dark,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StorageMobileAppearance> for MobileAppearance {
|
||||||
|
fn from(appearance: StorageMobileAppearance) -> Self {
|
||||||
|
match appearance {
|
||||||
|
StorageMobileAppearance::System => Self::System,
|
||||||
|
StorageMobileAppearance::Light => Self::Light,
|
||||||
|
StorageMobileAppearance::Dark => Self::Dark,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MobileAppearance> for StorageMobileAppearance {
|
||||||
|
fn from(appearance: MobileAppearance) -> Self {
|
||||||
|
match appearance {
|
||||||
|
MobileAppearance::System => Self::System,
|
||||||
|
MobileAppearance::Light => Self::Light,
|
||||||
|
MobileAppearance::Dark => Self::Dark,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
||||||
|
pub enum MobileWatchPreferenceState {
|
||||||
|
Unsupported,
|
||||||
|
NotPaired,
|
||||||
|
AppNotInstalled,
|
||||||
|
Ready,
|
||||||
|
Pending,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StorageWatchPreferenceState> for MobileWatchPreferenceState {
|
||||||
|
fn from(state: StorageWatchPreferenceState) -> Self {
|
||||||
|
match state {
|
||||||
|
StorageWatchPreferenceState::Unsupported => Self::Unsupported,
|
||||||
|
StorageWatchPreferenceState::NotPaired => Self::NotPaired,
|
||||||
|
StorageWatchPreferenceState::AppNotInstalled => Self::AppNotInstalled,
|
||||||
|
StorageWatchPreferenceState::Ready => Self::Ready,
|
||||||
|
StorageWatchPreferenceState::Pending => Self::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct MobilePreferences {
|
||||||
|
pub repository_title: String,
|
||||||
|
pub repository_url: String,
|
||||||
|
pub server_title: String,
|
||||||
|
pub server_identity: String,
|
||||||
|
pub application_account: Option<String>,
|
||||||
|
pub default_key_title: String,
|
||||||
|
pub default_key_fingerprint: String,
|
||||||
|
pub authentication_timeout_seconds: u64,
|
||||||
|
pub biometric_unlock_enabled: bool,
|
||||||
|
pub appearance: MobileAppearance,
|
||||||
|
pub watch_state: MobileWatchPreferenceState,
|
||||||
|
pub watch_title: String,
|
||||||
|
pub watch_detail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StorageMobilePreferences> for MobilePreferences {
|
||||||
|
fn from(preferences: StorageMobilePreferences) -> Self {
|
||||||
|
Self {
|
||||||
|
repository_title: preferences.repository_title().to_owned(),
|
||||||
|
repository_url: preferences.repository_url().to_owned(),
|
||||||
|
server_title: preferences.server_title().to_owned(),
|
||||||
|
server_identity: preferences.server_identity().to_owned(),
|
||||||
|
application_account: preferences.application_account().map(str::to_owned),
|
||||||
|
default_key_title: preferences.default_key_title().to_owned(),
|
||||||
|
default_key_fingerprint: preferences.default_key_fingerprint().to_owned(),
|
||||||
|
authentication_timeout_seconds: preferences.authentication_timeout_seconds(),
|
||||||
|
biometric_unlock_enabled: preferences.biometric_unlock_enabled(),
|
||||||
|
appearance: preferences.appearance().into(),
|
||||||
|
watch_state: preferences.watch_state().into(),
|
||||||
|
watch_title: preferences.watch_title().to_owned(),
|
||||||
|
watch_detail: preferences.watch_detail().to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
pub struct MobileGitIdentity {
|
pub struct MobileGitIdentity {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -1400,6 +1487,49 @@ impl MobileAuthentication {
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mobile_appearance(&self) -> Result<MobileAppearance, MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.mobile_appearance()
|
||||||
|
.map(Into::into)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn preferences(
|
||||||
|
&self,
|
||||||
|
watch_supported: bool,
|
||||||
|
watch_paired: bool,
|
||||||
|
watch_app_installed: bool,
|
||||||
|
) -> Result<MobilePreferences, MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.preferences(watch_supported, watch_paired, watch_app_installed)
|
||||||
|
.map(Into::into)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_authentication_timeout(
|
||||||
|
&self,
|
||||||
|
seconds: u64,
|
||||||
|
) -> Result<(), MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.set_authentication_timeout(seconds)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_mobile_appearance(
|
||||||
|
&self,
|
||||||
|
appearance: MobileAppearance,
|
||||||
|
) -> Result<(), MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.set_mobile_appearance(appearance.into())
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_application_token(&self) -> Result<(), MobileAuthenticationFfiError> {
|
||||||
|
self.authentication
|
||||||
|
.remove_application_token()
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_git_identity(
|
pub fn set_git_identity(
|
||||||
&self,
|
&self,
|
||||||
name: String,
|
name: String,
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ struct LeaseState {
|
|||||||
|
|
||||||
struct Shared<B: SecretStoreBackend, C: AuthenticationClock> {
|
struct Shared<B: SecretStoreBackend, C: AuthenticationClock> {
|
||||||
store: SecretStore<B>,
|
store: SecretStore<B>,
|
||||||
timeout: AuthenticationTimeout,
|
timeout: Mutex<AuthenticationTimeout>,
|
||||||
clock: C,
|
clock: C,
|
||||||
/// Serializes access checks with relock, eliminating check-then-use races.
|
/// Serializes access checks with relock, eliminating check-then-use races.
|
||||||
operation: Mutex<()>,
|
operation: Mutex<()>,
|
||||||
@@ -168,9 +168,14 @@ impl<B: SecretStoreBackend, C: AuthenticationClock> Clone for AuthenticationSess
|
|||||||
|
|
||||||
impl<B: SecretStoreBackend, C: AuthenticationClock> fmt::Debug for AuthenticationSession<B, C> {
|
impl<B: SecretStoreBackend, C: AuthenticationClock> fmt::Debug for AuthenticationSession<B, C> {
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let timeout = self
|
||||||
|
.shared
|
||||||
|
.timeout
|
||||||
|
.lock()
|
||||||
|
.map_or_else(|poisoned| *poisoned.into_inner(), |timeout| *timeout);
|
||||||
formatter
|
formatter
|
||||||
.debug_struct("AuthenticationSession")
|
.debug_struct("AuthenticationSession")
|
||||||
.field("timeout", &self.shared.timeout)
|
.field("timeout", &timeout)
|
||||||
.field("unlock_material", &"[REDACTED]")
|
.field("unlock_material", &"[REDACTED]")
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
@@ -203,7 +208,7 @@ impl<B: SecretStoreBackend, C: AuthenticationClock> AuthenticationSession<B, C>
|
|||||||
// The lease is the only cache. This prevents a second lifetime
|
// The lease is the only cache. This prevents a second lifetime
|
||||||
// policy in SecretStore from retaining duplicate records.
|
// policy in SecretStore from retaining duplicate records.
|
||||||
store: SecretStore::new(backend, SecretCachePolicy::Disabled, protections),
|
store: SecretStore::new(backend, SecretCachePolicy::Disabled, protections),
|
||||||
timeout,
|
timeout: Mutex::new(timeout),
|
||||||
clock,
|
clock,
|
||||||
operation: Mutex::new(()),
|
operation: Mutex::new(()),
|
||||||
state: Mutex::new(LeaseState {
|
state: Mutex::new(LeaseState {
|
||||||
@@ -308,6 +313,23 @@ impl<B: SecretStoreBackend, C: AuthenticationClock> AuthenticationSession<B, C>
|
|||||||
let _operation = self.shared.operation()?;
|
let _operation = self.shared.operation()?;
|
||||||
self.shared.revoke(RevocationReason::Cancelled)
|
self.shared.revoke(RevocationReason::Cancelled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply a new shared inactivity timeout to this process and any active lease.
|
||||||
|
pub fn set_timeout(&self, timeout: AuthenticationTimeout) -> Result<(), AuthenticationError> {
|
||||||
|
let _operation = self.shared.operation()?;
|
||||||
|
self.shared.expire_if_needed()?;
|
||||||
|
let deadline = self
|
||||||
|
.shared
|
||||||
|
.clock
|
||||||
|
.now()
|
||||||
|
.checked_add(timeout.duration())
|
||||||
|
.ok_or(AuthenticationError::ClockOverflow)?;
|
||||||
|
*self.shared.timeout()? = timeout;
|
||||||
|
if let Some(active) = self.shared.state()?.active.as_mut() {
|
||||||
|
active.deadline = deadline;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generation-bound secret access. Clones are revoked together.
|
/// Generation-bound secret access. Clones are revoked together.
|
||||||
@@ -351,7 +373,7 @@ impl<B: SecretStoreBackend, C: AuthenticationClock> AuthenticationHandle<B, C> {
|
|||||||
self.shared.expire_if_needed()?;
|
self.shared.expire_if_needed()?;
|
||||||
let now = self.shared.clock.now();
|
let now = self.shared.clock.now();
|
||||||
let deadline = now
|
let deadline = now
|
||||||
.checked_add(self.shared.timeout.duration())
|
.checked_add(self.shared.timeout()?.duration())
|
||||||
.ok_or(AuthenticationError::ClockOverflow)?;
|
.ok_or(AuthenticationError::ClockOverflow)?;
|
||||||
self.shared
|
self.shared
|
||||||
.with_active(self.generation, |active| active.deadline = deadline)
|
.with_active(self.generation, |active| active.deadline = deadline)
|
||||||
@@ -432,10 +454,18 @@ impl<B: SecretStoreBackend, C: AuthenticationClock> Shared<B, C> {
|
|||||||
.map_err(|_| AuthenticationError::SecretStore(SecretStoreError::Unavailable))
|
.map_err(|_| AuthenticationError::SecretStore(SecretStoreError::Unavailable))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn timeout(
|
||||||
|
&self,
|
||||||
|
) -> Result<std::sync::MutexGuard<'_, AuthenticationTimeout>, AuthenticationError> {
|
||||||
|
self.timeout
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| AuthenticationError::SecretStore(SecretStoreError::Unavailable))
|
||||||
|
}
|
||||||
|
|
||||||
fn begin_lease(&self) -> Result<u64, AuthenticationError> {
|
fn begin_lease(&self) -> Result<u64, AuthenticationError> {
|
||||||
let now = self.clock.now();
|
let now = self.clock.now();
|
||||||
let deadline = now
|
let deadline = now
|
||||||
.checked_add(self.timeout.duration())
|
.checked_add(self.timeout()?.duration())
|
||||||
.ok_or(AuthenticationError::ClockOverflow)?;
|
.ok_or(AuthenticationError::ClockOverflow)?;
|
||||||
let (generation, was_active) = {
|
let (generation, was_active) = {
|
||||||
let mut state = self.state()?;
|
let mut state = self.state()?;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub struct Config {
|
|||||||
clipboard_timeout: ClipboardTimeout,
|
clipboard_timeout: ClipboardTimeout,
|
||||||
authentication_timeout: AuthenticationTimeout,
|
authentication_timeout: AuthenticationTimeout,
|
||||||
biometric_unlock_enabled: bool,
|
biometric_unlock_enabled: bool,
|
||||||
|
mobile_appearance: MobileAppearance,
|
||||||
mobile_tab: MobileTab,
|
mobile_tab: MobileTab,
|
||||||
mobile_home_refreshed_at: Option<i64>,
|
mobile_home_refreshed_at: Option<i64>,
|
||||||
watch_shared_totp_entries: BTreeSet<EntryPath>,
|
watch_shared_totp_entries: BTreeSet<EntryPath>,
|
||||||
@@ -60,6 +61,35 @@ pub struct ConfigSettings {
|
|||||||
authentication_timeout: Duration,
|
authentication_timeout: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub enum MobileAppearance {
|
||||||
|
#[default]
|
||||||
|
System,
|
||||||
|
Light,
|
||||||
|
Dark,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobileAppearance {
|
||||||
|
fn from_config(value: &str) -> Result<Self, ConfigError> {
|
||||||
|
match value {
|
||||||
|
"system" => Ok(Self::System),
|
||||||
|
"light" => Ok(Self::Light),
|
||||||
|
"dark" => Ok(Self::Dark),
|
||||||
|
_ => Err(ConfigError::InvalidField {
|
||||||
|
field: "ui.mobile_appearance",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn config_value(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::System => "system",
|
||||||
|
Self::Light => "light",
|
||||||
|
Self::Dark => "dark",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ConfigSettings {
|
impl ConfigSettings {
|
||||||
pub fn vault(&self) -> &Path {
|
pub fn vault(&self) -> &Path {
|
||||||
&self.vault
|
&self.vault
|
||||||
@@ -133,6 +163,10 @@ impl Config {
|
|||||||
self.biometric_unlock_enabled
|
self.biometric_unlock_enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mobile_appearance(&self) -> MobileAppearance {
|
||||||
|
self.mobile_appearance
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mobile_tab(&self) -> MobileTab {
|
pub fn mobile_tab(&self) -> MobileTab {
|
||||||
self.mobile_tab
|
self.mobile_tab
|
||||||
}
|
}
|
||||||
@@ -280,6 +314,66 @@ impl Config {
|
|||||||
validate_config(self.source.clone(), document, raw)?.persist()
|
validate_config(self.source.clone(), document, raw)?.persist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_authentication_timeout(
|
||||||
|
&self,
|
||||||
|
timeout: AuthenticationTimeout,
|
||||||
|
) -> Result<(), ConfigError> {
|
||||||
|
let mut document = self.current_document()?;
|
||||||
|
let root = document
|
||||||
|
.as_table_mut()
|
||||||
|
.ok_or_else(|| ConfigError::Malformed {
|
||||||
|
path: self.source.clone(),
|
||||||
|
})?;
|
||||||
|
let security = root
|
||||||
|
.entry("security")
|
||||||
|
.or_insert_with(|| toml::Value::Table(toml::Table::new()))
|
||||||
|
.as_table_mut()
|
||||||
|
.ok_or(ConfigError::InvalidField { field: "security" })?;
|
||||||
|
let seconds =
|
||||||
|
i64::try_from(timeout.duration().as_secs()).map_err(|_| ConfigError::InvalidField {
|
||||||
|
field: "security.inactivity_timeout_seconds",
|
||||||
|
})?;
|
||||||
|
security.insert(
|
||||||
|
"inactivity_timeout_seconds".to_owned(),
|
||||||
|
toml::Value::Integer(seconds),
|
||||||
|
);
|
||||||
|
let raw = document
|
||||||
|
.clone()
|
||||||
|
.try_into::<RawConfig>()
|
||||||
|
.map_err(|_| ConfigError::Malformed {
|
||||||
|
path: self.source.clone(),
|
||||||
|
})?;
|
||||||
|
validate_config(self.source.clone(), document, raw)?.persist()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_mobile_appearance(
|
||||||
|
&self,
|
||||||
|
appearance: MobileAppearance,
|
||||||
|
) -> Result<(), ConfigError> {
|
||||||
|
let mut document = self.current_document()?;
|
||||||
|
let root = document
|
||||||
|
.as_table_mut()
|
||||||
|
.ok_or_else(|| ConfigError::Malformed {
|
||||||
|
path: self.source.clone(),
|
||||||
|
})?;
|
||||||
|
let ui = root
|
||||||
|
.entry("ui")
|
||||||
|
.or_insert_with(|| toml::Value::Table(toml::Table::new()))
|
||||||
|
.as_table_mut()
|
||||||
|
.ok_or(ConfigError::InvalidField { field: "ui" })?;
|
||||||
|
ui.insert(
|
||||||
|
"mobile_appearance".to_owned(),
|
||||||
|
toml::Value::String(appearance.config_value().to_owned()),
|
||||||
|
);
|
||||||
|
let raw = document
|
||||||
|
.clone()
|
||||||
|
.try_into::<RawConfig>()
|
||||||
|
.map_err(|_| ConfigError::Malformed {
|
||||||
|
path: self.source.clone(),
|
||||||
|
})?;
|
||||||
|
validate_config(self.source.clone(), document, raw)?.persist()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update_git_identity(&self, identity: &GitIdentity) -> Result<(), ConfigError> {
|
pub fn update_git_identity(&self, identity: &GitIdentity) -> Result<(), ConfigError> {
|
||||||
let mut document = self.current_document()?;
|
let mut document = self.current_document()?;
|
||||||
let root = document
|
let root = document
|
||||||
@@ -952,6 +1046,7 @@ struct RawSecurity {
|
|||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct RawUi {
|
struct RawUi {
|
||||||
selected_mobile_tab: Option<String>,
|
selected_mobile_tab: Option<String>,
|
||||||
|
mobile_appearance: Option<String>,
|
||||||
home_remote_refreshed_at_unix_seconds: Option<i64>,
|
home_remote_refreshed_at_unix_seconds: Option<i64>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
watch_shared_totp_entries: Vec<String>,
|
watch_shared_totp_entries: Vec<String>,
|
||||||
@@ -1032,6 +1127,13 @@ fn validate_config(
|
|||||||
field: "security.inactivity_timeout_seconds",
|
field: "security.inactivity_timeout_seconds",
|
||||||
})?;
|
})?;
|
||||||
let biometric_unlock_enabled = raw.security.biometric_unlock_enabled.unwrap_or(false);
|
let biometric_unlock_enabled = raw.security.biometric_unlock_enabled.unwrap_or(false);
|
||||||
|
let mobile_appearance = raw
|
||||||
|
.ui
|
||||||
|
.mobile_appearance
|
||||||
|
.as_deref()
|
||||||
|
.map(MobileAppearance::from_config)
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or_default();
|
||||||
let mobile_tab = raw
|
let mobile_tab = raw
|
||||||
.ui
|
.ui
|
||||||
.selected_mobile_tab
|
.selected_mobile_tab
|
||||||
@@ -1083,6 +1185,7 @@ fn validate_config(
|
|||||||
clipboard_timeout,
|
clipboard_timeout,
|
||||||
authentication_timeout,
|
authentication_timeout,
|
||||||
biometric_unlock_enabled,
|
biometric_unlock_enabled,
|
||||||
|
mobile_appearance,
|
||||||
mobile_tab,
|
mobile_tab,
|
||||||
mobile_home_refreshed_at,
|
mobile_home_refreshed_at,
|
||||||
watch_shared_totp_entries,
|
watch_shared_totp_entries,
|
||||||
@@ -1270,6 +1373,7 @@ fn validate_known_fields(value: &toml::Value, source: &Path) -> Result<(), Confi
|
|||||||
"ui",
|
"ui",
|
||||||
&[
|
&[
|
||||||
"selected_mobile_tab",
|
"selected_mobile_tab",
|
||||||
|
"mobile_appearance",
|
||||||
"home_remote_refreshed_at_unix_seconds",
|
"home_remote_refreshed_at_unix_seconds",
|
||||||
"watch_shared_totp_entries",
|
"watch_shared_totp_entries",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
//! Shared mobile authentication state; Swift only supplies input and presents results.
|
//! Shared mobile authentication state; Swift only supplies input and presents results.
|
||||||
|
|
||||||
use std::{collections::BTreeMap, error::Error, fmt, sync::Mutex};
|
use std::{collections::BTreeMap, error::Error, fmt, sync::Mutex, time::Duration};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
authentication::{
|
authentication::{
|
||||||
AuthenticationError, NativeAuthenticationHandle, NativeAuthenticationSession,
|
AuthenticationError, AuthenticationTimeout, NativeAuthenticationHandle,
|
||||||
|
NativeAuthenticationSession,
|
||||||
},
|
},
|
||||||
config::{Config, ConfigError},
|
config::{Config, ConfigError, GitRemote, MobileAppearance},
|
||||||
crypto::{CryptoError, KeyInfo, KeyStore, SecretProvider, SecretProviderError},
|
crypto::{CryptoError, KeyInfo, KeyStore, SecretProvider, SecretProviderError},
|
||||||
document::{DocumentError, EntryDocument, EntryDocumentService, EntryFieldId},
|
document::{DocumentError, EntryDocument, EntryDocumentService, EntryFieldId},
|
||||||
git::{AutomaticEntryCommitter, AutomaticTreeCommitter, GitError, GitIdentity},
|
git::{AutomaticEntryCommitter, AutomaticTreeCommitter, GitError, GitIdentity},
|
||||||
@@ -27,7 +28,9 @@ use crate::{
|
|||||||
repository::{
|
repository::{
|
||||||
DirectoryPath, EncryptedEntry, EntryPath, Repository, RepositoryError, SecretBytes,
|
DirectoryPath, EncryptedEntry, EntryPath, Repository, RepositoryError, SecretBytes,
|
||||||
},
|
},
|
||||||
secret_store::{SecretProtectionPolicy, SecretStoreError},
|
secret_store::{
|
||||||
|
NativeSecretStore, SecretCachePolicy, SecretProtectionPolicy, SecretStoreError,
|
||||||
|
},
|
||||||
write::{VaultWriter, WriteError},
|
write::{VaultWriter, WriteError},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -129,6 +132,86 @@ pub struct MobileAuthenticationState {
|
|||||||
remaining_seconds: u64,
|
remaining_seconds: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum MobileWatchPreferenceState {
|
||||||
|
Unsupported,
|
||||||
|
NotPaired,
|
||||||
|
AppNotInstalled,
|
||||||
|
Ready,
|
||||||
|
Pending,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MobilePreferences {
|
||||||
|
repository_title: String,
|
||||||
|
repository_url: String,
|
||||||
|
server_title: String,
|
||||||
|
server_identity: String,
|
||||||
|
application_account: Option<String>,
|
||||||
|
default_key_title: String,
|
||||||
|
default_key_fingerprint: String,
|
||||||
|
authentication_timeout_seconds: u64,
|
||||||
|
biometric_unlock_enabled: bool,
|
||||||
|
appearance: MobileAppearance,
|
||||||
|
watch_state: MobileWatchPreferenceState,
|
||||||
|
watch_title: String,
|
||||||
|
watch_detail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobilePreferences {
|
||||||
|
pub fn repository_title(&self) -> &str {
|
||||||
|
&self.repository_title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn repository_url(&self) -> &str {
|
||||||
|
&self.repository_url
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn server_title(&self) -> &str {
|
||||||
|
&self.server_title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn server_identity(&self) -> &str {
|
||||||
|
&self.server_identity
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn application_account(&self) -> Option<&str> {
|
||||||
|
self.application_account.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_key_title(&self) -> &str {
|
||||||
|
&self.default_key_title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_key_fingerprint(&self) -> &str {
|
||||||
|
&self.default_key_fingerprint
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authentication_timeout_seconds(&self) -> u64 {
|
||||||
|
self.authentication_timeout_seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn biometric_unlock_enabled(&self) -> bool {
|
||||||
|
self.biometric_unlock_enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn appearance(&self) -> MobileAppearance {
|
||||||
|
self.appearance
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn watch_state(&self) -> MobileWatchPreferenceState {
|
||||||
|
self.watch_state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn watch_title(&self) -> &str {
|
||||||
|
&self.watch_title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn watch_detail(&self) -> &str {
|
||||||
|
&self.watch_detail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct MobileEntryCopy {
|
pub struct MobileEntryCopy {
|
||||||
value: String,
|
value: String,
|
||||||
@@ -190,6 +273,8 @@ struct ActiveMobileLease {
|
|||||||
|
|
||||||
struct MobileAuthenticationStatus {
|
struct MobileAuthenticationStatus {
|
||||||
biometric_unlock_enabled: bool,
|
biometric_unlock_enabled: bool,
|
||||||
|
authentication_timeout: AuthenticationTimeout,
|
||||||
|
appearance: MobileAppearance,
|
||||||
git_identity: GitIdentity,
|
git_identity: GitIdentity,
|
||||||
active: Option<ActiveMobileLease>,
|
active: Option<ActiveMobileLease>,
|
||||||
next_editor_id: u64,
|
next_editor_id: u64,
|
||||||
@@ -256,6 +341,8 @@ impl MobileAuthentication {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
status: Mutex::new(MobileAuthenticationStatus {
|
status: Mutex::new(MobileAuthenticationStatus {
|
||||||
biometric_unlock_enabled: config.biometric_unlock_enabled(),
|
biometric_unlock_enabled: config.biometric_unlock_enabled(),
|
||||||
|
authentication_timeout: config.authentication_timeout(),
|
||||||
|
appearance: config.mobile_appearance(),
|
||||||
git_identity: config.git_identity().clone(),
|
git_identity: config.git_identity().clone(),
|
||||||
active: None,
|
active: None,
|
||||||
next_editor_id: 0,
|
next_editor_id: 0,
|
||||||
@@ -430,6 +517,10 @@ impl MobileAuthentication {
|
|||||||
Ok(self.status()?.git_identity.clone())
|
Ok(self.status()?.git_identity.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mobile_appearance(&self) -> Result<MobileAppearance, MobileAuthenticationError> {
|
||||||
|
Ok(self.status()?.appearance)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_git_identity(
|
pub fn set_git_identity(
|
||||||
&self,
|
&self,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -449,6 +540,112 @@ impl MobileAuthentication {
|
|||||||
Ok(identity)
|
Ok(identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn preferences(
|
||||||
|
&self,
|
||||||
|
watch_supported: bool,
|
||||||
|
watch_paired: bool,
|
||||||
|
watch_app_installed: bool,
|
||||||
|
) -> Result<MobilePreferences, MobileAuthenticationError> {
|
||||||
|
let remote = self
|
||||||
|
.config
|
||||||
|
.git_remotes()
|
||||||
|
.iter()
|
||||||
|
.find(|remote| remote.name().as_str() == "origin")
|
||||||
|
.or_else(|| self.config.git_remotes().first())
|
||||||
|
.ok_or_else(|| config_detail("No HTTPS Git remote is configured."))?;
|
||||||
|
let handle = self
|
||||||
|
.keys
|
||||||
|
.resolve(self.config.default_key().as_str())
|
||||||
|
.map_err(key_error)?;
|
||||||
|
let key = self
|
||||||
|
.keys
|
||||||
|
.infos()
|
||||||
|
.find(|key| key.fingerprint() == handle.fingerprint())
|
||||||
|
.ok_or_else(|| config_detail("The default GPG key is unavailable."))?;
|
||||||
|
let status = self.status()?;
|
||||||
|
let selected = status.watch_shared_totp_entries.len();
|
||||||
|
let (watch_state, watch_title, watch_detail) =
|
||||||
|
watch_preference(watch_supported, watch_paired, watch_app_installed, selected);
|
||||||
|
let authentication_timeout_seconds = status.authentication_timeout.duration().as_secs();
|
||||||
|
let biometric_unlock_enabled = status.biometric_unlock_enabled;
|
||||||
|
let appearance = status.appearance;
|
||||||
|
drop(status);
|
||||||
|
let server_title = remote.url().host_str().unwrap_or("HTTPS server").to_owned();
|
||||||
|
let repository_title = remote
|
||||||
|
.url()
|
||||||
|
.path_segments()
|
||||||
|
.and_then(Iterator::last)
|
||||||
|
.unwrap_or("Password Store")
|
||||||
|
.trim_end_matches(".git")
|
||||||
|
.to_owned();
|
||||||
|
Ok(MobilePreferences {
|
||||||
|
repository_title,
|
||||||
|
repository_url: remote.url().to_string(),
|
||||||
|
server_title,
|
||||||
|
server_identity: remote.server_id().as_str().to_owned(),
|
||||||
|
application_account: application_account(remote)?,
|
||||||
|
default_key_title: key
|
||||||
|
.user_ids()
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| self.config.default_key().to_string()),
|
||||||
|
default_key_fingerprint: key.fingerprint().to_string(),
|
||||||
|
authentication_timeout_seconds,
|
||||||
|
biometric_unlock_enabled,
|
||||||
|
appearance,
|
||||||
|
watch_state,
|
||||||
|
watch_title,
|
||||||
|
watch_detail,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_authentication_timeout(
|
||||||
|
&self,
|
||||||
|
seconds: u64,
|
||||||
|
) -> Result<(), MobileAuthenticationError> {
|
||||||
|
let timeout = AuthenticationTimeout::new(Duration::from_secs(seconds))
|
||||||
|
.map_err(|_| config_detail("Choose an inactivity timeout from 1 to 86400 seconds."))?;
|
||||||
|
let previous = self.status()?.authentication_timeout;
|
||||||
|
self.session
|
||||||
|
.set_timeout(timeout)
|
||||||
|
.map_err(MobileAuthenticationError::authentication)?;
|
||||||
|
if let Err(error) = self.config.update_authentication_timeout(timeout) {
|
||||||
|
let _ = self.session.set_timeout(previous);
|
||||||
|
return Err(config_error(error));
|
||||||
|
}
|
||||||
|
self.status()?.authentication_timeout = timeout;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_mobile_appearance(
|
||||||
|
&self,
|
||||||
|
appearance: MobileAppearance,
|
||||||
|
) -> Result<(), MobileAuthenticationError> {
|
||||||
|
self.config
|
||||||
|
.update_mobile_appearance(appearance)
|
||||||
|
.map_err(config_error)?;
|
||||||
|
self.status()?.appearance = appearance;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_application_token(&self) -> Result<(), MobileAuthenticationError> {
|
||||||
|
let remote = self
|
||||||
|
.config
|
||||||
|
.git_remotes()
|
||||||
|
.iter()
|
||||||
|
.find(|remote| remote.name().as_str() == "origin")
|
||||||
|
.or_else(|| self.config.git_remotes().first())
|
||||||
|
.ok_or_else(|| config_detail("No HTTPS Git remote is configured."))?;
|
||||||
|
let store = preference_secret_store()?;
|
||||||
|
store.unlock().map_err(preference_secret_error)?;
|
||||||
|
let result = store.delete_https_git_credential(remote.server_id(), remote.application_id());
|
||||||
|
let lock_result = store.lock();
|
||||||
|
if !matches!(result, Ok(()) | Err(SecretStoreError::Missing)) {
|
||||||
|
return Err(preference_secret_error(result.expect_err("checked error")));
|
||||||
|
}
|
||||||
|
lock_result.map_err(preference_secret_error)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn touch_user_activity(&self) -> Result<(), MobileAuthenticationError> {
|
pub fn touch_user_activity(&self) -> Result<(), MobileAuthenticationError> {
|
||||||
let status = self.status()?;
|
let status = self.status()?;
|
||||||
let active = status.active.as_ref().ok_or_else(|| {
|
let active = status.active.as_ref().ok_or_else(|| {
|
||||||
@@ -1183,6 +1380,99 @@ fn config_error(error: ConfigError) -> MobileAuthenticationError {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn config_detail(detail: &str) -> MobileAuthenticationError {
|
||||||
|
MobileAuthenticationError::new(
|
||||||
|
MobileAuthenticationErrorKind::Configuration,
|
||||||
|
"Preference Is Unavailable",
|
||||||
|
detail,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preference_secret_store() -> Result<NativeSecretStore, MobileAuthenticationError> {
|
||||||
|
NativeSecretStore::system(
|
||||||
|
SecretCachePolicy::Disabled,
|
||||||
|
SecretProtectionPolicy::device_unlocked(),
|
||||||
|
)
|
||||||
|
.map_err(preference_secret_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn application_account(remote: &GitRemote) -> Result<Option<String>, MobileAuthenticationError> {
|
||||||
|
let store = preference_secret_store()?;
|
||||||
|
store.unlock().map_err(preference_secret_error)?;
|
||||||
|
let result = store.https_git_credential_account(remote.server_id(), remote.application_id());
|
||||||
|
let lock_result = store.lock();
|
||||||
|
let account = match result {
|
||||||
|
Ok(account) => Some(account),
|
||||||
|
Err(SecretStoreError::Missing) => None,
|
||||||
|
Err(error) => return Err(preference_secret_error(error)),
|
||||||
|
};
|
||||||
|
lock_result
|
||||||
|
.map_err(preference_secret_error)
|
||||||
|
.map(|()| account)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preference_secret_error(error: SecretStoreError) -> MobileAuthenticationError {
|
||||||
|
MobileAuthenticationError::new(
|
||||||
|
MobileAuthenticationErrorKind::SecureStorage,
|
||||||
|
"Application Token Is Unavailable",
|
||||||
|
match error {
|
||||||
|
SecretStoreError::Denied => "Access to protected token storage was denied.".to_owned(),
|
||||||
|
SecretStoreError::Cancelled => {
|
||||||
|
"Access to protected token storage was cancelled.".to_owned()
|
||||||
|
}
|
||||||
|
_ => "Protected application-token storage is unavailable.".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn watch_preference(
|
||||||
|
supported: bool,
|
||||||
|
paired: bool,
|
||||||
|
app_installed: bool,
|
||||||
|
selected: usize,
|
||||||
|
) -> (MobileWatchPreferenceState, String, String) {
|
||||||
|
if !supported {
|
||||||
|
return (
|
||||||
|
MobileWatchPreferenceState::Unsupported,
|
||||||
|
"Unavailable".to_owned(),
|
||||||
|
"Apple Watch connectivity is unavailable on this device.".to_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !paired {
|
||||||
|
return (
|
||||||
|
MobileWatchPreferenceState::NotPaired,
|
||||||
|
"Not Paired".to_owned(),
|
||||||
|
"Pair an Apple Watch in the Watch app to enable synchronization.".to_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !app_installed {
|
||||||
|
return (
|
||||||
|
MobileWatchPreferenceState::AppNotInstalled,
|
||||||
|
"Watch App Not Installed".to_owned(),
|
||||||
|
"Install the IronStorage companion on the paired Apple Watch.".to_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if selected == 0 {
|
||||||
|
return (
|
||||||
|
MobileWatchPreferenceState::Ready,
|
||||||
|
"Ready".to_owned(),
|
||||||
|
"No TOTP codes are selected for Apple Watch.".to_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
(
|
||||||
|
MobileWatchPreferenceState::Pending,
|
||||||
|
"Synchronization Pending".to_owned(),
|
||||||
|
format!(
|
||||||
|
"{selected} selected TOTP {} pending Apple Watch synchronization.",
|
||||||
|
if selected == 1 {
|
||||||
|
"code is"
|
||||||
|
} else {
|
||||||
|
"codes are"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn entry_error(error: RepositoryError) -> MobileAuthenticationError {
|
fn entry_error(error: RepositoryError) -> MobileAuthenticationError {
|
||||||
MobileAuthenticationError::new(
|
MobileAuthenticationError::new(
|
||||||
MobileAuthenticationErrorKind::Entry,
|
MobileAuthenticationErrorKind::Entry,
|
||||||
@@ -1293,7 +1583,8 @@ fn entry_detail(title: &str, error: impl fmt::Display) -> MobileAuthenticationEr
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
MobileRepositoryOperation, MobileRepositoryOperationError, repository_operation_conflict,
|
MobileRepositoryOperation, MobileRepositoryOperationError, MobileWatchPreferenceState,
|
||||||
|
repository_operation_conflict, watch_preference,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1319,4 +1610,19 @@ mod tests {
|
|||||||
Some(MobileRepositoryOperationError::Busy)
|
Some(MobileRepositoryOperationError::Busy)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn watch_preferences_render_platform_and_selection_state() {
|
||||||
|
assert_eq!(
|
||||||
|
watch_preference(true, false, false, 2).0,
|
||||||
|
MobileWatchPreferenceState::NotPaired
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
watch_preference(true, true, true, 0).0,
|
||||||
|
MobileWatchPreferenceState::Ready
|
||||||
|
);
|
||||||
|
let pending = watch_preference(true, true, true, 2);
|
||||||
|
assert_eq!(pending.0, MobileWatchPreferenceState::Pending);
|
||||||
|
assert!(pending.2.contains("2 selected TOTP codes"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -557,6 +557,29 @@ impl<B: SecretStoreBackend> SecretStore<B> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return only the non-secret account identity for one configured Git credential.
|
||||||
|
pub fn https_git_credential_account(
|
||||||
|
&self,
|
||||||
|
server: &ServerId,
|
||||||
|
application: &ApplicationId,
|
||||||
|
) -> Result<String, SecretStoreError> {
|
||||||
|
self.retrieve_git_record(server, application)?
|
||||||
|
.reference
|
||||||
|
.account()
|
||||||
|
.map(str::to_owned)
|
||||||
|
.ok_or(SecretStoreError::Corrupted)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the credential selected by its configured server/application identity.
|
||||||
|
pub fn delete_https_git_credential(
|
||||||
|
&self,
|
||||||
|
server: &ServerId,
|
||||||
|
application: &ApplicationId,
|
||||||
|
) -> Result<(), SecretStoreError> {
|
||||||
|
let record = self.retrieve_git_record(server, application)?;
|
||||||
|
self.delete(&record.reference)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn delete(&self, reference: &SecretReference) -> Result<(), SecretStoreError> {
|
pub fn delete(&self, reference: &SecretReference) -> Result<(), SecretStoreError> {
|
||||||
let locator = reference.locator();
|
let locator = reference.locator();
|
||||||
let mut state = self.unlocked_state()?;
|
let mut state = self.unlocked_state()?;
|
||||||
|
|||||||
@@ -249,6 +249,30 @@ fn user_activity_extends_the_lease_but_secret_access_and_timer_polling_do_not()
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn timeout_updates_apply_to_the_active_lease_and_later_activity() -> TestResult {
|
||||||
|
let fixture = FixtureSet::load()?;
|
||||||
|
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||||
|
let alice = fixture_key(&fixture, &keys, "alice")?;
|
||||||
|
let backend = MemoryBackend::default();
|
||||||
|
provision_passphrase(
|
||||||
|
backend.clone(),
|
||||||
|
&alice,
|
||||||
|
fixture.key("alice")?.passphrase.as_bytes(),
|
||||||
|
)?;
|
||||||
|
let clock = ManualClock::default();
|
||||||
|
let session = session(backend, clock.clone(), 120)?;
|
||||||
|
let handle = session.authenticate(&alice)?;
|
||||||
|
|
||||||
|
clock.advance(Duration::from_secs(20));
|
||||||
|
session.set_timeout(AuthenticationTimeout::new(Duration::from_secs(30))?)?;
|
||||||
|
assert_eq!(handle.remaining_time()?, Duration::from_secs(30));
|
||||||
|
clock.advance(Duration::from_secs(10));
|
||||||
|
handle.touch_user_activity()?;
|
||||||
|
assert_eq!(handle.remaining_time()?, Duration::from_secs(30));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn manual_lock_cancellation_and_expiry_revoke_all_existing_handles() -> TestResult {
|
fn manual_lock_cancellation_and_expiry_revoke_all_existing_handles() -> TestResult {
|
||||||
let fixture = FixtureSet::load()?;
|
let fixture = FixtureSet::load()?;
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ use std::{collections::BTreeSet, error::Error, ffi::OsStr, fs, path::Path, time:
|
|||||||
|
|
||||||
use ironstorage::presentation::DEFAULT_CLIPBOARD_TIMEOUT;
|
use ironstorage::presentation::DEFAULT_CLIPBOARD_TIMEOUT;
|
||||||
use ironstorage::{
|
use ironstorage::{
|
||||||
authentication::{DEFAULT_AUTHENTICATION_TIMEOUT, MAX_AUTHENTICATION_TIMEOUT},
|
authentication::{
|
||||||
config::{ConfigError, ConfigLoader, EditorSource},
|
AuthenticationTimeout, DEFAULT_AUTHENTICATION_TIMEOUT, MAX_AUTHENTICATION_TIMEOUT,
|
||||||
|
},
|
||||||
|
config::{ConfigError, ConfigLoader, EditorSource, MobileAppearance},
|
||||||
desktop::DesktopStorage,
|
desktop::DesktopStorage,
|
||||||
git::GitIdentity,
|
git::GitIdentity,
|
||||||
mobile::MobileTab,
|
mobile::MobileTab,
|
||||||
@@ -299,6 +301,29 @@ fn biometric_preference_is_secret_free_and_defaults_to_disabled() -> TestResult
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mobile_timeout_and_appearance_share_the_secret_free_configuration() -> TestResult {
|
||||||
|
let fixture = ConfigurationFixture::new()?;
|
||||||
|
fs::create_dir_all(fixture.temporary.path().join("cwd/vault"))?;
|
||||||
|
fixture.write_explicit(fixture.valid_contents())?;
|
||||||
|
let config = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||||
|
assert_eq!(config.mobile_appearance(), MobileAppearance::System);
|
||||||
|
|
||||||
|
config.update_mobile_appearance(MobileAppearance::Dark)?;
|
||||||
|
config.update_authentication_timeout(AuthenticationTimeout::new(Duration::from_secs(300))?)?;
|
||||||
|
let reloaded = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||||
|
assert_eq!(reloaded.mobile_appearance(), MobileAppearance::Dark);
|
||||||
|
assert_eq!(
|
||||||
|
reloaded.authentication_timeout().duration(),
|
||||||
|
Duration::from_secs(300)
|
||||||
|
);
|
||||||
|
let contents = fs::read_to_string(fixture.explicit_path())?;
|
||||||
|
assert!(contents.contains("mobile_appearance = \"dark\""));
|
||||||
|
assert!(contents.contains("inactivity_timeout_seconds = 300"));
|
||||||
|
assert!(!contents.to_ascii_lowercase().contains("token ="));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn desktop_vault_switch_preserves_and_reloads_the_shared_configuration() -> TestResult {
|
fn desktop_vault_switch_preserves_and_reloads_the_shared_configuration() -> TestResult {
|
||||||
let fixture = ConfigurationFixture::new()?;
|
let fixture = ConfigurationFixture::new()?;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use ironstorage::{
|
use ironstorage::{
|
||||||
config::ConfigLoader,
|
config::{ConfigLoader, GitRemote},
|
||||||
crypto::{CryptoError, KeyInfo, KeyStore, SecretProvider as _, SecretProviderError},
|
crypto::{CryptoError, KeyInfo, KeyStore, SecretProvider as _, SecretProviderError},
|
||||||
git::{GitCredentialProvider as _, GitError},
|
git::{GitCredentialProvider as _, GitError},
|
||||||
repository::{EncryptedEntry, SecretBytes},
|
repository::{EncryptedEntry, SecretBytes},
|
||||||
@@ -325,6 +325,40 @@ fn bounded_cache_is_cleared_by_lock_and_never_aliases_git_accounts() -> TestResu
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn git_account_status_and_removal_never_expose_the_token() -> TestResult {
|
||||||
|
let backend = MemoryBackend::default();
|
||||||
|
let store = SecretStore::new(
|
||||||
|
backend,
|
||||||
|
SecretCachePolicy::Disabled,
|
||||||
|
SecretProtectionPolicy::device_unlocked(),
|
||||||
|
);
|
||||||
|
let remote = GitRemote::https(
|
||||||
|
"origin",
|
||||||
|
"https://git.example.test/alice/store.git",
|
||||||
|
"personal-git",
|
||||||
|
"ironstorage-mobile",
|
||||||
|
)?;
|
||||||
|
store.unlock()?;
|
||||||
|
store.store_https_git_credential(
|
||||||
|
remote.server_id(),
|
||||||
|
remote.application_id(),
|
||||||
|
"alice",
|
||||||
|
SecretBytes::new(b"private-token".to_vec()),
|
||||||
|
)?;
|
||||||
|
assert_eq!(
|
||||||
|
store.https_git_credential_account(remote.server_id(), remote.application_id())?,
|
||||||
|
"alice"
|
||||||
|
);
|
||||||
|
store.delete_https_git_credential(remote.server_id(), remote.application_id())?;
|
||||||
|
assert!(matches!(
|
||||||
|
store.https_git_credential_account(remote.server_id(), remote.application_id()),
|
||||||
|
Err(SecretStoreError::Missing)
|
||||||
|
));
|
||||||
|
assert!(!format!("{store:?}").contains("private-token"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn one_unlocked_provider_supplies_openpgp_and_https_git_secrets() -> TestResult {
|
fn one_unlocked_provider_supplies_openpgp_and_https_git_secrets() -> TestResult {
|
||||||
let fixture = FixtureSet::load()?;
|
let fixture = FixtureSet::load()?;
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ clipboard_timeout_seconds = 45
|
|||||||
# Optional; interactive frontends default to a two-minute inactivity lease.
|
# Optional; interactive frontends default to a two-minute inactivity lease.
|
||||||
[security]
|
[security]
|
||||||
inactivity_timeout_seconds = 120
|
inactivity_timeout_seconds = 120
|
||||||
|
biometric_unlock_enabled = false
|
||||||
|
|
||||||
|
# Optional iPhone appearance: "system", "light", or "dark".
|
||||||
|
[ui]
|
||||||
|
mobile_appearance = "system"
|
||||||
|
|
||||||
[git]
|
[git]
|
||||||
user_name = "Alice Example"
|
user_name = "Alice Example"
|
||||||
@@ -76,6 +81,11 @@ from 1 second through 24 hours. The storage crate owns deadline calculation and
|
|||||||
relock; frontends report only genuine input events as user activity. Repaints,
|
relock; frontends report only genuine input events as user activity. Repaints,
|
||||||
timers, background refresh, and Git work never extend the lease.
|
timers, background refresh, and Git work never extend the lease.
|
||||||
|
|
||||||
|
`security.biometric_unlock_enabled` records only whether biometric unlock is
|
||||||
|
enabled; the GPG passphrase remains in protected system storage.
|
||||||
|
`ui.mobile_appearance` is shared, secret-free iPhone presentation state and
|
||||||
|
defaults to the system appearance.
|
||||||
|
|
||||||
Passwords, passphrases, tokens, credentials, private keys, and other secret
|
Passwords, passphrases, tokens, credentials, private keys, and other secret
|
||||||
values are forbidden in TOML. Unknown fields are rejected. Parse errors never
|
values are forbidden in TOML. Unknown fields are rejected. Parse errors never
|
||||||
echo the source line or value, so an accidentally supplied secret is not
|
echo the source line or value, so an accidentally supplied secret is not
|
||||||
|
|||||||
Reference in New Issue
Block a user