-
-
Notifications
You must be signed in to change notification settings - Fork 261
feat(seedless-onboarding): add dataType support for secret data items #7284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- 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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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< |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
SecretTypeonly has 2 types: SRP or private key;EncAccountDataTypehas 3: Primary SRP, imported SRP, imported private key- Even if we were to extend
SecretTypeto 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?
There was a problem hiding this comment.
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: { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added more notes here
There was a problem hiding this comment.
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: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
…ckupSeedPhrase method
… PrimarySrp first, then createdAt
…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) => { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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: { |
There was a problem hiding this comment.
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: { |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
Explanation
The metadata service now supports a
dataTypecolumn for categorizing secret data (PrimarySrp, ImportedSrp, ImportedPrivateKey). This enables clients to distinguish between different types of backed-up secretsChanges include:
dataTypeparameter to insert operationsupdateSecretDataItemandbatchUpdateSecretDataItemsfor updating existing itemsfetchAllSecretDatato return storage metadata (itemId,dataType) alongside secret dataSecretMetadatato separate local metadata from storage-level metadataReferences
https://consensyssoftware.atlassian.net/browse/SL-350
Checklist