Skip to content

Commit

Permalink
feat(backup): allow backup and import of prism wallet
Browse files Browse the repository at this point in the history
This will enable backup of a wallet to another wallet given the seed is the same.
It adds a protocol to enable credential exports.

Fixes ATL-6610
  • Loading branch information
goncalo-frade-iohk committed Mar 12, 2024
1 parent 558164f commit 8cd2ebc
Show file tree
Hide file tree
Showing 23 changed files with 927 additions and 15 deletions.
114 changes: 114 additions & 0 deletions AtalaPrismSDK/Apollo/Sources/ApolloImpl+KeyRestoration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,124 @@ 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"])
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -40,6 +41,7 @@ extension Ed25519PublicKey: ExportableKey {
var jwk: JWK {
JWK(
kty: "OKP",
kid: identifier,
crv: getProperty(.curve)?.capitalized,
x: raw.base64UrlEncodedString()
)
Expand Down
2 changes: 2 additions & 0 deletions AtalaPrismSDK/Apollo/Sources/Model/X25519Key+Exportable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -40,6 +41,7 @@ extension X25519PublicKey: ExportableKey {
var jwk: JWK {
JWK(
kty: "OKP",
kid: identifier,
crv: getProperty(.curve)?.capitalized,
x: raw.base64UrlEncodedString()
)
Expand Down
2 changes: 1 addition & 1 deletion AtalaPrismSDK/Builders/Sources/PolluxBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public struct PolluxBuilder {
self.pluto = pluto
}

public func build() -> Pollux {
public func build() -> Pollux & CredentialImporter {
PolluxImpl(pluto: pluto)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation

public protocol ExportableCredential {
var exporting: Data { get }
var restorationType: String { get }
}

public protocol CredentialImporter {
func importCredential(
credentialData: Data,
restorationType: String,
options: [CredentialOperationsOptions]
) async throws -> Credential
}

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 }
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,30 @@ 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
}
6 changes: 5 additions & 1 deletion AtalaPrismSDK/Domain/Sources/Models/Message+Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ extension Message: Codable {
case pthid
case ack
case body
case direction
}

public func encode(to encoder: Encoder) throws {
Expand All @@ -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 {
Expand All @@ -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,
Expand All @@ -63,7 +66,8 @@ extension Message: Codable {
attachments: attachments ?? [],
thid: thid,
pthid: pthid,
ack: ack ?? []
ack: ack ?? [],
direction: direction ?? .received
)
}
}
2 changes: 1 addition & 1 deletion AtalaPrismSDK/Domain/Sources/Models/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ extension CDMessageDAO: MessageStore {
func addMessages(messages: [(Message, Message.Direction)]) -> AnyPublisher<Void, Error> {
messages
.publisher
.flatMap { self.addMessage(msg: $0.0, direction: $0.1) }
.flatMap {
self.addMessage(msg: $0.0, direction: $0.1)
}
.collect()
.map { _ in () }
.eraseToAnyPublisher()
}

Expand Down
4 changes: 4 additions & 0 deletions AtalaPrismSDK/Pluto/Tests/Helper/KeyRestoration+Test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Domain
import Foundation

extension AnoncredsCredentialStack: ExportableCredential {
var exporting: Data { (try? JSONEncoder().encode(credential)) ?? Data() }

var restorationType: String { "anoncred" }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Domain
import Foundation

extension JWTCredential: ExportableCredential {
var exporting: Data {
(try? jwtString.tryToData()) ?? Data()
}

var restorationType: String { "jwt" }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
54 changes: 54 additions & 0 deletions AtalaPrismSDK/Pollux/Sources/PolluxImpl+CredentialImporter.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
Loading

0 comments on commit 8cd2ebc

Please sign in to comment.