-
-
Notifications
You must be signed in to change notification settings - Fork 200
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
feat: revamp user storage encryption process #4981
Conversation
@@ -210,7 +236,7 @@ class EncryptorDecryptor { | |||
}; | |||
} | |||
|
|||
const newSalt = salt ?? randomBytes(SCRYPT_SALT_SIZE); | |||
const newSalt = salt ?? new Uint8Array(SCRYPT_SALT_SIZE); |
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 you validate both Scrypt KDFs work with an empty salt size?
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.
FUTURE - This is a missing test on mobile, but I would like us to add a test to assert that both scrypt functions generate the exact same encrypted data (to avoid the issue we had early on)
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 you validate both Scrypt KDFs work with an empty salt size?
I CAN! 😄
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'd argue that if we want to avoid running into multiple salts we can hardcode a salt here instead of fiddling with the size.
I'm also concerned about the behavior of different libraries when given empty salts
packages/profile-sync-controller/src/controllers/user-storage/services.test.ts
Outdated
Show resolved
Hide resolved
packages/profile-sync-controller/src/controllers/user-storage/services.test.ts
Outdated
Show resolved
Hide resolved
packages/profile-sync-controller/src/controllers/user-storage/services.test.ts
Outdated
Show resolved
Hide resolved
|
||
mockGetUserStorageAllFeatureEntries.done(); | ||
mockBatchUpsertUserStorage.done(); | ||
expect(result).toStrictEqual(['data1', 'data2', MOCK_STORAGE_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.
FUTURE - this tests have a lot of repetition, it would be nice to clean up these up so the tests clearly show us what we are testing instead of needing to read all this setup/acting.
packages/profile-sync-controller/src/controllers/user-storage/services.ts
Outdated
Show resolved
Hide resolved
packages/profile-sync-controller/src/shared/encryption/cache.ts
Outdated
Show resolved
Hide resolved
packages/profile-sync-controller/src/shared/encryption/utils.ts
Outdated
Show resolved
Hide resolved
packages/profile-sync-controller/src/shared/encryption/encryption.ts
Outdated
Show resolved
Hide resolved
packages/profile-sync-controller/src/shared/encryption/utils.ts
Outdated
Show resolved
Hide resolved
packages/profile-sync-controller/src/controllers/user-storage/services.ts
Show resolved
Hide resolved
packages/profile-sync-controller/src/controllers/user-storage/services.ts
Show resolved
Hide resolved
@@ -200,6 +201,47 @@ export class UserStorage { | |||
} | |||
} | |||
|
|||
async #batchUpsertUserStorageWithAlreadyHashedAndEncryptedEntries( |
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.
Future - I don't like how we've shoved all the user storage logic in a class, this bloats the SDK class and makes it harder to test things individually.
Similar to the authentication SDK, we should split out services
from the user storage SDK.
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.
@Prithpal-Sooriya Agreed! Added that to my to-do list!
packages/profile-sync-controller/src/shared/encryption/encryption.test.ts
Show resolved
Hide resolved
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.
LGTM.
Test this on mobile before merging. I want to ensure that both JS and Native Scrypt will generate same key / same encrypted payload.
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.
Overall looks good.
There's a spot where the comparison is still done by salt.length instead of comparing to the shared salt.
Once that's fixed it should be good to go.
|
||
// Re-encrypt the entry if it was encrypted with a random salt | ||
const salt = encryption.getSalt(encryptedData); | ||
if (salt.length) { |
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.
This is missing a comparison to the SHARED_SALT
if (salt.length) { | |
if (salt.toString() !== SHARED_SALT.toString()) |
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 wonder if it's difficult to cover this in a test too
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.
looks good
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 disagree with the approach to delete the data that fails to decrypt.
Such failures can happen for multiple unforeseen reasons so any such destructive operation should be guarded much more thoroughly.
If we intend to avoid spending time in KDF on seemingly corrupted entries perhaps an alternative approach can be considered: marking the corrupted entries locally so that decryption attempts are stopped before KDF is triggered.
return decryptedData; | ||
} catch { | ||
// If the data cannot be decrypted, delete it from user storage | ||
await deleteUserStorage(opts); |
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.
To me this is a foot gun.
Exceptions during decryption can happen in situations we can't predict now.
Also, this delete operation can be invoked if there's any other problem obtaining the salt, or deriving the decryption key, or there's any error during upsert, including any network error.
@@ -173,7 +181,8 @@ export async function getUserStorageAllFeatureEntries( | |||
]); | |||
} | |||
} catch { | |||
// do nothing | |||
// If the data cannot be decrypted, delete it from user storage | |||
entriesToDelete.push(entry.HashedKey); |
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 unintentional failure scenarios as with getUserStorage()
.
The try
block is too broad
This reverts commit 32621e1.
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.
Looks good again :)
nativeScryptCrypto, | ||
), | ||
]); | ||
} | ||
} catch { | ||
// do nothing |
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.
Let's address this in a separate issue.
Explanation
This PR adds new steps when both
UserStorageController
and SDK performs:getUserStorageAllFeatureEntries
getUserStorage
This steps looks for fetched entries' salts and, if random salts are found, re-encrypts these entries with a constant salt and uploads them back to user storage.
This PR also removes the salt randomness when generating the keys, by adding a new shared salt.
This is done to prevent performance issues when decrypting multiple entries that have different salts in applications using
UserStorageController
/ SDK.References
Changelog
@metamask/profile-sync-controller
getUserStorageAllFeatureEntries
andgetUserStorage
with the shared salt if fetched entries were encrypted with random saltsChecklist