Skip to content

Commit 9cf2022

Browse files
author
Nguyen Anh Tu
committed
Merge branch 'feat/seedless-multi-srp' into feat/seedless-onboarding-password-sync
2 parents ad2b854 + 7104c8c commit 9cf2022

File tree

8 files changed

+402
-207
lines changed

8 files changed

+402
-207
lines changed
Binary file not shown.
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import {
2+
base64ToBytes,
3+
bytesToBase64,
4+
stringToBytes,
5+
bytesToString,
6+
} from '@metamask/utils';
7+
8+
import {
9+
SeedlessOnboardingControllerError,
10+
SecretType,
11+
SecretMetadataVersion,
12+
} from './constants';
13+
import type { SecretDataType, SecretMetadataOptions } from './types';
14+
15+
type ISecretMetadata<DataType extends SecretDataType = Uint8Array> = {
16+
data: DataType;
17+
timestamp: number;
18+
type: SecretType;
19+
version: SecretMetadataVersion;
20+
toBytes: () => Uint8Array;
21+
};
22+
23+
// SecretMetadata type without the data and toBytes methods
24+
// in which the data is base64 encoded for more compacted metadata
25+
type SecretMetadataJson<DataType extends SecretDataType> = Omit<
26+
ISecretMetadata<DataType>,
27+
'data' | 'toBytes'
28+
> & {
29+
data: string; // base64 encoded string
30+
};
31+
32+
/**
33+
* SecretMetadata is a class that adds metadata to the secret.
34+
*
35+
* It contains the secret and the timestamp when it was created.
36+
* It is used to store the secret in the metadata store.
37+
*
38+
* @example
39+
* ```ts
40+
* const secretMetadata = new SecretMetadata(secret);
41+
* ```
42+
*/
43+
export class SecretMetadata<DataType extends SecretDataType = Uint8Array>
44+
implements ISecretMetadata<DataType>
45+
{
46+
readonly #data: DataType;
47+
48+
readonly #timestamp: number;
49+
50+
readonly #type: SecretType;
51+
52+
readonly #version: SecretMetadataVersion;
53+
54+
/**
55+
* Create a new SecretMetadata instance.
56+
*
57+
* @param data - The secret to add metadata to.
58+
* @param options - The options for the secret metadata.
59+
* @param options.timestamp - The timestamp when the secret was created.
60+
* @param options.type - The type of the secret.
61+
*/
62+
constructor(data: DataType, options?: Partial<SecretMetadataOptions>) {
63+
this.#data = data;
64+
this.#timestamp = options?.timestamp ?? Date.now();
65+
this.#type = options?.type ?? SecretType.Mnemonic;
66+
this.#version = options?.version ?? SecretMetadataVersion.V1;
67+
}
68+
69+
/**
70+
* Create an Array of SecretMetadata instances from an array of secrets.
71+
*
72+
* To respect the order of the secrets, we add the index to the timestamp
73+
* so that the first secret backup will have the oldest timestamp
74+
* and the last secret backup will have the newest timestamp.
75+
*
76+
* @param data - The data to add metadata to.
77+
* @param data.value - The SeedPhrase/PrivateKey to add metadata to.
78+
* @param data.options - The options for the seed phrase metadata.
79+
* @returns The SecretMetadata instances.
80+
*/
81+
static fromBatch<DataType extends SecretDataType = Uint8Array>(
82+
data: {
83+
value: DataType;
84+
options?: Partial<SecretMetadataOptions>;
85+
}[],
86+
): SecretMetadata<DataType>[] {
87+
const timestamp = Date.now();
88+
return data.map((d, index) => {
89+
// To respect the order of the seed phrases, we add the index to the timestamp
90+
// so that the first seed phrase backup will have the oldest timestamp
91+
// and the last seed phrase backup will have the newest timestamp
92+
const backupCreatedAt = d.options?.timestamp ?? timestamp + index * 5;
93+
return new SecretMetadata(d.value, {
94+
timestamp: backupCreatedAt,
95+
type: d.options?.type,
96+
});
97+
});
98+
}
99+
100+
/**
101+
* Assert that the provided value is a valid seed phrase metadata.
102+
*
103+
* @param value - The value to check.
104+
* @throws If the value is not a valid seed phrase metadata.
105+
*/
106+
static assertIsValidSecretMetadataJson<
107+
DataType extends SecretDataType = Uint8Array,
108+
>(value: unknown): asserts value is SecretMetadataJson<DataType> {
109+
if (
110+
typeof value !== 'object' ||
111+
!value ||
112+
!('data' in value) ||
113+
typeof value.data !== 'string' ||
114+
!('timestamp' in value) ||
115+
typeof value.timestamp !== 'number'
116+
) {
117+
throw new Error(SeedlessOnboardingControllerError.InvalidSecretMetadata);
118+
}
119+
}
120+
121+
/**
122+
* Parse the SecretMetadata from the metadata store and return the array of SecretMetadata instances.
123+
*
124+
* This method also sorts the secrets by timestamp in ascending order, i.e. the oldest secret will be the first element in the array.
125+
*
126+
* @param secretMetadataArr - The array of SecretMetadata from the metadata store.
127+
* @param filterType - The type of the secret to filter.
128+
* @returns The array of SecretMetadata instances.
129+
*/
130+
static parseSecretsFromMetadataStore<
131+
DataType extends SecretDataType = Uint8Array,
132+
>(
133+
secretMetadataArr: Uint8Array[],
134+
filterType?: SecretType,
135+
): SecretMetadata<DataType>[] {
136+
const parsedSecertMetadata = secretMetadataArr.map((metadata) =>
137+
SecretMetadata.fromRawMetadata<DataType>(metadata),
138+
);
139+
140+
const secrets = SecretMetadata.sort(parsedSecertMetadata);
141+
142+
if (filterType) {
143+
return secrets.filter((secret) => secret.type === filterType);
144+
}
145+
146+
return secrets;
147+
}
148+
149+
/**
150+
* Parse and create the SecretMetadata instance from the raw metadata bytes.
151+
*
152+
* @param rawMetadata - The raw metadata.
153+
* @returns The parsed secret metadata.
154+
*/
155+
static fromRawMetadata<DataType extends SecretDataType>(
156+
rawMetadata: Uint8Array,
157+
): SecretMetadata<DataType> {
158+
const serializedMetadata = bytesToString(rawMetadata);
159+
const parsedMetadata = JSON.parse(serializedMetadata);
160+
161+
SecretMetadata.assertIsValidSecretMetadataJson<DataType>(parsedMetadata);
162+
163+
// if the type is not provided, we default to Mnemonic for the backwards compatibility
164+
const type = parsedMetadata.type ?? SecretType.Mnemonic;
165+
const version = parsedMetadata.version ?? SecretMetadataVersion.V1;
166+
167+
let data: DataType;
168+
try {
169+
data = base64ToBytes(parsedMetadata.data) as DataType;
170+
} catch {
171+
data = parsedMetadata.data as DataType;
172+
}
173+
174+
return new SecretMetadata<DataType>(data, {
175+
timestamp: parsedMetadata.timestamp,
176+
type,
177+
version,
178+
});
179+
}
180+
181+
/**
182+
* Sort the seed phrases by timestamp.
183+
*
184+
* @param data - The secret metadata array to sort.
185+
* @param order - The order to sort the seed phrases. Default is `desc`.
186+
*
187+
* @returns The sorted secret metadata array.
188+
*/
189+
static sort<DataType extends SecretDataType = Uint8Array>(
190+
data: SecretMetadata<DataType>[],
191+
order: 'asc' | 'desc' = 'asc',
192+
): SecretMetadata<DataType>[] {
193+
return data.sort((a, b) => {
194+
if (order === 'asc') {
195+
return a.timestamp - b.timestamp;
196+
}
197+
return b.timestamp - a.timestamp;
198+
});
199+
}
200+
201+
get data(): DataType {
202+
return this.#data;
203+
}
204+
205+
get timestamp() {
206+
return this.#timestamp;
207+
}
208+
209+
get type() {
210+
return this.#type;
211+
}
212+
213+
get version() {
214+
return this.#version;
215+
}
216+
217+
/**
218+
* Serialize the secret metadata and convert it to a Uint8Array.
219+
*
220+
* @returns The serialized SecretMetadata value in bytes.
221+
*/
222+
toBytes(): Uint8Array {
223+
let _data: unknown = this.#data;
224+
if (this.#data instanceof Uint8Array) {
225+
// encode the raw secret to base64 encoded string
226+
// to create more compacted metadata
227+
_data = bytesToBase64(this.#data);
228+
}
229+
230+
// serialize the metadata to a JSON string
231+
const serializedMetadata = JSON.stringify({
232+
data: _data,
233+
timestamp: this.#timestamp,
234+
type: this.#type,
235+
version: this.#version,
236+
});
237+
238+
// convert the serialized metadata to bytes(Uint8Array)
239+
return stringToBytes(serializedMetadata);
240+
}
241+
}

0 commit comments

Comments
 (0)