Skip to content

Conversation

@huggingbot
Copy link
Member

Explanation

The metadata service now supports a dataType column for categorizing secret data (PrimarySrp, ImportedSrp, ImportedPrivateKey). This enables clients to distinguish between different types of backed-up secrets

Changes include:

  • Add dataType parameter to insert operations
  • Add updateSecretDataItem and batchUpdateSecretDataItems for updating existing items
  • Update fetchAllSecretData to return storage metadata (itemId, dataType) alongside secret data
  • Refactor SecretMetadata to separate local metadata from storage-level metadata

References

https://consensyssoftware.atlassian.net/browse/SL-350

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

- Add dataType parameter to createToprfKeyAndBackupSeedPhrase and addNewSecretData
- Add updateSecretDataItem and batchUpdateSecretDataItems methods
- Update fetchAllSecretData to return SecretDataItemWithMetadata[]
/**
* The client-assigned data type classification.
*/
dataType?: EncAccountDataType;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this extra type SecretMetadata already has it has type

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type is in encrypted data, which the server can't access to
data_type is a server-side column
the server needs to know the type of key for the deletion api: we're preventing clients from deleting the default srp

/**
* The server-assigned item ID for this row.
*/
itemId?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add to the SecretMetadata instead of creating another wrapping types

Copy link
Member Author

@huggingbot huggingbot Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

itemId is not part of the secret data, hence it shouldn't belong inside SecretMetadata. Only data, timestamp, type and version are part of SecretMetadata

/**
* A secret data item with storage-level metadata from the metadata store.
*/
export type SecretDataItemWithMetadata<
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the required metadata to the SecretMetadata class instead of creating another type defs.

The purpose the metadata class is to store and do things with extra non-secret data.

Copy link
Member Author

@huggingbot huggingbot Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

itemId and dataType are not part of the secret data. They belong outside of it, hence the wrapper type SecretDataItemWithMetadata is introduced.

The purpose the metadata class is to store and do things with extra non-secret data.

by the metadata class, did u mean the SecretMetadata class?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved itemId and dataType here

export { SecretMetadata } from './SecretMetadata';
export { RecoveryError } from './errors';

export { EncAccountDataType } from '@metamask/toprf-secure-backup';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why we export this, but this should not be determined from the users (ext or mobile).

Copy link
Member Author

@huggingbot huggingbot Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's exported so clients can use it in createToprfKeyAndBackupSeedPhrase, addNewSecretData, updateSecretDataItem and batchUpdateSecretDataItems. The clients do determine the type of secret, e.g. in addNewSecretData, there's a type param of type SecretType where the client can decide if it's a mnemonic or privateKey

password: string,
seedPhrase: Uint8Array,
keyringId: string,
dataType: EncAccountDataType = EncAccountDataType.PrimarySrp,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createToprfKeyAndBackupSeedPhrase only used for new users scenario, this param should not be exposed at all and type is always primary.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed here

