diff --git a/Cargo.lock b/Cargo.lock index 519499f..bfcb3c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5135,6 +5135,14 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ironstorage-watch-apple" +version = "0.1.0" +dependencies = [ + "ironstorage", + "uniffi", +] + [[package]] name = "is_executable" version = "1.0.6" diff --git a/Cargo.toml b/Cargo.toml index 60b8f37..f19264b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "apps/tui", "crates/apple", "crates/storage", + "crates/watch-apple", "tools/macos-packager", ] resolver = "3" diff --git a/apple/Generated/ironstorage_watch.modulemap b/apple/Generated/ironstorage_watch.modulemap new file mode 100644 index 0000000..0c6021b --- /dev/null +++ b/apple/Generated/ironstorage_watch.modulemap @@ -0,0 +1,7 @@ +module ironstorage_watch { + header "ironstorage_watchFFI.h" + export * + use "Darwin" + use "_Builtin_stdbool" + use "_Builtin_stdint" +} \ No newline at end of file diff --git a/apple/Generated/ironstorage_watch.swift b/apple/Generated/ironstorage_watch.swift new file mode 100644 index 0000000..b9e1e7c --- /dev/null +++ b/apple/Generated/ironstorage_watch.swift @@ -0,0 +1,1209 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +// swiftlint:disable all +import Foundation + +// Depending on the consumer's build setup, the low-level FFI code +// might be in a separate module, or it might be compiled inline into +// this module. This is a bit of light hackery to work with both. +#if canImport(ironstorage_watchFFI) +import ironstorage_watchFFI +#endif + +fileprivate extension RustBuffer { + // Allocate a new buffer, copying the contents of a `UInt8` array. + init(bytes: [UInt8]) { + let rbuf = bytes.withUnsafeBufferPointer { ptr in + RustBuffer.from(ptr) + } + self.init(capacity: rbuf.capacity, len: rbuf.len, data: rbuf.data) + } + + static func empty() -> RustBuffer { + RustBuffer(capacity: 0, len:0, data: nil) + } + + static func from(_ ptr: UnsafeBufferPointer) -> RustBuffer { + try! rustCall { ffi_ironstorage_watch_rustbuffer_from_bytes(ForeignBytes(bufferPointer: ptr), $0) } + } + + // Frees the buffer in place. + // The buffer must not be used after this is called. + func deallocate() { + try! rustCall { ffi_ironstorage_watch_rustbuffer_free(self, $0) } + } +} + +fileprivate extension ForeignBytes { + init(bufferPointer: UnsafeBufferPointer) { + self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress) + } + + init(rawBufferPointer: UnsafeRawBufferPointer) { + self.init( + len: Int32(rawBufferPointer.count), + data: rawBufferPointer.baseAddress?.assumingMemoryBound(to: UInt8.self) + ) + } +} + +// Converter for `&[u8]` / `[ByRef] bytes` arguments. +// +// Conforms to `FfiConverter` so the compiler enforces the full converter +// method set. Only the scope-bound `lower(_:_body:)` overload is sound — +// zero-copy byte buffers only flow foreign -> Rust, and only in argument +// position. The four protocol-witness methods (`lift`, `lower`, `read`, +// `write`) `fatalError` at runtime if anyone reaches them. +// +// The scope-bound `lower` takes a closure because the `ForeignBytes` +// pointer is only guaranteed valid for the duration of +// `Data.withUnsafeBytes`. Callers must run the full FFI call inside +// the closure body. +fileprivate enum FfiConverterByRefBytes: FfiConverter { + typealias SwiftType = Data + typealias FfiType = ForeignBytes + + static func lower(_ value: Data, _ body: (ForeignBytes) throws -> R) rethrows -> R { + return try value.withUnsafeBytes { rawBuf in + try body(ForeignBytes(rawBufferPointer: rawBuf)) + } + } + + static func lower(_ value: Data) -> ForeignBytes { + fatalError("ByRef bytes cannot use the plain lower: returning ForeignBytes escapes the Data.withUnsafeBytes scope. Use the scope-bound lower(_:_body:) overload instead.") + } + + static func lift(_ value: ForeignBytes) throws -> Data { + fatalError("ByRef bytes cannot be lifted: zero-copy &[u8] only flows foreign->Rust") + } + + static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Data { + fatalError("ByRef bytes cannot be read from a buffer: zero-copy &[u8] is only supported in argument position, not nested in records/options/etc.") + } + + static func write(_ value: Data, into buf: inout [UInt8]) { + fatalError("ByRef bytes cannot be written to a buffer: zero-copy &[u8] is only supported in argument position, not nested in records/options/etc.") + } +} + +// For every type used in the interface, we provide helper methods for conveniently +// lifting and lowering that type from C-compatible data, and for reading and writing +// values of that type in a buffer. + +// Helper classes/extensions that don't change. +// Someday, this will be in a library of its own. + +fileprivate extension Data { + init(rustBuffer: RustBuffer) { + self.init( + bytesNoCopy: rustBuffer.data!, + count: Int(rustBuffer.len), + deallocator: .none + ) + } +} + +// Define reader functionality. Normally this would be defined in a class or +// struct, but we use standalone functions instead in order to make external +// types work. +// +// With external types, one swift source file needs to be able to call the read +// method on another source file's FfiConverter, but then what visibility +// should Reader have? +// - If Reader is fileprivate, then this means the read() must also +// be fileprivate, which doesn't work with external types. +// - If Reader is internal/public, we'll get compile errors since both source +// files will try define the same type. +// +// Instead, the read() method and these helper functions input a tuple of data + +fileprivate func createReader(data: Data) -> (data: Data, offset: Data.Index) { + (data: data, offset: 0) +} + +// Reads an integer at the current offset, in big-endian order, and advances +// the offset on success. Throws if reading the integer would move the +// offset past the end of the buffer. +fileprivate func readInt(_ reader: inout (data: Data, offset: Data.Index)) throws -> T { + let range = reader.offset...size + guard reader.data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + if T.self == UInt8.self { + let value = reader.data[reader.offset] + reader.offset += 1 + return value as! T + } + var value: T = 0 + let _ = withUnsafeMutableBytes(of: &value, { reader.data.copyBytes(to: $0, from: range)}) + reader.offset = range.upperBound + return value.bigEndian +} + +// Reads an arbitrary number of bytes, to be used to read +// raw bytes, this is useful when lifting strings +fileprivate func readBytes(_ reader: inout (data: Data, offset: Data.Index), count: Int) throws -> Array { + let range = reader.offset..<(reader.offset+count) + guard reader.data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + var value = [UInt8](repeating: 0, count: count) + value.withUnsafeMutableBufferPointer({ buffer in + reader.data.copyBytes(to: buffer, from: range) + }) + reader.offset = range.upperBound + return value +} + +// Reads a float at the current offset. +fileprivate func readFloat(_ reader: inout (data: Data, offset: Data.Index)) throws -> Float { + return Float(bitPattern: try readInt(&reader)) +} + +// Reads a float at the current offset. +fileprivate func readDouble(_ reader: inout (data: Data, offset: Data.Index)) throws -> Double { + return Double(bitPattern: try readInt(&reader)) +} + +// Indicates if the offset has reached the end of the buffer. +fileprivate func hasRemaining(_ reader: (data: Data, offset: Data.Index)) -> Bool { + return reader.offset < reader.data.count +} + +// Define writer functionality. Normally this would be defined in a class or +// struct, but we use standalone functions instead in order to make external +// types work. See the above discussion on Readers for details. + +fileprivate func createWriter() -> [UInt8] { + return [] +} + +fileprivate func writeBytes(_ writer: inout [UInt8], _ byteArr: S) where S: Sequence, S.Element == UInt8 { + writer.append(contentsOf: byteArr) +} + +// Writes an integer in big-endian order. +// +// Warning: make sure what you are trying to write +// is in the correct type! +fileprivate func writeInt(_ writer: inout [UInt8], _ value: T) { + var value = value.bigEndian + withUnsafeBytes(of: &value) { writer.append(contentsOf: $0) } +} + +fileprivate func writeFloat(_ writer: inout [UInt8], _ value: Float) { + writeInt(&writer, value.bitPattern) +} + +fileprivate func writeDouble(_ writer: inout [UInt8], _ value: Double) { + writeInt(&writer, value.bitPattern) +} + +// Protocol for types that transfer other types across the FFI. This is +// analogous to the Rust trait of the same name. +fileprivate protocol FfiConverter { + associatedtype FfiType + associatedtype SwiftType + + static func lift(_ value: FfiType) throws -> SwiftType + static func lower(_ value: SwiftType) -> FfiType + static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType + static func write(_ value: SwiftType, into buf: inout [UInt8]) +} + +// Types conforming to `Primitive` pass themselves directly over the FFI. +fileprivate protocol FfiConverterPrimitive: FfiConverter where FfiType == SwiftType { } + +extension FfiConverterPrimitive { +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lift(_ value: FfiType) throws -> SwiftType { + return value + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lower(_ value: SwiftType) -> FfiType { + return value + } +} + +// Types conforming to `FfiConverterRustBuffer` lift and lower into a `RustBuffer`. +// Used for complex types where it's hard to write a custom lift/lower. +fileprivate protocol FfiConverterRustBuffer: FfiConverter where FfiType == RustBuffer {} + +extension FfiConverterRustBuffer { +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lift(_ buf: RustBuffer) throws -> SwiftType { + var reader = createReader(data: Data(rustBuffer: buf)) + let value = try read(from: &reader) + if hasRemaining(reader) { + throw UniffiInternalError.incompleteData + } + buf.deallocate() + return value + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public static func lower(_ value: SwiftType) -> RustBuffer { + var writer = createWriter() + write(value, into: &writer) + return RustBuffer(bytes: writer) + } +} +// An error type for FFI errors. These errors occur at the UniFFI level, not +// the library level. +fileprivate enum UniffiInternalError: LocalizedError { + case bufferOverflow + case incompleteData + case unexpectedOptionalTag + case unexpectedEnumCase + case unexpectedNullPointer + case unexpectedRustCallStatusCode + case unexpectedRustCallError + case unexpectedStaleHandle + case rustPanic(_ message: String) + + public var errorDescription: String? { + switch self { + case .bufferOverflow: return "Reading the requested value would read past the end of the buffer" + case .incompleteData: return "The buffer still has data after lifting its containing value" + case .unexpectedOptionalTag: return "Unexpected optional tag; should be 0 or 1" + case .unexpectedEnumCase: return "Raw enum value doesn't match any cases" + case .unexpectedNullPointer: return "Raw pointer value was null" + case .unexpectedRustCallStatusCode: return "Unexpected RustCallStatus code" + case .unexpectedRustCallError: return "CALL_ERROR but no errorClass specified" + case .unexpectedStaleHandle: return "The object in the handle map has been dropped already" + case let .rustPanic(message): return message + } + } +} + +fileprivate extension NSLock { + func withLock(f: () throws -> T) rethrows -> T { + self.lock() + defer { self.unlock() } + return try f() + } +} + +fileprivate let CALL_SUCCESS: Int8 = 0 +fileprivate let CALL_ERROR: Int8 = 1 +fileprivate let CALL_UNEXPECTED_ERROR: Int8 = 2 +fileprivate let CALL_CANCELLED: Int8 = 3 + +fileprivate extension RustCallStatus { + init() { + self.init( + code: CALL_SUCCESS, + errorBuf: RustBuffer.init( + capacity: 0, + len: 0, + data: nil + ) + ) + } +} + +private func rustCall(_ callback: (UnsafeMutablePointer) -> T) throws -> T { + let neverThrow: ((RustBuffer) throws -> Never)? = nil + return try makeRustCall(callback, errorHandler: neverThrow) +} + +private func rustCallWithError( + _ errorHandler: @escaping (RustBuffer) throws -> E, + _ callback: (UnsafeMutablePointer) -> T) throws -> T { + try makeRustCall(callback, errorHandler: errorHandler) +} + +private func makeRustCall( + _ callback: (UnsafeMutablePointer) -> T, + errorHandler: ((RustBuffer) throws -> E)? +) throws -> T { + uniffiEnsureIronstorageWatchInitialized() + var callStatus = RustCallStatus.init() + let returnedVal = callback(&callStatus) + try uniffiCheckCallStatus(callStatus: callStatus, errorHandler: errorHandler) + return returnedVal +} + +private func uniffiCheckCallStatus( + callStatus: RustCallStatus, + errorHandler: ((RustBuffer) throws -> E)? +) throws { + switch callStatus.code { + case CALL_SUCCESS: + return + + case CALL_ERROR: + if let errorHandler = errorHandler { + throw try errorHandler(callStatus.errorBuf) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.unexpectedRustCallError + } + + case CALL_UNEXPECTED_ERROR: + // When the rust code sees a panic, it tries to construct a RustBuffer + // with the message. But if that code panics, then it just sends back + // an empty buffer. + if callStatus.errorBuf.len > 0 { + throw UniffiInternalError.rustPanic(try FfiConverterString.lift(callStatus.errorBuf)) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.rustPanic("Rust panic") + } + + case CALL_CANCELLED: + fatalError("Cancellation not supported yet") + + default: + throw UniffiInternalError.unexpectedRustCallStatusCode + } +} + +private func uniffiTraitInterfaceCall( + callStatus: UnsafeMutablePointer, + makeCall: () throws -> T, + writeReturn: (T) -> () +) { + do { + try writeReturn(makeCall()) + } catch let error { + callStatus.pointee.code = CALL_UNEXPECTED_ERROR + callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) + } +} + +private func uniffiTraitInterfaceCallWithError( + callStatus: UnsafeMutablePointer, + makeCall: () throws -> T, + writeReturn: (T) -> (), + lowerError: (E) -> RustBuffer +) { + do { + try writeReturn(makeCall()) + } catch let error as E { + callStatus.pointee.code = CALL_ERROR + callStatus.pointee.errorBuf = lowerError(error) + } catch { + callStatus.pointee.code = CALL_UNEXPECTED_ERROR + callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) + } +} +// Initial value and increment amount for handles. +// These ensure that SWIFT handles always have the lowest bit set +fileprivate let UNIFFI_HANDLEMAP_INITIAL: UInt64 = 1 +fileprivate let UNIFFI_HANDLEMAP_DELTA: UInt64 = 2 + +fileprivate final class UniffiHandleMap: @unchecked Sendable { + // All mutation happens with this lock held, which is why we implement @unchecked Sendable. + private let lock = NSLock() + private var map: [UInt64: T] = [:] + private var currentHandle: UInt64 = UNIFFI_HANDLEMAP_INITIAL + + func insert(obj: T) -> UInt64 { + lock.withLock { + return doInsert(obj) + } + } + + // Low-level insert function, this assumes `lock` is held. + private func doInsert(_ obj: T) -> UInt64 { + let handle = currentHandle + currentHandle += UNIFFI_HANDLEMAP_DELTA + map[handle] = obj + return handle + } + + func get(handle: UInt64) throws -> T { + try lock.withLock { + guard let obj = map[handle] else { + throw UniffiInternalError.unexpectedStaleHandle + } + return obj + } + } + + func clone(handle: UInt64) throws -> UInt64 { + try lock.withLock { + guard let obj = map[handle] else { + throw UniffiInternalError.unexpectedStaleHandle + } + return doInsert(obj) + } + } + + @discardableResult + func remove(handle: UInt64) throws -> T { + try lock.withLock { + guard let obj = map.removeValue(forKey: handle) else { + throw UniffiInternalError.unexpectedStaleHandle + } + return obj + } + } + + var count: Int { + get { + map.count + } + } +} + + +// Public interface members begin here. + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterUInt32: FfiConverterPrimitive { + typealias FfiType = UInt32 + typealias SwiftType = UInt32 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt32 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterUInt64: FfiConverterPrimitive { + typealias FfiType = UInt64 + typealias SwiftType = UInt64 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UInt64 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterString: FfiConverter { + typealias SwiftType = String + typealias FfiType = RustBuffer + + public static func lift(_ value: RustBuffer) throws -> String { + defer { + value.deallocate() + } + if value.data == nil { + return String() + } + let bytes = UnsafeBufferPointer(start: value.data!, count: Int(value.len)) + // Use Swift's native UTF-8 decoder; `String(bytes:encoding:.utf8)` goes + // through Foundation's NSString and silently strips a leading U+FEFF BOM. + // Invalid UTF-8 substitutes U+FFFD instead of trapping (unreachable + // given Rust's `String` invariant). + return String(decoding: bytes, as: UTF8.self) + } + + public static func lower(_ value: String) -> RustBuffer { + return value.utf8CString.withUnsafeBufferPointer { ptr in + // The swift string gives us int8_t, we want uint8_t. + ptr.withMemoryRebound(to: UInt8.self) { ptr in + // The swift string gives us a trailing null byte, we don't want it. + let buf = UnsafeBufferPointer(rebasing: ptr.prefix(upTo: ptr.count - 1)) + return RustBuffer.from(buf) + } + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> String { + let len: Int32 = try readInt(&buf) + // See `lift` above for why we avoid Foundation's NSString-backed decoder here. + return String(decoding: try readBytes(&buf, count: Int(len)), as: UTF8.self) + } + + public static func write(_ value: String, into buf: inout [UInt8]) { + let len = Int32(value.utf8.count) + writeInt(&buf, len) + writeBytes(&buf, value.utf8) + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterData: FfiConverterRustBuffer { + typealias SwiftType = Data + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Data { + let len: Int32 = try readInt(&buf) + return Data(try readBytes(&buf, count: Int(len))) + } + + public static func write(_ value: Data, into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + writeBytes(&buf, value) + } +} + + + + +public protocol WatchCoreProtocol: AnyObject, Sendable { + + func applySnapshot(snapshot: Data) throws -> WatchSnapshotUpdate + + func noPersistedSnapshot() throws + + func protectedDataUnavailable() throws + + func recordsAt(unixSeconds: UInt64) throws -> [WatchTotpRecord] + +} +open class WatchCore: WatchCoreProtocol, @unchecked Sendable { + fileprivate let handle: UInt64 + + /// Used to instantiate a [FFIObject] without an actual handle, for fakes in tests, mostly. +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public struct NoHandle { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + required public init(unsafeFromHandle handle: UInt64) { + self.handle = handle + } + + // This constructor can be used to instantiate a fake object. + // - Parameter noHandle: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + // + // - Warning: + // Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing handle the FFI lower functions will crash. +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public init(noHandle: NoHandle) { + self.handle = 0 + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public func uniffiCloneHandle() -> UInt64 { + return try! rustCall { uniffi_ironstorage_watch_fn_clone_watchcore(self.handle, $0) } + } + // No primary constructor declared for this class. + + deinit { + if handle == 0 { + // Mock objects have handle=0 don't try to free them + return + } + + try! rustCall { uniffi_ironstorage_watch_fn_free_watchcore(handle, $0) } + } + + + + +open func applySnapshot(snapshot: Data)throws -> WatchSnapshotUpdate { + return try FfiConverterTypeWatchSnapshotUpdate_lift(try rustCallWithError(FfiConverterTypeWatchFfiError_lift) { + uniffiCallStatus in + uniffi_ironstorage_watch_fn_method_watchcore_apply_snapshot( + self.uniffiCloneHandle(), + FfiConverterData.lower(snapshot),uniffiCallStatus + ) +}) +} + +open func noPersistedSnapshot()throws {try rustCallWithError(FfiConverterTypeWatchFfiError_lift) { + uniffiCallStatus in + uniffi_ironstorage_watch_fn_method_watchcore_no_persisted_snapshot( + self.uniffiCloneHandle(),uniffiCallStatus + ) +} +} + +open func protectedDataUnavailable()throws {try rustCallWithError(FfiConverterTypeWatchFfiError_lift) { + uniffiCallStatus in + uniffi_ironstorage_watch_fn_method_watchcore_protected_data_unavailable( + self.uniffiCloneHandle(),uniffiCallStatus + ) +} +} + +open func recordsAt(unixSeconds: UInt64)throws -> [WatchTotpRecord] { + return try FfiConverterSequenceTypeWatchTotpRecord.lift(try rustCallWithError(FfiConverterTypeWatchFfiError_lift) { + uniffiCallStatus in + uniffi_ironstorage_watch_fn_method_watchcore_records_at( + self.uniffiCloneHandle(), + FfiConverterUInt64.lower(unixSeconds),uniffiCallStatus + ) +}) +} + + + +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeWatchCore: FfiConverter { + typealias FfiType = UInt64 + typealias SwiftType = WatchCore + + public static func lift(_ handle: UInt64) throws -> WatchCore { + return WatchCore(unsafeFromHandle: handle) + } + + public static func lower(_ value: WatchCore) -> UInt64 { + return value.uniffiCloneHandle() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WatchCore { + let handle: UInt64 = try readInt(&buf) + return try lift(handle) + } + + public static func write(_ value: WatchCore, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchCore_lift(_ handle: UInt64) throws -> WatchCore { + return try FfiConverterTypeWatchCore.lift(handle) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchCore_lower(_ value: WatchCore) -> UInt64 { + return FfiConverterTypeWatchCore.lower(value) +} + + + + +public struct WatchSnapshotUpdate: Equatable, Hashable { + public var apply: WatchSnapshotApply + public var persistence: WatchPersistenceAction + public var revision: UInt64? + public var selectedEntries: UInt32 + public var receipt: Data + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(apply: WatchSnapshotApply, persistence: WatchPersistenceAction, revision: UInt64?, selectedEntries: UInt32, receipt: Data) { + self.apply = apply + self.persistence = persistence + self.revision = revision + self.selectedEntries = selectedEntries + self.receipt = receipt + } + + + + +} + +#if compiler(>=6) +extension WatchSnapshotUpdate: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeWatchSnapshotUpdate: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WatchSnapshotUpdate { + return + try WatchSnapshotUpdate( + apply: FfiConverterTypeWatchSnapshotApply.read(from: &buf), + persistence: FfiConverterTypeWatchPersistenceAction.read(from: &buf), + revision: FfiConverterOptionUInt64.read(from: &buf), + selectedEntries: FfiConverterUInt32.read(from: &buf), + receipt: FfiConverterData.read(from: &buf) + ) + } + + public static func write(_ value: WatchSnapshotUpdate, into buf: inout [UInt8]) { + FfiConverterTypeWatchSnapshotApply.write(value.apply, into: &buf) + FfiConverterTypeWatchPersistenceAction.write(value.persistence, into: &buf) + FfiConverterOptionUInt64.write(value.revision, into: &buf) + FfiConverterUInt32.write(value.selectedEntries, into: &buf) + FfiConverterData.write(value.receipt, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchSnapshotUpdate_lift(_ buf: RustBuffer) throws -> WatchSnapshotUpdate { + return try FfiConverterTypeWatchSnapshotUpdate.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchSnapshotUpdate_lower(_ value: WatchSnapshotUpdate) -> RustBuffer { + return FfiConverterTypeWatchSnapshotUpdate.lower(value) +} + + +public struct WatchTotpRecord: Equatable, Hashable { + public var path: String + public var issuer: String? + public var account: String + public var code: String + public var period: UInt64 + public var validUntil: UInt64 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(path: String, issuer: String?, account: String, code: String, period: UInt64, validUntil: UInt64) { + self.path = path + self.issuer = issuer + self.account = account + self.code = code + self.period = period + self.validUntil = validUntil + } + + + + +} + +#if compiler(>=6) +extension WatchTotpRecord: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeWatchTotpRecord: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WatchTotpRecord { + return + try WatchTotpRecord( + path: FfiConverterString.read(from: &buf), + issuer: FfiConverterOptionString.read(from: &buf), + account: FfiConverterString.read(from: &buf), + code: FfiConverterString.read(from: &buf), + period: FfiConverterUInt64.read(from: &buf), + validUntil: FfiConverterUInt64.read(from: &buf) + ) + } + + public static func write(_ value: WatchTotpRecord, into buf: inout [UInt8]) { + FfiConverterString.write(value.path, into: &buf) + FfiConverterOptionString.write(value.issuer, into: &buf) + FfiConverterString.write(value.account, into: &buf) + FfiConverterString.write(value.code, into: &buf) + FfiConverterUInt64.write(value.period, into: &buf) + FfiConverterUInt64.write(value.validUntil, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchTotpRecord_lift(_ buf: RustBuffer) throws -> WatchTotpRecord { + return try FfiConverterTypeWatchTotpRecord.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchTotpRecord_lower(_ value: WatchTotpRecord) -> RustBuffer { + return FfiConverterTypeWatchTotpRecord.lower(value) +} + + +public +enum WatchFfiError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { + + + + case Failed(message: String + ) + + + + + + + public var errorDescription: String? { + String(reflecting: self) + } + +} + +#if compiler(>=6) +extension WatchFfiError: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeWatchFfiError: FfiConverterRustBuffer { + typealias SwiftType = WatchFfiError + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WatchFfiError { + let variant: Int32 = try readInt(&buf) + switch variant { + + + + + case 1: return .Failed( + message: try FfiConverterString.read(from: &buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: WatchFfiError, into buf: inout [UInt8]) { + switch value { + + + + + + case let .Failed(message): + writeInt(&buf, Int32(1)) + FfiConverterString.write(message, into: &buf) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchFfiError_lift(_ buf: RustBuffer) throws -> WatchFfiError { + return try FfiConverterTypeWatchFfiError.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchFfiError_lower(_ value: WatchFfiError) -> RustBuffer { + return FfiConverterTypeWatchFfiError.lower(value) +} + + + +public enum WatchPersistenceAction: Equatable, Hashable { + + case keep + case replace + case delete + + + + + +} + +#if compiler(>=6) +extension WatchPersistenceAction: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeWatchPersistenceAction: FfiConverterRustBuffer { + typealias SwiftType = WatchPersistenceAction + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WatchPersistenceAction { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .keep + + case 2: return .replace + + case 3: return .delete + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: WatchPersistenceAction, into buf: inout [UInt8]) { + switch value { + + + case .keep: + writeInt(&buf, Int32(1)) + + + case .replace: + writeInt(&buf, Int32(2)) + + + case .delete: + writeInt(&buf, Int32(3)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchPersistenceAction_lift(_ buf: RustBuffer) throws -> WatchPersistenceAction { + return try FfiConverterTypeWatchPersistenceAction.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchPersistenceAction_lower(_ value: WatchPersistenceAction) -> RustBuffer { + return FfiConverterTypeWatchPersistenceAction.lower(value) +} + + + + +public enum WatchSnapshotApply: Equatable, Hashable { + + case replaced + case revoked + case duplicate + case stale + case pairingChanged + + + + + +} + +#if compiler(>=6) +extension WatchSnapshotApply: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeWatchSnapshotApply: FfiConverterRustBuffer { + typealias SwiftType = WatchSnapshotApply + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WatchSnapshotApply { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .replaced + + case 2: return .revoked + + case 3: return .duplicate + + case 4: return .stale + + case 5: return .pairingChanged + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: WatchSnapshotApply, into buf: inout [UInt8]) { + switch value { + + + case .replaced: + writeInt(&buf, Int32(1)) + + + case .revoked: + writeInt(&buf, Int32(2)) + + + case .duplicate: + writeInt(&buf, Int32(3)) + + + case .stale: + writeInt(&buf, Int32(4)) + + + case .pairingChanged: + writeInt(&buf, Int32(5)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchSnapshotApply_lift(_ buf: RustBuffer) throws -> WatchSnapshotApply { + return try FfiConverterTypeWatchSnapshotApply.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchSnapshotApply_lower(_ value: WatchSnapshotApply) -> RustBuffer { + return FfiConverterTypeWatchSnapshotApply.lower(value) +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionUInt64: FfiConverterRustBuffer { + typealias SwiftType = UInt64? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterUInt64.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterUInt64.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionString: FfiConverterRustBuffer { + typealias SwiftType = String? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterString.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterString.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeWatchTotpRecord: FfiConverterRustBuffer { + typealias SwiftType = [WatchTotpRecord] + + public static func write(_ value: [WatchTotpRecord], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeWatchTotpRecord.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [WatchTotpRecord] { + let len: Int32 = try readInt(&buf) + var seq = [WatchTotpRecord]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeWatchTotpRecord.read(from: &buf)) + } + return seq + } +} +public func watchCore() -> WatchCore { + return try! FfiConverterTypeWatchCore_lift(try! rustCall() { + uniffiCallStatus in + uniffi_ironstorage_watch_fn_func_watch_core(uniffiCallStatus + ) +}) +} + +private enum InitializationResult { + case ok + case contractVersionMismatch + case apiChecksumMismatch +} +// Use a global variable to perform the versioning checks. Swift ensures that +// the code inside is only computed once. +private let initializationResult: InitializationResult = { + // Get the bindings contract version from our ComponentInterface + let bindings_contract_version = 30 + // Get the scaffolding contract version by calling the into the dylib + let scaffolding_contract_version = ffi_ironstorage_watch_uniffi_contract_version() + if bindings_contract_version != scaffolding_contract_version { + return InitializationResult.contractVersionMismatch + } + if (uniffi_ironstorage_watch_checksum_func_watch_core() != 47212) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ironstorage_watch_checksum_method_watchcore_apply_snapshot() != 6162) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ironstorage_watch_checksum_method_watchcore_no_persisted_snapshot() != 32214) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ironstorage_watch_checksum_method_watchcore_protected_data_unavailable() != 42217) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ironstorage_watch_checksum_method_watchcore_records_at() != 48946) { + return InitializationResult.apiChecksumMismatch + } + + return InitializationResult.ok +}() + +// Make the ensure init function public so that other modules which have external type references to +// our types can call it. +public func uniffiEnsureIronstorageWatchInitialized() { + switch initializationResult { + case .ok: + break + case .contractVersionMismatch: + fatalError("UniFFI contract version mismatch: try cleaning and rebuilding your project") + case .apiChecksumMismatch: + fatalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } +} + +// swiftlint:enable all \ No newline at end of file diff --git a/apple/Generated/ironstorage_watchFFI.h b/apple/Generated/ironstorage_watchFFI.h new file mode 100644 index 0000000..b0c264d --- /dev/null +++ b/apple/Generated/ironstorage_watchFFI.h @@ -0,0 +1,578 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +#pragma once + +#include +#include +#include + +// The following structs are used to implement the lowest level +// of the FFI, and thus useful to multiple uniffied crates. +// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H. +#ifdef UNIFFI_SHARED_H + // We also try to prevent mixing versions of shared uniffi header structs. + // If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V4 + #ifndef UNIFFI_SHARED_HEADER_V4 + #error Combining helper code from multiple versions of uniffi is not supported + #endif // ndef UNIFFI_SHARED_HEADER_V4 +#else +#define UNIFFI_SHARED_H +#define UNIFFI_SHARED_HEADER_V4 +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ + +typedef struct RustBuffer +{ + uint64_t capacity; + uint64_t len; + uint8_t *_Nullable data; +} RustBuffer; + +typedef struct ForeignBytes +{ + int32_t len; + const uint8_t *_Nullable data; +} ForeignBytes; + +// Error definitions +typedef struct RustCallStatus { + int8_t code; + RustBuffer errorBuf; +} RustCallStatus; + +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V4 in this file. ⚠️ +#endif // def UNIFFI_SHARED_H +#ifndef UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK +#define UNIFFI_FFIDEF_RUST_FUTURE_CONTINUATION_CALLBACK +typedef void (*UniffiRustFutureContinuationCallback)(uint64_t, int8_t + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK +typedef void (*UniffiForeignFutureDroppedCallback)(uint64_t + ); + +#endif +#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE +#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_FREE +typedef void (*UniffiCallbackInterfaceFree)(uint64_t + ); + +#endif +#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_CLONE +#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_CLONE +typedef uint64_t (*UniffiCallbackInterfaceClone)(uint64_t + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK_STRUCT +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_DROPPED_CALLBACK_STRUCT +typedef struct UniffiForeignFutureDroppedCallbackStruct { + uint64_t handle; + UniffiForeignFutureDroppedCallback _Nonnull free; +} UniffiForeignFutureDroppedCallbackStruct; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U8 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U8 +typedef struct UniffiForeignFutureResultU8 { + uint8_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureResultU8; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U8 +typedef void (*UniffiForeignFutureCompleteU8)(uint64_t, UniffiForeignFutureResultU8 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I8 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I8 +typedef struct UniffiForeignFutureResultI8 { + int8_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureResultI8; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I8 +typedef void (*UniffiForeignFutureCompleteI8)(uint64_t, UniffiForeignFutureResultI8 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U16 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U16 +typedef struct UniffiForeignFutureResultU16 { + uint16_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureResultU16; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U16 +typedef void (*UniffiForeignFutureCompleteU16)(uint64_t, UniffiForeignFutureResultU16 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I16 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I16 +typedef struct UniffiForeignFutureResultI16 { + int16_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureResultI16; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I16 +typedef void (*UniffiForeignFutureCompleteI16)(uint64_t, UniffiForeignFutureResultI16 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U32 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U32 +typedef struct UniffiForeignFutureResultU32 { + uint32_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureResultU32; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U32 +typedef void (*UniffiForeignFutureCompleteU32)(uint64_t, UniffiForeignFutureResultU32 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I32 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I32 +typedef struct UniffiForeignFutureResultI32 { + int32_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureResultI32; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I32 +typedef void (*UniffiForeignFutureCompleteI32)(uint64_t, UniffiForeignFutureResultI32 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U64 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_U64 +typedef struct UniffiForeignFutureResultU64 { + uint64_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureResultU64; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_U64 +typedef void (*UniffiForeignFutureCompleteU64)(uint64_t, UniffiForeignFutureResultU64 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I64 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_I64 +typedef struct UniffiForeignFutureResultI64 { + int64_t returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureResultI64; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_I64 +typedef void (*UniffiForeignFutureCompleteI64)(uint64_t, UniffiForeignFutureResultI64 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F32 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F32 +typedef struct UniffiForeignFutureResultF32 { + float returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureResultF32; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F32 +typedef void (*UniffiForeignFutureCompleteF32)(uint64_t, UniffiForeignFutureResultF32 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F64 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_F64 +typedef struct UniffiForeignFutureResultF64 { + double returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureResultF64; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64 +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_F64 +typedef void (*UniffiForeignFutureCompleteF64)(uint64_t, UniffiForeignFutureResultF64 + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_RUST_BUFFER +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_RUST_BUFFER +typedef struct UniffiForeignFutureResultRustBuffer { + RustBuffer returnValue; + RustCallStatus callStatus; +} UniffiForeignFutureResultRustBuffer; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_RUST_BUFFER +typedef void (*UniffiForeignFutureCompleteRustBuffer)(uint64_t, UniffiForeignFutureResultRustBuffer + ); + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_VOID +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_RESULT_VOID +typedef struct UniffiForeignFutureResultVoid { + RustCallStatus callStatus; +} UniffiForeignFutureResultVoid; + +#endif +#ifndef UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID +#define UNIFFI_FFIDEF_FOREIGN_FUTURE_COMPLETE_VOID +typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureResultVoid + ); + +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_CLONE_WATCHCORE +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_CLONE_WATCHCORE +uint64_t uniffi_ironstorage_watch_fn_clone_watchcore(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_FREE_WATCHCORE +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_FREE_WATCHCORE +void uniffi_ironstorage_watch_fn_free_watchcore(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_APPLY_SNAPSHOT +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_APPLY_SNAPSHOT +RustBuffer uniffi_ironstorage_watch_fn_method_watchcore_apply_snapshot(uint64_t ptr, RustBuffer snapshot, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_NO_PERSISTED_SNAPSHOT +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_NO_PERSISTED_SNAPSHOT +void uniffi_ironstorage_watch_fn_method_watchcore_no_persisted_snapshot(uint64_t ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_PROTECTED_DATA_UNAVAILABLE +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_PROTECTED_DATA_UNAVAILABLE +void uniffi_ironstorage_watch_fn_method_watchcore_protected_data_unavailable(uint64_t ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_RECORDS_AT +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_RECORDS_AT +RustBuffer uniffi_ironstorage_watch_fn_method_watchcore_records_at(uint64_t ptr, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_FUNC_WATCH_CORE +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_FUNC_WATCH_CORE +uint64_t uniffi_ironstorage_watch_fn_func_watch_core(RustCallStatus *_Nonnull out_status + +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_ALLOC +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_ALLOC +RustBuffer ffi_ironstorage_watch_rustbuffer_alloc(uint64_t size, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_FROM_BYTES +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_FROM_BYTES +RustBuffer ffi_ironstorage_watch_rustbuffer_from_bytes(ForeignBytes bytes, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_FREE +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_FREE +void ffi_ironstorage_watch_rustbuffer_free(RustBuffer buf, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_RESERVE +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_RESERVE +RustBuffer ffi_ironstorage_watch_rustbuffer_reserve(RustBuffer buf, uint64_t additional, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U8 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U8 +void ffi_ironstorage_watch_rust_future_poll_u8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U8 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U8 +void ffi_ironstorage_watch_rust_future_cancel_u8(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U8 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U8 +void ffi_ironstorage_watch_rust_future_free_u8(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U8 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U8 +uint8_t ffi_ironstorage_watch_rust_future_complete_u8(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I8 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I8 +void ffi_ironstorage_watch_rust_future_poll_i8(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I8 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I8 +void ffi_ironstorage_watch_rust_future_cancel_i8(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I8 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I8 +void ffi_ironstorage_watch_rust_future_free_i8(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I8 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I8 +int8_t ffi_ironstorage_watch_rust_future_complete_i8(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U16 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U16 +void ffi_ironstorage_watch_rust_future_poll_u16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U16 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U16 +void ffi_ironstorage_watch_rust_future_cancel_u16(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U16 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U16 +void ffi_ironstorage_watch_rust_future_free_u16(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U16 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U16 +uint16_t ffi_ironstorage_watch_rust_future_complete_u16(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I16 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I16 +void ffi_ironstorage_watch_rust_future_poll_i16(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I16 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I16 +void ffi_ironstorage_watch_rust_future_cancel_i16(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I16 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I16 +void ffi_ironstorage_watch_rust_future_free_i16(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I16 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I16 +int16_t ffi_ironstorage_watch_rust_future_complete_i16(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U32 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U32 +void ffi_ironstorage_watch_rust_future_poll_u32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U32 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U32 +void ffi_ironstorage_watch_rust_future_cancel_u32(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U32 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U32 +void ffi_ironstorage_watch_rust_future_free_u32(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U32 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U32 +uint32_t ffi_ironstorage_watch_rust_future_complete_u32(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I32 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I32 +void ffi_ironstorage_watch_rust_future_poll_i32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I32 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I32 +void ffi_ironstorage_watch_rust_future_cancel_i32(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I32 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I32 +void ffi_ironstorage_watch_rust_future_free_i32(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I32 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I32 +int32_t ffi_ironstorage_watch_rust_future_complete_i32(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U64 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_U64 +void ffi_ironstorage_watch_rust_future_poll_u64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U64 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_U64 +void ffi_ironstorage_watch_rust_future_cancel_u64(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U64 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_U64 +void ffi_ironstorage_watch_rust_future_free_u64(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U64 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_U64 +uint64_t ffi_ironstorage_watch_rust_future_complete_u64(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I64 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_I64 +void ffi_ironstorage_watch_rust_future_poll_i64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I64 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_I64 +void ffi_ironstorage_watch_rust_future_cancel_i64(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I64 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_I64 +void ffi_ironstorage_watch_rust_future_free_i64(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I64 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_I64 +int64_t ffi_ironstorage_watch_rust_future_complete_i64(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_F32 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_F32 +void ffi_ironstorage_watch_rust_future_poll_f32(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_F32 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_F32 +void ffi_ironstorage_watch_rust_future_cancel_f32(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_F32 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_F32 +void ffi_ironstorage_watch_rust_future_free_f32(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_F32 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_F32 +float ffi_ironstorage_watch_rust_future_complete_f32(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_F64 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_F64 +void ffi_ironstorage_watch_rust_future_poll_f64(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_F64 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_F64 +void ffi_ironstorage_watch_rust_future_cancel_f64(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_F64 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_F64 +void ffi_ironstorage_watch_rust_future_free_f64(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_F64 +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_F64 +double ffi_ironstorage_watch_rust_future_complete_f64(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_RUST_BUFFER +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_RUST_BUFFER +void ffi_ironstorage_watch_rust_future_poll_rust_buffer(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_RUST_BUFFER +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_RUST_BUFFER +void ffi_ironstorage_watch_rust_future_cancel_rust_buffer(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_RUST_BUFFER +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_RUST_BUFFER +void ffi_ironstorage_watch_rust_future_free_rust_buffer(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_RUST_BUFFER +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_RUST_BUFFER +RustBuffer ffi_ironstorage_watch_rust_future_complete_rust_buffer(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_VOID +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_POLL_VOID +void ffi_ironstorage_watch_rust_future_poll_void(uint64_t handle, UniffiRustFutureContinuationCallback _Nonnull callback, uint64_t callback_data +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_VOID +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_CANCEL_VOID +void ffi_ironstorage_watch_rust_future_cancel_void(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_VOID +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_FREE_VOID +void ffi_ironstorage_watch_rust_future_free_void(uint64_t handle +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_VOID +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUST_FUTURE_COMPLETE_VOID +void ffi_ironstorage_watch_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_FUNC_WATCH_CORE +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_FUNC_WATCH_CORE +uint16_t uniffi_ironstorage_watch_checksum_func_watch_core(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_APPLY_SNAPSHOT +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_APPLY_SNAPSHOT +uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_apply_snapshot(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_NO_PERSISTED_SNAPSHOT +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_NO_PERSISTED_SNAPSHOT +uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_no_persisted_snapshot(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_PROTECTED_DATA_UNAVAILABLE +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_PROTECTED_DATA_UNAVAILABLE +uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_protected_data_unavailable(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_RECORDS_AT +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_RECORDS_AT +uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_records_at(void + +); +#endif +#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_UNIFFI_CONTRACT_VERSION +#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_UNIFFI_CONTRACT_VERSION +uint32_t ffi_ironstorage_watch_uniffi_contract_version(void + +); +#endif + diff --git a/apple/IronStorage-Watch-Bridging-Header.h b/apple/IronStorage-Watch-Bridging-Header.h new file mode 100644 index 0000000..0eddcb9 --- /dev/null +++ b/apple/IronStorage-Watch-Bridging-Header.h @@ -0,0 +1 @@ +#include "Generated/ironstorage_watchFFI.h" diff --git a/apple/Sources/Watch/IronStorageWatchApp.swift b/apple/Sources/Watch/IronStorageWatchApp.swift index 0cda696..1ac94b5 100644 --- a/apple/Sources/Watch/IronStorageWatchApp.swift +++ b/apple/Sources/Watch/IronStorageWatchApp.swift @@ -1,57 +1,177 @@ +import Security import SwiftUI import WatchConnectivity +private enum SecureSnapshotStore { + private static let service = "de.rfc1437.ironstorage.watch.snapshot" + private static let account = "selected-totp" + + static func load() throws -> Data? { + var result: CFTypeRef? + let status = SecItemCopyMatching([ + kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: account, + kSecReturnData: true, + kSecMatchLimit: kSecMatchLimitOne, + ] as CFDictionary, &result) + if status == errSecItemNotFound { return nil } + guard status == errSecSuccess, let data = result as? Data else { + throw SnapshotStoreError(status: status) + } + return data + } + + static func replace(_ snapshot: Data) throws { + let query = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: account, + ] as CFDictionary + let status = SecItemUpdate(query, [kSecValueData: snapshot] as CFDictionary) + if status == errSecItemNotFound { + let added = SecItemAdd([ + kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: account, + kSecValueData: snapshot, + kSecAttrAccessible: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, + ] as CFDictionary, nil) + guard added == errSecSuccess else { throw SnapshotStoreError(status: added) } + } else if status != errSecSuccess { + throw SnapshotStoreError(status: status) + } + } + + static func delete() throws { + let status = SecItemDelete([ + kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: account, + ] as CFDictionary) + guard status == errSecSuccess || status == errSecItemNotFound else { + throw SnapshotStoreError(status: status) + } + } +} + +private struct SnapshotStoreError: Error { + let status: OSStatus +} + +@MainActor private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessionDelegate { private static let snapshotKey = "de.rfc1437.ironstorage.watch.snapshot" - private static let deliveredReceiptKey = "de.rfc1437.ironstorage.watch.delivered" + private static let receiptKey = "de.rfc1437.ironstorage.watch.delivered" + + @Published private(set) var records: [WatchTotpRecord] = [] + private let core = watchCore() override init() { super.init() + restoreSnapshot() guard WCSession.isSupported() else { return } let session = WCSession.default session.delegate = self session.activate() } - private func receive(_ applicationContext: [String: Any], session: WCSession) { - guard - applicationContext[Self.snapshotKey] is Data, - let receipt = applicationContext[Self.deliveredReceiptKey] as? Data - else { return } - let acknowledgement = [Self.deliveredReceiptKey: receipt] - if session.isReachable { - session.sendMessage(acknowledgement, replyHandler: nil) { _ in - session.transferUserInfo(acknowledgement) + func sceneBecameActive() { + restoreSnapshot() + } + + func sceneBecameInactive() { + try? core.protectedDataUnavailable() + records.removeAll(keepingCapacity: false) + } + + private func restoreSnapshot() { + do { + if var snapshot = try SecureSnapshotStore.load() { + defer { snapshot.resetBytes(in: 0..&2 + echo "Unsupported ${PLATFORM_NAME:-Apple} architecture: $arch" >&2 exit 1 fi if [[ "$profile" == "debug" ]]; then rustup run stable cargo build --locked \ - --target "$target" --package ironstorage-apple --lib + --target "$target" --package "$package" --lib else rustup run stable cargo build --locked --release \ - --target "$target" --package ironstorage-apple --lib + --target "$target" --package "$package" --lib fi - libraries+=("$CARGO_TARGET_DIR/$target/$profile/libironstorage_apple.a") + libraries+=("$CARGO_TARGET_DIR/$target/$profile/lib$library.a") done mkdir -p "$DERIVED_FILE_DIR/rust" -lipo -create -output "$DERIVED_FILE_DIR/rust/libironstorage_apple.a" "${libraries[@]}" +lipo -create -output "$DERIVED_FILE_DIR/rust/lib$library.a" "${libraries[@]}" diff --git a/apple/project.yml b/apple/project.yml index 844d1db..36bc887 100644 --- a/apple/project.yml +++ b/apple/project.yml @@ -77,6 +77,10 @@ targets: PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.ironstorage.watch PRODUCT_NAME: IronStorage Watch SKIP_INSTALL: YES + "EXCLUDED_ARCHS[sdk=watchsimulator*]": x86_64 + SWIFT_OBJC_BRIDGING_HEADER: IronStorage-Watch-Bridging-Header.h + LIBRARY_SEARCH_PATHS: "$(inherited) $(DERIVED_FILE_DIR)/rust" + OTHER_LDFLAGS: "$(inherited) -lironstorage_watch" info: path: Watch-Info.plist properties: @@ -86,3 +90,11 @@ targets: sources: - Assets.xcassets - Sources/Watch + - Generated/ironstorage_watch.swift + preBuildScripts: + - name: Build Rust Watch core + basedOnDependencyAnalysis: false + script: | + ./build_rust_core.bash + outputFiles: + - $(DERIVED_FILE_DIR)/rust/libironstorage_watch.a diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index 783dce1..07ccf7c 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -6,47 +6,79 @@ edition.workspace = true rust-version.workspace = true publish = false +[features] +default = ["full"] +full = [ + "dep:apple-native-keyring-store", + "dep:arboard", + "dep:cap-std", + "dep:cap-tempfile", + "dep:clap", + "dep:clap_complete", + "dep:flate2", + "dep:gix", + "dep:gix-config", + "dep:image", + "dep:keepass", + "dep:keyring-core", + "dep:pgp", + "dep:qrcode", + "dep:rand", + "dep:regex", + "dep:reqwest", + "dep:rqrr", + "dep:secret-service", + "dep:security-framework", + "dep:serde", + "dep:shlex", + "dep:toml", + "dep:url", + "dep:windows-native-keyring-store", + "dep:zbus-secret-service-keyring-store", +] +watch = [] + [dependencies] -cap-std.workspace = true -cap-tempfile.workspace = true -clap.workspace = true -clap_complete.workspace = true +cap-std = { workspace = true, optional = true } +cap-tempfile = { workspace = true, optional = true } +clap = { workspace = true, optional = true } +clap_complete = { workspace = true, optional = true } data-encoding.workspace = true -flate2.workspace = true -gix.workspace = true -gix-config.workspace = true +flate2 = { workspace = true, optional = true } +gix = { workspace = true, optional = true } +gix-config = { workspace = true, optional = true } hmac.workspace = true -image.workspace = true -keyring-core.workspace = true -keepass.workspace = true -pgp.workspace = true -qrcode.workspace = true -rand.workspace = true -regex.workspace = true -reqwest.workspace = true -rqrr.workspace = true -serde.workspace = true +image = { workspace = true, optional = true } +keyring-core = { workspace = true, optional = true } +keepass = { workspace = true, optional = true } +pgp = { workspace = true, optional = true } +qrcode = { workspace = true, optional = true } +rand = { workspace = true, optional = true } +regex = { workspace = true, optional = true } +reqwest = { workspace = true, optional = true } +rqrr = { workspace = true, optional = true } +serde = { workspace = true, optional = true } sha1.workspace = true sha2.workspace = true -shlex.workspace = true -toml.workspace = true -url.workspace = true +shlex = { workspace = true, optional = true } +toml = { workspace = true, optional = true } +url = { workspace = true, optional = true } zeroize.workspace = true [target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] -apple-native-keyring-store.workspace = true -security-framework.workspace = true +apple-native-keyring-store = { workspace = true, optional = true } +security-framework = { workspace = true, optional = true } [target.'cfg(target_os = "windows")'.dependencies] -windows-native-keyring-store.workspace = true +windows-native-keyring-store = { workspace = true, optional = true } [target.'cfg(target_os = "linux")'.dependencies] -arboard.workspace = true -secret-service.workspace = true -zbus-secret-service-keyring-store.workspace = true +arboard = { workspace = true, optional = true } +secret-service = { workspace = true, optional = true } +zbus-secret-service-keyring-store = { workspace = true, optional = true } [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] -arboard.workspace = true +arboard = { workspace = true, optional = true } [dev-dependencies] hex = "0.4" diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 734bd28..1073a59 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -5,32 +5,60 @@ //! //! This crate is the sole owner of stored and derived password-store objects. +#[cfg(feature = "full")] pub mod authentication; +#[cfg(feature = "full")] pub mod command; +#[cfg(feature = "full")] pub mod config; +#[cfg(feature = "full")] pub mod crypto; +#[cfg(feature = "full")] pub mod desktop; +#[cfg(feature = "full")] pub mod document; +#[cfg(feature = "full")] pub mod generate; +#[cfg(feature = "full")] pub mod git; +#[cfg(feature = "full")] pub mod kdbx; +#[cfg(feature = "full")] pub mod mobile; +#[cfg(feature = "full")] pub mod mobile_authentication; +#[cfg(feature = "full")] pub mod mobile_entry; +#[cfg(feature = "full")] pub mod mobile_home; +#[cfg(feature = "full")] pub mod mobile_key_transfer; +#[cfg(feature = "full")] pub mod mobile_mutation; +#[cfg(feature = "full")] pub mod mobile_onboarding; +#[cfg(feature = "full")] pub mod mobile_passwords; +#[cfg(feature = "full")] pub mod mobile_totp; pub mod mobile_watch; +#[cfg(feature = "full")] pub mod mutation; +#[cfg(feature = "full")] pub mod otp; +mod otp_core; +#[cfg(feature = "full")] pub mod presentation; +#[cfg(feature = "full")] pub mod read; +#[cfg(feature = "full")] pub mod recipient; +#[cfg(feature = "full")] pub mod repository; +mod secret; +#[cfg(feature = "full")] pub mod secret_store; +#[cfg(feature = "full")] pub mod write; /// Product name shared by the presentation adapters. diff --git a/crates/storage/src/mobile_totp.rs b/crates/storage/src/mobile_totp.rs index 246efeb..e4fcc23 100644 --- a/crates/storage/src/mobile_totp.rs +++ b/crates/storage/src/mobile_totp.rs @@ -521,7 +521,7 @@ impl<'a> MobileTotpService<'a> { let period = uri.period().ok_or(OtpError::NotTotp)?; retained.insert(path.clone()); entries.push(WatchSnapshotEntry::new( - path.clone(), + path.to_string(), uri.issuer().map(str::to_owned), uri.account().to_owned(), uri.algorithm(), diff --git a/crates/storage/src/mobile_watch.rs b/crates/storage/src/mobile_watch.rs index 6bedd18..721d443 100644 --- a/crates/storage/src/mobile_watch.rs +++ b/crates/storage/src/mobile_watch.rs @@ -1,26 +1,30 @@ -//! Versioned, replacement-only Apple Watch TOTP snapshots and sender state. +//! Versioned Apple Watch TOTP snapshots and the minimal offline Watch runtime. -use std::{ - error::Error, - fmt, fs, - io::Write as _, - path::{Path, PathBuf}, -}; +use std::{error::Error, fmt, path::Path}; +#[cfg(feature = "full")] use cap_std::{ambient_authority, fs::Dir}; +#[cfg(feature = "full")] use cap_tempfile::TempFile; -use data_encoding::{HEXLOWER, HEXLOWER_PERMISSIVE}; +use data_encoding::HEXLOWER; +#[cfg(feature = "full")] +use data_encoding::HEXLOWER_PERMISSIVE; +#[cfg(feature = "full")] use serde::{Deserialize, Serialize}; use sha2::{Digest as _, Sha256}; +#[cfg(feature = "full")] +use std::{fs, io::Write as _, path::PathBuf}; + use crate::{ - otp::OtpAlgorithm, - repository::{EntryPath, SecretBytes}, + otp_core::{self, OtpAlgorithm}, + secret::SecretBytes, }; const SNAPSHOT_MAGIC: &[u8; 4] = b"ISWS"; const RECEIPT_MAGIC: &[u8; 4] = b"ISWR"; const SNAPSHOT_VERSION: u16 = 1; +#[cfg(feature = "full")] const JOURNAL_VERSION: u32 = 1; const MAX_SNAPSHOT_BYTES: usize = 256 * 1024; const MAX_ENTRIES: usize = 256; @@ -57,7 +61,7 @@ impl MobileWatchSnapshotStatus { } pub struct WatchSnapshotEntry { - path: EntryPath, + path: String, issuer: Option, account: String, algorithm: OtpAlgorithm, @@ -68,7 +72,7 @@ pub struct WatchSnapshotEntry { impl WatchSnapshotEntry { pub fn new( - path: EntryPath, + path: String, issuer: Option, account: String, algorithm: OtpAlgorithm, @@ -87,7 +91,7 @@ impl WatchSnapshotEntry { } } - pub fn path(&self) -> &EntryPath { + pub fn path(&self) -> &str { &self.path } pub fn issuer(&self) -> Option<&str> { @@ -156,6 +160,7 @@ impl fmt::Debug for WatchSnapshot { } } +#[cfg(feature = "full")] pub struct WatchSnapshotTransfer { revision: u64, selected_entries: u32, @@ -163,6 +168,7 @@ pub struct WatchSnapshotTransfer { delivered_receipt: Vec, } +#[cfg(feature = "full")] impl WatchSnapshotTransfer { pub fn revision(&self) -> u64 { self.revision @@ -178,6 +184,7 @@ impl WatchSnapshotTransfer { } } +#[cfg(feature = "full")] impl fmt::Debug for WatchSnapshotTransfer { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter @@ -273,14 +280,184 @@ impl WatchSnapshotReceiver { self.current = None; self.accepted = None; } + + pub fn clear_secrets(&mut self) { + self.current = None; + } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WatchPersistenceAction { + Keep, + Replace, + Delete, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WatchSnapshotUpdate { + apply: WatchSnapshotApply, + persistence: WatchPersistenceAction, + revision: Option, + selected_entries: u32, + receipt: Vec, +} + +impl WatchSnapshotUpdate { + pub fn apply(&self) -> WatchSnapshotApply { + self.apply + } + pub fn persistence(&self) -> WatchPersistenceAction { + self.persistence + } + pub fn revision(&self) -> Option { + self.revision + } + pub fn selected_entries(&self) -> u32 { + self.selected_entries + } + pub fn receipt(&self) -> &[u8] { + &self.receipt + } +} + +pub struct WatchTotpRecord { + path: String, + issuer: Option, + account: String, + code: SecretBytes, + period: u64, + valid_until: u64, +} + +impl WatchTotpRecord { + pub fn path(&self) -> &str { + &self.path + } + pub fn issuer(&self) -> Option<&str> { + self.issuer.as_deref() + } + pub fn account(&self) -> &str { + &self.account + } + pub fn code(&self) -> &SecretBytes { + &self.code + } + pub fn period(&self) -> u64 { + self.period + } + pub fn valid_until(&self) -> u64 { + self.valid_until + } + pub fn remaining_at(&self, unix_seconds: u64) -> u64 { + self.valid_until.saturating_sub(unix_seconds) + } +} + +impl fmt::Debug for WatchTotpRecord { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WatchTotpRecord") + .field("path", &self.path) + .field("issuer", &self.issuer) + .field("account", &self.account) + .field("code", &"[REDACTED]") + .field("period", &self.period) + .field("valid_until", &self.valid_until) + .finish() + } +} + +#[derive(Default)] +pub struct WatchRuntime { + receiver: WatchSnapshotReceiver, + protected_data_available: bool, +} + +impl WatchRuntime { + pub fn apply_snapshot( + &mut self, + bytes: Vec, + ) -> Result { + let apply = self.receiver.apply(SecretBytes::new(bytes))?; + self.protected_data_available = true; + let persistence = match apply { + WatchSnapshotApply::Replaced | WatchSnapshotApply::PairingChanged => { + WatchPersistenceAction::Replace + } + WatchSnapshotApply::Revoked => WatchPersistenceAction::Delete, + WatchSnapshotApply::Duplicate | WatchSnapshotApply::Stale => { + WatchPersistenceAction::Keep + } + }; + let current = self.receiver.current(); + Ok(WatchSnapshotUpdate { + apply, + persistence, + revision: current.map(WatchSnapshot::revision), + selected_entries: current + .map(|snapshot| u32::try_from(snapshot.entries().len()).unwrap_or(u32::MAX)) + .unwrap_or(0), + receipt: self.receiver.current_receipt().unwrap_or_default(), + }) + } + + pub fn records_at( + &self, + unix_seconds: u64, + ) -> Result, WatchSnapshotError> { + if !self.protected_data_available { + return Err(WatchSnapshotError::ProtectedDataUnavailable); + } + let Some(snapshot) = self.receiver.current() else { + return Ok(Vec::new()); + }; + snapshot + .entries() + .iter() + .map(|entry| { + let counter = unix_seconds / entry.period; + let valid_until = counter + .checked_add(1) + .and_then(|counter| counter.checked_mul(entry.period)) + .ok_or(WatchSnapshotError::InvalidEntry)?; + let code = otp_core::code_for_counter( + entry.algorithm, + entry.secret.expose(), + entry.digits, + counter, + ) + .map_err(|_| WatchSnapshotError::InvalidEntry)?; + Ok(WatchTotpRecord { + path: entry.path.clone(), + issuer: entry.issuer.clone(), + account: entry.account.clone(), + code, + period: entry.period, + valid_until, + }) + }) + .collect() + } + + pub fn protected_data_unavailable(&mut self) { + self.receiver.clear_secrets(); + self.protected_data_available = false; + } + + pub fn no_persisted_snapshot(&mut self) { + self.receiver.revoke(); + self.protected_data_available = true; + } +} + +#[cfg(feature = "full")] pub struct WatchSnapshotSender { path: PathBuf, journal: SenderJournal, status: MobileWatchSnapshotStatus, } +#[cfg(feature = "full")] impl WatchSnapshotSender { pub fn load(path: PathBuf) -> Self { let journal = load_journal(&path).unwrap_or_default(); @@ -436,6 +613,7 @@ impl WatchSnapshotSender { } } +#[cfg(feature = "full")] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] struct SenderJournal { @@ -455,6 +633,7 @@ struct SenderJournal { current_revision: Option, } +#[cfg(feature = "full")] impl Default for SenderJournal { fn default() -> Self { Self { @@ -469,6 +648,7 @@ impl Default for SenderJournal { } } +#[cfg(feature = "full")] fn journal_version() -> u32 { JOURNAL_VERSION } @@ -476,10 +656,12 @@ fn journal_version() -> u32 { #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(u8)] enum ReceiptKind { + #[cfg(feature = "full")] Delivered = 1, Current = 2, } +#[cfg(feature = "full")] struct Receipt { kind: ReceiptKind, pairing: [u8; 32], @@ -487,6 +669,7 @@ struct Receipt { _digest: [u8; 32], } +#[cfg(feature = "full")] fn encode_entries(entries: &[WatchSnapshotEntry]) -> Result, WatchSnapshotError> { if entries.len() > MAX_ENTRIES { return Err(WatchSnapshotError::TooManyEntries); @@ -518,6 +701,7 @@ fn encode_entries(entries: &[WatchSnapshotEntry]) -> Result, WatchSnapsh Ok(output) } +#[cfg(feature = "full")] fn encode_snapshot( pairing: [u8; 32], revision: u64, @@ -563,8 +747,10 @@ fn decode_snapshot(bytes: &[u8]) -> Result { } let mut entries = Vec::with_capacity(count); for _ in 0..count { - let path = EntryPath::parse(&take_text(&mut input)?) - .map_err(|_| WatchSnapshotError::InvalidEntry)?; + let path = take_text(&mut input)?; + if !valid_entry_path(&path) { + return Err(WatchSnapshotError::InvalidEntry); + } let issuer = match take_u8(&mut input)? { 0 => None, 1 => Some(take_text(&mut input)?), @@ -631,6 +817,7 @@ fn encode_receipt( output } +#[cfg(feature = "full")] fn decode_receipt(bytes: &[u8]) -> Result { if bytes.len() != 4 + 2 + 1 + 32 + 8 + 32 + 32 { return Err(WatchSnapshotError::InvalidReceipt); @@ -663,6 +850,7 @@ fn decode_receipt(bytes: &[u8]) -> Result { }) } +#[cfg(feature = "full")] fn status_from_journal(journal: &SenderJournal) -> MobileWatchSnapshotStatus { let (state, revision, detail) = if let Some(revision) = journal.current_revision { ( @@ -699,6 +887,7 @@ fn status_from_journal(journal: &SenderJournal) -> MobileWatchSnapshotStatus { } } +#[cfg(feature = "full")] fn snapshot_detail(state: &str, count: u32, revision: u64) -> String { format!( "Snapshot revision {revision} with {count} selected TOTP {} is {state}.", @@ -706,6 +895,7 @@ fn snapshot_detail(state: &str, count: u32, revision: u64) -> String { ) } +#[cfg(feature = "full")] fn load_journal(path: &Path) -> Option { let metadata = fs::symlink_metadata(path).ok()?; if metadata.file_type().is_symlink() || !metadata.is_file() { @@ -715,6 +905,7 @@ fn load_journal(path: &Path) -> Option { (journal.version == JOURNAL_VERSION).then_some(journal) } +#[cfg(feature = "full")] fn save_journal(path: &Path, journal: &SenderJournal) -> Result<(), WatchSnapshotError> { let parent = path.parent().ok_or(WatchSnapshotError::JournalWrite)?; fs::create_dir_all(parent).map_err(|_| WatchSnapshotError::JournalWrite)?; @@ -737,7 +928,7 @@ fn save_journal(path: &Path, journal: &SenderJournal) -> Result<(), WatchSnapsho .map_err(|_| WatchSnapshotError::JournalWrite) } -#[cfg(unix)] +#[cfg(all(feature = "full", unix))] fn set_private_permissions(temporary: &TempFile<'_>) -> Result<(), WatchSnapshotError> { use cap_std::fs::{Permissions, PermissionsExt as _}; temporary @@ -746,7 +937,7 @@ fn set_private_permissions(temporary: &TempFile<'_>) -> Result<(), WatchSnapshot .map_err(|_| WatchSnapshotError::JournalWrite) } -#[cfg(not(unix))] +#[cfg(all(feature = "full", not(unix)))] fn set_private_permissions(_temporary: &TempFile<'_>) -> Result<(), WatchSnapshotError> { Ok(()) } @@ -754,6 +945,7 @@ fn set_private_permissions(_temporary: &TempFile<'_>) -> Result<(), WatchSnapsho fn digest(bytes: &[u8]) -> [u8; 32] { Sha256::digest(bytes).into() } +#[cfg(feature = "full")] fn decode_hash(text: &str) -> Result<[u8; 32], WatchSnapshotError> { let bytes = HEXLOWER_PERMISSIVE .decode(text.as_bytes()) @@ -762,15 +954,18 @@ fn decode_hash(text: &str) -> Result<[u8; 32], WatchSnapshotError> { .try_into() .map_err(|_| WatchSnapshotError::InvalidJournal) } +#[cfg(feature = "full")] fn put_u32(output: &mut Vec, value: u32) { output.extend_from_slice(&value.to_be_bytes()); } fn put_u64(output: &mut Vec, value: u64) { output.extend_from_slice(&value.to_be_bytes()); } +#[cfg(feature = "full")] fn put_text(output: &mut Vec, value: &str) -> Result<(), WatchSnapshotError> { put_bytes(output, value.as_bytes(), MAX_TEXT_BYTES) } +#[cfg(feature = "full")] fn put_bytes(output: &mut Vec, value: &[u8], max: usize) -> Result<(), WatchSnapshotError> { if value.len() > max { return Err(WatchSnapshotError::InvalidEntry); @@ -826,6 +1021,16 @@ fn take_text(input: &mut &[u8]) -> Result { .map_err(|_| WatchSnapshotError::InvalidEntry) } +fn valid_entry_path(value: &str) -> bool { + let path = Path::new(value); + !value.is_empty() + && !value.ends_with('/') + && !path.is_absolute() + && path + .components() + .all(|component| matches!(component, std::path::Component::Normal(_))) +} + #[derive(Clone, Debug, Eq, PartialEq)] pub enum WatchSnapshotError { InvalidPairing, @@ -841,6 +1046,7 @@ pub enum WatchSnapshotError { NoPendingSnapshot, InvalidJournal, JournalWrite, + ProtectedDataUnavailable, } impl fmt::Display for WatchSnapshotError { @@ -861,6 +1067,7 @@ impl fmt::Display for WatchSnapshotError { Self::NoPendingSnapshot => "there is no pending Apple Watch snapshot", Self::InvalidJournal => "the Apple Watch synchronization journal is invalid", Self::JournalWrite => "the Apple Watch synchronization journal could not be saved", + Self::ProtectedDataUnavailable => "protected Apple Watch data is unavailable", }) } } diff --git a/crates/storage/src/otp.rs b/crates/storage/src/otp.rs index b993271..3330d55 100644 --- a/crates/storage/src/otp.rs +++ b/crates/storage/src/otp.rs @@ -3,15 +3,13 @@ use std::{error::Error, fmt, ops::Range, str, sync::Mutex}; use data_encoding::BASE32_NOPAD; -use hmac::Hmac; -use sha1::Sha1; -use sha2::{Sha256, Sha512}; use zeroize::Zeroize as _; use crate::{ command::{OtpAppendRequest, OtpInputSource, OtpInsertRequest}, crypto::{CryptoError, KeyStore, SecretProvider}, git::{AutomaticEntryCommitter, GitError, GitIdentity}, + otp_core, recipient::{RecipientPolicyError, RecipientPolicyManager, SigningPolicy}, repository::{EncryptedEntry, EntryPath, Repository, RepositoryError, SecretBytes}, write::{EntryAction, EntryCommit, EntryCommitError, EntryCommitter, OverwriteDecision}, @@ -27,12 +25,7 @@ pub enum OtpKind { Hotp, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum OtpAlgorithm { - Sha1, - Sha256, - Sha512, -} +pub use crate::otp_core::OtpAlgorithm; /// A validated key URI whose encoded and decoded secrets zeroize on drop. pub struct OtpUri { @@ -251,21 +244,8 @@ impl OtpUri { } pub fn code_for_counter(&self, counter: u64) -> Result { - let message = counter.to_be_bytes(); - let mut digest = match self.algorithm { - OtpAlgorithm::Sha1 => hmac_digest::>(self.secret.expose(), &message)?, - OtpAlgorithm::Sha256 => hmac_digest::>(self.secret.expose(), &message)?, - OtpAlgorithm::Sha512 => hmac_digest::>(self.secret.expose(), &message)?, - }; - let offset = usize::from(digest[digest.len() - 1] & 0x0f); - let binary = (u32::from(digest[offset]) & 0x7f) << 24 - | u32::from(digest[offset + 1]) << 16 - | u32::from(digest[offset + 2]) << 8 - | u32::from(digest[offset + 3]); - digest.zeroize(); - let modulus = 10_u32.pow(self.digits); - let code = format!("{:0width$}", binary % modulus, width = self.digits as usize); - Ok(SecretBytes::new(code.into_bytes())) + otp_core::code_for_counter(self.algorithm, self.secret.expose(), self.digits, counter) + .map_err(|_| OtpError::InvalidSecret) } fn incremented_hotp(&self) -> Result<(u64, Self), OtpError> { @@ -1204,19 +1184,6 @@ fn percent_encode(value: &str) -> String { encoded } -fn hmac_digest(key: &[u8], message: &[u8]) -> Result, OtpError> -where - M: hmac::digest::Mac + hmac::digest::KeyInit, -{ - let mut mac = - ::new_from_slice(key).map_err(|_| OtpError::InvalidSecret)?; - mac.update(message); - let mut output = mac.finalize().into_bytes(); - let digest = output.to_vec(); - output.fill(0); - Ok(digest) -} - pub(crate) fn find_uri( plaintext: &SecretBytes, entry: &EntryPath, diff --git a/crates/storage/src/otp_core.rs b/crates/storage/src/otp_core.rs new file mode 100644 index 0000000..499f7f3 --- /dev/null +++ b/crates/storage/src/otp_core.rs @@ -0,0 +1,97 @@ +use std::{error::Error, fmt}; + +use hmac::Hmac; +use sha1::Sha1; +use sha2::{Sha256, Sha512}; +use zeroize::Zeroize as _; + +use crate::secret::SecretBytes; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OtpAlgorithm { + Sha1, + Sha256, + Sha512, +} + +pub fn code_for_counter( + algorithm: OtpAlgorithm, + secret: &[u8], + digits: u32, + counter: u64, +) -> Result { + if secret.is_empty() || !matches!(digits, 6 | 8) { + return Err(OtpCoreError); + } + let message = counter.to_be_bytes(); + let mut digest = match algorithm { + OtpAlgorithm::Sha1 => hmac_digest::>(secret, &message)?, + OtpAlgorithm::Sha256 => hmac_digest::>(secret, &message)?, + OtpAlgorithm::Sha512 => hmac_digest::>(secret, &message)?, + }; + let offset = usize::from(digest[digest.len() - 1] & 0x0f); + let binary = (u32::from(digest[offset]) & 0x7f) << 24 + | u32::from(digest[offset + 1]) << 16 + | u32::from(digest[offset + 2]) << 8 + | u32::from(digest[offset + 3]); + digest.zeroize(); + let code = format!( + "{:0width$}", + binary % 10_u32.pow(digits), + width = digits as usize + ); + Ok(SecretBytes::new(code.into_bytes())) +} + +fn hmac_digest(key: &[u8], message: &[u8]) -> Result, OtpCoreError> +where + M: hmac::digest::Mac + hmac::digest::KeyInit, +{ + let mut mac = ::new_from_slice(key).map_err(|_| OtpCoreError)?; + mac.update(message); + let mut output = mac.finalize().into_bytes(); + let digest = output.to_vec(); + output.fill(0); + Ok(digest) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct OtpCoreError; + +impl fmt::Display for OtpCoreError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("OTP parameters are invalid") + } +} + +impl Error for OtpCoreError {} + +#[cfg(test)] +mod tests { + use super::{OtpAlgorithm, code_for_counter}; + + #[test] + fn rfc_6238_vectors_cover_every_watch_algorithm() { + let vectors = [ + ( + OtpAlgorithm::Sha1, + b"12345678901234567890".as_slice(), + "94287082", + ), + ( + OtpAlgorithm::Sha256, + b"12345678901234567890123456789012".as_slice(), + "46119246", + ), + ( + OtpAlgorithm::Sha512, + b"1234567890123456789012345678901234567890123456789012345678901234".as_slice(), + "90693936", + ), + ]; + for (algorithm, secret, expected) in vectors { + let code = code_for_counter(algorithm, secret, 8, 59 / 30).expect("RFC vector"); + assert_eq!(code.expose(), expected.as_bytes()); + } + } +} diff --git a/crates/storage/src/repository.rs b/crates/storage/src/repository.rs index 1dc5eec..b12a569 100644 --- a/crates/storage/src/repository.rs +++ b/crates/storage/src/repository.rs @@ -12,9 +12,9 @@ use std::{ #[cfg(test)] use std::collections::BTreeSet; +pub use crate::secret::SecretBytes; use cap_std::{ambient_authority, fs::Dir}; use cap_tempfile::TempFile; -use zeroize::Zeroize; const ENTRY_EXTENSION: &str = "gpg"; const RECIPIENT_FILE: &str = ".gpg-id"; @@ -132,35 +132,6 @@ impl fmt::Debug for EncryptedEntry { } } -/// Decrypted bytes that are redacted in diagnostics and zeroed when dropped. -pub struct SecretBytes(Vec); - -impl SecretBytes { - pub fn new(bytes: Vec) -> Self { - Self(bytes) - } - - pub fn expose(&self) -> &[u8] { - &self.0 - } - - pub fn expose_mut(&mut self) -> &mut [u8] { - &mut self.0 - } -} - -impl fmt::Debug for SecretBytes { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("SecretBytes([REDACTED])") - } -} - -impl Drop for SecretBytes { - fn drop(&mut self) { - self.0.zeroize(); - } -} - #[derive(Clone, Debug, Eq, PartialEq)] pub struct EntryRecord { path: EntryPath, diff --git a/crates/storage/src/secret.rs b/crates/storage/src/secret.rs new file mode 100644 index 0000000..c1e5d48 --- /dev/null +++ b/crates/storage/src/secret.rs @@ -0,0 +1,32 @@ +use std::fmt; + +use zeroize::Zeroize as _; + +/// Secret bytes that are redacted in diagnostics and zeroed when dropped. +pub struct SecretBytes(Vec); + +impl SecretBytes { + pub fn new(bytes: Vec) -> Self { + Self(bytes) + } + + pub fn expose(&self) -> &[u8] { + &self.0 + } + + pub fn expose_mut(&mut self) -> &mut [u8] { + &mut self.0 + } +} + +impl fmt::Debug for SecretBytes { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("SecretBytes([REDACTED])") + } +} + +impl Drop for SecretBytes { + fn drop(&mut self) { + self.0.zeroize(); + } +} diff --git a/crates/storage/tests/mobile_watch.rs b/crates/storage/tests/mobile_watch.rs index 60ced49..7d5da60 100644 --- a/crates/storage/tests/mobile_watch.rs +++ b/crates/storage/tests/mobile_watch.rs @@ -4,8 +4,8 @@ use std::fs; use ironstorage::{ mobile_watch::{ - MobileWatchSnapshotState, WatchSnapshotApply, WatchSnapshotEntry, WatchSnapshotReceiver, - WatchSnapshotSender, + MobileWatchSnapshotState, WatchPersistenceAction, WatchRuntime, WatchSnapshotApply, + WatchSnapshotEntry, WatchSnapshotReceiver, WatchSnapshotSender, }, otp::OtpAlgorithm, repository::{EntryPath, SecretBytes}, @@ -15,7 +15,7 @@ type TestResult = Result<(), Box>; fn entry(path: &str, issuer: &str, account: &str, secret: &[u8]) -> WatchSnapshotEntry { WatchSnapshotEntry::new( - EntryPath::parse(path).expect("fixture path"), + EntryPath::parse(path).expect("fixture path").to_string(), Some(issuer.to_owned()), account.to_owned(), OtpAlgorithm::Sha256, @@ -165,3 +165,49 @@ fn journal_keeps_revisions_monotonic_across_sender_reloads() -> TestResult { assert_eq!(changed.revision(), 2); Ok(()) } + +#[test] +fn watch_runtime_generates_view_ready_totp_and_clears_secrets_when_locked() -> TestResult { + let directory = tempfile::tempdir()?; + let mut sender = WatchSnapshotSender::load(directory.path().join("watch-snapshot.toml")); + let snapshot = sender.prepare( + "paired-watch", + vec![WatchSnapshotEntry::new( + "otp/alice".to_owned(), + Some("Acme".to_owned()), + "alice".to_owned(), + OtpAlgorithm::Sha1, + 8, + 30, + SecretBytes::new(b"12345678901234567890".to_vec()), + )], + )?; + + let mut runtime = WatchRuntime::default(); + let update = runtime.apply_snapshot(snapshot.snapshot().expose().to_vec())?; + assert_eq!(update.apply(), WatchSnapshotApply::Replaced); + assert_eq!(update.persistence(), WatchPersistenceAction::Replace); + assert_eq!(update.selected_entries(), 1); + assert!(!update.receipt().is_empty()); + + let records = runtime.records_at(59)?; + assert_eq!(records.len(), 1); + assert_eq!(records[0].path(), "otp/alice"); + assert_eq!(records[0].code().expose(), b"94287082"); + assert_eq!(records[0].valid_until(), 60); + assert_eq!(records[0].remaining_at(59), 1); + + runtime.protected_data_unavailable(); + assert!(runtime.records_at(59).is_err()); + + let restored = runtime.apply_snapshot(snapshot.snapshot().expose().to_vec())?; + assert_eq!(restored.persistence(), WatchPersistenceAction::Replace); + assert_eq!(runtime.records_at(59)?[0].code().expose(), b"94287082"); + + let revocation = sender.prepare("paired-watch", Vec::new())?; + let revoked = runtime.apply_snapshot(revocation.snapshot().expose().to_vec())?; + assert_eq!(revoked.apply(), WatchSnapshotApply::Revoked); + assert_eq!(revoked.persistence(), WatchPersistenceAction::Delete); + assert!(runtime.records_at(59)?.is_empty()); + Ok(()) +} diff --git a/crates/watch-apple/Cargo.toml b/crates/watch-apple/Cargo.toml new file mode 100644 index 0000000..feab232 --- /dev/null +++ b/crates/watch-apple/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ironstorage-watch-apple" +description = "Minimal UniFFI boundary for the IronStorage watchOS TOTP core" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +publish = false + +[lib] +name = "ironstorage_watch" +crate-type = ["lib", "staticlib", "cdylib"] + +[dependencies] +ironstorage = { path = "../storage", default-features = false, features = ["watch"] } +uniffi.workspace = true diff --git a/crates/watch-apple/src/lib.rs b/crates/watch-apple/src/lib.rs new file mode 100644 index 0000000..01c696c --- /dev/null +++ b/crates/watch-apple/src/lib.rs @@ -0,0 +1,190 @@ +#![forbid(unsafe_code)] +#![deny(clippy::disallowed_types)] + +//! Mechanical UniFFI exports for the minimal watchOS Rust runtime. + +use std::{ + error::Error, + fmt, + sync::{Arc, Mutex}, +}; + +use ironstorage::mobile_watch::{ + WatchPersistenceAction as StoragePersistenceAction, WatchRuntime as StorageWatchRuntime, + WatchSnapshotApply as StorageSnapshotApply, WatchSnapshotError as StorageWatchError, + WatchSnapshotUpdate as StorageSnapshotUpdate, WatchTotpRecord as StorageTotpRecord, +}; +uniffi::setup_scaffolding!(); + +#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] +pub enum WatchSnapshotApply { + Replaced, + Revoked, + Duplicate, + Stale, + PairingChanged, +} + +impl From for WatchSnapshotApply { + fn from(value: StorageSnapshotApply) -> Self { + match value { + StorageSnapshotApply::Replaced => Self::Replaced, + StorageSnapshotApply::Revoked => Self::Revoked, + StorageSnapshotApply::Duplicate => Self::Duplicate, + StorageSnapshotApply::Stale => Self::Stale, + StorageSnapshotApply::PairingChanged => Self::PairingChanged, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] +pub enum WatchPersistenceAction { + Keep, + Replace, + Delete, +} + +impl From for WatchPersistenceAction { + fn from(value: StoragePersistenceAction) -> Self { + match value { + StoragePersistenceAction::Keep => Self::Keep, + StoragePersistenceAction::Replace => Self::Replace, + StoragePersistenceAction::Delete => Self::Delete, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] +pub struct WatchSnapshotUpdate { + pub apply: WatchSnapshotApply, + pub persistence: WatchPersistenceAction, + pub revision: Option, + pub selected_entries: u32, + pub receipt: Vec, +} + +impl From for WatchSnapshotUpdate { + fn from(value: StorageSnapshotUpdate) -> Self { + Self { + apply: value.apply().into(), + persistence: value.persistence().into(), + revision: value.revision(), + selected_entries: value.selected_entries(), + receipt: value.receipt().to_vec(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] +pub struct WatchTotpRecord { + pub path: String, + pub issuer: Option, + pub account: String, + pub code: String, + pub period: u64, + pub valid_until: u64, +} + +impl From for WatchTotpRecord { + fn from(value: StorageTotpRecord) -> Self { + Self { + path: value.path().to_owned(), + issuer: value.issuer().map(str::to_owned), + account: value.account().to_owned(), + code: String::from_utf8(value.code().expose().to_vec()) + .expect("storage-generated TOTP codes are ASCII"), + period: value.period(), + valid_until: value.valid_until(), + } + } +} + +#[derive(Debug, uniffi::Error)] +pub enum WatchFfiError { + Failed { message: String }, +} + +impl fmt::Display for WatchFfiError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Failed { message } => formatter.write_str(message), + } + } +} + +impl Error for WatchFfiError {} + +impl From for WatchFfiError { + fn from(value: StorageWatchError) -> Self { + Self::Failed { + message: value.to_string(), + } + } +} + +#[derive(uniffi::Object)] +pub struct WatchCore { + runtime: Mutex, +} + +#[uniffi::export] +impl WatchCore { + pub fn apply_snapshot(&self, snapshot: Vec) -> Result { + self.runtime + .lock() + .map_err(|_| lock_error())? + .apply_snapshot(snapshot) + .map(Into::into) + .map_err(Into::into) + } + + pub fn records_at(&self, unix_seconds: u64) -> Result, WatchFfiError> { + self.runtime + .lock() + .map_err(|_| lock_error())? + .records_at(unix_seconds) + .map(|records| records.into_iter().map(Into::into).collect()) + .map_err(Into::into) + } + + pub fn protected_data_unavailable(&self) -> Result<(), WatchFfiError> { + self.runtime + .lock() + .map_err(|_| lock_error())? + .protected_data_unavailable(); + Ok(()) + } + + pub fn no_persisted_snapshot(&self) -> Result<(), WatchFfiError> { + self.runtime + .lock() + .map_err(|_| lock_error())? + .no_persisted_snapshot(); + Ok(()) + } +} + +fn lock_error() -> WatchFfiError { + WatchFfiError::Failed { + message: "Apple Watch TOTP state is unavailable".to_owned(), + } +} + +#[uniffi::export] +pub fn watch_core() -> Arc { + Arc::new(WatchCore { + runtime: Mutex::new(StorageWatchRuntime::default()), + }) +} + +#[cfg(test)] +mod tests { + #[test] + fn bridge_masks_records_when_protected_data_is_unavailable() { + let core = super::watch_core(); + core.no_persisted_snapshot().expect("available Keychain"); + assert!(core.records_at(59).expect("empty snapshot").is_empty()); + core.protected_data_unavailable().expect("lock transition"); + assert!(core.records_at(59).is_err()); + } +}