Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/seedless-onboarding-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ 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 ([#7284](https://github.com/MetaMask/core/pull/7284))
- Add `updateSecretDataItem` method to update fields for existing items by `itemId` ([#7284](https://github.com/MetaMask/core/pull/7284))
- Add `batchUpdateSecretDataItems` method to batch update fields for multiple items ([#7284](https://github.com/MetaMask/core/pull/7284))
- Add `itemId`, `dataType`, and `createdAt` storage-level properties to `SecretMetadata` ([#7284](https://github.com/MetaMask/core/pull/7284))
- Add `SecretMetadata.compareByTimestamp` static method for comparing metadata by timestamp ([#7284](https://github.com/MetaMask/core/pull/7284))
- Add `SecretMetadata.matchesType` static method for checking if metadata matches a given type ([#7284](https://github.com/MetaMask/core/pull/7284))
- Re-export `EncAccountDataType` from `@metamask/toprf-secure-backup` ([#7284](https://github.com/MetaMask/core/pull/7284))

### Changed

- **BREAKING:** Remove `parseSecretsFromMetadataStore`, `fromBatch`, and `sort` methods from `SecretMetadata` ([#7284](https://github.com/MetaMask/core/pull/7284))
- Use `SecretMetadata.compareByTimestamp` for sorting
- Use `SecretMetadata.matchesType` for filtering

### Fixed

- Fix TIMEUUID sorting by extracting actual timestamps instead of using lexicographic comparison ([#7284](https://github.com/MetaMask/core/pull/7284))

## [7.1.0]

### Added
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 @@
* 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 @@

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 batchData - The data to add metadata to.
* @param batchData.value - The SeedPhrase/PrivateKey to add metadata to.
* @param batchData.options - The options for the seed phrase metadata.
* @returns The SecretMetadata instances.
*/
static fromBatch<DataType extends SecretDataType = Uint8Array>(
batchData: {
value: DataType;
options?: Partial<SecretMetadataOptions>;
}[],
): SecretMetadata<DataType>[] {
const timestamp = Date.now();
return batchData.map((data, 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 = data.options?.timestamp ?? timestamp + index * 5;
return new SecretMetadata(data.value, {
timestamp: backupCreatedAt,
type: data.options?.type,
});
});
this.#itemId = storageMetadata?.itemId;
this.#dataType = storageMetadata?.dataType;
this.#createdAt = storageMetadata?.createdAt;
}

/**
Expand All @@ -122,42 +117,16 @@
}
}

/**
* 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 @@
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 @@
return this.#version;
}

get itemId() {

Check failure on line 206 in packages/seedless-onboarding-controller/src/SecretMetadata.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (22.x)

Missing return type on function

Check failure on line 206 in packages/seedless-onboarding-controller/src/SecretMetadata.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (22.x)

Missing return type on function
return this.#itemId;
}

get dataType() {

Check failure on line 210 in packages/seedless-onboarding-controller/src/SecretMetadata.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (22.x)

Missing return type on function

Check failure on line 210 in packages/seedless-onboarding-controller/src/SecretMetadata.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (22.x)

Missing return type on function
return this.#dataType;
}

get createdAt() {

Check failure on line 214 in packages/seedless-onboarding-controller/src/SecretMetadata.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (22.x)

Missing return type on function

Check failure on line 214 in packages/seedless-onboarding-controller/src/SecretMetadata.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (22.x)

Missing return type on function
return this.#createdAt;
}

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