type: SecretType,
options?: {
keyringId?: string;
dataType?: EncAccountDataType;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have type field in 2nd param, we don't need new field here.

Copy link
Member Author

@huggingbot huggingbot Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SecretType and EncAccountDataType are slightly different:

  • SecretType only has 2 types: SRP or private key; EncAccountDataType has 3: Primary SRP, imported SRP, imported private key
  • Even if we were to extend SecretType to include a primary SRP, using string values should be avoided to protect user privacy when data is transmitted to the server

we can attempt to migrate SecretType from string to number, making it essentially the same as EncAccountDataType. Though they are essentially different things, and in the future we might want to remove type from the encrypted data since we will use dataType as the source of truth. What do u think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we intend to do the migration at the unlock and rehydration, during this addNewSecretData method call, we can assume that the user data is already migrated. Hence, we don't need both SecretType and EncAccountDataType to be present.

We can use EncAccountDataType type for 2nd param type.

* @param params.dataType - The data type to set for the item.
* @returns A promise that resolves when the update is complete.
*/
async updateSecretDataItem(params: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we specify why and when we need to call this? since in the clients, update* is not something we usually do with SRPs

Copy link
Member Author

@huggingbot huggingbot Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's used to migrate existing users to update the data_type column in the metadata service. Currently the server doesn't know the type of secret (it's encrypted). It needs to know for the deletion API to work: to prevent clients from deleting the default SRP

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added more notes here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo, I don't think we should expose this method. This is purely internal controller logic. Rather than adding migration code to the client sides (ext/mobile), we should handle this internally inside the controller package (with proper versioning).

* @param params.updates - Array of objects containing itemId and fields to update.
* @returns A promise that resolves when all updates are complete.
*/
async batchUpdateSecretDataItems(params: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added more notes here

// validate the primary secret data is a mnemonic (SRP)
const primarySecret = secrets[0];
if (primarySecret.type !== SecretType.Mnemonic) {
if (!SecretMetadata.matchesType(results[0].secret, SecretType.Mnemonic)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have Primary type, why don't we use here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed here
now we're validating both the .type in the encrypted data and also dataType

authKeyPair: KeyPair;
options?: {
keyringId?: string;
dataType?: EncAccountDataType;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already have type in the params object. Please use it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

related question and answer here: #7284 (comment)

…etadata

- Add itemId, dataType, createdAt properties to SecretMetadata class
- Remove SecretDataItemWithMetadata wrapper type
- Update fetchAllSecretData to return SecretMetadata[] directly
- Add tests for storage metadata properties
…EUUID sorting

TIMEUUID strings are not lexicographically sortable. Replace localeCompare
with compareTimeuuid utility that extracts and compares actual timestamps.
);

// Sort: PrimarySrp first, then by createdAt/timestamp (oldest first)
results.sort((a, b) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorting is required here even though /get in metadata already does sorting because we need this for the migration since dataType and createdAt could be null thus we'll need a fallback to use the client-side timestamp

this.#data = data;
this.#timestamp = options?.timestamp ?? Date.now();
this.#type = options?.type ?? SecretType.Mnemonic;
this.#version = options?.version ?? SecretMetadataVersion.V1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should maintain the metadata type version with this.

The existing (old) items will have version number SecretMetadataVersion.V1 after decryption. We can do the increment on version number (V2) for new structure.

storageMetadata?: StorageMetadata,
): SecretMetadata<DataType> {
const serializedMetadata = bytesToString(rawMetadata);
const parsedMetadata = JSON.parse(serializedMetadata);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the SecretMetadata.assertIsValidSecretMetadataJson with the new structure.

});
): number {
return order === 'asc'
? a.timestamp - b.timestamp
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be compared using the server timestamp (createdAt?)

type: SecretType,
options?: {
keyringId?: string;
dataType?: EncAccountDataType;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we intend to do the migration at the unlock and rehydration, during this addNewSecretData method call, we can assume that the user data is already migrated. Hence, we don't need both SecretType and EncAccountDataType to be present.

We can use EncAccountDataType type for 2nd param type.

* @param params.dataType - The data type to set for the item.
* @returns A promise that resolves when the update is complete.
*/
async updateSecretDataItem(params: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo, I don't think we should expose this method. This is purely internal controller logic. Rather than adding migration code to the client sides (ext/mobile), we should handle this internally inside the controller package (with proper versioning).

* @param params.updates - Array of objects containing itemId and dataType to assign.
* @returns A promise that resolves when all updates are complete.
*/
async batchUpdateSecretDataItems(params: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the migration logics to internal scope.

);

// Sort: PrimarySrp first, then by createdAt/timestamp (oldest first)
results.sort((a, b) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we move the whole logic into SecretMetadata class?

});

// Validate the first item is the primary SRP
const firstItem = results[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can refactor this logics into SecretMetadata class.

const isDataTypePrimary =
firstItem.dataType === undefined || // Legacy data (before dataType was introduced)
firstItem.dataType === EncAccountDataType.PrimarySrp;
const isMnemonic = SecretMetadata.matchesType(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the migration, this become invalid right? We won't have the type anymore and we will be relying on the EncAccountDataType.PrimarySrp.

So, this will incorrectly result in false and the below error will be thrown.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants