Skip to content
Draft
14 changes: 14 additions & 0 deletions packages/seedless-onboarding-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add optional `dataType` parameter to `addNewSecretData` method for categorizing secret data on insert
- Add `updateSecretDataItem` method to update fields for existing items by `itemId`
- Add `batchUpdateSecretDataItems` method to batch update fields for multiple items
- Add `itemId`, `dataType`, and `createdAt` storage-level properties to `SecretMetadata`
- Add `SecretMetadata.compareByTimestamp` static method for comparing metadata by timestamp
- Add `SecretMetadata.matchesType` static method for checking if metadata matches a given type
- Re-export `EncAccountDataType` from `@metamask/toprf-secure-backup`

### Changed

- **BREAKING:** Remove `parseSecretsFromMetadataStore`, `fromBatch`, and `sort` methods from `SecretMetadata`
- Use `SecretMetadata.compareByTimestamp` for sorting
- Use `SecretMetadata.matchesType` for filtering
- Bump `@metamask/toprf-secure-backup` from `^0.10.0` to `^0.11.0`
- Move peer dependencies for controller and service packages to direct dependencies ([#7209](https://github.com/MetaMask/core/pull/7209))
- The dependencies moved are:
- `@metamask/keyring-controller` (^25.0.0)
Expand Down
2 changes: 1 addition & 1 deletion packages/seedless-onboarding-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"@metamask/browser-passworder": "^6.0.0",
"@metamask/keyring-controller": "^25.0.0",
"@metamask/messenger": "^0.3.0",
"@metamask/toprf-secure-backup": "^0.10.0",
"@metamask/toprf-secure-backup": "^0.11.0",
"@metamask/utils": "^11.8.1",
"@noble/ciphers": "^1.3.0",
"@noble/curves": "^1.9.2",
Expand Down
155 changes: 76 additions & 79 deletions packages/seedless-onboarding-controller/src/SecretMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EncAccountDataType } from '@metamask/toprf-secure-backup';
import {
base64ToBytes,
bytesToBase64,
Expand Down Expand Up @@ -42,6 +43,15 @@ type SecretMetadataJson<DataType extends SecretDataType> = Omit<
* const secretMetadata = new SecretMetadata(secret);
* ```
*/
/**
* Storage-level metadata from the metadata store (not encrypted).
*/
type StorageMetadata = {
itemId?: string;
dataType?: EncAccountDataType;
createdAt?: string;
};

export class SecretMetadata<DataType extends SecretDataType = Uint8Array>
implements ISecretMetadata<DataType>
{
Expand All @@ -53,50 +63,35 @@ export class SecretMetadata<DataType extends SecretDataType = Uint8Array>

readonly #version: SecretMetadataVersion;

// Storage-level metadata (not encrypted)
readonly #itemId?: string;

readonly #dataType?: EncAccountDataType;

readonly #createdAt?: string;

/**
* Create a new SecretMetadata instance.
*
* @param data - The secret to add metadata to.
* @param options - The options for the secret metadata.
* @param options.timestamp - The timestamp when the secret was created.
* @param options.type - The type of the secret.
* @param options.version - The version of the secret metadata.
* @param storageMetadata - Storage-level metadata from the metadata store.
*/
constructor(data: DataType, options?: Partial<SecretMetadataOptions>) {
constructor(
data: DataType,
options?: Partial<SecretMetadataOptions>,
storageMetadata?: StorageMetadata,
) {
this.#data = data;
this.#timestamp = options?.timestamp ?? Date.now();
this.#type = options?.type ?? SecretType.Mnemonic;
this.#version = options?.version ?? SecretMetadataVersion.V1;
}

/**
* Create an Array of SecretMetadata instances from an array of secrets.
*
* To respect the order of the secrets, we add the index to the timestamp
* so that the first secret backup will have the oldest timestamp
* and the last secret backup will have the newest timestamp.
*
* @param data - The data to add metadata to.
* @param data.value - The SeedPhrase/PrivateKey to add metadata to.
* @param data.options - The options for the seed phrase metadata.
* @returns The SecretMetadata instances.
*/
static fromBatch<DataType extends SecretDataType = Uint8Array>(
data: {
value: DataType;
options?: Partial<SecretMetadataOptions>;
}[],
): SecretMetadata<DataType>[] {
const timestamp = Date.now();
return data.map((d, index) => {
// To respect the order of the seed phrases, we add the index to the timestamp
// so that the first seed phrase backup will have the oldest timestamp
// and the last seed phrase backup will have the newest timestamp
const backupCreatedAt = d.options?.timestamp ?? timestamp + index * 5;
return new SecretMetadata(d.value, {
timestamp: backupCreatedAt,
type: d.options?.type,
});
});
this.#itemId = storageMetadata?.itemId;
this.#dataType = storageMetadata?.dataType;
this.#createdAt = storageMetadata?.createdAt;
}

/**
Expand All @@ -122,42 +117,16 @@ export class SecretMetadata<DataType extends SecretDataType = Uint8Array>
}
}

/**
* Parse the SecretMetadata from the metadata store and return the array of SecretMetadata instances.
*
* This method also sorts the secrets by timestamp in ascending order, i.e. the oldest secret will be the first element in the array.
*
* @param secretMetadataArr - The array of SecretMetadata from the metadata store.
* @param filterType - The type of the secret to filter.
* @returns The array of SecretMetadata instances.
*/
static parseSecretsFromMetadataStore<
DataType extends SecretDataType = Uint8Array,
>(
secretMetadataArr: Uint8Array[],
filterType?: SecretType,
): SecretMetadata<DataType>[] {
const parsedSecertMetadata = secretMetadataArr.map((metadata) =>
SecretMetadata.fromRawMetadata<DataType>(metadata),
);

const secrets = SecretMetadata.sort(parsedSecertMetadata);

if (filterType) {
return secrets.filter((secret) => secret.type === filterType);
}

return secrets;
}

/**
* Parse and create the SecretMetadata instance from the raw metadata bytes.
*
* @param rawMetadata - The raw metadata.
* @param storageMetadata - Storage-level metadata from the metadata store.
* @returns The parsed secret metadata.
*/
static fromRawMetadata<DataType extends SecretDataType>(
rawMetadata: Uint8Array,
storageMetadata?: StorageMetadata,
): SecretMetadata<DataType> {
const serializedMetadata = bytesToString(rawMetadata);
const parsedMetadata = JSON.parse(serializedMetadata);
Expand All @@ -175,31 +144,47 @@ export class SecretMetadata<DataType extends SecretDataType = Uint8Array>
data = parsedMetadata.data as DataType;
}

return new SecretMetadata<DataType>(data, {
timestamp: parsedMetadata.timestamp,
type,
version,
});
return new SecretMetadata<DataType>(
data,
{
timestamp: parsedMetadata.timestamp,
type,
version,
},
storageMetadata,
);
}

/**
* Sort the seed phrases by timestamp.
*
* @param data - The secret metadata array to sort.
* @param order - The order to sort the seed phrases. Default is `desc`.
* Compare two SecretMetadata instances by timestamp.
*
* @returns The sorted secret metadata array.
* @param a - The first SecretMetadata instance.
* @param b - The second SecretMetadata instance.
* @param order - The sort order. Default is 'asc'.
* @returns A negative number if a < b, positive if a > b, zero if equal.
*/
static sort<DataType extends SecretDataType = Uint8Array>(
data: SecretMetadata<DataType>[],
static compareByTimestamp<DataType extends SecretDataType = SecretDataType>(
a: SecretMetadata<DataType>,
b: SecretMetadata<DataType>,
order: 'asc' | 'desc' = 'asc',
): SecretMetadata<DataType>[] {
return data.sort((a, b) => {
if (order === 'asc') {
return a.timestamp - b.timestamp;
}
return b.timestamp - a.timestamp;
});
): number {
return order === 'asc'
? a.timestamp - b.timestamp
: b.timestamp - a.timestamp;
}

/**
* Check if a SecretMetadata instance matches the given type.
*
* @param secret - The SecretMetadata instance to check.
* @param type - The type to match against.
* @returns True if the secret matches the type.
*/
static matchesType<DataType extends SecretDataType = SecretDataType>(
secret: SecretMetadata<DataType>,
type: SecretType,
): boolean {
return secret.type === type;
}

get data(): DataType {
Expand All @@ -218,6 +203,18 @@ export class SecretMetadata<DataType extends SecretDataType = Uint8Array>
return this.#version;
}

get itemId() {
return this.#itemId;
}

get dataType() {
return this.#dataType;
}

get createdAt() {
return this.#createdAt;
}

/**
* Serialize the secret metadata and convert it to a Uint8Array.
*
Expand Down
Loading