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 May 28, 2024
1 parent 423daf4 commit e519192
Show file tree
Hide file tree
Showing 28 changed files with 864 additions and 23 deletions.
115 changes: 115 additions & 0 deletions EdgeAgentSDK/Apollo/Sources/ApolloImpl+KeyRestoration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
}
}

}
20 changes: 20 additions & 0 deletions EdgeAgentSDK/Apollo/Sources/ApolloImpl+Public.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()
}
Expand Down
2 changes: 2 additions & 0 deletions EdgeAgentSDK/Apollo/Sources/Model/Ed25519Key+Exportable.swift
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 EdgeAgentSDK/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
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
2 changes: 1 addition & 1 deletion EdgeAgentSDK/Builders/Sources/PolluxBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public struct PolluxBuilder {
self.castor = castor
}

public func build() -> Pollux {
public func build() -> Pollux & CredentialImporter {
PolluxImpl(castor: castor, pluto: pluto)
}
}
Original file line number Diff line number Diff line change
@@ -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 }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

}
6 changes: 5 additions & 1 deletion EdgeAgentSDK/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 EdgeAgentSDK/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
Loading

0 comments on commit e519192

Please sign in to comment.