diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/FFI/KeychainSigner.swift b/packages/swift-sdk/Sources/SwiftDashSDK/FFI/KeychainSigner.swift index 3064c0d7069..7cf64398021 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/FFI/KeychainSigner.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/FFI/KeychainSigner.swift @@ -706,77 +706,29 @@ public final class KeychainSigner: Signer, @unchecked Sendable { return .success(signature) } - /// v1 sign primitive. Wraps the raw 32-byte ECDSA scalar in a - /// throwaway FFI signer just long enough to produce a signature; - /// the keychain bytes are zeroed from the local copy as soon as - /// the FFI call returns. + /// v1 sign primitive. Delegates to `RawKeySigner.sign` (the shared + /// create-signer → sign → destroy round-trip; the key copy is zeroed + /// there), mapping its typed errors onto this signer's error space. /// - /// TODO(KeychainSigner v2): replace this whole function with a - /// native-Swift `secp256k1` invocation once we add the + /// TODO(KeychainSigner v2): replace `RawKeySigner`'s FFI round-trip + /// with a native-Swift `secp256k1` invocation once we add the /// `swift-secp256k1` SPM dep. fileprivate func ffiSign( privateKey: Data, data: Data ) -> Result { - // Defensive copy into a mutable buffer we can zero on exit. - var keyCopy = [UInt8](privateKey) - defer { - // Best-effort scrub. Swift doesn't guarantee this won't be - // optimised away, but an explicit `withContiguousMutableStorageIfAvailable` - // pattern does keep the touch in the IR. - keyCopy.withUnsafeMutableBufferPointer { buf in - if let base = buf.baseAddress { - memset_s(UnsafeMutableRawPointer(base), buf.count, 0, buf.count) - } - } - } - - let signerResult = keyCopy.withUnsafeBufferPointer { keyBuf -> DashSDKResult in - dash_sdk_signer_create_from_private_key( - keyBuf.baseAddress!, - UInt(keyBuf.count), - self.network.ffiValue - ) - } - - if let errPtr = signerResult.error { - let message = errPtr.pointee.message.map { String(cString: $0) } ?? "unknown" - dash_sdk_error_free(errPtr) + do { + return .success( + try RawKeySigner.sign(data: data, privateKey: privateKey, network: self.network)) + } catch KeyManagerError.signerCreationFailed(let message) { return .failure(.ffiSignerCreationFailed(message: message)) - } - guard let rawSigner = signerResult.data else { - return .failure(.ffiSignerCreationFailed(message: "null handle")) - } - let signerHandle = OpaquePointer(rawSigner) - defer { dash_sdk_signer_destroy(signerHandle) } - - let signResult = data.withUnsafeBytes { dataBuf -> DashSDKResult in - dash_sdk_signer_sign( - signerHandle, - dataBuf.bindMemory(to: UInt8.self).baseAddress, - UInt(dataBuf.count) - ) - } - - if let errPtr = signResult.error { - let message = errPtr.pointee.message.map { String(cString: $0) } ?? "unknown" - dash_sdk_error_free(errPtr) + } catch KeyManagerError.invalidKeyFormat(let message) { + return .failure(.ffiSignerCreationFailed(message: "invalid key format: \(message)")) + } catch KeyManagerError.signingFailed(let message) { return .failure(.ffiSignFailed(message: message)) + } catch { + return .failure(.ffiSignFailed(message: String(describing: error))) } - guard let sigPtr = signResult.data else { - return .failure(.ffiSignFailed(message: "null signature")) - } - - let sigStruct = sigPtr.assumingMemoryBound(to: DashSDKSignature.self) - defer { dash_sdk_signature_free(sigStruct) } - - let sigBytes: Data - if let bytes = sigStruct.pointee.signature { - sigBytes = Data(bytes: bytes, count: Int(sigStruct.pointee.signature_len)) - } else { - sigBytes = Data() - } - return .success(sigBytes) } // MARK: - Signer protocol conformance (legacy) diff --git a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyManager.swift b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyManager.swift index 7eb83a0bdc6..28a1a098370 100644 --- a/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyManager.swift +++ b/packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/KeyManager.swift @@ -9,6 +9,7 @@ public enum KeyManagerError: LocalizedError, Sendable { case privateKeyNotFound(KeyID) case invalidKeyFormat(String) case signerCreationFailed(String) + case signingFailed(String) case keychainError(String) case noSuitableKey(String) @@ -22,6 +23,8 @@ public enum KeyManagerError: LocalizedError, Sendable { return "Invalid key format: \(message)" case .signerCreationFailed(let message): return "Failed to create signer: \(message)" + case .signingFailed(let message): + return "Failed to sign: \(message)" case .keychainError(let message): return "Keychain error: \(message)" case .noSuitableKey(let message): @@ -30,6 +33,92 @@ public enum KeyManagerError: LocalizedError, Sendable { } } +// MARK: - Raw-key one-shot signing + +/// One-shot ECDSA signing with a raw 32-byte private key. +/// +/// The `dash_sdk_signer_create_from_private_key` → `dash_sdk_signer_sign` +/// → `dash_sdk_signer_destroy` FFI round-trip, extracted from +/// `KeychainSigner` so callers that already hold key material (e.g. a +/// derivation-path lookup) can sign without constructing a +/// `KeychainSigner`/`KeyManager` stack. `KeychainSigner.ffiSign` +/// delegates here — keep the two in sync by keeping this the only +/// implementation. +/// +/// The FFI double-SHA256es `data` before signing +/// (`dashcore::signer::sign`) and returns the 65-byte compact +/// recoverable signature — recovery header byte first, compressed-key +/// range 31–34 — i.e. the Dash Core `signmessage` wire format. +public enum RawKeySigner { + /// - Parameters: + /// - data: Raw bytes to sign. Hashed (SHA256d) inside the FFI — + /// pass the full message, never a pre-computed digest. + /// - privateKey: 32-byte ECDSA scalar. A local copy is zeroed + /// before returning. + /// - network: Affects WIF/address metadata inside the signer only, + /// not the signature bytes. + public static func sign(data: Data, privateKey: Data, network: Network) throws -> Data { + guard privateKey.count == 32 else { + throw KeyManagerError.invalidKeyFormat( + "Private key must be 32 bytes, got \(privateKey.count)") + } + + // Defensive copy into a mutable buffer we can zero on exit. + var keyCopy = [UInt8](privateKey) + defer { + keyCopy.withUnsafeMutableBufferPointer { buf in + if let base = buf.baseAddress { + memset_s(UnsafeMutableRawPointer(base), buf.count, 0, buf.count) + } + } + } + + let signerResult = keyCopy.withUnsafeBufferPointer { keyBuf -> DashSDKResult in + dash_sdk_signer_create_from_private_key( + keyBuf.baseAddress!, + UInt(keyBuf.count), + network.ffiValue + ) + } + + if let errPtr = signerResult.error { + let message = errPtr.pointee.message.map { String(cString: $0) } ?? "unknown" + dash_sdk_error_free(errPtr) + throw KeyManagerError.signerCreationFailed(message) + } + guard let rawSigner = signerResult.data else { + throw KeyManagerError.signerCreationFailed("null handle") + } + let signerHandle = OpaquePointer(rawSigner) + defer { dash_sdk_signer_destroy(signerHandle) } + + let signResult = data.withUnsafeBytes { dataBuf -> DashSDKResult in + dash_sdk_signer_sign( + signerHandle, + dataBuf.bindMemory(to: UInt8.self).baseAddress, + UInt(dataBuf.count) + ) + } + + if let errPtr = signResult.error { + let message = errPtr.pointee.message.map { String(cString: $0) } ?? "unknown" + dash_sdk_error_free(errPtr) + throw KeyManagerError.signingFailed(message) + } + guard let sigPtr = signResult.data else { + throw KeyManagerError.signingFailed("null signature") + } + + let sigStruct = sigPtr.assumingMemoryBound(to: DashSDKSignature.self) + defer { dash_sdk_signature_free(sigStruct) } + + guard let bytes = sigStruct.pointee.signature else { + throw KeyManagerError.signingFailed("empty signature") + } + return Data(bytes: bytes, count: Int(sigStruct.pointee.signature_len)) + } +} + // MARK: - Key Manager /// Centralized key management for Dash Platform identities.