Skip to content

Commit e519192

Browse files
feat(backup): allow backup and import of prism wallet
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
1 parent 423daf4 commit e519192

28 files changed

+864
-23
lines changed

EdgeAgentSDK/Apollo/Sources/ApolloImpl+KeyRestoration.swift

+115
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,125 @@ extension ApolloImpl: KeyRestoration {
6666

6767
public func restoreKey(_ key: StorableKey) async throws -> Key {
6868
switch key.restorationIdentifier {
69+
case "secp256k1+priv":
70+
guard let index = key.index else {
71+
throw ApolloError.restoratonFailedNoIdentifierOrInvalid
72+
}
73+
return Secp256k1PrivateKey(
74+
identifier: key.identifier,
75+
internalKey: .init(raw: key.storableData.toKotlinByteArray()), derivationPath: DerivationPath(index: index)
76+
)
77+
case "x25519+priv":
78+
return try CreateX25519KeyPairOperation(logger: Self.logger)
79+
.compute(
80+
identifier: key.identifier,
81+
fromPrivateKey: key.storableData
82+
)
83+
case "ed25519+priv":
84+
return try CreateEd25519KeyPairOperation(logger: Self.logger)
85+
.compute(
86+
identifier: key.identifier,
87+
fromPrivateKey: key.storableData
88+
)
89+
case "secp256k1+pub":
90+
return Secp256k1PublicKey(
91+
identifier: key.identifier,
92+
internalKey: .init(raw: key.storableData.toKotlinByteArray())
93+
)
94+
case "x25519+pub":
95+
return X25519PublicKey(
96+
identifier: key.identifier,
97+
internalKey: .init(raw: key.storableData.toKotlinByteArray())
98+
)
99+
case "ed25519+pub":
100+
return Ed25519PublicKey(
101+
identifier: key.identifier,
102+
internalKey: .init(raw: key.storableData.toKotlinByteArray())
103+
)
69104
case "linkSecret+key":
70105
return try LinkSecret(data: key.storableData)
71106
default:
72107
throw ApolloError.restoratonFailedNoIdentifierOrInvalid
73108
}
74109
}
110+
111+
public func restoreKey(_ key: JWK, index: Int?) async throws -> Key {
112+
switch key.kty {
113+
case "EC":
114+
switch key.crv?.lowercased() {
115+
case "secp256k1":
116+
guard
117+
let d = key.d,
118+
let dData = Data(fromBase64URL: d)
119+
else {
120+
guard
121+
let x = key.x,
122+
let y = key.y,
123+
let xData = Data(fromBase64URL: x),
124+
let yData = Data(fromBase64URL: y)
125+
else {
126+
throw ApolloError.invalidJWKError
127+
}
128+
return Secp256k1PublicKey(
129+
identifier: key.kid ?? UUID().uuidString,
130+
internalKey: .init(raw: (xData + yData).toKotlinByteArray())
131+
)
132+
}
133+
return Secp256k1PrivateKey(
134+
identifier: key.kid ?? UUID().uuidString,
135+
internalKey: .init(raw: dData.toKotlinByteArray()), derivationPath: DerivationPath(index: index ?? 0)
136+
)
137+
default:
138+
throw ApolloError.invalidKeyCurve(invalid: key.crv ?? "", valid: ["secp256k1"])
139+
}
140+
case "OKP":
141+
switch key.crv?.lowercased() {
142+
case "ed25519":
143+
guard
144+
let d = key.d,
145+
let dData = Data(fromBase64URL: d)
146+
else {
147+
guard
148+
let x = key.x,
149+
let xData = Data(fromBase64URL: x)
150+
else {
151+
throw ApolloError.invalidJWKError
152+
}
153+
return Ed25519PublicKey(
154+
identifier: key.kid ?? UUID().uuidString,
155+
internalKey: .init(raw: xData.toKotlinByteArray())
156+
)
157+
}
158+
return Ed25519PrivateKey(
159+
identifier: key.kid ?? UUID().uuidString,
160+
internalKey: .init(raw: dData.toKotlinByteArray())
161+
)
162+
case "x25519":
163+
guard
164+
let d = key.d,
165+
let dData = Data(fromBase64URL: d)
166+
else {
167+
guard
168+
let x = key.x,
169+
let xData = Data(fromBase64URL: x)
170+
else {
171+
throw ApolloError.invalidJWKError
172+
}
173+
return X25519PublicKey(
174+
identifier: key.kid ?? UUID().uuidString,
175+
internalKey: .init(raw: xData.toKotlinByteArray())
176+
)
177+
}
178+
return X25519PrivateKey(
179+
identifier: key.kid ?? UUID().uuidString,
180+
internalKey: .init(raw: dData.toKotlinByteArray())
181+
)
182+
default:
183+
throw ApolloError.invalidKeyCurve(invalid: key.crv ?? "", valid: ["ed25519", "x25519"])
184+
}
185+
default:
186+
throw ApolloError.invalidKeyType(invalid: key.kty, valid: ["EC", "OKP"])
187+
}
188+
}
189+
75190
}

