diff --git a/EdgeAgentSDK/Apollo/Sources/ApolloImpl+KeyRestoration.swift b/EdgeAgentSDK/Apollo/Sources/ApolloImpl+KeyRestoration.swift index 9c1149f4..519e4173 100644 --- a/EdgeAgentSDK/Apollo/Sources/ApolloImpl+KeyRestoration.swift +++ b/EdgeAgentSDK/Apollo/Sources/ApolloImpl+KeyRestoration.swift @@ -66,10 +66,125 @@ extension ApolloImpl: KeyRestoration { public func restoreKey(_ key: StorableKey) async throws -> Key { switch key.restorationIdentifier { + case "secp256k1+priv": + guard let index = key.index else { + throw ApolloError.restoratonFailedNoIdentifierOrInvalid + } + return Secp256k1PrivateKey( + identifier: key.identifier, + internalKey: .init(raw: key.storableData.toKotlinByteArray()), derivationPath: DerivationPath(index: index) + ) + case "x25519+priv": + return try CreateX25519KeyPairOperation(logger: Self.logger) + .compute( + identifier: key.identifier, + fromPrivateKey: key.storableData + ) + case "ed25519+priv": + return try CreateEd25519KeyPairOperation(logger: Self.logger) + .compute( + identifier: key.identifier, + fromPrivateKey: key.storableData + ) + case "secp256k1+pub": + return Secp256k1PublicKey( + identifier: key.identifier, + internalKey: .init(raw: key.storableData.toKotlinByteArray()) + ) + case "x25519+pub": + return X25519PublicKey( + identifier: key.identifier, + internalKey: .init(raw: key.storableData.toKotlinByteArray()) + ) + case "ed25519+pub": + return Ed25519PublicKey( + identifier: key.identifier, + internalKey: .init(raw: key.storableData.toKotlinByteArray()) + ) case "linkSecret+key": return try LinkSecret(data: key.storableData) default: throw ApolloError.restoratonFailedNoIdentifierOrInvalid } } + + public func restoreKey(_ key: JWK, index: Int?) async throws -> Key { + switch key.kty { + case "EC": + switch key.crv?.lowercased() { + case "secp256k1": + guard + let d = key.d, + let dData = Data(fromBase64URL: d) + else { + guard + let x = key.x, + let y = key.y, + let xData = Data(fromBase64URL: x), + let yData = Data(fromBase64URL: y) + else { + throw ApolloError.invalidJWKError + } + return Secp256k1PublicKey( + identifier: key.kid ?? UUID().uuidString, + internalKey: .init(raw: (xData + yData).toKotlinByteArray()) + ) + } + return Secp256k1PrivateKey( + identifier: key.kid ?? UUID().uuidString, + internalKey: .init(raw: dData.toKotlinByteArray()), derivationPath: DerivationPath(index: index ?? 0) + ) + default: + throw ApolloError.invalidKeyCurve(invalid: key.crv ?? "", valid: ["secp256k1"]) + } + case "OKP": + switch key.crv?.lowercased() { + case "ed25519": + guard + let d = key.d, + let dData = Data(fromBase64URL: d) + else { + guard + let x = key.x, + let xData = Data(fromBase64URL: x) + else { + throw ApolloError.invalidJWKError + } + return Ed25519PublicKey( + identifier: key.kid ?? UUID().uuidString, + internalKey: .init(raw: xData.toKotlinByteArray()) + ) + } + return Ed25519PrivateKey( + identifier: key.kid ?? UUID().uuidString, + internalKey: .init(raw: dData.toKotlinByteArray()) + ) + case "x25519": + guard + let d = key.d, + let dData = Data(fromBase64URL: d) + else { + guard + let x = key.x, + let xData = Data(fromBase64URL: x) + else { + throw ApolloError.invalidJWKError + } + return X25519PublicKey( + identifier: key.kid ?? UUID().uuidString, + internalKey: .init(raw: xData.toKotlinByteArray()) + ) + } + return X25519PrivateKey( + identifier: key.kid ?? UUID().uuidString, + internalKey: .init(raw: dData.toKotlinByteArray()) + ) + default: + throw ApolloError.invalidKeyCurve(invalid: key.crv ?? "", valid: ["ed25519", "x25519"]) + } + default: + throw ApolloError.invalidKeyType(invalid: key.kty, valid: ["EC", "OKP"]) + } + } + } diff --git a/EdgeAgentSDK/Apollo/Sources/ApolloImpl+Public.swift b/EdgeAgentSDK/Apollo/Sources/ApolloImpl+Public.swift index cbc2f367..fcb9937b 100644 --- a/EdgeAgentSDK/Apollo/Sources/ApolloImpl+Public.swift +++ b/EdgeAgentSDK/Apollo/Sources/ApolloImpl+Public.swift @@ -84,6 +84,16 @@ extension ApolloImpl: Apollo { let keyData = Data(base64Encoded: keyStr) { return try CreateEd25519KeyPairOperation(logger: ApolloImpl.logger).compute(fromPrivateKey: keyData) + } else if + let derivationPathStr = parameters[KeyProperties.derivationPath.rawValue], + let seedStr = parameters[KeyProperties.seed.rawValue], + let seed = Data(base64Encoded: seedStr) + { + let derivationPath = try DerivationPath(string: derivationPathStr) + return try CreateEd25519KeyPairOperation(logger: ApolloImpl.logger).compute( + seed: Seed(value: seed), + keyPath: derivationPath + ) } return CreateEd25519KeyPairOperation(logger: ApolloImpl.logger).compute() case .x25519: @@ -92,6 +102,16 @@ extension ApolloImpl: Apollo { let keyData = Data(base64Encoded: keyStr) { return try CreateX25519KeyPairOperation(logger: ApolloImpl.logger).compute(fromPrivateKey: keyData) + } else if + let derivationPathStr = parameters[KeyProperties.derivationPath.rawValue], + let seedStr = parameters[KeyProperties.seed.rawValue], + let seed = Data(base64Encoded: seedStr) + { + let derivationPath = try DerivationPath(string: derivationPathStr) + return try CreateX25519KeyPairOperation(logger: ApolloImpl.logger).compute( + seed: Seed(value: seed), + keyPath: derivationPath + ) } return CreateX25519KeyPairOperation(logger: ApolloImpl.logger).compute() } diff --git a/EdgeAgentSDK/Apollo/Sources/Model/Ed25519Key+Exportable.swift b/EdgeAgentSDK/Apollo/Sources/Model/Ed25519Key+Exportable.swift index 7ee55129..e0b22931 100644 --- a/EdgeAgentSDK/Apollo/Sources/Model/Ed25519Key+Exportable.swift +++ b/EdgeAgentSDK/Apollo/Sources/Model/Ed25519Key+Exportable.swift @@ -12,6 +12,7 @@ extension Ed25519PrivateKey: ExportableKey { var jwk: JWK { JWK( kty: "OKP", + kid: identifier, d: raw.base64UrlEncodedString(), crv: getProperty(.curve)?.capitalized, x: publicKey().raw.base64UrlEncodedString() @@ -40,6 +41,7 @@ extension Ed25519PublicKey: ExportableKey { var jwk: JWK { JWK( kty: "OKP", + kid: identifier, crv: getProperty(.curve)?.capitalized, x: raw.base64UrlEncodedString() ) diff --git a/EdgeAgentSDK/Apollo/Sources/Model/X25519Key+Exportable.swift b/EdgeAgentSDK/Apollo/Sources/Model/X25519Key+Exportable.swift index 4fc73cfc..fc527d14 100644 --- a/EdgeAgentSDK/Apollo/Sources/Model/X25519Key+Exportable.swift +++ b/EdgeAgentSDK/Apollo/Sources/Model/X25519Key+Exportable.swift @@ -12,6 +12,7 @@ extension X25519PrivateKey: ExportableKey { var jwk: JWK { JWK( kty: "OKP", + kid: identifier, d: raw.base64UrlEncodedString(), crv: getProperty(.curve)?.capitalized, x: publicKey().raw.base64UrlEncodedString() @@ -40,6 +41,7 @@ extension X25519PublicKey: ExportableKey { var jwk: JWK { JWK( kty: "OKP", + kid: identifier, crv: getProperty(.curve)?.capitalized, x: raw.base64UrlEncodedString() ) diff --git a/EdgeAgentSDK/Apollo/Sources/Operations/CreateEd25519KeyPairOperation.swift b/EdgeAgentSDK/Apollo/Sources/Operations/CreateEd25519KeyPairOperation.swift index 60a29a0b..79fe560c 100644 --- a/EdgeAgentSDK/Apollo/Sources/Operations/CreateEd25519KeyPairOperation.swift +++ b/EdgeAgentSDK/Apollo/Sources/Operations/CreateEd25519KeyPairOperation.swift @@ -18,4 +18,15 @@ struct CreateEd25519KeyPairOperation { internalKey: KMMEdPrivateKey(raw: fromPrivateKey.toKotlinByteArray()) ) } + + func compute(seed: Seed, keyPath: Domain.DerivationPath) throws -> PrivateKey { + let derivedHdKey = ApolloLibrary + .EdHDKey + .companion + .doInitFromSeed(seed: seed.value.toKotlinByteArray()) + .derive(path: keyPath.keyPathString() + ) + + return Ed25519PrivateKey(internalKey: .init(raw: derivedHdKey.privateKey)) + } } diff --git a/EdgeAgentSDK/Apollo/Sources/Operations/CreateSec256k1KeyPairOperation.swift b/EdgeAgentSDK/Apollo/Sources/Operations/CreateSec256k1KeyPairOperation.swift index 3f26fa7a..6c98b934 100644 --- a/EdgeAgentSDK/Apollo/Sources/Operations/CreateSec256k1KeyPairOperation.swift +++ b/EdgeAgentSDK/Apollo/Sources/Operations/CreateSec256k1KeyPairOperation.swift @@ -14,7 +14,7 @@ struct CreateSec256k1KeyPairOperation { let derivedHdKey = ApolloLibrary.HDKey( seed: seed.value.toKotlinByteArray(), depth: 0, - childIndex: BigIntegerWrapper(int: 0) + childIndex: 0 ).derive(path: keyPath.keyPathString()) return Secp256k1PrivateKey(internalKey: derivedHdKey.getKMMSecp256k1PrivateKey(), derivationPath: keyPath) diff --git a/EdgeAgentSDK/Apollo/Sources/Operations/CreateX25519KeyPairOperation.swift b/EdgeAgentSDK/Apollo/Sources/Operations/CreateX25519KeyPairOperation.swift index a7fe70a8..48de1609 100644 --- a/EdgeAgentSDK/Apollo/Sources/Operations/CreateX25519KeyPairOperation.swift +++ b/EdgeAgentSDK/Apollo/Sources/Operations/CreateX25519KeyPairOperation.swift @@ -20,4 +20,15 @@ struct CreateX25519KeyPairOperation { internalKey: privateKey ) } + + func compute(seed: Seed, keyPath: Domain.DerivationPath) throws -> PrivateKey { + let derivedHdKey = ApolloLibrary + .EdHDKey + .companion + .doInitFromSeed(seed: seed.value.toKotlinByteArray()) + .derive(path: keyPath.keyPathString() + ) + + return X25519PrivateKey(internalKey: KMMEdPrivateKey(raw: derivedHdKey.privateKey).x25519PrivateKey()) + } } diff --git a/EdgeAgentSDK/Builders/Sources/PolluxBuilder.swift b/EdgeAgentSDK/Builders/Sources/PolluxBuilder.swift index 44c29c7c..11746caf 100644 --- a/EdgeAgentSDK/Builders/Sources/PolluxBuilder.swift +++ b/EdgeAgentSDK/Builders/Sources/PolluxBuilder.swift @@ -10,7 +10,7 @@ public struct PolluxBuilder { self.castor = castor } - public func build() -> Pollux { + public func build() -> Pollux & CredentialImporter { PolluxImpl(castor: castor, pluto: pluto) } } diff --git a/EdgeAgentSDK/Domain/Sources/Models/Credentials/ExportableCredential.swift b/EdgeAgentSDK/Domain/Sources/Models/Credentials/ExportableCredential.swift new file mode 100644 index 00000000..7eb81994 --- /dev/null +++ b/EdgeAgentSDK/Domain/Sources/Models/Credentials/ExportableCredential.swift @@ -0,0 +1,59 @@ +import Foundation + +/** + A protocol that defines the requirements for credentials that can be exported. + + This protocol ensures that any credential conforming to it can be serialized into a data format suitable for export and specifies the type of restoration that will be required when the credential is imported back. This is particularly useful for scenarios where credentials need to be securely backed up, transferred, or shared between different systems or platforms. + + - Properties: + - exporting: A `Data` representation of the credential that can be serialized and securely stored or transferred. This data should encapsulate all necessary information to recreate the credential upon import. + - restorationType: A `String` that indicates the method or requirements for restoring the credential from the exported data. This could relate to specific security measures, encryption standards, or data formats that are necessary for the credential's reconstruction. + + Implementers of this protocol must ensure that the `exporting` property effectively captures the credential's state and that the `restorationType` accurately describes the requirements for its restoration. + */ +public protocol ExportableCredential { + var exporting: Data { get } + var restorationType: String { get } +} + +/** + A protocol that defines the functionality required to import credentials from a serialized data format. + + This interface is crucial for scenarios where credentials, previously exported using the `ExportableCredential` protocol, need to be reconstructed or imported back into the system. It allows for the implementation of a customizable import process that can handle various types of credentials and their respective restoration requirements. + + - Method `importCredential`: + - Parameters: + - credentialData: The serialized `Data` representation of the credential to be imported. This data should have been generated by the `exporting` property of an `ExportableCredential`. + - restorationType: A `String` indicating the method or requirements for restoring the credential. This should match the `restorationType` provided during the export process. + - options: An array of `CredentialOperationsOptions` that may modify or provide additional context for the import process, allowing for more flexibility in handling different credential types and scenarios. + - Returns: An asynchronous operation that, upon completion, returns the imported `Credential` object, reconstructed from the provided data. + - Throws: An error if the import process encounters issues, such as data corruption, incompatible restoration types, or if the provided options are not supported. + + Implementers should ensure robust error handling and validation to securely and accurately restore credentials from their exported state. + */ +public protocol CredentialImporter { + func importCredential( + credentialData: Data, + restorationType: String, + options: [CredentialOperationsOptions] + ) async throws -> Credential +} + +/** + Extension to the `Credential` class or struct, providing convenience properties related to the exportability of credentials. + + This extension adds functionality to easily determine whether a credential conforms to the `ExportableCredential` protocol and, if so, obtain its exportable form. This simplifies the process of exporting credentials by abstracting the type checking and casting logic. + + - Properties: + - isExportable: A Boolean value that indicates whether the `Credential` instance conforms to the `ExportableCredential` protocol, thereby supporting export operations. + - exportable: An optional `ExportableCredential` that returns the instance cast to `ExportableCredential` if it conforms to the protocol, or `nil` if it does not. This allows for direct access to the exporting capabilities of the credential without manual type checking or casting. + + These properties enhance the usability of credentials by providing straightforward mechanisms to interact with export-related functionality. + */ +public extension Credential { + /// A Boolean value indicating whether the credential is exportable. + var isExportable: Bool { self is ExportableCredential } + + /// Returns the exportable representation of the credential. + var exportable: ExportableCredential? { self as? ExportableCredential } +} diff --git a/EdgeAgentSDK/Domain/Sources/Models/KeyManagement/KeyRestoration.swift b/EdgeAgentSDK/Domain/Sources/Models/KeyManagement/KeyRestoration.swift index 57553c8c..a66cae0c 100644 --- a/EdgeAgentSDK/Domain/Sources/Models/KeyManagement/KeyRestoration.swift +++ b/EdgeAgentSDK/Domain/Sources/Models/KeyManagement/KeyRestoration.swift @@ -28,25 +28,31 @@ public protocol KeyRestoration { /// Restores a private key from the given data. /// - Parameters: - /// - identifier: An optional string used to identify the key. - /// - data: The raw data representing the key. + /// - key: A storableKey instance. /// - Throws: If the restoration process fails, this method throws an error. /// - Returns: The restored `PrivateKey` instance. func restorePrivateKey(_ key: StorableKey) async throws -> PrivateKey /// Restores a public key from the given data. /// - Parameters: - /// - identifier: An optional string used to identify the key. - /// - data: The raw data representing the key. + /// - key: A storableKey instance. /// - Throws: If the restoration process fails, this method throws an error. /// - Returns: The restored `PublicKey` instance. func restorePublicKey(_ key: StorableKey) async throws -> PublicKey /// Restores a key from the given data. /// - Parameters: - /// - identifier: An optional string used to identify the key. - /// - data: The raw data representing the key. + /// - key: A storableKey instance. /// - Throws: If the restoration process fails, this method throws an error. /// - Returns: The restored `Key` instance. func restoreKey(_ key: StorableKey) async throws -> Key + + /// Restores a key from a JWK. + /// - Parameters: + /// - key: A JWK instance. + /// - index: An Int for the derivation index path. + /// - Throws: If the restoration process fails, this method throws an error. + /// - Returns: The restored `Key` instance. + func restoreKey(_ key: JWK, index: Int?) async throws -> Key + } diff --git a/EdgeAgentSDK/Domain/Sources/Models/Message+Codable.swift b/EdgeAgentSDK/Domain/Sources/Models/Message+Codable.swift index 0569eaed..67d68083 100644 --- a/EdgeAgentSDK/Domain/Sources/Models/Message+Codable.swift +++ b/EdgeAgentSDK/Domain/Sources/Models/Message+Codable.swift @@ -15,6 +15,7 @@ extension Message: Codable { case pthid case ack case body + case direction } public func encode(to encoder: Encoder) throws { @@ -32,6 +33,7 @@ extension Message: Codable { try fromPrior.map { try container.encode($0, forKey: .fromPrior) } try thid.map { try container.encode($0, forKey: .thid) } try pthid.map { try container.encode($0, forKey: .pthid) } + try container.encode(direction, forKey: .direction) } public init(from decoder: Decoder) throws { @@ -49,6 +51,7 @@ extension Message: Codable { let fromPrior = try? container.decodeIfPresent(String.self, forKey: .fromPrior) let thid = try? container.decodeIfPresent(String.self, forKey: .thid) let pthid = try? container.decodeIfPresent(String.self, forKey: .pthid) + let direction = try? container.decodeIfPresent(Direction.self, forKey: .direction) self.init( id: id, @@ -63,7 +66,8 @@ extension Message: Codable { attachments: attachments ?? [], thid: thid, pthid: pthid, - ack: ack ?? [] + ack: ack ?? [], + direction: direction ?? .received ) } } diff --git a/EdgeAgentSDK/Domain/Sources/Models/Message.swift b/EdgeAgentSDK/Domain/Sources/Models/Message.swift index 8c848950..acfcb886 100644 --- a/EdgeAgentSDK/Domain/Sources/Models/Message.swift +++ b/EdgeAgentSDK/Domain/Sources/Models/Message.swift @@ -3,7 +3,7 @@ import Foundation /// The `Message` struct represents a DIDComm message, which is used for secure, decentralized communication in the Atala PRISM architecture. A `Message` object includes information about the sender, recipient, message body, and other metadata. `Message` objects are typically exchanged between DID controllers using the `Mercury` building block. public struct Message: Identifiable, Hashable { /// The direction of the message (sent or received). - public enum Direction: String { + public enum Direction: String, Codable { case sent case received } diff --git a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Backup.swift b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Backup.swift new file mode 100644 index 00000000..00e61588 --- /dev/null +++ b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Backup.swift @@ -0,0 +1,357 @@ +import Domain +import Foundation +import JSONWebEncryption +import JSONWebKey + +extension EdgeAgent { + + struct Backup: Codable { + struct Key: Codable { + let key: String + let did: String? + let recoveryType: String + } + + struct Credential: Codable { + let data: String + let recoveryId: String + } + + struct Pair: Codable { + let holder: String + let receiver: String + let alias: String? + } + + struct DIDs: Codable { + let did: String + let alias: String? + } + + struct Mediator: Codable { + let mediatorDid: String + let holderDid: String + let routingDid: String + } + + let keys: [Key] + let linkSecret: String? + let dids: [DIDs] + let didPairs: [Pair] + let credentials: [Credential] + let messages: [String] + let mediators: [Mediator] + } + + /** + Initiates the backup process for the wallet, encapsulating the wallet's essential data into a secure format. + + This method serializes the current state of the wallet, including keys, credentials, digital identity information (DIDs), message history, and mediator configurations into a structured JSON format. The output is then encrypted into a Json Web Encryption (JWE) format, ensuring the confidentiality and integrity of the backup data. The encryption uses the `secp256k1` elliptic curve digital signature algorithm, specifically the master key derived from the seed path `m'/0'/0'/0'`. This choice of encryption and key derivation ensures that the backup can be securely stored and, crucially, recovered, as long as the original seed used to initialize the wallet is available. + + - Returns: A `String` representation of the encrypted backup in JWE format. This string should be securely stored by the user, as it contains sensitive data crucial for wallet recovery. + + - Throws: An error if the backup process encounters any issues, including problems with data serialization, encryption, or internal wallet state inconsistencies. + + - Note: The backup's security is heavily dependent on the secrecy and strength of the seed used to initialize the wallet. Users must ensure that their seed is kept in a secure location and never shared. + */ + public func backupWallet() async throws -> String { + let backup = Backup( + keys: try await backupKeys(), + linkSecret: try await backupLinkSecret(), + dids: try await backupDIDs(), + didPairs: try await backupDIDPairs(), + credentials: try await backupCredentials(), + messages: try await backupMessages(), + mediators: try await backupMediator() + ) + + let backupData = try JSONEncoder.didComm().encode(backup) + + let masterKey = try self.apollo.createPrivateKey(parameters: [ + KeyProperties.type.rawValue: "EC", + KeyProperties.curve.rawValue: KnownKeyCurves.x25519.rawValue, + KeyProperties.seed.rawValue: seed.value.base64Encoded(), + KeyProperties.derivationPath.rawValue: DerivationPath(index: 0).keyPathString() + ]) + + guard let exporting = masterKey.exporting else { + throw EdgeAgentError.keyIsNotExportable + } + + let jwk = try JSONEncoder.didComm().encode(exporting.jwk) + let jwe = try JWE( + payload: backupData, + keyManagementAlg: .ecdhESA256KW, + encryptionAlgorithm: .a256CBCHS512, + recipientKey: try JSONDecoder().decode(JSONWebKey.JWK.self, from: jwk) + ).compactSerialization() + + return jwe + } + + /** + Recovers the wallet's state from a previously generated backup. + + This method takes an encrypted string in JWE format, which was produced by the `backupWallet` method, and decrypts it using the `secp256k1` master key. This master key is derived from the same seed path `m'/0'/0'/0'` used during the backup process, emphasizing the necessity of initializing the wallet with the same seed to ensure successful recovery. Once decrypted, the method reconstructs the wallet's state, including keys, digital identities (DIDs), credentials, and other stored information, from the JSON payload contained within the JWE. + + - Parameters: + - encrypted: A `String` containing the encrypted backup data in JWE format. This data must be the output of a previous `backupWallet` operation and encrypted with the corresponding master key. + + - Returns: A void promise, indicating the completion of the recovery process. Upon successful completion, the wallet's state is restored to its condition at the time of the backup. + + - Throws: An error if the recovery process fails, which can occur due to issues like incorrect encryption details, failure to decrypt with the provided seed, or corruption of the backup data. + + - Note: The accuracy of the wallet recovery is entirely reliant on the backup being created and encrypted correctly, as well as the wallet being initialized with the same seed used during backup. Users must ensure that the seed and encrypted backup are securely stored and handled. + */ + public func recoverWallet(encrypted: String) async throws { + let masterKey = try self.apollo.createPrivateKey(parameters: [ + KeyProperties.type.rawValue: "EC", + KeyProperties.curve.rawValue: KnownKeyCurves.x25519.rawValue, + KeyProperties.seed.rawValue: seed.value.base64Encoded(), + KeyProperties.derivationPath.rawValue: DerivationPath(index: 0).keyPathString() + ]) + + guard let exporting = masterKey.exporting else { + throw EdgeAgentError.keyIsNotExportable + } + + let jwk = try JSONEncoder.didComm().encode(exporting.jwk) + let backupData = try JWE(compactString: encrypted) + .decrypt(recipientKey: try JSONDecoder.didComm().decode(JSONWebKey.JWK.self, from: jwk)) + + print(try backupData.tryToString()) + + let backup = try JSONDecoder.didComm().decode(Backup.self, from: backupData) + + try await recoverDidsWithKeys(dids: backup.dids, keys: backup.keys) + try await recoverDIDPairs(pairs: backup.didPairs) + try await recoverMessages(messages: backup.messages) + try await recoverCredentials(credentials: backup.credentials) + try await recoverMediators(mediators: backup.mediators) + try await backup.linkSecret.asyncMap { try await recoverLinkSecret(secret: $0) } + } + + func backupKeys() async throws -> [Backup.Key] { + let dids = try await pluto.getAllDIDs() + .first() + .await() + + let backupDIDKeys = try await dids.asyncMap { did in + try await did.privateKeys.asyncCompactMap { key -> Backup.Key? in + guard let keyStr = try await keyToJWK(key: key, restoration: self.apollo) else { + return nil + } + return Backup.Key( + key: keyStr, + did: did.did.string, + recoveryType: key.restorationIdentifier + ) + } + }.flatMap { $0 } + + let backupKeys = try await pluto.getAllKeys() + .first() + .await() + .asyncCompactMap { key -> Backup.Key? in + guard let keyStr = try await keyToJWK(key: key, restoration: self.apollo) else { + return nil + } + return Backup.Key( + key: keyStr, + did: nil, + recoveryType: key.restorationIdentifier + ) + } + return backupKeys + backupDIDKeys + } + + func recoverDidsWithKeys(dids: [Backup.DIDs], keys: [Backup.Key]) async throws { + try await dids.asyncForEach { [weak self] did in + let storableKeys = try await keys + .filter { $0.did == did.did } + .compactMap { + return Data(base64URLEncoded: $0.key) + } + .asyncCompactMap { + guard let self else { + throw UnknownError.somethingWentWrongError(customMessage: nil, underlyingErrors: nil) + } + return try await jwkToKey(key: $0, restoration: self.apollo) + } + + try await self?.pluto.storeDID( + did: DID(string: did.did), + privateKeys: storableKeys, + alias: did.alias + ) + .first() + .await() + } + } + + func recoverDIDPairs(pairs: [Backup.Pair]) async throws { + try await pairs.asyncForEach { [weak self] in + try await self?.pluto.storeDIDPair(pair: .init( + holder: DID(string: $0.holder), + other: DID(string: $0.receiver), + name: $0.alias + )) + .first() + .await() + } + } + + func recoverMessages(messages: [String]) async throws { + let messages = messages.compactMap { messageStr -> (Message, Message.Direction)? in + guard + let messageData = Data(base64URLEncoded: messageStr), + let message = try? JSONDecoder.didComm().decode(Message.self, from: messageData) + else { + return nil + } + + return (message, message.direction) + } + + try await pluto.storeMessages(messages: messages) + .first() + .await() + } + + func recoverCredentials(credentials: [Backup.Credential]) async throws { + let downloader = DownloadDataWithResolver(castor: castor) + let pollux = self.pollux + return try await credentials + .asyncCompactMap { bakCredential -> StorableCredential? in + guard + let data = Data(base64URLEncoded: bakCredential.data) + else { + return nil + } + return try? await pollux.importCredential( + credentialData: data, + restorationType: bakCredential.recoveryId, + options: [ + .credentialDefinitionDownloader(downloader: downloader), + .schemaDownloader(downloader: downloader) + ] + ).storable + } + .asyncForEach { [weak self] in + try await self?.pluto.storeCredential(credential: $0).first().await() + } + } + + func recoverMediators(mediators: [Backup.Mediator]) async throws { + try await mediators.asyncForEach { [weak self] in + try await self?.pluto.storeMediator( + peer: DID(string: $0.holderDid), + routingDID: DID(string: $0.routingDid), + mediatorDID: DID(string: $0.mediatorDid) + ) + .first() + .await() + } + } + + func recoverLinkSecret(secret: String) async throws { + struct LinkSecretStorableKey: StorableKey { + var identifier = "linkSecret" + let index: Int? = nil + let storableData: Data + let restorationIdentifier = "linkSecret+key" + } + + try await pluto.storeLinkSecret(secret: LinkSecretStorableKey(storableData: try secret.tryToData())) + .first() + .await() + } + + func backupDIDs() async throws -> [Backup.DIDs] { + let dids = try await pluto.getAllDIDs() + .first() + .await() + + return dids.map { + return Backup.DIDs( + did: $0.did.string, + alias: $0.alias + ) + } + } + + func backupDIDPairs() async throws -> [Backup.Pair] { + return try await pluto.getAllDidPairs() + .first() + .await() + .map { + Backup.Pair(holder: $0.holder.string, receiver: $0.other.string, alias: $0.name) + } + } + + func backupLinkSecret() async throws -> String { + guard let linkSecret = try await pluto.getLinkSecret().first().await()?.storableData.tryToString() else { + throw EdgeAgentError.noLinkSecretConfigured + } + return linkSecret + } + + func backupCredentials() async throws -> [Backup.Credential] { + let pollux = self.pollux + return try await pluto + .getAllCredentials() + .tryMap { + $0.compactMap { + try? pollux.restoreCredential( + restorationIdentifier: $0.recoveryId, + credentialData: $0.credentialData + ) + }.compactMap { $0.exportable } + } + .first() + .await() + .map { + Backup.Credential( + data: $0.exporting.base64UrlEncodedString(), + recoveryId: $0.restorationType + ) + } + + } + + func backupMessages() async throws -> [String] { + try await pluto.getAllMessages() + .first() + .await() + .compactMap { + try JSONEncoder.didComm().encode($0).base64UrlEncodedString() + } + } + + func backupMediator() async throws -> [Backup.Mediator] { + try await pluto.getAllMediators() + .first() + .await() + .map { + Backup.Mediator( + mediatorDid: $0.mediatorDID.string, + holderDid: $0.did.string, + routingDid: $0.routingDID.string + ) + } + } +} + +private func keyToJWK(key: StorableKey, restoration: KeyRestoration) async throws -> String? { + let key = try await restoration.restoreKey(key) + guard let exportable = key.exporting else { + return nil + } + return try JSONEncoder().encode(exportable.jwk).base64UrlEncodedString() +} + +private func jwkToKey(key: Data, restoration: KeyRestoration) async throws -> StorableKey? { + let jwk = try JSONDecoder().decode(Domain.JWK.self, from: key) + let key = try await restoration.restoreKey(jwk, index: nil) + return key.storable +} diff --git a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent.swift b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent.swift index 35a4c43d..95e004e6 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent.swift @@ -35,7 +35,7 @@ public class EdgeAgent { let apollo: Apollo & KeyRestoration let castor: Castor let pluto: Pluto - let pollux: Pollux + let pollux: Pollux & CredentialImporter let mercury: Mercury var mediationHandler: MediatorHandler? @@ -62,7 +62,7 @@ public class EdgeAgent { apollo: Apollo & KeyRestoration, castor: Castor, pluto: Pluto, - pollux: Pollux, + pollux: Pollux & CredentialImporter, mercury: Mercury, mediationHandler: MediatorHandler? = nil, seed: Seed? = nil diff --git a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgentErrors.swift b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgentErrors.swift index 7a092007..2a7d51be 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgentErrors.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgentErrors.swift @@ -33,6 +33,13 @@ public enum EdgeAgentError: KnownPrismError { /// An error case representing an invalid attachment format. case invalidAttachmentFormat(String?) + /// An error case representing that a key requires to conform to the Exportable protocol. + case keyIsNotExportable + + /// An error case representing that the wallet was not initialized and a Link secret is not set yet. + case noLinkSecretConfigured + + /// The error code returned by the server. public var code: Int { switch self { @@ -52,6 +59,11 @@ public enum EdgeAgentError: KnownPrismError { return 117 case .invalidAttachmentFormat: return 118 + case .keyIsNotExportable: + return 117 + case .noLinkSecretConfigured: + return 118 + } } @@ -90,6 +102,10 @@ You need to provide a mediation handler and start the prism agent before doing s return "Credential doesnt conform with proof protocol" case .invalidAttachmentFormat(let format): return "Attachment format is not supported \(format ?? "")" + case .keyIsNotExportable: + return "The key requires to conform to the Exportable protocol" + case .noLinkSecretConfigured: + return "The link secret was not initialized, please run start() once" } } } diff --git a/EdgeAgentSDK/EdgeAgent/Tests/AnoncredsPresentationFlowTest.swift b/EdgeAgentSDK/EdgeAgent/Tests/AnoncredsPresentationFlowTest.swift index 2278d57d..592ad376 100644 --- a/EdgeAgentSDK/EdgeAgent/Tests/AnoncredsPresentationFlowTest.swift +++ b/EdgeAgentSDK/EdgeAgent/Tests/AnoncredsPresentationFlowTest.swift @@ -11,7 +11,7 @@ final class AnoncredsPresentationFlowTest: XCTestCase { var apollo: Apollo & KeyRestoration = ApolloBuilder().build() var pluto = MockPluto() var castor: Castor! - var pollux: Pollux! + var pollux: (Pollux & CredentialImporter)! var mercury = MercuryStub() var edgeAgent: EdgeAgent! var linkSecret: Key! diff --git a/EdgeAgentSDK/EdgeAgent/Tests/BackupWalletTests.swift b/EdgeAgentSDK/EdgeAgent/Tests/BackupWalletTests.swift new file mode 100644 index 00000000..b805d8c7 --- /dev/null +++ b/EdgeAgentSDK/EdgeAgent/Tests/BackupWalletTests.swift @@ -0,0 +1,63 @@ +import Apollo +import Castor +import Domain +import Pluto +import Pollux +@testable import EdgeAgent +import XCTest + +final class BackupWalletTests: XCTestCase { + let seed = { + let apollo = ApolloImpl() + return try! apollo.createSeed(mnemonics: ["pig", "fork", "educate", "gun", "entire", "scatter", "satoshi", "laugh", "project", "buffalo", "race", "enroll", "shiver", "theme", "similar", "thought", "prepare", "velvet", "wild", "mention", "jelly", "match", "document", "rapid"], passphrase: "") + }() + + func createAgent() throws -> (EdgeAgent, MockPluto) { + let apollo = ApolloImpl() + let castor = CastorImpl(apollo: apollo) + let pluto = MockPluto() + let pollux = MockPollux() + let agent = EdgeAgent( + apollo: apollo, + castor: castor, + pluto: pluto, + pollux: pollux, + mercury: MercuryStub(), + seed: seed + ) + return (agent, pluto) + } + + func testBackup() async throws { + let (backupAgent, backupPluto) = try createAgent() + _ = try await backupAgent.createNewPeerDID(updateMediator: false) + _ = try await backupAgent.createNewPrismDID() + + backupPluto.didPairs = [ + .init( + holder: .init(method: "peer", methodId: "alice"), + other: .init(method: "peer", methodId: "bob"), + name: "test" + ) + ] + + let mockedCredential = MockCredential(exporting: Data(count: 10), restorationType: "mock") + backupPluto.credentials = [mockedCredential] + + backupPluto.messages = [Message(piuri: "mock", body: Data(count: 20))] + backupPluto.mediators = [(.init(method: "peer", methodId: "holder"), .init(method: "peer", methodId: "mediator"),.init(method: "peer", methodId: "routing"))] + backupPluto.linkSecret = try ApolloImpl().createNewLinkSecret().storable! + + // Ask for backup + let str = try await backupAgent.backupWallet() + let (receivingAgent, receivingPluto) = try createAgent() + try await receivingAgent.recoverWallet(encrypted: str) + + XCTAssertEqual(backupPluto.dids.count, receivingPluto.dids.count) + XCTAssertEqual(backupPluto.credentials.count, receivingPluto.credentials.count) + XCTAssertEqual(backupPluto.didPairs.count, receivingPluto.didPairs.count) + XCTAssertEqual(backupPluto.messages.count, receivingPluto.messages.count) + XCTAssertEqual(backupPluto.mediators.count, receivingPluto.mediators.count) + XCTAssertNotNil(receivingPluto.linkSecret) + } +} diff --git a/EdgeAgentSDK/EdgeAgent/Tests/Helper/MockKeychain.swift b/EdgeAgentSDK/EdgeAgent/Tests/Helper/MockKeychain.swift new file mode 100644 index 00000000..3750e43b --- /dev/null +++ b/EdgeAgentSDK/EdgeAgent/Tests/Helper/MockKeychain.swift @@ -0,0 +1,28 @@ +import Domain +import Foundation +@testable import Pluto + +class KeychainMock: KeychainStore, KeychainProvider { + var keys: [String: KeychainStorableKey] = [:] + + func getKey( + service: String, + account: String, + tag: String?, + algorithm: KeychainStorableKeyProperties.KeyAlgorithm, + type: KeychainStorableKeyProperties.KeyType + ) throws -> Data { + guard let key = keys[service+account] else { + throw PlutoError.errorRetrivingKeyFromKeychainKeyNotFound(service: service, account: account, applicationLabel: tag) + } + return key.storableData + } + + func addKey( + _ key: KeychainStorableKey, + service: String, + account: String + ) throws { + keys[service+account] = key + } +} diff --git a/EdgeAgentSDK/EdgeAgent/Tests/Helper/MockPluto.swift b/EdgeAgentSDK/EdgeAgent/Tests/Helper/MockPluto.swift index 1d6173c8..d2dec7c3 100644 --- a/EdgeAgentSDK/EdgeAgent/Tests/Helper/MockPluto.swift +++ b/EdgeAgentSDK/EdgeAgent/Tests/Helper/MockPluto.swift @@ -4,6 +4,9 @@ import Domain import Foundation class MockPluto: Pluto { + var dids = [(did: DID, privateKeys: [StorableKey], alias: String?)]() + var didPairs = [DIDPair]() + var mediators = [(peer: Domain.DID, routingDID: Domain.DID, mediatorDID: Domain.DID)]() var linkSecret: StorableKey? var messages = [Message]() var credentials = [StorableCredential]() @@ -14,16 +17,19 @@ class MockPluto: Pluto { } func storePeerDID(did: Domain.DID, privateKeys: [Domain.StorableKey], alias: String?) -> AnyPublisher { - Just(()).tryMap { $0 }.eraseToAnyPublisher() + dids.append((did, privateKeys, alias)) + return Just(()).tryMap { $0 }.eraseToAnyPublisher() } func storeDID(did: Domain.DID, privateKeys: [Domain.StorableKey], alias: String?) -> AnyPublisher { + dids.append((did, privateKeys, alias)) keys[did.string] = privateKeys return Just(()).tryMap { $0 }.eraseToAnyPublisher() } func storeDIDPair(pair: Domain.DIDPair) -> AnyPublisher { - Just(()).tryMap { $0 }.eraseToAnyPublisher() + didPairs.append(pair) + return Just(()).tryMap { $0 }.eraseToAnyPublisher() } func storeMessage(message: Domain.Message, direction: Domain.Message.Direction) -> AnyPublisher { @@ -37,7 +43,8 @@ class MockPluto: Pluto { } func storeMediator(peer: Domain.DID, routingDID: Domain.DID, mediatorDID: Domain.DID) -> AnyPublisher { - Just(()).tryMap { $0 }.eraseToAnyPublisher() + self.mediators = self.mediators + [(peer, routingDID, mediatorDID)] + return Just(()).tryMap { $0 }.eraseToAnyPublisher() } func storeCredential(credential: Domain.StorableCredential) -> AnyPublisher { @@ -87,7 +94,7 @@ class MockPluto: Pluto { } func getAllDIDs() -> AnyPublisher<[(did: Domain.DID, privateKeys: [Domain.StorableKey], alias: String?)], Error> { - Just([]).tryMap { $0 }.eraseToAnyPublisher() + Just(dids).tryMap { $0 }.eraseToAnyPublisher() } func getDIDInfo(did: Domain.DID) -> AnyPublisher<(did: Domain.DID, privateKeys: [Domain.StorableKey], alias: String?)?, Error> { @@ -111,7 +118,7 @@ class MockPluto: Pluto { } func getAllDidPairs() -> AnyPublisher<[Domain.DIDPair], Error> { - Just([]).tryMap { $0 }.eraseToAnyPublisher() + Just(didPairs).tryMap { $0 }.eraseToAnyPublisher() } func getPair(otherDID: Domain.DID) -> AnyPublisher { @@ -163,7 +170,7 @@ class MockPluto: Pluto { } func getAllMediators() -> AnyPublisher<[(did: Domain.DID, routingDID: Domain.DID, mediatorDID: Domain.DID)], Error> { - Just([]).tryMap { $0 }.eraseToAnyPublisher() + Just(mediators).tryMap { $0.map { ($0.peer, $0.routingDID, $0.mediatorDID) } }.eraseToAnyPublisher() } func getAllCredentials() -> AnyPublisher<[Domain.StorableCredential], Error> { diff --git a/EdgeAgentSDK/EdgeAgent/Tests/Helper/MockPollux.swift b/EdgeAgentSDK/EdgeAgent/Tests/Helper/MockPollux.swift new file mode 100644 index 00000000..3fd0f109 --- /dev/null +++ b/EdgeAgentSDK/EdgeAgent/Tests/Helper/MockPollux.swift @@ -0,0 +1,60 @@ +import Domain +import Foundation + +struct MockCredential: Credential, StorableCredential, ExportableCredential { + var storingId: String = "" + var recoveryId: String = "" + var credentialData: Data = Data() + var queryIssuer: String? = nil + var querySubject: String? = nil + var queryCredentialCreated: Date? = nil + var queryCredentialUpdated: Date? = nil + var queryCredentialSchema: String? = nil + var queryValidUntil: Date? = nil + var queryRevoked: Bool? = nil + var queryAvailableClaims: [String] = [] + var id: String = "" + var issuer: String = "" + var subject: String? = nil + var claims: [Domain.Claim] = [] + var properties: [String : Any] = [:] + var credentialType: String = "" + var index: Int? = nil + var exporting: Data + var restorationType: String +} + +struct MockPollux: Pollux & CredentialImporter { + func importCredential( + credentialData: Data, + restorationType: String, + options: [CredentialOperationsOptions] + ) async throws -> Credential { + return MockCredential(exporting: Data(count: 5), restorationType: "mocked") + } + func restoreCredential(restorationIdentifier: String, credentialData: Data) throws -> Domain.Credential { + return MockCredential(exporting: Data(count: 5), restorationType: "mocked") + } + + func parseCredential(issuedCredential: Domain.Message, options: [Domain.CredentialOperationsOptions]) async throws -> Domain.Credential { + return MockCredential(exporting: Data(count: 5), restorationType: "mocked") + } + + func processCredentialRequest(offerMessage: Domain.Message, options: [Domain.CredentialOperationsOptions]) async throws -> String { + "" + } + + func createPresentationRequest( + type: Domain.CredentialType, + toDID: Domain.DID, + name: String, + version: String, + claimFilters: [Domain.ClaimFilter] + ) throws -> Data { + Data() + } + + func verifyPresentation(message: Domain.Message, options: [Domain.CredentialOperationsOptions]) async throws -> Bool { + false + } +} diff --git a/EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift b/EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift index 2a5c23a0..30750b5c 100644 --- a/EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift +++ b/EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift @@ -12,7 +12,7 @@ final class PresentationExchangeFlowTests: XCTestCase { var apollo: Apollo & KeyRestoration = ApolloBuilder().build() var pluto = MockPluto() var castor: Castor! - var pollux: Pollux! + var pollux: (Pollux & CredentialImporter)! var mercury = MercuryStub() var edgeAgent: EdgeAgent! let logger = Logger(label: "presentation_exchange_test") diff --git a/EdgeAgentSDK/Pluto/Sources/PersistentStorage/DAO/CDMessageDAO+MessageStore.swift b/EdgeAgentSDK/Pluto/Sources/PersistentStorage/DAO/CDMessageDAO+MessageStore.swift index f4bb9ebb..1e4129e1 100644 --- a/EdgeAgentSDK/Pluto/Sources/PersistentStorage/DAO/CDMessageDAO+MessageStore.swift +++ b/EdgeAgentSDK/Pluto/Sources/PersistentStorage/DAO/CDMessageDAO+MessageStore.swift @@ -6,7 +6,11 @@ extension CDMessageDAO: MessageStore { func addMessages(messages: [(Message, Message.Direction)]) -> AnyPublisher { messages .publisher - .flatMap { self.addMessage(msg: $0.0, direction: $0.1) } + .flatMap { + self.addMessage(msg: $0.0, direction: $0.1) + } + .collect() + .map { _ in () } .eraseToAnyPublisher() } diff --git a/EdgeAgentSDK/Pluto/Tests/Helper/KeyRestoration+Test.swift b/EdgeAgentSDK/Pluto/Tests/Helper/KeyRestoration+Test.swift index 8ee86fd8..04cfb0ef 100644 --- a/EdgeAgentSDK/Pluto/Tests/Helper/KeyRestoration+Test.swift +++ b/EdgeAgentSDK/Pluto/Tests/Helper/KeyRestoration+Test.swift @@ -25,4 +25,8 @@ struct MockKeyRestoration: KeyRestoration { func restoreKey(_ key: StorableKey) async throws -> Key { MockPublicKey(raw: key.storableData) } + + func restoreKey(_ key: JWK, index: Int?) async throws -> Key { + MockPublicKey(raw: Data()) + } } diff --git a/EdgeAgentSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack+ExportableCredential.swift b/EdgeAgentSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack+ExportableCredential.swift new file mode 100644 index 00000000..d4783e5d --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack+ExportableCredential.swift @@ -0,0 +1,8 @@ +import Domain +import Foundation + +extension AnoncredsCredentialStack: ExportableCredential { + var exporting: Data { (try? JSONEncoder().encode(credential)) ?? Data() } + + var restorationType: String { "anoncred" } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Models/AnonCreds/JWTCredential+ExportableCredential.swift b/EdgeAgentSDK/Pollux/Sources/Models/AnonCreds/JWTCredential+ExportableCredential.swift new file mode 100644 index 00000000..e1605b20 --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Models/AnonCreds/JWTCredential+ExportableCredential.swift @@ -0,0 +1,10 @@ +import Domain +import Foundation + +extension JWTCredential: ExportableCredential { + public var exporting: Data { + (try? jwtString.tryToData()) ?? Data() + } + + public var restorationType: String { "jwt" } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Operation/Anoncreds/CreateAnoncredCredentialRequest.swift b/EdgeAgentSDK/Pollux/Sources/Operation/Anoncreds/CreateAnoncredCredentialRequest.swift index e6736c47..2e711485 100644 --- a/EdgeAgentSDK/Pollux/Sources/Operation/Anoncreds/CreateAnoncredCredentialRequest.swift +++ b/EdgeAgentSDK/Pollux/Sources/Operation/Anoncreds/CreateAnoncredCredentialRequest.swift @@ -37,7 +37,7 @@ struct CreateAnoncredCredentialRequest { pluto: Pluto ) async throws -> String { let linkSecretObj = try LinkSecret.newFromValue(valueString: linkSecret) - let offer = try CredentialOffer(jsonString: String(data: offerData, encoding: .utf8)!) + let offer = try CredentialOffer(jsonString: offerData.tryToString()) let credDefId = offer.getCredDefId() let credentialDefinitionData = try await credentialDefinitionDownloader.downloadFromEndpoint(urlOrDID: credDefId) diff --git a/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialImporter.swift b/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialImporter.swift new file mode 100644 index 00000000..3ddc449e --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialImporter.swift @@ -0,0 +1,54 @@ +import Domain +import Foundation + +extension PolluxImpl: CredentialImporter { + public func importCredential(credentialData: Data, restorationType: String, options: [CredentialOperationsOptions]) async throws -> Credential { + switch restorationType { + case "anoncred": + guard + let credDefinitionDownloaderOption = options.first(where: { + if case .credentialDefinitionDownloader = $0 { return true } + return false + }), + case let CredentialOperationsOptions.credentialDefinitionDownloader(defDownloader) = credDefinitionDownloaderOption + else { + throw PolluxError.invalidPrismDID + } + guard + let credSchemaDownloaderOption = options.first(where: { + if case .schemaDownloader = $0 { return true } + return false + }), + case let CredentialOperationsOptions.schemaDownloader(schemaDownloader) = credSchemaDownloaderOption + else { + throw PolluxError.invalidPrismDID + } + return try await importAnoncredCredential( + credentialData: credentialData, + credentialDefinitionDownloader: defDownloader, + schemaDownloader: schemaDownloader + ) + case "jwt": + return try JWTCredential(data: credentialData) + default: + throw PolluxError.invalidCredentialError + } + } +} + +private func importAnoncredCredential( + credentialData: Data, + credentialDefinitionDownloader: Downloader, + schemaDownloader: Downloader +) async throws -> Credential { + let domainCred = try JSONDecoder().decode(AnonCredential.self, from: credentialData) + let credentialDefinitionData = try await credentialDefinitionDownloader + .downloadFromEndpoint(urlOrDID: domainCred.credentialDefinitionId) + let schemaData = try await schemaDownloader + .downloadFromEndpoint(urlOrDID: domainCred.schemaId) + return AnoncredsCredentialStack( + schema: try? JSONDecoder.didComm().decode(AnonCredentialSchema.self, from: schemaData), + definition: try JSONDecoder.didComm().decode(AnonCredentialDefinition.self, from: credentialDefinitionData), + credential: domainCred + ) +} diff --git a/Package.swift b/Package.swift index be6c7b74..85eede98 100644 --- a/Package.swift +++ b/Package.swift @@ -60,7 +60,7 @@ let package = Package( .package(url: "https://github.com/beatt83/jose-swift.git", from: "2.2.2"), .package(url: "https://github.com/beatt83/peerdid-swift.git", from: "2.0.2"), .package(url: "https://github.com/input-output-hk/anoncreds-rs.git", exact: "0.4.1"), - .package(url: "https://github.com/input-output-hk/atala-prism-apollo.git", exact: "1.2.13"), + .package(url: "https://github.com/input-output-hk/atala-prism-apollo.git", exact: "1.3.3"), .package(url: "https://github.com/KittyMac/Sextant.git", exact: "0.4.31"), .package(url: "https://github.com/kylef/JSONSchema.swift.git", exact: "0.6.0") ],