Implement GPG key QR transfer
This commit is contained in:
@@ -580,6 +580,24 @@ fileprivate struct FfiConverterString: FfiConverter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1172,6 +1190,279 @@ public func FfiConverterTypeMobileHomeOperation_lower(_ value: MobileHomeOperati
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public protocol MobileKeyTransferProtocol: AnyObject, Sendable {
|
||||||
|
|
||||||
|
func export(fingerprint: String, kind: MobileKeyTransferKind, passphrase: String?) throws -> MobileKeyTransferExport
|
||||||
|
|
||||||
|
func importer() -> MobileKeyTransferImport
|
||||||
|
|
||||||
|
func keys() -> [MobileKeyTransferKey]
|
||||||
|
|
||||||
|
}
|
||||||
|
open class MobileKeyTransfer: MobileKeyTransferProtocol, @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_apple_fn_clone_mobilekeytransfer(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_apple_fn_free_mobilekeytransfer(handle, $0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
open func export(fingerprint: String, kind: MobileKeyTransferKind, passphrase: String?)throws -> MobileKeyTransferExport {
|
||||||
|
return try FfiConverterTypeMobileKeyTransferExport_lift(try rustCallWithError(FfiConverterTypeMobileKeyTransferFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobilekeytransfer_export(
|
||||||
|
self.uniffiCloneHandle(),
|
||||||
|
FfiConverterString.lower(fingerprint),
|
||||||
|
FfiConverterTypeMobileKeyTransferKind_lower(kind),
|
||||||
|
FfiConverterOptionString.lower(passphrase),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
open func importer() -> MobileKeyTransferImport {
|
||||||
|
return try! FfiConverterTypeMobileKeyTransferImport_lift(try! rustCall() {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobilekeytransfer_importer(
|
||||||
|
self.uniffiCloneHandle(),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
open func keys() -> [MobileKeyTransferKey] {
|
||||||
|
return try! FfiConverterSequenceTypeMobileKeyTransferKey.lift(try! rustCall() {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobilekeytransfer_keys(
|
||||||
|
self.uniffiCloneHandle(),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileKeyTransfer: FfiConverter {
|
||||||
|
typealias FfiType = UInt64
|
||||||
|
typealias SwiftType = MobileKeyTransfer
|
||||||
|
|
||||||
|
public static func lift(_ handle: UInt64) throws -> MobileKeyTransfer {
|
||||||
|
return MobileKeyTransfer(unsafeFromHandle: handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func lower(_ value: MobileKeyTransfer) -> UInt64 {
|
||||||
|
return value.uniffiCloneHandle()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileKeyTransfer {
|
||||||
|
let handle: UInt64 = try readInt(&buf)
|
||||||
|
return try lift(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileKeyTransfer, into buf: inout [UInt8]) {
|
||||||
|
writeInt(&buf, lower(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransfer_lift(_ handle: UInt64) throws -> MobileKeyTransfer {
|
||||||
|
return try FfiConverterTypeMobileKeyTransfer.lift(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransfer_lower(_ value: MobileKeyTransfer) -> UInt64 {
|
||||||
|
return FfiConverterTypeMobileKeyTransfer.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public protocol MobileKeyTransferImportProtocol: AnyObject, Sendable {
|
||||||
|
|
||||||
|
func addFrame(payload: String) throws -> MobileKeyTransferProgress
|
||||||
|
|
||||||
|
func `import`(passphrase: String?, makeDefault: Bool) throws -> MobileKeyTransferOutcome
|
||||||
|
|
||||||
|
}
|
||||||
|
open class MobileKeyTransferImport: MobileKeyTransferImportProtocol, @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_apple_fn_clone_mobilekeytransferimport(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_apple_fn_free_mobilekeytransferimport(handle, $0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
open func addFrame(payload: String)throws -> MobileKeyTransferProgress {
|
||||||
|
return try FfiConverterTypeMobileKeyTransferProgress_lift(try rustCallWithError(FfiConverterTypeMobileKeyTransferFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobilekeytransferimport_add_frame(
|
||||||
|
self.uniffiCloneHandle(),
|
||||||
|
FfiConverterString.lower(payload),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
open func `import`(passphrase: String?, makeDefault: Bool)throws -> MobileKeyTransferOutcome {
|
||||||
|
return try FfiConverterTypeMobileKeyTransferOutcome_lift(try rustCallWithError(FfiConverterTypeMobileKeyTransferFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_method_mobilekeytransferimport_import(
|
||||||
|
self.uniffiCloneHandle(),
|
||||||
|
FfiConverterOptionString.lower(passphrase),
|
||||||
|
FfiConverterBool.lower(makeDefault),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileKeyTransferImport: FfiConverter {
|
||||||
|
typealias FfiType = UInt64
|
||||||
|
typealias SwiftType = MobileKeyTransferImport
|
||||||
|
|
||||||
|
public static func lift(_ handle: UInt64) throws -> MobileKeyTransferImport {
|
||||||
|
return MobileKeyTransferImport(unsafeFromHandle: handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func lower(_ value: MobileKeyTransferImport) -> UInt64 {
|
||||||
|
return value.uniffiCloneHandle()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileKeyTransferImport {
|
||||||
|
let handle: UInt64 = try readInt(&buf)
|
||||||
|
return try lift(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileKeyTransferImport, into buf: inout [UInt8]) {
|
||||||
|
writeInt(&buf, lower(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferImport_lift(_ handle: UInt64) throws -> MobileKeyTransferImport {
|
||||||
|
return try FfiConverterTypeMobileKeyTransferImport.lift(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferImport_lower(_ value: MobileKeyTransferImport) -> UInt64 {
|
||||||
|
return FfiConverterTypeMobileKeyTransferImport.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public protocol MobileOnboardingOperationProtocol: AnyObject, Sendable {
|
public protocol MobileOnboardingOperationProtocol: AnyObject, Sendable {
|
||||||
|
|
||||||
func cancel()
|
func cancel()
|
||||||
@@ -2319,6 +2610,304 @@ public func FfiConverterTypeMobileHomeSummaryRow_lower(_ value: MobileHomeSummar
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct MobileKeyTransferExport: Equatable, Hashable {
|
||||||
|
public var key: MobileKeyTransferKey
|
||||||
|
public var frames: [MobileKeyTransferFrame]
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(key: MobileKeyTransferKey, frames: [MobileKeyTransferFrame]) {
|
||||||
|
self.key = key
|
||||||
|
self.frames = frames
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobileKeyTransferExport: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileKeyTransferExport: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileKeyTransferExport {
|
||||||
|
return
|
||||||
|
try MobileKeyTransferExport(
|
||||||
|
key: FfiConverterTypeMobileKeyTransferKey.read(from: &buf),
|
||||||
|
frames: FfiConverterSequenceTypeMobileKeyTransferFrame.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileKeyTransferExport, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterTypeMobileKeyTransferKey.write(value.key, into: &buf)
|
||||||
|
FfiConverterSequenceTypeMobileKeyTransferFrame.write(value.frames, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferExport_lift(_ buf: RustBuffer) throws -> MobileKeyTransferExport {
|
||||||
|
return try FfiConverterTypeMobileKeyTransferExport.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferExport_lower(_ value: MobileKeyTransferExport) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobileKeyTransferExport.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct MobileKeyTransferFrame: Equatable, Hashable {
|
||||||
|
public var sequence: UInt32
|
||||||
|
public var total: UInt32
|
||||||
|
public var width: UInt32
|
||||||
|
public var modules: Data
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(sequence: UInt32, total: UInt32, width: UInt32, modules: Data) {
|
||||||
|
self.sequence = sequence
|
||||||
|
self.total = total
|
||||||
|
self.width = width
|
||||||
|
self.modules = modules
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobileKeyTransferFrame: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileKeyTransferFrame: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileKeyTransferFrame {
|
||||||
|
return
|
||||||
|
try MobileKeyTransferFrame(
|
||||||
|
sequence: FfiConverterUInt32.read(from: &buf),
|
||||||
|
total: FfiConverterUInt32.read(from: &buf),
|
||||||
|
width: FfiConverterUInt32.read(from: &buf),
|
||||||
|
modules: FfiConverterData.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileKeyTransferFrame, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterUInt32.write(value.sequence, into: &buf)
|
||||||
|
FfiConverterUInt32.write(value.total, into: &buf)
|
||||||
|
FfiConverterUInt32.write(value.width, into: &buf)
|
||||||
|
FfiConverterData.write(value.modules, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferFrame_lift(_ buf: RustBuffer) throws -> MobileKeyTransferFrame {
|
||||||
|
return try FfiConverterTypeMobileKeyTransferFrame.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferFrame_lower(_ value: MobileKeyTransferFrame) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobileKeyTransferFrame.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct MobileKeyTransferKey: Equatable, Hashable {
|
||||||
|
public var fingerprint: String
|
||||||
|
public var title: String
|
||||||
|
public var detail: String
|
||||||
|
public var kind: MobileKeyTransferKind
|
||||||
|
public var requiresPassphrase: Bool
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(fingerprint: String, title: String, detail: String, kind: MobileKeyTransferKind, requiresPassphrase: Bool) {
|
||||||
|
self.fingerprint = fingerprint
|
||||||
|
self.title = title
|
||||||
|
self.detail = detail
|
||||||
|
self.kind = kind
|
||||||
|
self.requiresPassphrase = requiresPassphrase
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobileKeyTransferKey: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileKeyTransferKey: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileKeyTransferKey {
|
||||||
|
return
|
||||||
|
try MobileKeyTransferKey(
|
||||||
|
fingerprint: FfiConverterString.read(from: &buf),
|
||||||
|
title: FfiConverterString.read(from: &buf),
|
||||||
|
detail: FfiConverterString.read(from: &buf),
|
||||||
|
kind: FfiConverterTypeMobileKeyTransferKind.read(from: &buf),
|
||||||
|
requiresPassphrase: FfiConverterBool.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileKeyTransferKey, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterString.write(value.fingerprint, into: &buf)
|
||||||
|
FfiConverterString.write(value.title, into: &buf)
|
||||||
|
FfiConverterString.write(value.detail, into: &buf)
|
||||||
|
FfiConverterTypeMobileKeyTransferKind.write(value.kind, into: &buf)
|
||||||
|
FfiConverterBool.write(value.requiresPassphrase, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferKey_lift(_ buf: RustBuffer) throws -> MobileKeyTransferKey {
|
||||||
|
return try FfiConverterTypeMobileKeyTransferKey.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferKey_lower(_ value: MobileKeyTransferKey) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobileKeyTransferKey.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct MobileKeyTransferOutcome: Equatable, Hashable {
|
||||||
|
public var title: String
|
||||||
|
public var detail: String
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(title: String, detail: String) {
|
||||||
|
self.title = title
|
||||||
|
self.detail = detail
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobileKeyTransferOutcome: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileKeyTransferOutcome: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileKeyTransferOutcome {
|
||||||
|
return
|
||||||
|
try MobileKeyTransferOutcome(
|
||||||
|
title: FfiConverterString.read(from: &buf),
|
||||||
|
detail: FfiConverterString.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileKeyTransferOutcome, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterString.write(value.title, into: &buf)
|
||||||
|
FfiConverterString.write(value.detail, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferOutcome_lift(_ buf: RustBuffer) throws -> MobileKeyTransferOutcome {
|
||||||
|
return try FfiConverterTypeMobileKeyTransferOutcome.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferOutcome_lower(_ value: MobileKeyTransferOutcome) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobileKeyTransferOutcome.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct MobileKeyTransferProgress: Equatable, Hashable {
|
||||||
|
public var received: UInt32
|
||||||
|
public var total: UInt32
|
||||||
|
public var duplicate: Bool
|
||||||
|
public var key: MobileKeyTransferKey?
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(received: UInt32, total: UInt32, duplicate: Bool, key: MobileKeyTransferKey?) {
|
||||||
|
self.received = received
|
||||||
|
self.total = total
|
||||||
|
self.duplicate = duplicate
|
||||||
|
self.key = key
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobileKeyTransferProgress: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileKeyTransferProgress: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileKeyTransferProgress {
|
||||||
|
return
|
||||||
|
try MobileKeyTransferProgress(
|
||||||
|
received: FfiConverterUInt32.read(from: &buf),
|
||||||
|
total: FfiConverterUInt32.read(from: &buf),
|
||||||
|
duplicate: FfiConverterBool.read(from: &buf),
|
||||||
|
key: FfiConverterOptionTypeMobileKeyTransferKey.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileKeyTransferProgress, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterUInt32.write(value.received, into: &buf)
|
||||||
|
FfiConverterUInt32.write(value.total, into: &buf)
|
||||||
|
FfiConverterBool.write(value.duplicate, into: &buf)
|
||||||
|
FfiConverterOptionTypeMobileKeyTransferKey.write(value.key, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferProgress_lift(_ buf: RustBuffer) throws -> MobileKeyTransferProgress {
|
||||||
|
return try FfiConverterTypeMobileKeyTransferProgress.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferProgress_lower(_ value: MobileKeyTransferProgress) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobileKeyTransferProgress.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public struct MobileOnboardingDiscovery: Equatable, Hashable {
|
public struct MobileOnboardingDiscovery: Equatable, Hashable {
|
||||||
public var branches: [String]
|
public var branches: [String]
|
||||||
public var selectedBranch: UInt32
|
public var selectedBranch: UInt32
|
||||||
@@ -3880,6 +4469,147 @@ public func FfiConverterTypeMobileHomePhase_lower(_ value: MobileHomePhase) -> R
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public
|
||||||
|
enum MobileKeyTransferFfiError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
case Failed(message: String
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
String(reflecting: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobileKeyTransferFfiError: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileKeyTransferFfiError: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = MobileKeyTransferFfiError
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileKeyTransferFfiError {
|
||||||
|
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: MobileKeyTransferFfiError, 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 FfiConverterTypeMobileKeyTransferFfiError_lift(_ buf: RustBuffer) throws -> MobileKeyTransferFfiError {
|
||||||
|
return try FfiConverterTypeMobileKeyTransferFfiError.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferFfiError_lower(_ value: MobileKeyTransferFfiError) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobileKeyTransferFfiError.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public enum MobileKeyTransferKind: Equatable, Hashable {
|
||||||
|
|
||||||
|
case `public`
|
||||||
|
case `private`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension MobileKeyTransferKind: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypeMobileKeyTransferKind: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = MobileKeyTransferKind
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileKeyTransferKind {
|
||||||
|
let variant: Int32 = try readInt(&buf)
|
||||||
|
switch variant {
|
||||||
|
|
||||||
|
case 1: return .`public`
|
||||||
|
|
||||||
|
case 2: return .`private`
|
||||||
|
|
||||||
|
default: throw UniffiInternalError.unexpectedEnumCase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: MobileKeyTransferKind, into buf: inout [UInt8]) {
|
||||||
|
switch value {
|
||||||
|
|
||||||
|
|
||||||
|
case .`public`:
|
||||||
|
writeInt(&buf, Int32(1))
|
||||||
|
|
||||||
|
|
||||||
|
case .`private`:
|
||||||
|
writeInt(&buf, Int32(2))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferKind_lift(_ buf: RustBuffer) throws -> MobileKeyTransferKind {
|
||||||
|
return try FfiConverterTypeMobileKeyTransferKind.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypeMobileKeyTransferKind_lower(_ value: MobileKeyTransferKind) -> RustBuffer {
|
||||||
|
return FfiConverterTypeMobileKeyTransferKind.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public enum MobileOnboardingErrorKind: Equatable, Hashable {
|
public enum MobileOnboardingErrorKind: Equatable, Hashable {
|
||||||
|
|
||||||
@@ -4796,6 +5526,30 @@ fileprivate struct FfiConverterOptionTypeMobileHomeNotice: FfiConverterRustBuffe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
fileprivate struct FfiConverterOptionTypeMobileKeyTransferKey: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = MobileKeyTransferKey?
|
||||||
|
|
||||||
|
public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
|
||||||
|
guard let value = value else {
|
||||||
|
writeInt(&buf, Int8(0))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeInt(&buf, Int8(1))
|
||||||
|
FfiConverterTypeMobileKeyTransferKey.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 FfiConverterTypeMobileKeyTransferKey.read(from: &buf)
|
||||||
|
default: throw UniffiInternalError.unexpectedOptionalTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if swift(>=5.8)
|
#if swift(>=5.8)
|
||||||
@_documentation(visibility: private)
|
@_documentation(visibility: private)
|
||||||
#endif
|
#endif
|
||||||
@@ -4996,6 +5750,56 @@ fileprivate struct FfiConverterSequenceTypeMobileHomeSummaryRow: FfiConverterRus
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
fileprivate struct FfiConverterSequenceTypeMobileKeyTransferFrame: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = [MobileKeyTransferFrame]
|
||||||
|
|
||||||
|
public static func write(_ value: [MobileKeyTransferFrame], into buf: inout [UInt8]) {
|
||||||
|
let len = Int32(value.count)
|
||||||
|
writeInt(&buf, len)
|
||||||
|
for item in value {
|
||||||
|
FfiConverterTypeMobileKeyTransferFrame.write(item, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [MobileKeyTransferFrame] {
|
||||||
|
let len: Int32 = try readInt(&buf)
|
||||||
|
var seq = [MobileKeyTransferFrame]()
|
||||||
|
seq.reserveCapacity(Int(len))
|
||||||
|
for _ in 0 ..< len {
|
||||||
|
seq.append(try FfiConverterTypeMobileKeyTransferFrame.read(from: &buf))
|
||||||
|
}
|
||||||
|
return seq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
fileprivate struct FfiConverterSequenceTypeMobileKeyTransferKey: FfiConverterRustBuffer {
|
||||||
|
typealias SwiftType = [MobileKeyTransferKey]
|
||||||
|
|
||||||
|
public static func write(_ value: [MobileKeyTransferKey], into buf: inout [UInt8]) {
|
||||||
|
let len = Int32(value.count)
|
||||||
|
writeInt(&buf, len)
|
||||||
|
for item in value {
|
||||||
|
FfiConverterTypeMobileKeyTransferKey.write(item, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [MobileKeyTransferKey] {
|
||||||
|
let len: Int32 = try readInt(&buf)
|
||||||
|
var seq = [MobileKeyTransferKey]()
|
||||||
|
seq.reserveCapacity(Int(len))
|
||||||
|
for _ in 0 ..< len {
|
||||||
|
seq.append(try FfiConverterTypeMobileKeyTransferKey.read(from: &buf))
|
||||||
|
}
|
||||||
|
return seq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if swift(>=5.8)
|
#if swift(>=5.8)
|
||||||
@_documentation(visibility: private)
|
@_documentation(visibility: private)
|
||||||
#endif
|
#endif
|
||||||
@@ -5084,6 +5888,13 @@ public func mobileHomeOperation() -> MobileHomeOperation {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
public func mobileKeyTransfer()throws -> MobileKeyTransfer {
|
||||||
|
return try FfiConverterTypeMobileKeyTransfer_lift(try rustCallWithError(FfiConverterTypeMobileKeyTransferFfiError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_ironstorage_apple_fn_func_mobile_key_transfer(uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
public func mobileOnboardingOperation(serverUrl: String, account: String, repositoryPath: String, applicationToken: String)throws -> MobileOnboardingOperation {
|
public func mobileOnboardingOperation(serverUrl: String, account: String, repositoryPath: String, applicationToken: String)throws -> MobileOnboardingOperation {
|
||||||
return try FfiConverterTypeMobileOnboardingOperation_lift(try rustCallWithError(FfiConverterTypeMobileOnboardingFfiError_lift) {
|
return try FfiConverterTypeMobileOnboardingOperation_lift(try rustCallWithError(FfiConverterTypeMobileOnboardingFfiError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
@@ -5162,6 +5973,9 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_ironstorage_apple_checksum_func_mobile_home_operation() != 10595) {
|
if (uniffi_ironstorage_apple_checksum_func_mobile_home_operation() != 10595) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_func_mobile_key_transfer() != 57389) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_func_mobile_onboarding_operation() != 8354) {
|
if (uniffi_ironstorage_apple_checksum_func_mobile_onboarding_operation() != 8354) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
@@ -5276,6 +6090,21 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_refresh_if_stale() != 27818) {
|
if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_refresh_if_stale() != 27818) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobilekeytransfer_export() != 50467) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobilekeytransfer_importer() != 55825) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobilekeytransfer_keys() != 1261) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobilekeytransferimport_add_frame() != 27319) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
|
if (uniffi_ironstorage_apple_checksum_method_mobilekeytransferimport_import() != 37403) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_ironstorage_apple_checksum_method_mobileonboardingoperation_cancel() != 49755) {
|
if (uniffi_ironstorage_apple_checksum_method_mobileonboardingoperation_cancel() != 49755) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -418,6 +418,51 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobilehomeoperation_refresh(uint64
|
|||||||
RustBuffer uniffi_ironstorage_apple_fn_method_mobilehomeoperation_refresh_if_stale(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobilehomeoperation_refresh_if_stale(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEKEYTRANSFER
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEKEYTRANSFER
|
||||||
|
uint64_t uniffi_ironstorage_apple_fn_clone_mobilekeytransfer(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FREE_MOBILEKEYTRANSFER
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FREE_MOBILEKEYTRANSFER
|
||||||
|
void uniffi_ironstorage_apple_fn_free_mobilekeytransfer(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEKEYTRANSFER_EXPORT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEKEYTRANSFER_EXPORT
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobilekeytransfer_export(uint64_t ptr, RustBuffer fingerprint, RustBuffer kind, RustBuffer passphrase, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEKEYTRANSFER_IMPORTER
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEKEYTRANSFER_IMPORTER
|
||||||
|
uint64_t uniffi_ironstorage_apple_fn_method_mobilekeytransfer_importer(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEKEYTRANSFER_KEYS
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEKEYTRANSFER_KEYS
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobilekeytransfer_keys(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEKEYTRANSFERIMPORT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEKEYTRANSFERIMPORT
|
||||||
|
uint64_t uniffi_ironstorage_apple_fn_clone_mobilekeytransferimport(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FREE_MOBILEKEYTRANSFERIMPORT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FREE_MOBILEKEYTRANSFERIMPORT
|
||||||
|
void uniffi_ironstorage_apple_fn_free_mobilekeytransferimport(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEKEYTRANSFERIMPORT_ADD_FRAME
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEKEYTRANSFERIMPORT_ADD_FRAME
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobilekeytransferimport_add_frame(uint64_t ptr, RustBuffer payload, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEKEYTRANSFERIMPORT_IMPORT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEKEYTRANSFERIMPORT_IMPORT
|
||||||
|
RustBuffer uniffi_ironstorage_apple_fn_method_mobilekeytransferimport_import(uint64_t ptr, RustBuffer passphrase, int8_t make_default, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEONBOARDINGOPERATION
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEONBOARDINGOPERATION
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEONBOARDINGOPERATION
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEONBOARDINGOPERATION
|
||||||
uint64_t uniffi_ironstorage_apple_fn_clone_mobileonboardingoperation(uint64_t handle, RustCallStatus *_Nonnull out_status
|
uint64_t uniffi_ironstorage_apple_fn_clone_mobileonboardingoperation(uint64_t handle, RustCallStatus *_Nonnull out_status
|
||||||
@@ -458,6 +503,12 @@ uint64_t uniffi_ironstorage_apple_fn_func_mobile_authentication(RustCallStatus *
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_HOME_OPERATION
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_HOME_OPERATION
|
||||||
uint64_t uniffi_ironstorage_apple_fn_func_mobile_home_operation(RustCallStatus *_Nonnull out_status
|
uint64_t uniffi_ironstorage_apple_fn_func_mobile_home_operation(RustCallStatus *_Nonnull out_status
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_KEY_TRANSFER
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_KEY_TRANSFER
|
||||||
|
uint64_t uniffi_ironstorage_apple_fn_func_mobile_key_transfer(RustCallStatus *_Nonnull out_status
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_ONBOARDING_OPERATION
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_ONBOARDING_OPERATION
|
||||||
@@ -767,6 +818,12 @@ uint16_t uniffi_ironstorage_apple_checksum_func_mobile_authentication(void
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_HOME_OPERATION
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_HOME_OPERATION
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_home_operation(void
|
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_home_operation(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_KEY_TRANSFER
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_KEY_TRANSFER
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_key_transfer(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_ONBOARDING_OPERATION
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_ONBOARDING_OPERATION
|
||||||
@@ -995,6 +1052,36 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_refresh(vo
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_REFRESH_IF_STALE
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_REFRESH_IF_STALE
|
||||||
uint16_t uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_refresh_if_stale(void
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_refresh_if_stale(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEKEYTRANSFER_EXPORT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEKEYTRANSFER_EXPORT
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobilekeytransfer_export(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEKEYTRANSFER_IMPORTER
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEKEYTRANSFER_IMPORTER
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobilekeytransfer_importer(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEKEYTRANSFER_KEYS
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEKEYTRANSFER_KEYS
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobilekeytransfer_keys(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEKEYTRANSFERIMPORT_ADD_FRAME
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEKEYTRANSFERIMPORT_ADD_FRAME
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobilekeytransferimport_add_frame(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEKEYTRANSFERIMPORT_IMPORT
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEKEYTRANSFERIMPORT_IMPORT
|
||||||
|
uint16_t uniffi_ironstorage_apple_checksum_method_mobilekeytransferimport_import(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEONBOARDINGOPERATION_CANCEL
|
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEONBOARDINGOPERATION_CANCEL
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
<false/>
|
<false/>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>Unlock your GPG key for protected password operations.</string>
|
<string>Unlock your GPG key for protected password operations.</string>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Scan GPG key transfer QR codes that you choose to import.</string>
|
||||||
<key>UILaunchScreen</key>
|
<key>UILaunchScreen</key>
|
||||||
<dict/>
|
<dict/>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
import AVFoundation
|
||||||
|
import Vision
|
||||||
|
import VisionKit
|
||||||
|
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let ironStorageLocalStoreDidChange = Notification.Name(
|
static let ironStorageLocalStoreDidChange = Notification.Name(
|
||||||
@@ -10,6 +13,9 @@ extension Notification.Name {
|
|||||||
static let ironStorageWatchSnapshotDidChange = Notification.Name(
|
static let ironStorageWatchSnapshotDidChange = Notification.Name(
|
||||||
"de.rfc1437.ironstorage.watch-snapshot-did-change"
|
"de.rfc1437.ironstorage.watch-snapshot-did-change"
|
||||||
)
|
)
|
||||||
|
static let ironStorageKeyMaterialDidChange = Notification.Name(
|
||||||
|
"de.rfc1437.ironstorage.key-material-did-change"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@@ -17,6 +23,20 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
private var context: AppContext?
|
private var context: AppContext?
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(keyMaterialDidChange),
|
||||||
|
name: .ironStorageKeyMaterialDidChange,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
func application(
|
func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||||
@@ -33,6 +53,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||||
context?.lockForBackground()
|
context?.lockForBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func keyMaterialDidChange() {
|
||||||
|
guard let window else { return }
|
||||||
|
context?.lockForBackground()
|
||||||
|
let context = AppContext()
|
||||||
|
self.context = context
|
||||||
|
window.rootViewController = context.makeRootController()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -676,6 +704,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
fileprivate let shellTab = MobileTab.preferences
|
fileprivate let shellTab = MobileTab.preferences
|
||||||
private var page: MobilePage
|
private var page: MobilePage
|
||||||
private let authentication: MobileAuthentication?
|
private let authentication: MobileAuthentication?
|
||||||
|
private let keyTransfer = try? mobileKeyTransfer()
|
||||||
private var state: MobileAuthenticationState?
|
private var state: MobileAuthenticationState?
|
||||||
private var preferenceTask: Task<Void, Never>?
|
private var preferenceTask: Task<Void, Never>?
|
||||||
private var loadTask: Task<Void, Never>?
|
private var loadTask: Task<Void, Never>?
|
||||||
@@ -718,21 +747,25 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
page.state == .ready ? 2 : 0
|
page.state == .ready ? 3 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(
|
override func tableView(
|
||||||
_ tableView: UITableView,
|
_ tableView: UITableView,
|
||||||
numberOfRowsInSection section: Int
|
numberOfRowsInSection section: Int
|
||||||
) -> Int {
|
) -> Int {
|
||||||
section == 0 ? 1 : 2
|
section == 0 ? 2 : (section == 1 ? 1 : 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(
|
override func tableView(
|
||||||
_ tableView: UITableView,
|
_ tableView: UITableView,
|
||||||
titleForHeaderInSection section: Int
|
titleForHeaderInSection section: Int
|
||||||
) -> String? {
|
) -> String? {
|
||||||
section == 0 ? "Secure Unlock" : "Authentication Session"
|
switch section {
|
||||||
|
case 0: "GPG Key Transfer"
|
||||||
|
case 1: "Secure Unlock"
|
||||||
|
default: "Authentication Session"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(
|
override func tableView(
|
||||||
@@ -740,6 +773,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
titleForFooterInSection section: Int
|
titleForFooterInSection section: Int
|
||||||
) -> String? {
|
) -> String? {
|
||||||
if section == 0 {
|
if section == 0 {
|
||||||
|
return "Scan or display ASCII-armored GPG keys. Private-key transfers require explicit confirmation and passphrase validation."
|
||||||
|
}
|
||||||
|
if section == 1 {
|
||||||
return "When enabled, the GPG passphrase is device-only, requires a device passcode, and is invalidated when enrolled biometrics change."
|
return "When enabled, the GPG passphrase is device-only, requires a device passcode, and is invalidated when enrolled biometrics change."
|
||||||
}
|
}
|
||||||
return "Manual lock and inactivity expiry immediately revoke the shared Rust authentication lease."
|
return "Manual lock and inactivity expiry immediately revoke the shared Rust authentication lease."
|
||||||
@@ -752,6 +788,16 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
||||||
var content = cell.defaultContentConfiguration()
|
var content = cell.defaultContentConfiguration()
|
||||||
if indexPath.section == 0 {
|
if indexPath.section == 0 {
|
||||||
|
let importing = indexPath.row == 0
|
||||||
|
content.image = UIImage(systemName: importing ? "qrcode.viewfinder" : "qrcode")
|
||||||
|
content.text = importing ? "Import GPG Key" : "Export GPG Key"
|
||||||
|
content.secondaryText = importing
|
||||||
|
? "Scan one or more transfer QR codes"
|
||||||
|
: "Display public or private key armor"
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
cell.isUserInteractionEnabled = keyTransfer != nil
|
||||||
|
cell.contentView.alpha = keyTransfer == nil ? 0.45 : 1
|
||||||
|
} else if indexPath.section == 1 {
|
||||||
content.image = UIImage(systemName: "faceid")
|
content.image = UIImage(systemName: "faceid")
|
||||||
content.text = "Biometric Unlock"
|
content.text = "Biometric Unlock"
|
||||||
content.secondaryText = state?.biometricUnlockEnabled == true
|
content.secondaryText = state?.biometricUnlockEnabled == true
|
||||||
@@ -788,7 +834,19 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
guard indexPath.section == 1, indexPath.row == 1, let authentication else { return }
|
if indexPath.section == 0 {
|
||||||
|
guard let keyTransfer else { return }
|
||||||
|
if indexPath.row == 0 {
|
||||||
|
requestKeyScanner(keyTransfer)
|
||||||
|
} else {
|
||||||
|
navigationController?.pushViewController(
|
||||||
|
KeyExportListViewController(transfer: keyTransfer),
|
||||||
|
animated: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard indexPath.section == 2, indexPath.row == 1, let authentication else { return }
|
||||||
do {
|
do {
|
||||||
try authentication.manualLock()
|
try authentication.manualLock()
|
||||||
refreshState()
|
refreshState()
|
||||||
@@ -845,6 +903,67 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
navigationController?.pushViewController(TokenUpdateViewController(), animated: true)
|
navigationController?.pushViewController(TokenUpdateViewController(), animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func requestKeyScanner(_ transfer: MobileKeyTransfer) {
|
||||||
|
guard DataScannerViewController.isSupported else {
|
||||||
|
presentMessage(
|
||||||
|
title: "Key Scanning Is Unavailable",
|
||||||
|
detail: "This device does not support live QR scanning."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||||
|
case .authorized:
|
||||||
|
showKeyScanner(transfer)
|
||||||
|
case .notDetermined:
|
||||||
|
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
|
||||||
|
Task { @MainActor in
|
||||||
|
guard let self else { return }
|
||||||
|
if granted {
|
||||||
|
self.showKeyScanner(transfer)
|
||||||
|
} else {
|
||||||
|
self.showCameraDenied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
showCameraDenied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showKeyScanner(_ transfer: MobileKeyTransfer) {
|
||||||
|
guard DataScannerViewController.isAvailable else {
|
||||||
|
presentMessage(
|
||||||
|
title: "Camera Is Unavailable",
|
||||||
|
detail: "Close other camera apps and try again."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
navigationController?.pushViewController(
|
||||||
|
KeyImportScannerViewController(transfer: transfer),
|
||||||
|
animated: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showCameraDenied() {
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: "Camera Access Is Off",
|
||||||
|
message: "Allow camera access in Settings to scan GPG key QR codes.",
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
alert.addAction(UIAlertAction(title: "Open Settings", style: .default) { _ in
|
||||||
|
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
})
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentMessage(title: String, detail: String) {
|
||||||
|
let alert = UIAlertController(title: title, message: detail, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func authenticationDidChange() {
|
@objc private func authenticationDidChange() {
|
||||||
refreshState()
|
refreshState()
|
||||||
}
|
}
|
||||||
@@ -893,6 +1012,569 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private final class KeyImportScannerViewController: UIViewController,
|
||||||
|
DataScannerViewControllerDelegate
|
||||||
|
{
|
||||||
|
private let importer: MobileKeyTransferImport
|
||||||
|
private let scanner = DataScannerViewController(
|
||||||
|
recognizedDataTypes: [.barcode(symbologies: [.qr])],
|
||||||
|
qualityLevel: .balanced,
|
||||||
|
recognizesMultipleItems: true,
|
||||||
|
isHighFrameRateTrackingEnabled: false,
|
||||||
|
isPinchToZoomEnabled: true,
|
||||||
|
isGuidanceEnabled: true,
|
||||||
|
isHighlightingEnabled: true
|
||||||
|
)
|
||||||
|
private let progressLabel = UILabel()
|
||||||
|
private let shield = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
||||||
|
private var completing = false
|
||||||
|
|
||||||
|
init(transfer: MobileKeyTransfer) {
|
||||||
|
importer = transfer.importer()
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
title = "Import GPG Key"
|
||||||
|
navigationItem.largeTitleDisplayMode = .never
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
view.backgroundColor = .systemBackground
|
||||||
|
scanner.delegate = self
|
||||||
|
addChild(scanner)
|
||||||
|
scanner.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(scanner.view)
|
||||||
|
scanner.didMove(toParent: self)
|
||||||
|
|
||||||
|
let material = UIBlurEffect(style: .systemMaterial)
|
||||||
|
let status = UIVisualEffectView(effect: material)
|
||||||
|
status.layer.cornerRadius = 12
|
||||||
|
status.clipsToBounds = true
|
||||||
|
status.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
progressLabel.text = "Point the camera at a key-transfer QR code"
|
||||||
|
progressLabel.font = .preferredFont(forTextStyle: .callout)
|
||||||
|
progressLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
progressLabel.numberOfLines = 0
|
||||||
|
progressLabel.textAlignment = .center
|
||||||
|
progressLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
status.contentView.addSubview(progressLabel)
|
||||||
|
view.addSubview(status)
|
||||||
|
|
||||||
|
let shieldLabel = UILabel()
|
||||||
|
shieldLabel.text = "Camera hidden while IronStorage is inactive or the screen is captured"
|
||||||
|
shieldLabel.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
shieldLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
shieldLabel.numberOfLines = 0
|
||||||
|
shieldLabel.textAlignment = .center
|
||||||
|
shieldLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
shield.contentView.addSubview(shieldLabel)
|
||||||
|
shield.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
shield.isHidden = true
|
||||||
|
shield.accessibilityViewIsModal = true
|
||||||
|
view.addSubview(shield)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
scanner.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
scanner.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
scanner.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
scanner.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
status.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
|
||||||
|
status.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
|
||||||
|
status.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
|
||||||
|
progressLabel.leadingAnchor.constraint(equalTo: status.contentView.leadingAnchor, constant: 16),
|
||||||
|
progressLabel.trailingAnchor.constraint(equalTo: status.contentView.trailingAnchor, constant: -16),
|
||||||
|
progressLabel.topAnchor.constraint(equalTo: status.contentView.topAnchor, constant: 12),
|
||||||
|
progressLabel.bottomAnchor.constraint(equalTo: status.contentView.bottomAnchor, constant: -12),
|
||||||
|
shield.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
shield.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
shield.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
shield.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
shieldLabel.leadingAnchor.constraint(equalTo: shield.contentView.leadingAnchor, constant: 32),
|
||||||
|
shieldLabel.trailingAnchor.constraint(equalTo: shield.contentView.trailingAnchor, constant: -32),
|
||||||
|
shieldLabel.centerYAnchor.constraint(equalTo: shield.contentView.centerYAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(appWillResignActive),
|
||||||
|
name: UIApplication.willResignActiveNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(appDidBecomeActive),
|
||||||
|
name: UIApplication.didBecomeActiveNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
registerForTraitChanges([UITraitSceneCaptureState.self]) {
|
||||||
|
(controller: KeyImportScannerViewController, _: UITraitCollection) in
|
||||||
|
controller.updateCaptureShield()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
if !completing {
|
||||||
|
do {
|
||||||
|
try scanner.startScanning()
|
||||||
|
} catch {
|
||||||
|
presentFailure(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
scanner.stopScanning()
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dataScanner(
|
||||||
|
_ dataScanner: DataScannerViewController,
|
||||||
|
didAdd addedItems: [RecognizedItem],
|
||||||
|
allItems: [RecognizedItem]
|
||||||
|
) {
|
||||||
|
guard !completing else { return }
|
||||||
|
for item in addedItems {
|
||||||
|
guard case let .barcode(barcode) = item, let payload = barcode.payloadStringValue else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let progress = try importer.addFrame(payload: payload)
|
||||||
|
if progress.duplicate {
|
||||||
|
progressLabel.text = "Already scanned — show the next QR code"
|
||||||
|
} else {
|
||||||
|
progressLabel.text = progress.key == nil
|
||||||
|
? "Scanned \(progress.received) of \(progress.total)"
|
||||||
|
: "Key transfer complete"
|
||||||
|
}
|
||||||
|
UIAccessibility.post(notification: .announcement, argument: progressLabel.text)
|
||||||
|
if let key = progress.key {
|
||||||
|
completing = true
|
||||||
|
scanner.stopScanning()
|
||||||
|
confirmImport(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
progressLabel.text = "That QR code could not be added"
|
||||||
|
UIAccessibility.post(notification: .announcement, argument: progressLabel.text)
|
||||||
|
presentFailure(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dataScanner(
|
||||||
|
_ dataScanner: DataScannerViewController,
|
||||||
|
becameUnavailableWithError error: DataScannerViewController.ScanningUnavailable
|
||||||
|
) {
|
||||||
|
presentFailure("Live scanning became unavailable. Try again when the camera is free.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func appWillResignActive() {
|
||||||
|
shield.isHidden = false
|
||||||
|
scanner.stopScanning()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func appDidBecomeActive() {
|
||||||
|
updateCaptureShield()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateCaptureShield() {
|
||||||
|
let captured = traitCollection.sceneCaptureState == .active
|
||||||
|
shield.isHidden = !captured
|
||||||
|
if captured || completing || view.window == nil {
|
||||||
|
scanner.stopScanning()
|
||||||
|
} else {
|
||||||
|
try? scanner.startScanning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func confirmImport(_ key: MobileKeyTransferKey) {
|
||||||
|
let privateKey = key.kind == .private
|
||||||
|
let warning = privateKey
|
||||||
|
? "Import this private key only if you trust the device that displayed it."
|
||||||
|
: "Import this public key?"
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: privateKey ? "Import Private GPG Key?" : "Import Public GPG Key?",
|
||||||
|
message: "\(warning)\n\n\(key.title)\n\(key.detail)",
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
if privateKey && key.requiresPassphrase {
|
||||||
|
alert.addTextField { field in
|
||||||
|
field.placeholder = "GPG key passphrase"
|
||||||
|
field.isSecureTextEntry = true
|
||||||
|
field.textContentType = .password
|
||||||
|
field.clearButtonMode = .whileEditing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { [weak self] _ in
|
||||||
|
self?.navigationController?.popViewController(animated: true)
|
||||||
|
})
|
||||||
|
alert.addAction(UIAlertAction(title: "Import", style: .default) { [weak self, weak alert] _ in
|
||||||
|
self?.finishImport(passphrase: alert?.textFields?.first?.text, makeDefault: false)
|
||||||
|
})
|
||||||
|
if privateKey {
|
||||||
|
alert.addAction(UIAlertAction(title: "Import as Default", style: .default) {
|
||||||
|
[weak self, weak alert] _ in
|
||||||
|
self?.finishImport(passphrase: alert?.textFields?.first?.text, makeDefault: true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finishImport(passphrase: String?, makeDefault: Bool) {
|
||||||
|
do {
|
||||||
|
let outcome = try importer.import(passphrase: passphrase, makeDefault: makeDefault)
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: outcome.title,
|
||||||
|
message: outcome.detail,
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
alert.addAction(UIAlertAction(title: "Done", style: .default) { [weak self] _ in
|
||||||
|
guard self != nil else { return }
|
||||||
|
NotificationCenter.default.post(name: .ironStorageKeyMaterialDidChange, object: nil)
|
||||||
|
})
|
||||||
|
present(alert, animated: true)
|
||||||
|
} catch {
|
||||||
|
completing = false
|
||||||
|
presentFailure(error.localizedDescription) { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
try? scanner.startScanning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presentFailure(_ detail: String, completion: (() -> Void)? = nil) {
|
||||||
|
guard presentedViewController == nil else { return }
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: "GPG Key Transfer Failed",
|
||||||
|
message: detail,
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in completion?() })
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private final class KeyExportListViewController: UITableViewController {
|
||||||
|
private let transfer: MobileKeyTransfer
|
||||||
|
private let keys: [MobileKeyTransferKey]
|
||||||
|
|
||||||
|
init(transfer: MobileKeyTransfer) {
|
||||||
|
self.transfer = transfer
|
||||||
|
keys = transfer.keys()
|
||||||
|
super.init(style: .insetGrouped)
|
||||||
|
title = "Export GPG Key"
|
||||||
|
navigationItem.largeTitleDisplayMode = .never
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
if keys.isEmpty {
|
||||||
|
var configuration = UIContentUnavailableConfiguration.empty()
|
||||||
|
configuration.image = UIImage(systemName: "key.slash")
|
||||||
|
configuration.text = "No GPG Keys"
|
||||||
|
configuration.secondaryText = "Import a key before exporting one."
|
||||||
|
contentUnavailableConfiguration = configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
keys.count
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(
|
||||||
|
_ tableView: UITableView,
|
||||||
|
cellForRowAt indexPath: IndexPath
|
||||||
|
) -> UITableViewCell {
|
||||||
|
let key = keys[indexPath.row]
|
||||||
|
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
|
||||||
|
var content = cell.defaultContentConfiguration()
|
||||||
|
content.image = UIImage(systemName: key.kind == .private ? "key.fill" : "key")
|
||||||
|
content.text = key.title
|
||||||
|
content.secondaryText = key.detail
|
||||||
|
content.secondaryTextProperties.numberOfLines = 0
|
||||||
|
cell.contentConfiguration = content
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
let key = keys[indexPath.row]
|
||||||
|
let sheet = UIAlertController(title: key.title, message: key.detail, preferredStyle: .actionSheet)
|
||||||
|
sheet.addAction(UIAlertAction(title: "Export Public Key", style: .default) {
|
||||||
|
[weak self] _ in self?.export(key, kind: .public, passphrase: nil)
|
||||||
|
})
|
||||||
|
if key.kind == .private {
|
||||||
|
sheet.addAction(UIAlertAction(title: "Export Private Key…", style: .destructive) {
|
||||||
|
[weak self] _ in self?.confirmPrivateExport(key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
if let popover = sheet.popoverPresentationController {
|
||||||
|
popover.sourceView = tableView.cellForRow(at: indexPath)
|
||||||
|
popover.sourceRect = tableView.cellForRow(at: indexPath)?.bounds ?? .zero
|
||||||
|
}
|
||||||
|
present(sheet, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func confirmPrivateExport(_ key: MobileKeyTransferKey) {
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: "Display Private GPG Key?",
|
||||||
|
message: "Anyone who scans these QR codes can use this private key.\n\n\(key.title)\n\(key.detail)",
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
if key.requiresPassphrase {
|
||||||
|
alert.addTextField { field in
|
||||||
|
field.placeholder = "GPG key passphrase"
|
||||||
|
field.isSecureTextEntry = true
|
||||||
|
field.textContentType = .password
|
||||||
|
field.clearButtonMode = .whileEditing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
alert.addAction(UIAlertAction(title: "Display", style: .destructive) {
|
||||||
|
[weak self, weak alert] _ in
|
||||||
|
self?.export(key, kind: .private, passphrase: alert?.textFields?.first?.text)
|
||||||
|
})
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func export(
|
||||||
|
_ key: MobileKeyTransferKey,
|
||||||
|
kind: MobileKeyTransferKind,
|
||||||
|
passphrase: String?
|
||||||
|
) {
|
||||||
|
do {
|
||||||
|
let exported = try transfer.export(
|
||||||
|
fingerprint: key.fingerprint,
|
||||||
|
kind: kind,
|
||||||
|
passphrase: passphrase
|
||||||
|
)
|
||||||
|
navigationController?.pushViewController(
|
||||||
|
KeyQrExportViewController(exported: exported),
|
||||||
|
animated: true
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: "GPG Key Could Not Be Exported",
|
||||||
|
message: error.localizedDescription,
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private final class KeyQrExportViewController: UIViewController {
|
||||||
|
private let exported: MobileKeyTransferExport
|
||||||
|
private let qrView = KeyQrView()
|
||||||
|
private let pageControl = UIPageControl()
|
||||||
|
private let progressLabel = UILabel()
|
||||||
|
private let shield = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
||||||
|
private var index = 0
|
||||||
|
|
||||||
|
init(exported: MobileKeyTransferExport) {
|
||||||
|
self.exported = exported
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
title = exported.key.kind == .private ? "Private GPG Key" : "Public GPG Key"
|
||||||
|
navigationItem.largeTitleDisplayMode = .never
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
view.backgroundColor = .systemBackground
|
||||||
|
qrView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
qrView.layer.cornerRadius = 16
|
||||||
|
qrView.clipsToBounds = true
|
||||||
|
|
||||||
|
let identity = UILabel()
|
||||||
|
identity.text = exported.key.title
|
||||||
|
identity.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
identity.adjustsFontForContentSizeCategory = true
|
||||||
|
identity.numberOfLines = 0
|
||||||
|
identity.textAlignment = .center
|
||||||
|
|
||||||
|
let fingerprint = UILabel()
|
||||||
|
fingerprint.text = exported.key.detail
|
||||||
|
fingerprint.font = .preferredFont(forTextStyle: .footnote)
|
||||||
|
fingerprint.adjustsFontForContentSizeCategory = true
|
||||||
|
fingerprint.numberOfLines = 0
|
||||||
|
fingerprint.textAlignment = .center
|
||||||
|
fingerprint.textColor = .secondaryLabel
|
||||||
|
|
||||||
|
progressLabel.font = .preferredFont(forTextStyle: .callout)
|
||||||
|
progressLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
progressLabel.textAlignment = .center
|
||||||
|
pageControl.numberOfPages = exported.frames.count
|
||||||
|
pageControl.addTarget(self, action: #selector(pageChanged), for: .valueChanged)
|
||||||
|
pageControl.isHidden = exported.frames.count == 1
|
||||||
|
|
||||||
|
let stack = UIStackView(arrangedSubviews: [identity, fingerprint, qrView, progressLabel, pageControl])
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.alignment = .fill
|
||||||
|
stack.spacing = 14
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(stack)
|
||||||
|
|
||||||
|
let shieldLabel = UILabel()
|
||||||
|
shieldLabel.text = "Private key hidden while IronStorage is inactive or the screen is captured"
|
||||||
|
shieldLabel.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
shieldLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
shieldLabel.numberOfLines = 0
|
||||||
|
shieldLabel.textAlignment = .center
|
||||||
|
shieldLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
shield.contentView.addSubview(shieldLabel)
|
||||||
|
shield.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
shield.isHidden = true
|
||||||
|
shield.accessibilityViewIsModal = true
|
||||||
|
view.addSubview(shield)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stack.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 24),
|
||||||
|
stack.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -24),
|
||||||
|
stack.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
|
||||||
|
qrView.widthAnchor.constraint(equalTo: qrView.heightAnchor),
|
||||||
|
qrView.widthAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.widthAnchor, constant: -48),
|
||||||
|
shield.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
shield.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
shield.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
shield.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
shieldLabel.leadingAnchor.constraint(equalTo: shield.contentView.leadingAnchor, constant: 32),
|
||||||
|
shieldLabel.trailingAnchor.constraint(equalTo: shield.contentView.trailingAnchor, constant: -32),
|
||||||
|
shieldLabel.centerYAnchor.constraint(equalTo: shield.contentView.centerYAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
let left = UISwipeGestureRecognizer(target: self, action: #selector(swipedLeft))
|
||||||
|
left.direction = .left
|
||||||
|
let right = UISwipeGestureRecognizer(target: self, action: #selector(swipedRight))
|
||||||
|
right.direction = .right
|
||||||
|
qrView.addGestureRecognizer(left)
|
||||||
|
qrView.addGestureRecognizer(right)
|
||||||
|
qrView.isUserInteractionEnabled = true
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(appWillResignActive),
|
||||||
|
name: UIApplication.willResignActiveNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(appDidBecomeActive),
|
||||||
|
name: UIApplication.didBecomeActiveNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
registerForTraitChanges([UITraitSceneCaptureState.self]) {
|
||||||
|
(controller: KeyQrExportViewController, _: UITraitCollection) in
|
||||||
|
controller.updateCaptureShield()
|
||||||
|
}
|
||||||
|
showFrame(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
updateCaptureShield()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pageChanged() {
|
||||||
|
showFrame(pageControl.currentPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func swipedLeft() {
|
||||||
|
showFrame(min(index + 1, exported.frames.count - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func swipedRight() {
|
||||||
|
showFrame(max(index - 1, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showFrame(_ index: Int) {
|
||||||
|
self.index = index
|
||||||
|
pageControl.currentPage = index
|
||||||
|
qrView.transferFrame = exported.frames[index]
|
||||||
|
progressLabel.text = exported.frames.count == 1
|
||||||
|
? "Scan this QR code"
|
||||||
|
: "QR code \(index + 1) of \(exported.frames.count)"
|
||||||
|
qrView.accessibilityLabel = progressLabel.text
|
||||||
|
UIAccessibility.post(notification: .pageScrolled, argument: progressLabel.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func appWillResignActive() {
|
||||||
|
if exported.key.kind == .private {
|
||||||
|
shield.isHidden = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func appDidBecomeActive() {
|
||||||
|
updateCaptureShield()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateCaptureShield() {
|
||||||
|
shield.isHidden = exported.key.kind != .private || traitCollection.sceneCaptureState != .active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class KeyQrView: UIView {
|
||||||
|
var transferFrame: MobileKeyTransferFrame? {
|
||||||
|
didSet { setNeedsDisplay() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override func draw(_ rect: CGRect) {
|
||||||
|
UIColor.white.setFill()
|
||||||
|
UIRectFill(rect)
|
||||||
|
guard let transferFrame, transferFrame.width > 0 else { return }
|
||||||
|
let width = Int(transferFrame.width)
|
||||||
|
let padded = width + 8
|
||||||
|
let scale = floor(min(rect.width, rect.height) / CGFloat(padded))
|
||||||
|
guard scale >= 1 else { return }
|
||||||
|
let symbolSize = CGFloat(padded) * scale
|
||||||
|
let origin = CGPoint(
|
||||||
|
x: rect.midX - symbolSize / 2 + 4 * scale,
|
||||||
|
y: rect.midY - symbolSize / 2 + 4 * scale
|
||||||
|
)
|
||||||
|
guard let context = UIGraphicsGetCurrentContext() else { return }
|
||||||
|
context.setFillColor(UIColor.black.cgColor)
|
||||||
|
context.interpolationQuality = .none
|
||||||
|
for y in 0..<width {
|
||||||
|
for x in 0..<width where transferFrame.modules[y * width + x] != 0 {
|
||||||
|
context.fill(CGRect(
|
||||||
|
x: origin.x + CGFloat(x) * scale,
|
||||||
|
y: origin.y + CGFloat(y) * scale,
|
||||||
|
width: scale,
|
||||||
|
height: scale
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private final class TotpListViewController: UITableViewController, MobileTabRoot {
|
private final class TotpListViewController: UITableViewController, MobileTabRoot {
|
||||||
fileprivate let shellTab = MobileTab.totp
|
fileprivate let shellTab = MobileTab.totp
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ targets:
|
|||||||
CFBundleDisplayName: IronStorage
|
CFBundleDisplayName: IronStorage
|
||||||
ITSAppUsesNonExemptEncryption: false
|
ITSAppUsesNonExemptEncryption: false
|
||||||
NSFaceIDUsageDescription: Unlock your GPG key for protected password operations.
|
NSFaceIDUsageDescription: Unlock your GPG key for protected password operations.
|
||||||
|
NSCameraUsageDescription: Scan GPG key transfer QR codes that you choose to import.
|
||||||
UILaunchScreen: {}
|
UILaunchScreen: {}
|
||||||
UISupportedInterfaceOrientations:
|
UISupportedInterfaceOrientations:
|
||||||
- UIInterfaceOrientationPortrait
|
- UIInterfaceOrientationPortrait
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
|
|
||||||
//! Mechanical UniFFI exports for Apple presentation code.
|
//! Mechanical UniFFI exports for Apple presentation code.
|
||||||
|
|
||||||
use std::{error::Error, fmt, sync::Arc};
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
fmt,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
use ironstorage::{
|
use ironstorage::{
|
||||||
config::ConfigError,
|
config::ConfigError,
|
||||||
@@ -27,6 +31,12 @@ use ironstorage::{
|
|||||||
MobileHomeErrorKind as StorageHomeErrorKind, MobileHomeFreshness as StorageHomeFreshness,
|
MobileHomeErrorKind as StorageHomeErrorKind, MobileHomeFreshness as StorageHomeFreshness,
|
||||||
MobileHomePhase as StorageHomePhase,
|
MobileHomePhase as StorageHomePhase,
|
||||||
},
|
},
|
||||||
|
mobile_key_transfer::{
|
||||||
|
MobileKeyTransferError as StorageKeyTransferError,
|
||||||
|
MobileKeyTransferKey as StorageKeyTransferKey,
|
||||||
|
MobileKeyTransferKind as StorageKeyTransferKind,
|
||||||
|
MobileKeyTransferProgress as StorageKeyTransferProgress,
|
||||||
|
},
|
||||||
mobile_onboarding::{
|
mobile_onboarding::{
|
||||||
self, MobileOnboardingError as StorageOnboardingError,
|
self, MobileOnboardingError as StorageOnboardingError,
|
||||||
MobileOnboardingErrorKind as StorageOnboardingErrorKind,
|
MobileOnboardingErrorKind as StorageOnboardingErrorKind,
|
||||||
@@ -868,6 +878,213 @@ impl From<StorageAuthenticationState> for MobileAuthenticationState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
|
||||||
|
pub enum MobileKeyTransferKind {
|
||||||
|
Public,
|
||||||
|
Private,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StorageKeyTransferKind> for MobileKeyTransferKind {
|
||||||
|
fn from(kind: StorageKeyTransferKind) -> Self {
|
||||||
|
match kind {
|
||||||
|
StorageKeyTransferKind::Public => Self::Public,
|
||||||
|
StorageKeyTransferKind::Private => Self::Private,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MobileKeyTransferKind> for StorageKeyTransferKind {
|
||||||
|
fn from(kind: MobileKeyTransferKind) -> Self {
|
||||||
|
match kind {
|
||||||
|
MobileKeyTransferKind::Public => Self::Public,
|
||||||
|
MobileKeyTransferKind::Private => Self::Private,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct MobileKeyTransferKey {
|
||||||
|
pub fingerprint: String,
|
||||||
|
pub title: String,
|
||||||
|
pub detail: String,
|
||||||
|
pub kind: MobileKeyTransferKind,
|
||||||
|
pub requires_passphrase: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&StorageKeyTransferKey> for MobileKeyTransferKey {
|
||||||
|
fn from(key: &StorageKeyTransferKey) -> Self {
|
||||||
|
Self {
|
||||||
|
fingerprint: key.fingerprint().to_owned(),
|
||||||
|
title: key.title().to_owned(),
|
||||||
|
detail: key.detail().to_owned(),
|
||||||
|
kind: key.kind().into(),
|
||||||
|
requires_passphrase: key.requires_passphrase(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct MobileKeyTransferFrame {
|
||||||
|
pub sequence: u32,
|
||||||
|
pub total: u32,
|
||||||
|
pub width: u32,
|
||||||
|
pub modules: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct MobileKeyTransferExport {
|
||||||
|
pub key: MobileKeyTransferKey,
|
||||||
|
pub frames: Vec<MobileKeyTransferFrame>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct MobileKeyTransferProgress {
|
||||||
|
pub received: u32,
|
||||||
|
pub total: u32,
|
||||||
|
pub duplicate: bool,
|
||||||
|
pub key: Option<MobileKeyTransferKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StorageKeyTransferProgress> for MobileKeyTransferProgress {
|
||||||
|
fn from(progress: StorageKeyTransferProgress) -> Self {
|
||||||
|
Self {
|
||||||
|
received: u32::try_from(progress.received()).unwrap_or(u32::MAX),
|
||||||
|
total: u32::try_from(progress.total()).unwrap_or(u32::MAX),
|
||||||
|
duplicate: progress.duplicate(),
|
||||||
|
key: progress.key().map(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct MobileKeyTransferOutcome {
|
||||||
|
pub title: String,
|
||||||
|
pub detail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, uniffi::Error)]
|
||||||
|
pub enum MobileKeyTransferFfiError {
|
||||||
|
Failed { message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for MobileKeyTransferFfiError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Failed { message } => formatter.write_str(message),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for MobileKeyTransferFfiError {}
|
||||||
|
|
||||||
|
impl From<StorageKeyTransferError> for MobileKeyTransferFfiError {
|
||||||
|
fn from(error: StorageKeyTransferError) -> Self {
|
||||||
|
Self::Failed {
|
||||||
|
message: error.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(uniffi::Object)]
|
||||||
|
pub struct MobileKeyTransfer {
|
||||||
|
service: ironstorage::mobile_key_transfer::MobileKeyTransferService,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
impl MobileKeyTransfer {
|
||||||
|
pub fn keys(&self) -> Vec<MobileKeyTransferKey> {
|
||||||
|
self.service.keys().iter().map(Into::into).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export(
|
||||||
|
&self,
|
||||||
|
fingerprint: String,
|
||||||
|
kind: MobileKeyTransferKind,
|
||||||
|
passphrase: Option<String>,
|
||||||
|
) -> Result<MobileKeyTransferExport, MobileKeyTransferFfiError> {
|
||||||
|
let exported = self.service.export(
|
||||||
|
&fingerprint,
|
||||||
|
kind.into(),
|
||||||
|
passphrase.map(|value| ironstorage::repository::SecretBytes::new(value.into_bytes())),
|
||||||
|
)?;
|
||||||
|
let frames = exported
|
||||||
|
.frames()
|
||||||
|
.iter()
|
||||||
|
.map(|frame| {
|
||||||
|
let matrix = frame.matrix();
|
||||||
|
let mut modules = Vec::with_capacity(matrix.width() * matrix.width());
|
||||||
|
for y in 0..matrix.width() {
|
||||||
|
for x in 0..matrix.width() {
|
||||||
|
modules.push(u8::from(matrix.is_dark(x, y).unwrap_or(false)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MobileKeyTransferFrame {
|
||||||
|
sequence: u32::try_from(frame.sequence()).unwrap_or(u32::MAX),
|
||||||
|
total: u32::try_from(frame.total()).unwrap_or(u32::MAX),
|
||||||
|
width: u32::try_from(matrix.width()).unwrap_or(u32::MAX),
|
||||||
|
modules,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(MobileKeyTransferExport {
|
||||||
|
key: exported.key().into(),
|
||||||
|
frames,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn importer(&self) -> Arc<MobileKeyTransferImport> {
|
||||||
|
Arc::new(MobileKeyTransferImport {
|
||||||
|
importer: Mutex::new(self.service.importer()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(uniffi::Object)]
|
||||||
|
pub struct MobileKeyTransferImport {
|
||||||
|
importer: Mutex<ironstorage::mobile_key_transfer::MobileKeyTransferImport>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
impl MobileKeyTransferImport {
|
||||||
|
pub fn add_frame(
|
||||||
|
&self,
|
||||||
|
payload: String,
|
||||||
|
) -> Result<MobileKeyTransferProgress, MobileKeyTransferFfiError> {
|
||||||
|
self.importer
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| MobileKeyTransferFfiError::Failed {
|
||||||
|
message: "the key-transfer session is unavailable".to_owned(),
|
||||||
|
})?
|
||||||
|
.add_frame(ironstorage::repository::SecretBytes::new(
|
||||||
|
payload.into_bytes(),
|
||||||
|
))
|
||||||
|
.map(Into::into)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import(
|
||||||
|
&self,
|
||||||
|
passphrase: Option<String>,
|
||||||
|
make_default: bool,
|
||||||
|
) -> Result<MobileKeyTransferOutcome, MobileKeyTransferFfiError> {
|
||||||
|
let outcome = self
|
||||||
|
.importer
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| MobileKeyTransferFfiError::Failed {
|
||||||
|
message: "the key-transfer session is unavailable".to_owned(),
|
||||||
|
})?
|
||||||
|
.import(
|
||||||
|
passphrase
|
||||||
|
.map(|value| ironstorage::repository::SecretBytes::new(value.into_bytes())),
|
||||||
|
make_default,
|
||||||
|
)?;
|
||||||
|
Ok(MobileKeyTransferOutcome {
|
||||||
|
title: outcome.title().to_owned(),
|
||||||
|
detail: outcome.detail().to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, uniffi::Error)]
|
#[derive(Debug, uniffi::Error)]
|
||||||
pub enum MobileAuthenticationFfiError {
|
pub enum MobileAuthenticationFfiError {
|
||||||
Failed {
|
Failed {
|
||||||
@@ -1388,6 +1605,13 @@ pub fn mobile_authentication() -> Result<Arc<MobileAuthentication>, MobileAuthen
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[uniffi::export]
|
||||||
|
pub fn mobile_key_transfer() -> Result<Arc<MobileKeyTransfer>, MobileKeyTransferFfiError> {
|
||||||
|
Ok(Arc::new(MobileKeyTransfer {
|
||||||
|
service: ironstorage::mobile_key_transfer::MobileKeyTransferService::load()?,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
pub fn mobile_onboarding_operation(
|
pub fn mobile_onboarding_operation(
|
||||||
server_url: String,
|
server_url: String,
|
||||||
|
|||||||
@@ -265,6 +265,52 @@ impl KeyStore {
|
|||||||
self.keys.values().map(key_info)
|
self.keys.values().map(key_info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serialize one verified certificate as transferable ASCII armor.
|
||||||
|
pub fn export_armored(
|
||||||
|
&self,
|
||||||
|
identity: &str,
|
||||||
|
include_secret: bool,
|
||||||
|
passphrase: Option<&SecretBytes>,
|
||||||
|
) -> Result<SecretBytes, CryptoError> {
|
||||||
|
let handle = self.resolve(identity)?;
|
||||||
|
let material = self.material(&handle)?;
|
||||||
|
let bytes = if include_secret {
|
||||||
|
let secret = material
|
||||||
|
.secret
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(CryptoError::MissingSecretKey)?;
|
||||||
|
let password = Password::from(passphrase.map_or(&[][..], |value| value.expose()));
|
||||||
|
if !secret_unlocks(secret, &password) {
|
||||||
|
return Err(CryptoError::InvalidKeyPassphrase);
|
||||||
|
}
|
||||||
|
secret.to_armored_bytes(ArmorOptions::default())
|
||||||
|
} else {
|
||||||
|
material.public.to_armored_bytes(ArmorOptions::default())
|
||||||
|
}
|
||||||
|
.map_err(|_| CryptoError::CorruptKeyMaterial)?;
|
||||||
|
Ok(SecretBytes::new(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prove that a supplied passphrase unlocks every protected packet in a secret key.
|
||||||
|
pub fn validate_secret_passphrase(
|
||||||
|
&self,
|
||||||
|
identity: &str,
|
||||||
|
passphrase: Option<&SecretBytes>,
|
||||||
|
) -> Result<(), CryptoError> {
|
||||||
|
let handle = self.resolve(identity)?;
|
||||||
|
let secret = self
|
||||||
|
.material(&handle)?
|
||||||
|
.secret
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(CryptoError::MissingSecretKey)?;
|
||||||
|
let password = Password::from(passphrase.map_or(&[][..], |value| value.expose()));
|
||||||
|
if secret_unlocks(secret, &password) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(CryptoError::InvalidKeyPassphrase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve a full primary/subkey fingerprint, 8/16-digit key ID, or exact UTF-8 user ID.
|
/// Resolve a full primary/subkey fingerprint, 8/16-digit key ID, or exact UTF-8 user ID.
|
||||||
pub fn resolve(&self, identity: &str) -> Result<KeyHandle, CryptoError> {
|
pub fn resolve(&self, identity: &str) -> Result<KeyHandle, CryptoError> {
|
||||||
let hex_identity = normalize_hex_identity(identity);
|
let hex_identity = normalize_hex_identity(identity);
|
||||||
@@ -646,6 +692,7 @@ pub enum CryptoError {
|
|||||||
reason: SecretProviderError,
|
reason: SecretProviderError,
|
||||||
},
|
},
|
||||||
DecryptionFailed,
|
DecryptionFailed,
|
||||||
|
InvalidKeyPassphrase,
|
||||||
MissingSigningKey {
|
MissingSigningKey {
|
||||||
fingerprint: KeyFingerprint,
|
fingerprint: KeyFingerprint,
|
||||||
},
|
},
|
||||||
@@ -704,6 +751,9 @@ impl fmt::Display for CryptoError {
|
|||||||
"secret provider could not unlock OpenPGP key {fingerprint}: {reason:?}"
|
"secret provider could not unlock OpenPGP key {fingerprint}: {reason:?}"
|
||||||
),
|
),
|
||||||
Self::DecryptionFailed => formatter.write_str("OpenPGP decryption failed"),
|
Self::DecryptionFailed => formatter.write_str("OpenPGP decryption failed"),
|
||||||
|
Self::InvalidKeyPassphrase => {
|
||||||
|
formatter.write_str("the OpenPGP key passphrase is incorrect")
|
||||||
|
}
|
||||||
Self::MissingSigningKey { fingerprint } => {
|
Self::MissingSigningKey { fingerprint } => {
|
||||||
write!(formatter, "OpenPGP key cannot sign: {fingerprint}")
|
write!(formatter, "OpenPGP key cannot sign: {fingerprint}")
|
||||||
}
|
}
|
||||||
@@ -875,6 +925,14 @@ fn secret_requires_password(key: &SignedSecretKey) -> bool {
|
|||||||
.any(|subkey| subkey.secret_params().is_encrypted())
|
.any(|subkey| subkey.secret_params().is_encrypted())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn secret_unlocks(key: &SignedSecretKey, password: &Password) -> bool {
|
||||||
|
matches!(key.primary_key.unlock(password, |_, _| Ok(())), Ok(Ok(())))
|
||||||
|
&& key
|
||||||
|
.secret_subkeys
|
||||||
|
.iter()
|
||||||
|
.all(|subkey| matches!(subkey.key.unlock(password, |_, _| Ok(())), Ok(Ok(()))))
|
||||||
|
}
|
||||||
|
|
||||||
fn signing_target_requires_password(target: SigningTarget<'_>) -> bool {
|
fn signing_target_requires_password(target: SigningTarget<'_>) -> bool {
|
||||||
match target {
|
match target {
|
||||||
SigningTarget::Primary(key) => key.secret_params().is_encrypted(),
|
SigningTarget::Primary(key) => key.secret_params().is_encrypted(),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pub mod mobile;
|
|||||||
pub mod mobile_authentication;
|
pub mod mobile_authentication;
|
||||||
pub mod mobile_entry;
|
pub mod mobile_entry;
|
||||||
pub mod mobile_home;
|
pub mod mobile_home;
|
||||||
|
pub mod mobile_key_transfer;
|
||||||
pub mod mobile_onboarding;
|
pub mod mobile_onboarding;
|
||||||
pub mod mobile_passwords;
|
pub mod mobile_passwords;
|
||||||
pub mod mobile_totp;
|
pub mod mobile_totp;
|
||||||
|
|||||||
764
crates/storage/src/mobile_key_transfer.rs
Normal file
764
crates/storage/src/mobile_key_transfer.rs
Normal file
@@ -0,0 +1,764 @@
|
|||||||
|
//! Storage-owned OpenPGP key transfer for native mobile QR adapters.
|
||||||
|
|
||||||
|
use std::{collections::BTreeMap, error::Error, fmt, fs, io::Write as _, path::Path};
|
||||||
|
|
||||||
|
use cap_std::{ambient_authority, fs::Dir};
|
||||||
|
use cap_tempfile::TempFile;
|
||||||
|
use data_encoding::{BASE64URL_NOPAD, HEXLOWER};
|
||||||
|
use sha2::{Digest as _, Sha256};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::{Config, ConfigError},
|
||||||
|
crypto::{CryptoError, KeyInfo, KeyStore},
|
||||||
|
presentation::{QrError, QrMatrix},
|
||||||
|
repository::SecretBytes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FRAME_PREFIX: &str = "ISKT1";
|
||||||
|
const FRAME_CHUNK_BYTES: usize = 1_600;
|
||||||
|
const MAX_TRANSFER_FRAMES: usize = 11_000;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum MobileKeyTransferKind {
|
||||||
|
Public,
|
||||||
|
Private,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MobileKeyTransferKey {
|
||||||
|
fingerprint: String,
|
||||||
|
title: String,
|
||||||
|
detail: String,
|
||||||
|
kind: MobileKeyTransferKind,
|
||||||
|
requires_passphrase: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobileKeyTransferKey {
|
||||||
|
pub fn fingerprint(&self) -> &str {
|
||||||
|
&self.fingerprint
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detail(&self) -> &str {
|
||||||
|
&self.detail
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kind(&self) -> MobileKeyTransferKind {
|
||||||
|
self.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn requires_passphrase(&self) -> bool {
|
||||||
|
self.requires_passphrase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MobileKeyTransferFrame {
|
||||||
|
sequence: usize,
|
||||||
|
total: usize,
|
||||||
|
payload: SecretBytes,
|
||||||
|
matrix: QrMatrix,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobileKeyTransferFrame {
|
||||||
|
pub fn sequence(&self) -> usize {
|
||||||
|
self.sequence
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total(&self) -> usize {
|
||||||
|
self.total
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn payload(&self) -> &[u8] {
|
||||||
|
self.payload.expose()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matrix(&self) -> &QrMatrix {
|
||||||
|
&self.matrix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MobileKeyTransferExport {
|
||||||
|
key: MobileKeyTransferKey,
|
||||||
|
frames: Vec<MobileKeyTransferFrame>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobileKeyTransferExport {
|
||||||
|
pub fn key(&self) -> &MobileKeyTransferKey {
|
||||||
|
&self.key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn frames(&self) -> &[MobileKeyTransferFrame] {
|
||||||
|
&self.frames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MobileKeyTransferProgress {
|
||||||
|
received: usize,
|
||||||
|
total: usize,
|
||||||
|
duplicate: bool,
|
||||||
|
key: Option<MobileKeyTransferKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobileKeyTransferProgress {
|
||||||
|
pub fn received(&self) -> usize {
|
||||||
|
self.received
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total(&self) -> usize {
|
||||||
|
self.total
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn duplicate(&self) -> bool {
|
||||||
|
self.duplicate
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&self) -> Option<&MobileKeyTransferKey> {
|
||||||
|
self.key.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct MobileKeyTransferOutcome {
|
||||||
|
title: String,
|
||||||
|
detail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobileKeyTransferOutcome {
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detail(&self) -> &str {
|
||||||
|
&self.detail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MobileKeyTransferService {
|
||||||
|
config: Config,
|
||||||
|
keys: KeyStore,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobileKeyTransferService {
|
||||||
|
pub fn load() -> Result<Self, MobileKeyTransferError> {
|
||||||
|
let config = Config::load(None)?;
|
||||||
|
let keys = KeyStore::load(config.key_material())?;
|
||||||
|
Ok(Self { config, keys })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keys(&self) -> Vec<MobileKeyTransferKey> {
|
||||||
|
self.keys.infos().map(|key| transfer_key(&key)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export(
|
||||||
|
&self,
|
||||||
|
fingerprint: &str,
|
||||||
|
kind: MobileKeyTransferKind,
|
||||||
|
passphrase: Option<SecretBytes>,
|
||||||
|
) -> Result<MobileKeyTransferExport, MobileKeyTransferError> {
|
||||||
|
let include_secret = kind == MobileKeyTransferKind::Private;
|
||||||
|
let armor = self
|
||||||
|
.keys
|
||||||
|
.export_armored(fingerprint, include_secret, passphrase.as_ref())?;
|
||||||
|
let info = self
|
||||||
|
.keys
|
||||||
|
.infos()
|
||||||
|
.find(|key| key.fingerprint().as_str() == fingerprint)
|
||||||
|
.ok_or(MobileKeyTransferError::KeyUnavailable)?;
|
||||||
|
let frames = encode_frames(armor)?;
|
||||||
|
Ok(MobileKeyTransferExport {
|
||||||
|
key: transfer_key_with_kind(&info, kind),
|
||||||
|
frames,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn importer(&self) -> MobileKeyTransferImport {
|
||||||
|
MobileKeyTransferImport::new(self.config.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MobileKeyTransferImport {
|
||||||
|
config: Config,
|
||||||
|
digest: Option<String>,
|
||||||
|
total: Option<usize>,
|
||||||
|
chunks: BTreeMap<usize, Vec<u8>>,
|
||||||
|
complete: Option<SecretBytes>,
|
||||||
|
key: Option<MobileKeyTransferKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MobileKeyTransferImport {
|
||||||
|
fn new(config: Config) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
digest: None,
|
||||||
|
total: None,
|
||||||
|
chunks: BTreeMap::new(),
|
||||||
|
complete: None,
|
||||||
|
key: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_frame(
|
||||||
|
&mut self,
|
||||||
|
payload: SecretBytes,
|
||||||
|
) -> Result<MobileKeyTransferProgress, MobileKeyTransferError> {
|
||||||
|
if self.complete.is_some() {
|
||||||
|
return Err(MobileKeyTransferError::AlreadyComplete);
|
||||||
|
}
|
||||||
|
if payload.expose().starts_with(b"-----BEGIN PGP ") {
|
||||||
|
if self.total.is_some() {
|
||||||
|
return Err(MobileKeyTransferError::MixedTransfers);
|
||||||
|
}
|
||||||
|
self.finish(payload)?;
|
||||||
|
return Ok(self.progress(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = std::str::from_utf8(payload.expose())
|
||||||
|
.map_err(|_| MobileKeyTransferError::InvalidFrame)?;
|
||||||
|
let mut fields = text.splitn(5, ':');
|
||||||
|
if fields.next() != Some(FRAME_PREFIX) {
|
||||||
|
return Err(MobileKeyTransferError::InvalidFrame);
|
||||||
|
}
|
||||||
|
let digest = fields.next().ok_or(MobileKeyTransferError::InvalidFrame)?;
|
||||||
|
if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) {
|
||||||
|
return Err(MobileKeyTransferError::InvalidFrame);
|
||||||
|
}
|
||||||
|
let sequence = parse_positive(fields.next())?;
|
||||||
|
let total = parse_positive(fields.next())?;
|
||||||
|
if sequence > total || total > MAX_TRANSFER_FRAMES {
|
||||||
|
return Err(MobileKeyTransferError::InvalidFrame);
|
||||||
|
}
|
||||||
|
let chunk = BASE64URL_NOPAD
|
||||||
|
.decode(
|
||||||
|
fields
|
||||||
|
.next()
|
||||||
|
.ok_or(MobileKeyTransferError::InvalidFrame)?
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.map_err(|_| MobileKeyTransferError::InvalidFrame)?;
|
||||||
|
if chunk.is_empty() || chunk.len() > FRAME_CHUNK_BYTES {
|
||||||
|
return Err(MobileKeyTransferError::InvalidFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.digest.as_deref().is_some_and(|value| value != digest)
|
||||||
|
|| self.total.is_some_and(|value| value != total)
|
||||||
|
{
|
||||||
|
return Err(MobileKeyTransferError::MixedTransfers);
|
||||||
|
}
|
||||||
|
self.digest
|
||||||
|
.get_or_insert_with(|| digest.to_ascii_lowercase());
|
||||||
|
self.total.get_or_insert(total);
|
||||||
|
if let Some(existing) = self.chunks.get(&sequence) {
|
||||||
|
if existing == &chunk {
|
||||||
|
return Ok(self.progress(true));
|
||||||
|
}
|
||||||
|
return Err(MobileKeyTransferError::ConflictingFrame);
|
||||||
|
}
|
||||||
|
self.chunks.insert(sequence, chunk);
|
||||||
|
if self.chunks.len() == total {
|
||||||
|
let mut armor = Vec::new();
|
||||||
|
for sequence in 1..=total {
|
||||||
|
armor.extend(
|
||||||
|
self.chunks
|
||||||
|
.get(&sequence)
|
||||||
|
.ok_or(MobileKeyTransferError::IncompleteTransfer)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let actual = HEXLOWER.encode(&Sha256::digest(&armor));
|
||||||
|
if self.digest.as_deref() != Some(actual.as_str()) {
|
||||||
|
return Err(MobileKeyTransferError::ChecksumMismatch);
|
||||||
|
}
|
||||||
|
self.finish(SecretBytes::new(armor))?;
|
||||||
|
}
|
||||||
|
Ok(self.progress(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import(
|
||||||
|
&mut self,
|
||||||
|
passphrase: Option<SecretBytes>,
|
||||||
|
make_default: bool,
|
||||||
|
) -> Result<MobileKeyTransferOutcome, MobileKeyTransferError> {
|
||||||
|
let armor = self
|
||||||
|
.complete
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(MobileKeyTransferError::IncompleteTransfer)?;
|
||||||
|
let key = self
|
||||||
|
.key
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(MobileKeyTransferError::IncompleteTransfer)?;
|
||||||
|
let mut imported = KeyStore::new();
|
||||||
|
imported.import(armor.expose())?;
|
||||||
|
if key.kind == MobileKeyTransferKind::Private {
|
||||||
|
imported.validate_secret_passphrase(&key.fingerprint, passphrase.as_ref())?;
|
||||||
|
}
|
||||||
|
if make_default && key.kind != MobileKeyTransferKind::Private {
|
||||||
|
return Err(MobileKeyTransferError::PublicDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing = KeyStore::load(self.config.key_material())?;
|
||||||
|
if let Some(found) = existing
|
||||||
|
.infos()
|
||||||
|
.find(|info| info.fingerprint().as_str() == key.fingerprint)
|
||||||
|
&& (found.has_secret() || key.kind == MobileKeyTransferKind::Public)
|
||||||
|
{
|
||||||
|
return Err(MobileKeyTransferError::DuplicateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
persist_key_material(self.config.key_material(), key, armor.expose())?;
|
||||||
|
if make_default {
|
||||||
|
let mut settings = self.config.settings();
|
||||||
|
settings.set_default_key(key.fingerprint.clone());
|
||||||
|
self.config.with_settings(settings)?.persist()?;
|
||||||
|
}
|
||||||
|
let kind = if key.kind == MobileKeyTransferKind::Private {
|
||||||
|
"private"
|
||||||
|
} else {
|
||||||
|
"public"
|
||||||
|
};
|
||||||
|
Ok(MobileKeyTransferOutcome {
|
||||||
|
title: "GPG Key Imported".to_owned(),
|
||||||
|
detail: format!("Imported the {kind} key {}.", key.fingerprint),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&mut self, armor: SecretBytes) -> Result<(), MobileKeyTransferError> {
|
||||||
|
validate_transfer_armor(armor.expose())?;
|
||||||
|
let mut store = KeyStore::new();
|
||||||
|
let infos = store.import(armor.expose())?;
|
||||||
|
if infos.len() != 1 {
|
||||||
|
return Err(MobileKeyTransferError::MismatchedKeys);
|
||||||
|
}
|
||||||
|
self.key = infos.first().map(transfer_key);
|
||||||
|
self.complete = Some(armor);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn progress(&self, duplicate: bool) -> MobileKeyTransferProgress {
|
||||||
|
MobileKeyTransferProgress {
|
||||||
|
received: self.chunks.len().max(usize::from(self.complete.is_some())),
|
||||||
|
total: self.total.unwrap_or(1),
|
||||||
|
duplicate,
|
||||||
|
key: self.key.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum MobileKeyTransferError {
|
||||||
|
Configuration,
|
||||||
|
KeyUnavailable,
|
||||||
|
InvalidFrame,
|
||||||
|
MixedTransfers,
|
||||||
|
ConflictingFrame,
|
||||||
|
IncompleteTransfer,
|
||||||
|
AlreadyComplete,
|
||||||
|
ChecksumMismatch,
|
||||||
|
MismatchedKeys,
|
||||||
|
DuplicateKey,
|
||||||
|
PublicDefault,
|
||||||
|
InvalidKeyMaterial,
|
||||||
|
IncorrectPassphrase,
|
||||||
|
QrPayload,
|
||||||
|
Write,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for MobileKeyTransferError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
formatter.write_str(match self {
|
||||||
|
Self::Configuration => "the IronStorage configuration is unavailable",
|
||||||
|
Self::KeyUnavailable => "the selected GPG key is unavailable",
|
||||||
|
Self::InvalidFrame => "this is not a valid IronStorage key-transfer QR code",
|
||||||
|
Self::MixedTransfers => "the scanned QR codes belong to different key transfers",
|
||||||
|
Self::ConflictingFrame => "a scanned QR frame conflicts with an earlier frame",
|
||||||
|
Self::IncompleteTransfer => {
|
||||||
|
"more QR frames are required before this key can be imported"
|
||||||
|
}
|
||||||
|
Self::AlreadyComplete => "this key transfer is already complete",
|
||||||
|
Self::ChecksumMismatch => {
|
||||||
|
"the reconstructed key transfer did not pass its integrity check"
|
||||||
|
}
|
||||||
|
Self::MismatchedKeys => "the transfer contains multiple or mismatched GPG keys",
|
||||||
|
Self::DuplicateKey => "this GPG key is already installed",
|
||||||
|
Self::PublicDefault => "a public-only key cannot become the default GPG key",
|
||||||
|
Self::InvalidKeyMaterial => "the GPG key material is invalid",
|
||||||
|
Self::IncorrectPassphrase => "the GPG key passphrase is incorrect",
|
||||||
|
Self::QrPayload => "the GPG key could not be represented as QR codes",
|
||||||
|
Self::Write => "the GPG key could not be stored safely",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for MobileKeyTransferError {}
|
||||||
|
|
||||||
|
impl From<ConfigError> for MobileKeyTransferError {
|
||||||
|
fn from(_: ConfigError) -> Self {
|
||||||
|
Self::Configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CryptoError> for MobileKeyTransferError {
|
||||||
|
fn from(error: CryptoError) -> Self {
|
||||||
|
match error {
|
||||||
|
CryptoError::InvalidKeyPassphrase => Self::IncorrectPassphrase,
|
||||||
|
CryptoError::MissingIdentity { .. } | CryptoError::MissingSecretKey => {
|
||||||
|
Self::KeyUnavailable
|
||||||
|
}
|
||||||
|
_ => Self::InvalidKeyMaterial,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<QrError> for MobileKeyTransferError {
|
||||||
|
fn from(_: QrError) -> Self {
|
||||||
|
Self::QrPayload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transfer_key(info: &KeyInfo) -> MobileKeyTransferKey {
|
||||||
|
transfer_key_with_kind(
|
||||||
|
info,
|
||||||
|
if info.has_secret() {
|
||||||
|
MobileKeyTransferKind::Private
|
||||||
|
} else {
|
||||||
|
MobileKeyTransferKind::Public
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transfer_key_with_kind(info: &KeyInfo, kind: MobileKeyTransferKind) -> MobileKeyTransferKey {
|
||||||
|
let title = info
|
||||||
|
.user_ids()
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| format!("GPG key {}", info.key_id()));
|
||||||
|
MobileKeyTransferKey {
|
||||||
|
fingerprint: info.fingerprint().as_str().to_owned(),
|
||||||
|
title,
|
||||||
|
detail: format!("Fingerprint {}", info.fingerprint()),
|
||||||
|
kind,
|
||||||
|
requires_passphrase: info.requires_passphrase(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_frames(
|
||||||
|
armor: SecretBytes,
|
||||||
|
) -> Result<Vec<MobileKeyTransferFrame>, MobileKeyTransferError> {
|
||||||
|
if let Ok(matrix) = QrMatrix::encode(&armor) {
|
||||||
|
return Ok(vec![MobileKeyTransferFrame {
|
||||||
|
sequence: 1,
|
||||||
|
total: 1,
|
||||||
|
payload: armor,
|
||||||
|
matrix,
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
encode_multipart(armor, FRAME_CHUNK_BYTES)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_multipart(
|
||||||
|
armor: SecretBytes,
|
||||||
|
chunk_bytes: usize,
|
||||||
|
) -> Result<Vec<MobileKeyTransferFrame>, MobileKeyTransferError> {
|
||||||
|
if chunk_bytes == 0 {
|
||||||
|
return Err(MobileKeyTransferError::QrPayload);
|
||||||
|
}
|
||||||
|
let digest = HEXLOWER.encode(&Sha256::digest(armor.expose()));
|
||||||
|
let total = armor.expose().len().div_ceil(chunk_bytes);
|
||||||
|
if total == 0 || total > MAX_TRANSFER_FRAMES {
|
||||||
|
return Err(MobileKeyTransferError::QrPayload);
|
||||||
|
}
|
||||||
|
let mut frames = Vec::with_capacity(total);
|
||||||
|
for (index, chunk) in armor.expose().chunks(chunk_bytes).enumerate() {
|
||||||
|
let payload = SecretBytes::new(
|
||||||
|
format!(
|
||||||
|
"{FRAME_PREFIX}:{digest}:{}:{total}:{}",
|
||||||
|
index + 1,
|
||||||
|
BASE64URL_NOPAD.encode(chunk)
|
||||||
|
)
|
||||||
|
.into_bytes(),
|
||||||
|
);
|
||||||
|
let matrix = QrMatrix::encode(&payload)?;
|
||||||
|
frames.push(MobileKeyTransferFrame {
|
||||||
|
sequence: index + 1,
|
||||||
|
total,
|
||||||
|
payload,
|
||||||
|
matrix,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(frames)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_positive(value: Option<&str>) -> Result<usize, MobileKeyTransferError> {
|
||||||
|
value
|
||||||
|
.ok_or(MobileKeyTransferError::InvalidFrame)?
|
||||||
|
.parse::<usize>()
|
||||||
|
.ok()
|
||||||
|
.filter(|value| *value > 0)
|
||||||
|
.ok_or(MobileKeyTransferError::InvalidFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_transfer_armor(bytes: &[u8]) -> Result<(), MobileKeyTransferError> {
|
||||||
|
let text =
|
||||||
|
std::str::from_utf8(bytes).map_err(|_| MobileKeyTransferError::InvalidKeyMaterial)?;
|
||||||
|
let end = if text.starts_with("-----BEGIN PGP PUBLIC KEY BLOCK-----") {
|
||||||
|
"-----END PGP PUBLIC KEY BLOCK-----"
|
||||||
|
} else if text.starts_with("-----BEGIN PGP PRIVATE KEY BLOCK-----") {
|
||||||
|
"-----END PGP PRIVATE KEY BLOCK-----"
|
||||||
|
} else {
|
||||||
|
return Err(MobileKeyTransferError::InvalidKeyMaterial);
|
||||||
|
};
|
||||||
|
if text.matches("-----BEGIN PGP ").count() != 1 {
|
||||||
|
return Err(MobileKeyTransferError::MismatchedKeys);
|
||||||
|
}
|
||||||
|
let end_offset = text
|
||||||
|
.find(end)
|
||||||
|
.map(|offset| offset + end.len())
|
||||||
|
.ok_or(MobileKeyTransferError::InvalidKeyMaterial)?;
|
||||||
|
if !text[end_offset..].trim().is_empty() {
|
||||||
|
return Err(MobileKeyTransferError::InvalidKeyMaterial);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn persist_key_material(
|
||||||
|
configured: &Path,
|
||||||
|
key: &MobileKeyTransferKey,
|
||||||
|
armor: &[u8],
|
||||||
|
) -> Result<(), MobileKeyTransferError> {
|
||||||
|
if configured.is_dir() {
|
||||||
|
let suffix = if key.kind == MobileKeyTransferKind::Private {
|
||||||
|
"secret"
|
||||||
|
} else {
|
||||||
|
"public"
|
||||||
|
};
|
||||||
|
let name = format!("{}-{suffix}.asc", key.fingerprint.to_ascii_lowercase());
|
||||||
|
atomic_replace(configured, Path::new(&name), armor)
|
||||||
|
} else {
|
||||||
|
let existing = fs::read(configured).map_err(|_| MobileKeyTransferError::Write)?;
|
||||||
|
let mut combined = Vec::with_capacity(existing.len() + armor.len() + 1);
|
||||||
|
combined.extend_from_slice(&existing);
|
||||||
|
if !combined.ends_with(b"\n") {
|
||||||
|
combined.push(b'\n');
|
||||||
|
}
|
||||||
|
combined.extend_from_slice(armor);
|
||||||
|
let parent = configured.parent().ok_or(MobileKeyTransferError::Write)?;
|
||||||
|
let name = configured
|
||||||
|
.file_name()
|
||||||
|
.ok_or(MobileKeyTransferError::Write)?;
|
||||||
|
atomic_replace(parent, Path::new(name), &combined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn atomic_replace(
|
||||||
|
parent: &Path,
|
||||||
|
name: &Path,
|
||||||
|
contents: &[u8],
|
||||||
|
) -> Result<(), MobileKeyTransferError> {
|
||||||
|
let directory = Dir::open_ambient_dir(parent, ambient_authority())
|
||||||
|
.map_err(|_| MobileKeyTransferError::Write)?;
|
||||||
|
if let Ok(metadata) = directory.symlink_metadata(name)
|
||||||
|
&& (metadata.file_type().is_symlink() || !metadata.is_file())
|
||||||
|
{
|
||||||
|
return Err(MobileKeyTransferError::Write);
|
||||||
|
}
|
||||||
|
let mut temporary = TempFile::new(&directory).map_err(|_| MobileKeyTransferError::Write)?;
|
||||||
|
set_private_permissions(&temporary)?;
|
||||||
|
temporary
|
||||||
|
.write_all(contents)
|
||||||
|
.and_then(|()| temporary.as_file().sync_all())
|
||||||
|
.and_then(|()| temporary.replace(name))
|
||||||
|
.and_then(|()| directory.open(".").and_then(|file| file.sync_all()))
|
||||||
|
.map_err(|_| MobileKeyTransferError::Write)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn set_private_permissions(temporary: &TempFile<'_>) -> Result<(), MobileKeyTransferError> {
|
||||||
|
use cap_std::fs::{Permissions, PermissionsExt as _};
|
||||||
|
|
||||||
|
temporary
|
||||||
|
.as_file()
|
||||||
|
.set_permissions(Permissions::from_mode(0o600))
|
||||||
|
.map_err(|_| MobileKeyTransferError::Write)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn set_private_permissions(_temporary: &TempFile<'_>) -> Result<(), MobileKeyTransferError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{
|
||||||
|
MobileKeyTransferError, MobileKeyTransferImport, MobileKeyTransferKind,
|
||||||
|
MobileKeyTransferService, encode_multipart,
|
||||||
|
};
|
||||||
|
use crate::{config::Config, crypto::KeyStore, repository::SecretBytes};
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
const ALICE_PUBLIC: &[u8] =
|
||||||
|
include_bytes!("../tests/fixtures/compatibility/keys/alice-public.asc");
|
||||||
|
const ALICE_SECRET: &[u8] =
|
||||||
|
include_bytes!("../tests/fixtures/compatibility/keys/alice-secret.asc");
|
||||||
|
const ALICE_FINGERPRINT: &str = "7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multipart_frames_reconstruct_exact_armor_out_of_order()
|
||||||
|
-> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let fixture = configured_public_key()?;
|
||||||
|
let mut importer = MobileKeyTransferImport::new(fixture.config);
|
||||||
|
let mut frames = encode_multipart(SecretBytes::new(ALICE_SECRET.to_vec()), 128)?;
|
||||||
|
frames.reverse();
|
||||||
|
for frame in frames {
|
||||||
|
importer.add_frame(SecretBytes::new(frame.payload().to_vec()))?;
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
importer.complete.as_ref().map(SecretBytes::expose),
|
||||||
|
Some(ALICE_SECRET)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
importer.key.as_ref().map(|key| key.kind()),
|
||||||
|
Some(MobileKeyTransferKind::Private)
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_frame_export_validates_private_passphrase_and_reconstructs_exactly()
|
||||||
|
-> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let fixture = configured_secret_key()?;
|
||||||
|
let service = MobileKeyTransferService {
|
||||||
|
config: fixture.config.clone(),
|
||||||
|
keys: KeyStore::load(&fixture.key_path)?,
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
service.export(
|
||||||
|
ALICE_FINGERPRINT,
|
||||||
|
MobileKeyTransferKind::Private,
|
||||||
|
Some(SecretBytes::new(b"wrong".to_vec())),
|
||||||
|
),
|
||||||
|
Err(MobileKeyTransferError::IncorrectPassphrase)
|
||||||
|
));
|
||||||
|
let exported = service.export(
|
||||||
|
ALICE_FINGERPRINT,
|
||||||
|
MobileKeyTransferKind::Private,
|
||||||
|
Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec())),
|
||||||
|
)?;
|
||||||
|
assert_eq!(exported.frames().len(), 1);
|
||||||
|
let expected = exported.frames()[0].payload().to_vec();
|
||||||
|
let mut importer = service.importer();
|
||||||
|
let progress = importer.add_frame(SecretBytes::new(expected.clone()))?;
|
||||||
|
assert!(progress.key().is_some());
|
||||||
|
assert_eq!(
|
||||||
|
importer.complete.as_ref().map(SecretBytes::expose),
|
||||||
|
Some(expected.as_slice())
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mismatched_public_keys_are_rejected_as_one_transfer()
|
||||||
|
-> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let fixture = configured_public_key()?;
|
||||||
|
let mut importer = MobileKeyTransferImport::new(fixture.config);
|
||||||
|
let mut mismatched = ALICE_PUBLIC.to_vec();
|
||||||
|
mismatched.extend_from_slice(include_bytes!(
|
||||||
|
"../tests/fixtures/compatibility/keys/bob-public.asc"
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
importer.add_frame(SecretBytes::new(mismatched)),
|
||||||
|
Err(MobileKeyTransferError::MismatchedKeys)
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_duplicate_and_incomplete_frames_do_not_replace_keys()
|
||||||
|
-> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let fixture = configured_public_key()?;
|
||||||
|
let before = std::fs::read(fixture.key_path.join("alice-public.asc"))?;
|
||||||
|
let mut importer = MobileKeyTransferImport::new(fixture.config);
|
||||||
|
let frames = encode_multipart(SecretBytes::new(ALICE_SECRET.to_vec()), 128)?;
|
||||||
|
let first = frames[0].payload();
|
||||||
|
let progress = importer.add_frame(SecretBytes::new(first.to_vec()))?;
|
||||||
|
assert!(
|
||||||
|
importer
|
||||||
|
.add_frame(SecretBytes::new(first.to_vec()))?
|
||||||
|
.duplicate()
|
||||||
|
);
|
||||||
|
assert!(progress.received() < progress.total());
|
||||||
|
assert_eq!(
|
||||||
|
importer.import(None, false),
|
||||||
|
Err(MobileKeyTransferError::IncompleteTransfer)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read(fixture.key_path.join("alice-public.asc"))?,
|
||||||
|
before
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn private_import_validates_passphrase_before_atomic_persistence()
|
||||||
|
-> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let fixture = configured_public_key()?;
|
||||||
|
let mut importer = MobileKeyTransferImport::new(fixture.config.clone());
|
||||||
|
importer.add_frame(SecretBytes::new(ALICE_SECRET.to_vec()))?;
|
||||||
|
assert_eq!(
|
||||||
|
importer.import(Some(SecretBytes::new(b"wrong".to_vec())), true),
|
||||||
|
Err(MobileKeyTransferError::IncorrectPassphrase)
|
||||||
|
);
|
||||||
|
assert_eq!(std::fs::read_dir(&fixture.key_path)?.count(), 1);
|
||||||
|
importer.import(
|
||||||
|
Some(SecretBytes::new(b"fixture-alice-passphrase".to_vec())),
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
let keys = KeyStore::load(&fixture.key_path)?;
|
||||||
|
assert!(keys.infos().any(|key| key.has_secret()));
|
||||||
|
assert_eq!(
|
||||||
|
Config::load(Some(fixture.config.source()))?
|
||||||
|
.default_key()
|
||||||
|
.as_str(),
|
||||||
|
ALICE_FINGERPRINT
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Fixture {
|
||||||
|
_root: tempfile::TempDir,
|
||||||
|
config: Config,
|
||||||
|
key_path: std::path::PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configured_public_key() -> Result<Fixture, Box<dyn std::error::Error>> {
|
||||||
|
configured_key("alice-public.asc", ALICE_PUBLIC)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configured_secret_key() -> Result<Fixture, Box<dyn std::error::Error>> {
|
||||||
|
configured_key("alice-secret.asc", ALICE_SECRET)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configured_key(name: &str, bytes: &[u8]) -> Result<Fixture, Box<dyn std::error::Error>> {
|
||||||
|
let root = tempdir()?;
|
||||||
|
let vault = root.path().join("vault");
|
||||||
|
let keys = root.path().join("keys");
|
||||||
|
std::fs::create_dir(&vault)?;
|
||||||
|
std::fs::create_dir(&keys)?;
|
||||||
|
std::fs::write(keys.join(name), bytes)?;
|
||||||
|
let config_path = root.path().join("config.toml");
|
||||||
|
std::fs::write(
|
||||||
|
&config_path,
|
||||||
|
format!(
|
||||||
|
"vault = {:?}\ndefault_key = {:?}\nkey_material = {:?}\n",
|
||||||
|
vault, ALICE_FINGERPRINT, keys
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
let config = Config::load(Some(&config_path))?;
|
||||||
|
Ok(Fixture {
|
||||||
|
_root: root,
|
||||||
|
config,
|
||||||
|
key_path: keys,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user