EdgeAgentSDK/Apollo/Sources/ApolloImpl+Public.swift

+20
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ extension ApolloImpl: Apollo {
8484
let keyData = Data(base64Encoded: keyStr)
8585
{
8686
return try CreateEd25519KeyPairOperation(logger: ApolloImpl.logger).compute(fromPrivateKey: keyData)
87+
} else if
88+
let derivationPathStr = parameters[KeyProperties.derivationPath.rawValue],
89+
let seedStr = parameters[KeyProperties.seed.rawValue],
90+
let seed = Data(base64Encoded: seedStr)
91+
{
92+
let derivationPath = try DerivationPath(string: derivationPathStr)
93+
return try CreateEd25519KeyPairOperation(logger: ApolloImpl.logger).compute(
94+
seed: Seed(value: seed),
95+
keyPath: derivationPath
96+
)
8797
}
8898
return CreateEd25519KeyPairOperation(logger: ApolloImpl.logger).compute()
8999
case .x25519:
@@ -92,6 +102,16 @@ extension ApolloImpl: Apollo {
92102
let keyData = Data(base64Encoded: keyStr)
93103
{
94104
return try CreateX25519KeyPairOperation(logger: ApolloImpl.logger).compute(fromPrivateKey: keyData)
105+
} else if
106+
let derivationPathStr = parameters[KeyProperties.derivationPath.rawValue],
107+
let seedStr = parameters[KeyProperties.seed.rawValue],
108+
let seed = Data(base64Encoded: seedStr)
109+
{
110+
let derivationPath = try DerivationPath(string: derivationPathStr)
111+
return try CreateX25519KeyPairOperation(logger: ApolloImpl.logger).compute(
112+
seed: Seed(value: seed),
113+
keyPath: derivationPath
114+
)
95115
}
96116
return CreateX25519KeyPairOperation(logger: ApolloImpl.logger).compute()
97117
}

EdgeAgentSDK/Apollo/Sources/Model/Ed25519Key+Exportable.swift

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ extension Ed25519PrivateKey: ExportableKey {
1212
var jwk: JWK {
1313
JWK(
1414
kty: "OKP",
15+
kid: identifier,
1516
d: raw.base64UrlEncodedString(),
1617
crv: getProperty(.curve)?.capitalized,
1718
x: publicKey().raw.base64UrlEncodedString()
@@ -40,6 +41,7 @@ extension Ed25519PublicKey: ExportableKey {
4041
var jwk: JWK {
4142
JWK(
4243
kty: "OKP",
44+
kid: identifier,
4345
crv: getProperty(.curve)?.capitalized,
4446
x: raw.base64UrlEncodedString()
4547
)

EdgeAgentSDK/Apollo/Sources/Model/X25519Key+Exportable.swift

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ extension X25519PrivateKey: ExportableKey {
1212
var jwk: JWK {
1313
JWK(
1414
kty: "OKP",
15+
kid: identifier,
1516
d: raw.base64UrlEncodedString(),
1617
crv: getProperty(.curve)?.capitalized,
1718
x: publicKey().raw.base64UrlEncodedString()
@@ -40,6 +41,7 @@ extension X25519PublicKey: ExportableKey {
4041
var jwk: JWK {
4142
JWK(
4243
kty: "OKP",
44+
kid: identifier,
4345
crv: getProperty(.curve)?.capitalized,
4446
x: raw.base64UrlEncodedString()
4547
)

EdgeAgentSDK/Apollo/Sources/Operations/CreateEd25519KeyPairOperation.swift

+11
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,15 @@ struct CreateEd25519KeyPairOperation {
1818
internalKey: KMMEdPrivateKey(raw: fromPrivateKey.toKotlinByteArray())
1919
)
2020
}
21+
22+
func compute(seed: Seed, keyPath: Domain.DerivationPath) throws -> PrivateKey {
23+
let derivedHdKey = ApolloLibrary
24+
.EdHDKey
25+
.companion
26+
.doInitFromSeed(seed: seed.value.toKotlinByteArray())
27+
.derive(path: keyPath.keyPathString()
28+
)
29+
30+
return Ed25519PrivateKey(internalKey: .init(raw: derivedHdKey.privateKey))
31+
}
2132
}

EdgeAgentSDK/Apollo/Sources/Operations/CreateSec256k1KeyPairOperation.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ struct CreateSec256k1KeyPairOperation {
1414
let derivedHdKey = ApolloLibrary.HDKey(
1515
seed: seed.value.toKotlinByteArray(),
1616
depth: 0,
17-
childIndex: BigIntegerWrapper(int: 0)
17+
childIndex: 0
1818
).derive(path: keyPath.keyPathString())
1919
return Secp256k1PrivateKey(internalKey: derivedHdKey.getKMMSecp256k1PrivateKey(), derivationPath: keyPath)
2020

EdgeAgentSDK/Apollo/Sources/Operations/CreateX25519KeyPairOperation.swift

+11
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,15 @@ struct CreateX25519KeyPairOperation {
2020
internalKey: privateKey
2121
)
2222
}
23+
24+
func compute(seed: Seed, keyPath: Domain.DerivationPath) throws -> PrivateKey {
25+
let derivedHdKey = ApolloLibrary
26+
.EdHDKey
27+
.companion
28+
.doInitFromSeed(seed: seed.value.toKotlinByteArray())
29+
.derive(path: keyPath.keyPathString()
30+
)
31+
32+
return X25519PrivateKey(internalKey: KMMEdPrivateKey(raw: derivedHdKey.privateKey).x25519PrivateKey())
33+
}
2334
}

EdgeAgentSDK/Builders/Sources/PolluxBuilder.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public struct PolluxBuilder {
1010
self.castor = castor
1111
}
1212

13-
public func build() -> Pollux {
13+
public func build() -> Pollux & CredentialImporter {
1414
PolluxImpl(castor: castor, pluto: pluto)
1515
}
1616
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import Foundation
2+
3+
/**
4+
A protocol that defines the requirements for credentials that can be exported.
5+
6+
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.
7+
8+
- Properties:
9+
- 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.
10+
- 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.
11+
12+
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.
13+
*/
14+
public protocol ExportableCredential {
15+
var exporting: Data { get }
16+
var restorationType: String { get }
17+
}
18+
19+
/**
20+
A protocol that defines the functionality required to import credentials from a serialized data format.
21+
22+
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.
23+
24+
- Method `importCredential`:
25+
- Parameters:
26+
- credentialData: The serialized `Data` representation of the credential to be imported. This data should have been generated by the `exporting` property of an `ExportableCredential`.
27+
- restorationType: A `String` indicating the method or requirements for restoring the credential. This should match the `restorationType` provided during the export process.
28+
- 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.
29+
- Returns: An asynchronous operation that, upon completion, returns the imported `Credential` object, reconstructed from the provided data.
30+
- Throws: An error if the import process encounters issues, such as data corruption, incompatible restoration types, or if the provided options are not supported.
31+
32+
Implementers should ensure robust error handling and validation to securely and accurately restore credentials from their exported state.
33+
*/
34+
public protocol CredentialImporter {
35+
func importCredential(
36+
credentialData: Data,
37+
restorationType: String,
38+
options: [CredentialOperationsOptions]
39+
) async throws -> Credential
40+
}
41+
42+
/**
43+
Extension to the `Credential` class or struct, providing convenience properties related to the exportability of credentials.
44+
45+
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.
46+
47+
- Properties:
48+
- isExportable: A Boolean value that indicates whether the `Credential` instance conforms to the `ExportableCredential` protocol, thereby supporting export operations.
49+
- 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.
50+
51+
These properties enhance the usability of credentials by providing straightforward mechanisms to interact with export-related functionality.
52+
*/
53+
public extension Credential {
54+
/// A Boolean value indicating whether the credential is exportable.
55+
var isExportable: Bool { self is ExportableCredential }
56+
57+
/// Returns the exportable representation of the credential.
58+
var exportable: ExportableCredential? { self as? ExportableCredential }
59+
}

EdgeAgentSDK/Domain/Sources/Models/KeyManagement/KeyRestoration.swift

+12-6
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,31 @@ public protocol KeyRestoration {
2828

2929
/// Restores a private key from the given data.
3030
/// - Parameters:
31-
/// - identifier: An optional string used to identify the key.
32-
/// - data: The raw data representing the key.
31+
/// - key: A storableKey instance.
3332
/// - Throws: If the restoration process fails, this method throws an error.
3433
/// - Returns: The restored `PrivateKey` instance.
3534
func restorePrivateKey(_ key: StorableKey) async throws -> PrivateKey
3635

3736
/// Restores a public key from the given data.
3837
/// - Parameters:
39-
/// - identifier: An optional string used to identify the key.
40-
/// - data: The raw data representing the key.
38+
/// - key: A storableKey instance.
4139
/// - Throws: If the restoration process fails, this method throws an error.
4240
/// - Returns: The restored `PublicKey` instance.
4341
func restorePublicKey(_ key: StorableKey) async throws -> PublicKey
4442

4543
/// Restores a key from the given data.
4644
/// - Parameters:
47-
/// - identifier: An optional string used to identify the key.
48-
/// - data: The raw data representing the key.
45+
/// - key: A storableKey instance.
4946
/// - Throws: If the restoration process fails, this method throws an error.
5047
/// - Returns: The restored `Key` instance.
5148
func restoreKey(_ key: StorableKey) async throws -> Key
49+
50+
/// Restores a key from a JWK.
51+
/// - Parameters:
52+
/// - key: A JWK instance.
53+
/// - index: An Int for the derivation index path.
54+
/// - Throws: If the restoration process fails, this method throws an error.
55+
/// - Returns: The restored `Key` instance.
56+
func restoreKey(_ key: JWK, index: Int?) async throws -> Key
57+
5258
}

EdgeAgentSDK/Domain/Sources/Models/Message+Codable.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ extension Message: Codable {
1515
case pthid
1616
case ack
1717
case body
18+
case direction
1819
}
1920

2021
public func encode(to encoder: Encoder) throws {
@@ -32,6 +33,7 @@ extension Message: Codable {
3233
try fromPrior.map { try container.encode($0, forKey: .fromPrior) }
3334
try thid.map { try container.encode($0, forKey: .thid) }
3435
try pthid.map { try container.encode($0, forKey: .pthid) }
36+
try container.encode(direction, forKey: .direction)
3537
}
3638

3739
public init(from decoder: Decoder) throws {
@@ -49,6 +51,7 @@ extension Message: Codable {
4951
let fromPrior = try? container.decodeIfPresent(String.self, forKey: .fromPrior)
5052
let thid = try? container.decodeIfPresent(String.self, forKey: .thid)
5153
let pthid = try? container.decodeIfPresent(String.self, forKey: .pthid)
54+
let direction = try? container.decodeIfPresent(Direction.self, forKey: .direction)
5255

5356
self.init(
5457
id: id,
@@ -63,7 +66,8 @@ extension Message: Codable {
6366
attachments: attachments ?? [],
6467
thid: thid,
6568
pthid: pthid,
66-
ack: ack ?? []
69+
ack: ack ?? [],
70+
direction: direction ?? .received
6771
)
6872
}
6973
}

EdgeAgentSDK/Domain/Sources/Models/Message.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Foundation
33
/// 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.
44
public struct Message: Identifiable, Hashable {
55
/// The direction of the message (sent or received).
6-
public enum Direction: String {
6+
public enum Direction: String, Codable {
77
case sent
88
case received
99
}

0 commit comments

Comments
 (0)