mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM-21034] Feature Branch - "User Crypto V2" (#5982)
* [PM-21034] Database changes for signature keypairs (#5906) * Add signing key repositories, models, and sql migration scripts * Rename UserSigningKeys table to UserSigningKey * Rename signedpublickeyownershipclaim to signedpublickey * Move signedPublicKey to last parameter * Add newline at end of file * Rename to signature key pair * Further rename to signaturekeypair * Rename to UserSignatureKeyPairRepository * Add newline * Rename more instances to UserSignatureKeyPair * Update parameter order * Fix order * Add more renames * Cleanup * Fix sql * Add ef migrations * Fix difference in SQL SP compared to migration SP * Fix difference in SQL SP vs migration * Fix difference in SQL SP vs migration * Attempt to fix sql * Rename migration to start later * Address feedback * Move UserSignatureKeyPair to KM codeownership * Fix build * Fix build * Fix build * Move out entitytypeconfiguration * Use view for reading usersignaturekeypairs * Fix migration script * Fix migration script * Drop view if exists * Enable nullable * Replace with create or alter view * Switch go generatecomb * Switch to generatecomb * Move signature algorithm * Move useresignaturekeypairentitytypeconfiguration to km ownership * Move userSignatureKeyPair model * Unswap file names * Move sql files to km ownership * Add index on userid for signature keys * Fix wrong filename * Remove string length limit * Regenerate EF migrations * Undo changes to program.cs * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Rename dbset to plural * Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * [PM-21034] Implement api changes to retreive signing keys (#5932) * Add signing key repositories, models, and sql migration scripts * Rename UserSigningKeys table to UserSigningKey * Rename signedpublickeyownershipclaim to signedpublickey * Move signedPublicKey to last parameter * Add newline at end of file * Rename to signature key pair * Further rename to signaturekeypair * Rename to UserSignatureKeyPairRepository * Add newline * Rename more instances to UserSignatureKeyPair * Update parameter order * Fix order * Add more renames * Cleanup * Fix sql * Add ef migrations * Fix difference in SQL SP compared to migration SP * Fix difference in SQL SP vs migration * Fix difference in SQL SP vs migration * Attempt to fix sql * Rename migration to start later * Address feedback * Move UserSignatureKeyPair to KM codeownership * Fix build * Fix build * Fix build * Move out entitytypeconfiguration * Use view for reading usersignaturekeypairs * Fix migration script * Fix migration script * Add initial get keys endpoint * Add sync response * Cleanup * Add query and fix types * Add tests and cleanup * Fix test * Drop view if exists * Add km queries * Cleanup * Enable nullable * Cleanup * Cleanup * Enable nullable * Fix incorrect namespace * Remove unused using * Fix test build * Fix build error * Fix build * Attempt to fix tests * Attempt to fix tests * Replace with create or alter view * Attempt to fix tests * Attempt to fix build * Rename to include async suffix * Fix test * Rename repo * Attempt to fix tests * Cleanup * Test * Undo test * Fix tests * Fix test * Switch go generatecomb * Switch to generatecomb * Move signature algorithm * Move useresignaturekeypairentitytypeconfiguration to km ownership * Move userSignatureKeyPair model * Unswap file names * Move sql files to km ownership * Add index on userid for signature keys * Fix wrong filename * Fix build * Remove string length limit * Regenerate EF migrations * Undo changes to program.cs * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Rename dbset to plural * Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Controllers/UsersController.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Cleanup and move query to core * Fix test * Fix build * Fix tests * Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Switch away from primary constructor * Use argumentNullException * Add test * Pass user account keys directly to profileresponsemodel * Move registration to core * Update src/Api/Startup.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/Startup.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Remove empty line * Apply suggestions * Fix tests * Fix tests --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * [PM-22384] Implement key-rotation based enrollment to user-crypto v2 (#5934) * Add signing key repositories, models, and sql migration scripts * Rename UserSigningKeys table to UserSigningKey * Rename signedpublickeyownershipclaim to signedpublickey * Move signedPublicKey to last parameter * Add newline at end of file * Rename to signature key pair * Further rename to signaturekeypair * Rename to UserSignatureKeyPairRepository * Add newline * Rename more instances to UserSignatureKeyPair * Update parameter order * Fix order * Add more renames * Cleanup * Fix sql * Add ef migrations * Fix difference in SQL SP compared to migration SP * Fix difference in SQL SP vs migration * Fix difference in SQL SP vs migration * Attempt to fix sql * Rename migration to start later * Address feedback * Move UserSignatureKeyPair to KM codeownership * Fix build * Fix build * Fix build * Move out entitytypeconfiguration * Use view for reading usersignaturekeypairs * Fix migration script * Fix migration script * Add initial get keys endpoint * Add sync response * Cleanup * Add query and fix types * Add tests and cleanup * Fix test * Drop view if exists * Add km queries * Cleanup * Enable nullable * Cleanup * Cleanup * Enable nullable * Fix incorrect namespace * Remove unused using * Fix test build * Fix build error * Fix build * Attempt to fix tests * Attempt to fix tests * Replace with create or alter view * Attempt to fix tests * Attempt to fix build * Rename to include async suffix * Fix test * Rename repo * Attempt to fix tests * Cleanup * Test * Undo test * Fix tests * Fix test * Switch go generatecomb * Switch to generatecomb * Move signature algorithm * Move useresignaturekeypairentitytypeconfiguration to km ownership * Move userSignatureKeyPair model * Unswap file names * Move sql files to km ownership * Add index on userid for signature keys * Fix wrong filename * Fix build * Remove string length limit * Regenerate EF migrations * Undo changes to program.cs * Cleanup * Add migration to user encryption v2 * Fix build * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Rename dbset to plural * Cleanup * Cleanup * Fix build * Fix test * Add validation * Fix test * Apply fixes * Fix tests * Improve tests * Add tests * Add error message validation * Fix tests * Fix tests * Fix test * Add test * Fix tests and errors * Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Controllers/UsersController.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Cleanup and move query to core * Fix test * Fix build * Fix tests * Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Switch away from primary constructor * Use argumentNullException * Add test * Pass user account keys directly to profileresponsemodel * Fix build * Fix namespace * Make signedpublickey optional * Remove unused file * Fix cases for request data conversion * Revert constructor change * Undo comments change * Apply fixes * Move registration to core * Update src/Api/Startup.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/Startup.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Remove empty line * Apply suggestions * Fix tests * Fix tests * Fix build of integration tests * Attempt to fix tests * Add test * Move v2 encryption user async below public functions * Add todo * Rename to have async suffix * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Address feedback * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Add test coverage * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Split up validation from rotation * Fix tests * Increase test coverage * Rename tests * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Add test for no signature keypair data * Fix build * Enable nullable * Fix build * Clean up data model * Fix tests * Cleanup --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Fix build * [PM-22862] Account security version (#5995) * Add signing key repositories, models, and sql migration scripts * Rename UserSigningKeys table to UserSigningKey * Rename signedpublickeyownershipclaim to signedpublickey * Move signedPublicKey to last parameter * Add newline at end of file * Rename to signature key pair * Further rename to signaturekeypair * Rename to UserSignatureKeyPairRepository * Add newline * Rename more instances to UserSignatureKeyPair * Update parameter order * Fix order * Add more renames * Cleanup * Fix sql * Add ef migrations * Fix difference in SQL SP compared to migration SP * Fix difference in SQL SP vs migration * Fix difference in SQL SP vs migration * Attempt to fix sql * Rename migration to start later * Address feedback * Move UserSignatureKeyPair to KM codeownership * Fix build * Fix build * Fix build * Move out entitytypeconfiguration * Use view for reading usersignaturekeypairs * Fix migration script * Fix migration script * Add initial get keys endpoint * Add sync response * Cleanup * Add query and fix types * Add tests and cleanup * Fix test * Drop view if exists * Add km queries * Cleanup * Enable nullable * Cleanup * Cleanup * Enable nullable * Fix incorrect namespace * Remove unused using * Fix test build * Fix build error * Fix build * Attempt to fix tests * Attempt to fix tests * Replace with create or alter view * Attempt to fix tests * Attempt to fix build * Rename to include async suffix * Fix test * Rename repo * Attempt to fix tests * Cleanup * Test * Undo test * Fix tests * Fix test * Switch go generatecomb * Switch to generatecomb * Move signature algorithm * Move useresignaturekeypairentitytypeconfiguration to km ownership * Move userSignatureKeyPair model * Unswap file names * Move sql files to km ownership * Add index on userid for signature keys * Fix wrong filename * Fix build * Remove string length limit * Regenerate EF migrations * Undo changes to program.cs * Cleanup * Add migration to user encryption v2 * Fix build * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Rename dbset to plural * Cleanup * Cleanup * Fix build * Fix test * Add validation * Fix test * Apply fixes * Fix tests * Improve tests * Add tests * Add error message validation * Fix tests * Fix tests * Fix test * Add test * Fix tests and errors * Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Controllers/UsersController.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Cleanup and move query to core * Fix test * Fix build * Fix tests * Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Switch away from primary constructor * Use argumentNullException * Add test * Pass user account keys directly to profileresponsemodel * Fix build * Fix namespace * Make signedpublickey optional * Remove unused file * Fix cases for request data conversion * Revert constructor change * Undo comments change * Apply fixes * Move registration to core * Update src/Api/Startup.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/Startup.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Remove empty line * Apply suggestions * Fix tests * Fix tests * Fix build of integration tests * Attempt to fix tests * Add test * Move v2 encryption user async below public functions * Add todo * Rename to have async suffix * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Address feedback * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Add test coverage * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Split up validation from rotation * Fix tests * Increase test coverage * Rename tests * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Add test for no signature keypair data * Fix build * Enable nullable * Fix build * Clean up data model * Fix tests * Merge branch 'km/signing-upgrade-rotation' into km/account-security-version * Add security state to rotation * Update tests * Update tests and check for security state in v2 model * Cleanup * Add tests * Add security state data to integration test * Re-sort and remove limit * Update migrations * Fix sql * Fix sql * Fix sql * Fix fixture * Fix test * Fix test * Fix test --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * [PM-22853] Add feature flag (#6090) * Add signing key repositories, models, and sql migration scripts * Rename UserSigningKeys table to UserSigningKey * Rename signedpublickeyownershipclaim to signedpublickey * Move signedPublicKey to last parameter * Add newline at end of file * Rename to signature key pair * Further rename to signaturekeypair * Rename to UserSignatureKeyPairRepository * Add newline * Rename more instances to UserSignatureKeyPair * Update parameter order * Fix order * Add more renames * Cleanup * Fix sql * Add ef migrations * Fix difference in SQL SP compared to migration SP * Fix difference in SQL SP vs migration * Fix difference in SQL SP vs migration * Attempt to fix sql * Rename migration to start later * Address feedback * Move UserSignatureKeyPair to KM codeownership * Fix build * Fix build * Fix build * Move out entitytypeconfiguration * Use view for reading usersignaturekeypairs * Fix migration script * Fix migration script * Add initial get keys endpoint * Add sync response * Cleanup * Add query and fix types * Add tests and cleanup * Fix test * Drop view if exists * Add km queries * Cleanup * Enable nullable * Cleanup * Cleanup * Enable nullable * Fix incorrect namespace * Remove unused using * Fix test build * Fix build error * Fix build * Attempt to fix tests * Attempt to fix tests * Replace with create or alter view * Attempt to fix tests * Attempt to fix build * Rename to include async suffix * Fix test * Rename repo * Attempt to fix tests * Cleanup * Test * Undo test * Fix tests * Fix test * Switch go generatecomb * Switch to generatecomb * Move signature algorithm * Move useresignaturekeypairentitytypeconfiguration to km ownership * Move userSignatureKeyPair model * Unswap file names * Move sql files to km ownership * Add index on userid for signature keys * Fix wrong filename * Fix build * Remove string length limit * Regenerate EF migrations * Undo changes to program.cs * Cleanup * Add migration to user encryption v2 * Fix build * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Rename dbset to plural * Cleanup * Cleanup * Fix build * Fix test * Add validation * Fix test * Apply fixes * Fix tests * Improve tests * Add tests * Add error message validation * Fix tests * Fix tests * Fix test * Add test * Fix tests and errors * Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Controllers/UsersController.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Cleanup and move query to core * Fix test * Fix build * Fix tests * Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Switch away from primary constructor * Use argumentNullException * Add test * Pass user account keys directly to profileresponsemodel * Fix build * Fix namespace * Make signedpublickey optional * Remove unused file * Fix cases for request data conversion * Revert constructor change * Undo comments change * Apply fixes * Move registration to core * Update src/Api/Startup.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/Startup.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Remove empty line * Apply suggestions * Fix tests * Fix tests * Fix build of integration tests * Attempt to fix tests * Add test * Move v2 encryption user async below public functions * Add todo * Rename to have async suffix * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Address feedback * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Add test coverage * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Split up validation from rotation * Fix tests * Increase test coverage * Rename tests * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Add test for no signature keypair data * Fix build * Enable nullable * Fix build * Clean up data model * Fix tests * Merge branch 'km/signing-upgrade-rotation' into km/account-security-version * Add security state to rotation * Update tests * Add feature flag * Update tests and check for security state in v2 model * Cleanup * Add tests * Add security state data to integration test * Re-sort and remove limit * Update migrations * Fix sql * Fix sql * Fix sql * Fix fixture * Fix test * Fix test * Fix test --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * [PM-23222] Update revision date on key rotation (#6038) * Add signing key repositories, models, and sql migration scripts * Rename UserSigningKeys table to UserSigningKey * Rename signedpublickeyownershipclaim to signedpublickey * Move signedPublicKey to last parameter * Add newline at end of file * Rename to signature key pair * Further rename to signaturekeypair * Rename to UserSignatureKeyPairRepository * Add newline * Rename more instances to UserSignatureKeyPair * Update parameter order * Fix order * Add more renames * Cleanup * Fix sql * Add ef migrations * Fix difference in SQL SP compared to migration SP * Fix difference in SQL SP vs migration * Fix difference in SQL SP vs migration * Attempt to fix sql * Rename migration to start later * Address feedback * Move UserSignatureKeyPair to KM codeownership * Fix build * Fix build * Fix build * Move out entitytypeconfiguration * Use view for reading usersignaturekeypairs * Fix migration script * Fix migration script * Add initial get keys endpoint * Add sync response * Cleanup * Add query and fix types * Add tests and cleanup * Fix test * Drop view if exists * Add km queries * Cleanup * Enable nullable * Cleanup * Cleanup * Enable nullable * Fix incorrect namespace * Remove unused using * Fix test build * Fix build error * Fix build * Attempt to fix tests * Attempt to fix tests * Replace with create or alter view * Attempt to fix tests * Attempt to fix build * Rename to include async suffix * Fix test * Rename repo * Attempt to fix tests * Cleanup * Test * Undo test * Fix tests * Fix test * Switch go generatecomb * Switch to generatecomb * Move signature algorithm * Move useresignaturekeypairentitytypeconfiguration to km ownership * Move userSignatureKeyPair model * Unswap file names * Move sql files to km ownership * Add index on userid for signature keys * Fix wrong filename * Fix build * Remove string length limit * Regenerate EF migrations * Undo changes to program.cs * Cleanup * Add migration to user encryption v2 * Fix build * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Rename dbset to plural * Cleanup * Cleanup * Fix build * Fix test * Add validation * Fix test * Apply fixes * Fix tests * Improve tests * Add tests * Add error message validation * Fix tests * Fix tests * Fix test * Add test * Fix tests and errors * Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Controllers/UsersController.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Cleanup and move query to core * Fix test * Fix build * Fix tests * Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Switch away from primary constructor * Use argumentNullException * Add test * Pass user account keys directly to profileresponsemodel * Fix build * Fix namespace * Make signedpublickey optional * Remove unused file * Fix cases for request data conversion * Revert constructor change * Undo comments change * Apply fixes * Move registration to core * Update src/Api/Startup.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/Startup.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Remove empty line * Apply suggestions * Fix tests * Fix tests * Fix build of integration tests * Attempt to fix tests * Add test * Move v2 encryption user async below public functions * Add todo * Rename to have async suffix * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Address feedback * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Add test coverage * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Split up validation from rotation * Fix tests * Increase test coverage * Rename tests * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Add test for no signature keypair data * Fix build * Enable nullable * Fix build * Clean up data model * Fix tests * Merge branch 'km/signing-upgrade-rotation' into km/account-security-version * Add security state to rotation * Update tests * Update revision date on key rotation * Update tests and check for security state in v2 model * Cleanup * Add tests * Add security state data to integration test * Re-sort and remove limit * Update migrations * Fix sql * Fix sql * Fix sql * Fix fixture * Fix test * Fix test * Fix test * Add test for change date --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Fix signing keys * Update sql migrations * Fix tests * Add keys to identity token response * Fix tests * Fix tests * Fix formatting * Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Controllers/UsersController.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Models/Requests/SignatureKeyPairRequestModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Models/Requests/PublicKeyEncryptionKeyPairRequestModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Repositories/IUserSignatureKeyPairRepository.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Models/Data/PublicKeyEncryptionKeyPairData.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Models/Data/SignatureKeyPairData.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Models/Data/SecurityStateData.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Models/Request/SecurityStateModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Models/Response/PrivateKeysResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Models/Response/PublicKeysResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Models/Response/PublicKeyEncryptionKeyPairResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Queries/Interfaces/IUserAcountKeysQuery.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Core/KeyManagement/Models/Response/SignatureKeyPairResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Remove unnecessary file * Add eof spacing * Move models * Fix build * Move models to API subdirectory * Rename model * Remove migrations * Add new ef migrations * Remove empty line * Only query account keys if the user has keys * Dotnet format * Fix test * Update test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Apply suggestion * Fix whitespace * Force camel case on response models * Address feedback for sql files * Fix build * Make index unique * Add contstraints * Fix sql * Fix order * Cleanup * Fix build * Update migrations * Update EF migrations * Change parameters to nvarchar * Update to Varchar * Apply feedback * Move refresh view * Attempt to fix build * Undo sql changes * Apply feedback about varchar * Apply feedback about refresh view * Apply feedback about new lines * Address SQL feedback * Re-sort columns * Fix build * Fix order * Fix build --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>
This commit is contained in:
@@ -18,6 +18,7 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Kdf;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -40,6 +41,7 @@ public class AccountsController : Controller
|
||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
||||
private readonly IChangeKdfCommand _changeKdfCommand;
|
||||
|
||||
@@ -53,6 +55,7 @@ public class AccountsController : Controller
|
||||
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IFeatureService featureService,
|
||||
IUserAccountKeysQuery userAccountKeysQuery,
|
||||
ITwoFactorEmailService twoFactorEmailService,
|
||||
IChangeKdfCommand changeKdfCommand
|
||||
)
|
||||
@@ -66,6 +69,7 @@ public class AccountsController : Controller
|
||||
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_featureService = featureService;
|
||||
_userAccountKeysQuery = userAccountKeysQuery;
|
||||
_twoFactorEmailService = twoFactorEmailService;
|
||||
_changeKdfCommand = changeKdfCommand;
|
||||
}
|
||||
@@ -332,7 +336,9 @@ public class AccountsController : Controller
|
||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
|
||||
var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
|
||||
var accountKeys = await _userAccountKeysQuery.Run(user);
|
||||
|
||||
var response = new ProfileResponseModel(user, accountKeys, organizationUserDetails, providerUserDetails,
|
||||
providerUserOrganizationDetails, twoFactorEnabled,
|
||||
hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
||||
return response;
|
||||
@@ -364,8 +370,9 @@ public class AccountsController : Controller
|
||||
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
var userAccountKeys = await _userAccountKeysQuery.Run(user);
|
||||
|
||||
var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
||||
var response = new ProfileResponseModel(user, userAccountKeys, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -389,8 +396,9 @@ public class AccountsController : Controller
|
||||
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
var accountKeys = await _userAccountKeysQuery.Run(user);
|
||||
|
||||
var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
||||
var response = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Response;
|
||||
@@ -8,6 +9,7 @@ using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@@ -21,7 +23,8 @@ namespace Bit.Api.Billing.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class AccountsController(
|
||||
IUserService userService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IUserAccountKeysQuery userAccountKeysQuery) : Controller
|
||||
{
|
||||
[HttpPost("premium")]
|
||||
public async Task<PaymentResponseModel> PostPremiumAsync(
|
||||
@@ -58,8 +61,9 @@ public class AccountsController(
|
||||
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
var accountKeys = await userAccountKeysQuery.Run(user);
|
||||
|
||||
var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled,
|
||||
var profile = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled,
|
||||
userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
||||
return new PaymentResponseModel
|
||||
{
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers;
|
||||
|
||||
[Route("users")]
|
||||
[Authorize("Application")]
|
||||
public class UsersController : Controller
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public UsersController(
|
||||
IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
[HttpGet("{id}/public-key")]
|
||||
public async Task<UserKeyResponseModel> Get(string id)
|
||||
{
|
||||
var guidId = new Guid(id);
|
||||
var key = await _userRepository.GetPublicKeyAsync(guidId);
|
||||
if (key == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new UserKeyResponseModel(guidId, key);
|
||||
}
|
||||
}
|
||||
@@ -106,8 +106,7 @@ public class AccountsKeyManagementController : Controller
|
||||
{
|
||||
OldMasterKeyAuthenticationHash = model.OldMasterKeyAuthenticationHash,
|
||||
|
||||
UserKeyEncryptedAccountPrivateKey = model.AccountKeys.UserKeyEncryptedAccountPrivateKey,
|
||||
AccountPublicKey = model.AccountKeys.AccountPublicKey,
|
||||
AccountKeys = model.AccountKeys.ToAccountKeysData(),
|
||||
|
||||
MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(),
|
||||
EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.AccountUnlockData.EmergencyAccessUnlockData),
|
||||
|
||||
39
src/Api/KeyManagement/Controllers/UsersController.cs
Normal file
39
src/Api/KeyManagement/Controllers/UsersController.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using UserKeyResponseModel = Bit.Api.Models.Response.UserKeyResponseModel;
|
||||
|
||||
|
||||
namespace Bit.Api.KeyManagement.Controllers;
|
||||
|
||||
[Route("users")]
|
||||
[Authorize("Application")]
|
||||
public class UsersController : Controller
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
|
||||
public UsersController(IUserRepository userRepository, IUserAccountKeysQuery userAccountKeysQuery)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_userAccountKeysQuery = userAccountKeysQuery;
|
||||
}
|
||||
|
||||
[HttpGet("{id}/public-key")]
|
||||
public async Task<UserKeyResponseModel> GetPublicKeyAsync([FromRoute] Guid id)
|
||||
{
|
||||
var key = await _userRepository.GetPublicKeyAsync(id) ?? throw new NotFoundException();
|
||||
return new UserKeyResponseModel(id, key);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/keys")]
|
||||
public async Task<PublicKeysResponseModel> GetAccountKeysAsync([FromRoute] Guid id)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(id) ?? throw new NotFoundException();
|
||||
var accountKeys = await _userAccountKeysQuery.Run(user) ?? throw new NotFoundException("User account keys not found.");
|
||||
return new PublicKeysResponseModel(accountKeys);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#nullable enable
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
@@ -7,4 +8,44 @@ public class AccountKeysRequestModel
|
||||
{
|
||||
[EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { get; set; }
|
||||
public required string AccountPublicKey { get; set; }
|
||||
|
||||
public PublicKeyEncryptionKeyPairRequestModel? PublicKeyEncryptionKeyPair { get; set; }
|
||||
public SignatureKeyPairRequestModel? SignatureKeyPair { get; set; }
|
||||
public SecurityStateModel? SecurityState { get; set; }
|
||||
|
||||
public UserAccountKeysData ToAccountKeysData()
|
||||
{
|
||||
// This will be cleaned up, after a compatibility period, at which point PublicKeyEncryptionKeyPair and SignatureKeyPair will be required.
|
||||
// TODO: https://bitwarden.atlassian.net/browse/PM-23751
|
||||
if (PublicKeyEncryptionKeyPair == null)
|
||||
{
|
||||
return new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData
|
||||
(
|
||||
UserKeyEncryptedAccountPrivateKey,
|
||||
AccountPublicKey
|
||||
),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
if (SignatureKeyPair == null || SecurityState == null)
|
||||
{
|
||||
return new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = PublicKeyEncryptionKeyPair.ToPublicKeyEncryptionKeyPairData(),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = PublicKeyEncryptionKeyPair.ToPublicKeyEncryptionKeyPairData(),
|
||||
SignatureKeyPairData = SignatureKeyPair.ToSignatureKeyPairData(),
|
||||
SecurityStateData = SecurityState.ToSecurityState()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
public class PublicKeyEncryptionKeyPairRequestModel
|
||||
{
|
||||
[EncryptedString] public required string WrappedPrivateKey { get; set; }
|
||||
public required string PublicKey { get; set; }
|
||||
public string? SignedPublicKey { get; set; }
|
||||
|
||||
public PublicKeyEncryptionKeyPairData ToPublicKeyEncryptionKeyPairData()
|
||||
{
|
||||
return new PublicKeyEncryptionKeyPairData(
|
||||
WrappedPrivateKey,
|
||||
PublicKey,
|
||||
SignedPublicKey
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
public class SignatureKeyPairRequestModel
|
||||
{
|
||||
public required string SignatureAlgorithm { get; set; }
|
||||
[EncryptedString] public required string WrappedSigningKey { get; set; }
|
||||
public required string VerifyingKey { get; set; }
|
||||
|
||||
public SignatureKeyPairData ToSignatureKeyPairData()
|
||||
{
|
||||
if (SignatureAlgorithm != "ed25519")
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Unsupported signature algorithm: {SignatureAlgorithm}"
|
||||
);
|
||||
}
|
||||
var algorithm = Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519;
|
||||
|
||||
return new SignatureKeyPairData(
|
||||
algorithm,
|
||||
WrappedSigningKey,
|
||||
VerifyingKey
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
@@ -5,6 +5,8 @@ using Bit.Api.AdminConsole.Models.Response;
|
||||
using Bit.Api.AdminConsole.Models.Response.Providers;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
@@ -13,6 +15,7 @@ namespace Bit.Api.Models.Response;
|
||||
public class ProfileResponseModel : ResponseModel
|
||||
{
|
||||
public ProfileResponseModel(User user,
|
||||
UserAccountKeysData userAccountKeysData,
|
||||
IEnumerable<OrganizationUserOrganizationDetails> organizationsUserDetails,
|
||||
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
|
||||
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
|
||||
@@ -35,6 +38,7 @@ public class ProfileResponseModel : ResponseModel
|
||||
TwoFactorEnabled = twoFactorEnabled;
|
||||
Key = user.Key;
|
||||
PrivateKey = user.PrivateKey;
|
||||
AccountKeys = userAccountKeysData != null ? new PrivateKeysResponseModel(userAccountKeysData) : null;
|
||||
SecurityStamp = user.SecurityStamp;
|
||||
ForcePasswordReset = user.ForcePasswordReset;
|
||||
UsesKeyConnector = user.UsesKeyConnector;
|
||||
@@ -60,7 +64,9 @@ public class ProfileResponseModel : ResponseModel
|
||||
public string Culture { get; set; }
|
||||
public bool TwoFactorEnabled { get; set; }
|
||||
public string Key { get; set; }
|
||||
[Obsolete("Use AccountKeys instead.")]
|
||||
public string PrivateKey { get; set; }
|
||||
public PrivateKeysResponseModel AccountKeys { get; set; }
|
||||
public string SecurityStamp { get; set; }
|
||||
public bool ForcePasswordReset { get; set; }
|
||||
public bool UsesKeyConnector { get; set; }
|
||||
|
||||
@@ -11,6 +11,8 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -42,6 +44,7 @@ public class SyncController : Controller
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
|
||||
public SyncController(
|
||||
IUserService userService,
|
||||
@@ -57,7 +60,8 @@ public class SyncController : Controller
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IUserAccountKeysQuery userAccountKeysQuery)
|
||||
{
|
||||
_userService = userService;
|
||||
_folderRepository = folderRepository;
|
||||
@@ -73,6 +77,7 @@ public class SyncController : Controller
|
||||
_featureService = featureService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_userAccountKeysQuery = userAccountKeysQuery;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@@ -116,7 +121,14 @@ public class SyncController : Controller
|
||||
|
||||
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
|
||||
var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
|
||||
UserAccountKeysData userAccountKeys = null;
|
||||
// JIT TDE users and some broken/old users may not have a private key.
|
||||
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
|
||||
{
|
||||
userAccountKeys = await _userAccountKeysQuery.Run(user);
|
||||
}
|
||||
|
||||
var response = new SyncResponseModel(_globalSettings, user, userAccountKeys, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
|
||||
organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
|
||||
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
||||
return response;
|
||||
|
||||
@@ -7,7 +7,8 @@ using Bit.Api.Tools.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Response;
|
||||
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
@@ -24,6 +25,7 @@ public class SyncResponseModel() : ResponseModel("sync")
|
||||
public SyncResponseModel(
|
||||
GlobalSettings globalSettings,
|
||||
User user,
|
||||
UserAccountKeysData userAccountKeysData,
|
||||
bool userTwoFactorEnabled,
|
||||
bool userHasPremiumFromOrganization,
|
||||
IDictionary<Guid, OrganizationAbility> organizationAbilities,
|
||||
@@ -40,7 +42,7 @@ public class SyncResponseModel() : ResponseModel("sync")
|
||||
IEnumerable<Send> sends)
|
||||
: this()
|
||||
{
|
||||
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
|
||||
Profile = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails,
|
||||
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);
|
||||
Folders = folders.Select(f => new FolderResponseModel(f));
|
||||
Ciphers = ciphers.Select(cipher =>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.KeyManagement.Models.Response;
|
||||
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Api.Response;
|
||||
|
||||
@@ -192,6 +192,7 @@ public static class FeatureFlagKeys
|
||||
public const string UserkeyRotationV2 = "userkey-rotation-v2";
|
||||
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
||||
public const string UserSdkForDecryption = "use-sdk-for-decryption";
|
||||
public const string EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation";
|
||||
public const string PM17987_BlockType0 = "pm-17987-block-type-0";
|
||||
public const string ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings";
|
||||
public const string UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data";
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Text.Json;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
@@ -21,6 +22,9 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
||||
[MaxLength(256)]
|
||||
public string Email { get; set; } = null!;
|
||||
public bool EmailVerified { get; set; }
|
||||
/// <summary>
|
||||
/// The server-side master-password hash
|
||||
/// </summary>
|
||||
[MaxLength(300)]
|
||||
public string? MasterPassword { get; set; }
|
||||
[MaxLength(50)]
|
||||
@@ -41,9 +45,30 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
||||
/// organization membership.
|
||||
/// </summary>
|
||||
public DateTime AccountRevisionDate { get; set; } = DateTime.UtcNow;
|
||||
/// <summary>
|
||||
/// The master-password-sealed user key.
|
||||
/// </summary>
|
||||
public string? Key { get; set; }
|
||||
/// <summary>
|
||||
/// The raw public key, without a signature from the user's signature key.
|
||||
/// </summary>
|
||||
public string? PublicKey { get; set; }
|
||||
/// <summary>
|
||||
/// User key wrapped private key.
|
||||
/// </summary>
|
||||
public string? PrivateKey { get; set; }
|
||||
/// <summary>
|
||||
/// The public key, signed by the user's signature key.
|
||||
/// </summary>
|
||||
public string? SignedPublicKey { get; set; }
|
||||
/// <summary>
|
||||
/// The security version is included in the security state, but needs COSE parsing
|
||||
/// </summary>
|
||||
public int? SecurityVersion { get; set; }
|
||||
/// <summary>
|
||||
/// The security state is a signed object attesting to the version of the user's account.
|
||||
/// </summary>
|
||||
public string? SecurityState { get; set; }
|
||||
public bool Premium { get; set; }
|
||||
public DateTime? PremiumExpirationDate { get; set; }
|
||||
public DateTime? RenewalReminderDate { get; set; }
|
||||
@@ -180,6 +205,12 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
||||
return Premium;
|
||||
}
|
||||
|
||||
public int GetSecurityVersion()
|
||||
{
|
||||
// If no security version is set, it is version 1. The minimum initialized version is 2.
|
||||
return SecurityVersion ?? 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the C# object to the User.TwoFactorProviders property in JSON format.
|
||||
/// </summary>
|
||||
@@ -243,4 +274,14 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
||||
{
|
||||
return MasterPassword != null;
|
||||
}
|
||||
|
||||
public PublicKeyEncryptionKeyPairData GetPublicKeyEncryptionKeyPair()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PrivateKey) || string.IsNullOrWhiteSpace(PublicKey))
|
||||
{
|
||||
throw new InvalidOperationException("User public key encryption key pair is not fully initialized.");
|
||||
}
|
||||
|
||||
return new PublicKeyEncryptionKeyPairData(PrivateKey, PublicKey, SignedPublicKey);
|
||||
}
|
||||
}
|
||||
|
||||
30
src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs
Normal file
30
src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
|
||||
namespace Bit.Core.KeyManagement.Entities;
|
||||
|
||||
public class UserSignatureKeyPair : ITableObject<Guid>, IRevisable
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public SignatureAlgorithm SignatureAlgorithm { get; set; }
|
||||
|
||||
public required string VerifyingKey { get; set; }
|
||||
public required string SigningKey { get; set; }
|
||||
|
||||
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
|
||||
public SignatureKeyPairData ToSignatureKeyPairData()
|
||||
{
|
||||
return new SignatureKeyPairData(SignatureAlgorithm, SigningKey, VerifyingKey);
|
||||
}
|
||||
}
|
||||
9
src/Core/KeyManagement/Enums/SignatureAlgorithm.cs
Normal file
9
src/Core/KeyManagement/Enums/SignatureAlgorithm.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.KeyManagement.Enums;
|
||||
|
||||
// <summary>
|
||||
// Represents the algorithm / digital signature scheme used for a signature key pair.
|
||||
// </summary>
|
||||
public enum SignatureAlgorithm : byte
|
||||
{
|
||||
Ed25519 = 0
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||
using Bit.Core.KeyManagement.Kdf;
|
||||
using Bit.Core.KeyManagement.Kdf.Implementations;
|
||||
using Bit.Core.KeyManagement.Queries;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.KeyManagement;
|
||||
@@ -11,6 +13,7 @@ public static class KeyManagementServiceCollectionExtensions
|
||||
public static void AddKeyManagementServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddKeyManagementCommands();
|
||||
services.AddKeyManagementQueries();
|
||||
services.AddSendPasswordServices();
|
||||
}
|
||||
|
||||
@@ -19,4 +22,9 @@ public static class KeyManagementServiceCollectionExtensions
|
||||
services.AddScoped<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>();
|
||||
services.AddScoped<IChangeKdfCommand, ChangeKdfCommand>();
|
||||
}
|
||||
|
||||
private static void AddKeyManagementQueries(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IUserAccountKeysQuery, UserAccountKeysQuery>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Request;
|
||||
|
||||
public class SecurityStateModel
|
||||
{
|
||||
[StringLength(1000)]
|
||||
[JsonPropertyName("securityState")]
|
||||
public required string SecurityState { get; set; }
|
||||
[JsonPropertyName("securityVersion")]
|
||||
public required int SecurityVersion { get; set; }
|
||||
|
||||
public SecurityStateData ToSecurityState()
|
||||
{
|
||||
return new SecurityStateData
|
||||
{
|
||||
SecurityState = SecurityState,
|
||||
SecurityVersion = SecurityVersion
|
||||
};
|
||||
}
|
||||
|
||||
public static SecurityStateModel FromSecurityStateData(SecurityStateData data)
|
||||
{
|
||||
return new SecurityStateModel
|
||||
{
|
||||
SecurityState = data.SecurityState,
|
||||
SecurityVersion = data.SecurityVersion
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Response;
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Response;
|
||||
|
||||
public class MasterPasswordUnlockResponseModel
|
||||
{
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Response;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This response model is used to return the asymmetric encryption keys,
|
||||
/// and signature keys of an entity. This includes the private keys of the key pairs,
|
||||
/// (private key, signing key), and the public keys of the key pairs (unsigned public key,
|
||||
/// signed public key, verification key).
|
||||
/// </summary>
|
||||
public class PrivateKeysResponseModel : ResponseModel
|
||||
{
|
||||
// Not all accounts have signature keys, but all accounts have public encryption keys.
|
||||
[JsonPropertyName("signatureKeyPair")]
|
||||
public SignatureKeyPairResponseModel? SignatureKeyPair { get; set; }
|
||||
|
||||
[JsonPropertyName("publicKeyEncryptionKeyPair")]
|
||||
public required PublicKeyEncryptionKeyPairResponseModel PublicKeyEncryptionKeyPair { get; set; }
|
||||
|
||||
[JsonPropertyName("securityState")]
|
||||
public SecurityStateModel? SecurityState { get; set; }
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]
|
||||
public PrivateKeysResponseModel(UserAccountKeysData accountKeys) : base("privateKeys")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(accountKeys);
|
||||
PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairResponseModel(accountKeys.PublicKeyEncryptionKeyPairData);
|
||||
|
||||
if (accountKeys.SignatureKeyPairData != null && accountKeys.SecurityStateData != null)
|
||||
{
|
||||
SignatureKeyPair = new SignatureKeyPairResponseModel(accountKeys.SignatureKeyPairData);
|
||||
SecurityState = SecurityStateModel.FromSecurityStateData(accountKeys.SecurityStateData!);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public PrivateKeysResponseModel(SignatureKeyPairResponseModel? signatureKeyPair, PublicKeyEncryptionKeyPairResponseModel publicKeyEncryptionKeyPair, SecurityStateModel? securityState)
|
||||
: base("privateKeys")
|
||||
{
|
||||
SignatureKeyPair = signatureKeyPair;
|
||||
PublicKeyEncryptionKeyPair = publicKeyEncryptionKeyPair ?? throw new ArgumentNullException(nameof(publicKeyEncryptionKeyPair));
|
||||
SecurityState = securityState;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Response;
|
||||
|
||||
|
||||
public class PublicKeyEncryptionKeyPairResponseModel : ResponseModel
|
||||
{
|
||||
[JsonPropertyName("wrappedPrivateKey")]
|
||||
public required string WrappedPrivateKey { get; set; }
|
||||
[JsonPropertyName("publicKey")]
|
||||
public required string PublicKey { get; set; }
|
||||
[JsonPropertyName("signedPublicKey")]
|
||||
public string? SignedPublicKey { get; set; }
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]
|
||||
public PublicKeyEncryptionKeyPairResponseModel(PublicKeyEncryptionKeyPairData keyPair)
|
||||
: base("publicKeyEncryptionKeyPair")
|
||||
{
|
||||
WrappedPrivateKey = keyPair.WrappedPrivateKey;
|
||||
PublicKey = keyPair.PublicKey;
|
||||
SignedPublicKey = keyPair.SignedPublicKey;
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public PublicKeyEncryptionKeyPairResponseModel(string wrappedPrivateKey, string publicKey, string? signedPublicKey)
|
||||
: base("publicKeyEncryptionKeyPair")
|
||||
{
|
||||
WrappedPrivateKey = wrappedPrivateKey ?? throw new ArgumentNullException(nameof(wrappedPrivateKey));
|
||||
PublicKey = publicKey ?? throw new ArgumentNullException(nameof(publicKey));
|
||||
SignedPublicKey = signedPublicKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Response;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This response model is used to return the public keys of a user, to any other registered user or entity on the server.
|
||||
/// It can contain public keys (signature/encryption), and proofs between the two. It does not contain (encrypted) private keys.
|
||||
/// </summary>
|
||||
public class PublicKeysResponseModel : ResponseModel
|
||||
{
|
||||
[System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]
|
||||
public PublicKeysResponseModel(UserAccountKeysData accountKeys)
|
||||
: base("publicKeys")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(accountKeys);
|
||||
PublicKey = accountKeys.PublicKeyEncryptionKeyPairData.PublicKey;
|
||||
|
||||
if (accountKeys.SignatureKeyPairData != null)
|
||||
{
|
||||
SignedPublicKey = accountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey;
|
||||
VerifyingKey = accountKeys.SignatureKeyPairData.VerifyingKey;
|
||||
}
|
||||
}
|
||||
|
||||
public string? VerifyingKey { get; set; }
|
||||
public string? SignedPublicKey { get; set; }
|
||||
public required string PublicKey { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Response;
|
||||
|
||||
|
||||
public class SignatureKeyPairResponseModel : ResponseModel
|
||||
{
|
||||
[JsonPropertyName("wrappedSigningKey")]
|
||||
public required string WrappedSigningKey { get; set; }
|
||||
[JsonPropertyName("verifyingKey")]
|
||||
public required string VerifyingKey { get; set; }
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]
|
||||
public SignatureKeyPairResponseModel(SignatureKeyPairData signatureKeyPair)
|
||||
: base("signatureKeyPair")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(signatureKeyPair);
|
||||
WrappedSigningKey = signatureKeyPair.WrappedSigningKey;
|
||||
VerifyingKey = signatureKeyPair.VerifyingKey;
|
||||
}
|
||||
|
||||
|
||||
[JsonConstructor]
|
||||
public SignatureKeyPairResponseModel(string wrappedSigningKey, string verifyingKey)
|
||||
: base("signatureKeyPair")
|
||||
{
|
||||
WrappedSigningKey = wrappedSigningKey ?? throw new ArgumentNullException(nameof(wrappedSigningKey));
|
||||
VerifyingKey = verifyingKey ?? throw new ArgumentNullException(nameof(verifyingKey));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Bit.Core.KeyManagement.Models.Response;
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Response;
|
||||
|
||||
public class UserDecryptionResponseModel
|
||||
{
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
|
||||
public class PublicKeyEncryptionKeyPairData
|
||||
{
|
||||
public required string WrappedPrivateKey { get; set; }
|
||||
public string? SignedPublicKey { get; set; }
|
||||
public required string PublicKey { get; set; }
|
||||
|
||||
[JsonConstructor]
|
||||
[System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]
|
||||
public PublicKeyEncryptionKeyPairData(string wrappedPrivateKey, string publicKey, string? signedPublicKey = null)
|
||||
{
|
||||
WrappedPrivateKey = wrappedPrivateKey ?? throw new ArgumentNullException(nameof(wrappedPrivateKey));
|
||||
PublicKey = publicKey ?? throw new ArgumentNullException(nameof(publicKey));
|
||||
SignedPublicKey = signedPublicKey;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
@@ -12,21 +10,19 @@ namespace Bit.Core.KeyManagement.Models.Data;
|
||||
public class RotateUserAccountKeysData
|
||||
{
|
||||
// Authentication for this requests
|
||||
public string OldMasterKeyAuthenticationHash { get; set; }
|
||||
public required string OldMasterKeyAuthenticationHash { get; set; }
|
||||
|
||||
// Other keys encrypted by the userkey
|
||||
public string UserKeyEncryptedAccountPrivateKey { get; set; }
|
||||
public string AccountPublicKey { get; set; }
|
||||
public required UserAccountKeysData AccountKeys { get; set; }
|
||||
|
||||
// All methods to get to the userkey
|
||||
public MasterPasswordUnlockAndAuthenticationData MasterPasswordUnlockData { get; set; }
|
||||
public IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; }
|
||||
public IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
|
||||
public IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; }
|
||||
public IEnumerable<Device> DeviceKeys { get; set; }
|
||||
public required MasterPasswordUnlockAndAuthenticationData MasterPasswordUnlockData { get; set; }
|
||||
public required IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; }
|
||||
public required IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
|
||||
public required IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; }
|
||||
public required IEnumerable<Device> DeviceKeys { get; set; }
|
||||
|
||||
// User vault data encrypted by the userkey
|
||||
public IEnumerable<Cipher> Ciphers { get; set; }
|
||||
public IEnumerable<Folder> Folders { get; set; }
|
||||
public IReadOnlyList<Send> Sends { get; set; }
|
||||
public required IEnumerable<Cipher> Ciphers { get; set; }
|
||||
public required IEnumerable<Folder> Folders { get; set; }
|
||||
public required IReadOnlyList<Send> Sends { get; set; }
|
||||
}
|
||||
|
||||
10
src/Core/KeyManagement/Models/Data/SecurityStateData.cs
Normal file
10
src/Core/KeyManagement/Models/Data/SecurityStateData.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
public class SecurityStateData
|
||||
{
|
||||
public required string SecurityState { get; set; }
|
||||
// The security version is included in the security state, but needs COSE parsing,
|
||||
// so this is a separate copy that can be used directly.
|
||||
public required int SecurityVersion { get; set; }
|
||||
}
|
||||
21
src/Core/KeyManagement/Models/Data/SignatureKeyPairData.cs
Normal file
21
src/Core/KeyManagement/Models/Data/SignatureKeyPairData.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.KeyManagement.Enums;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
public class SignatureKeyPairData
|
||||
{
|
||||
public required SignatureAlgorithm SignatureAlgorithm { get; set; }
|
||||
public required string WrappedSigningKey { get; set; }
|
||||
public required string VerifyingKey { get; set; }
|
||||
|
||||
[JsonConstructor]
|
||||
[System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]
|
||||
public SignatureKeyPairData(SignatureAlgorithm signatureAlgorithm, string wrappedSigningKey, string verifyingKey)
|
||||
{
|
||||
SignatureAlgorithm = signatureAlgorithm;
|
||||
WrappedSigningKey = wrappedSigningKey ?? throw new ArgumentNullException(nameof(wrappedSigningKey));
|
||||
VerifyingKey = verifyingKey ?? throw new ArgumentNullException(nameof(verifyingKey));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
|
||||
public class UserAccountKeysData
|
||||
{
|
||||
public required PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPairData { get; set; }
|
||||
public SignatureKeyPairData? SignatureKeyPairData { get; set; }
|
||||
public SecurityStateData? SecurityStateData { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
|
||||
public interface IUserAccountKeysQuery
|
||||
{
|
||||
Task<UserAccountKeysData> Run(User user);
|
||||
}
|
||||
35
src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs
Normal file
35
src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Queries;
|
||||
|
||||
|
||||
public class UserAccountKeysQuery(IUserSignatureKeyPairRepository signatureKeyPairRepository) : IUserAccountKeysQuery
|
||||
{
|
||||
public async Task<UserAccountKeysData> Run(User user)
|
||||
{
|
||||
if (user.GetSecurityVersion() < 2)
|
||||
{
|
||||
return new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = await signatureKeyPairRepository.GetByUserIdAsync(user.Id),
|
||||
SecurityStateData = new SecurityStateData
|
||||
{
|
||||
SecurityState = user.SecurityState!,
|
||||
SecurityVersion = user.GetSecurityVersion(),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
using Bit.Core.KeyManagement.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Repositories;
|
||||
|
||||
public interface IUserSignatureKeyPairRepository : IRepository<UserSignatureKeyPair, Guid>
|
||||
{
|
||||
public Task<SignatureKeyPairData?> GetByUserIdAsync(Guid userId);
|
||||
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signatureKeyPair);
|
||||
public UpdateEncryptedDataForKeyRotation SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signatureKeyPair);
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
using Bit.Core.Auth.Repositories;
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -25,6 +30,8 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
private readonly IdentityErrorDescriber _identityErrorDescriber;
|
||||
private readonly IWebAuthnCredentialRepository _credentialRepository;
|
||||
private readonly IPasswordHasher<User> _passwordHasher;
|
||||
private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a new <see cref="RotateUserAccountKeysCommand"/>
|
||||
@@ -36,16 +43,19 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
/// <param name="sendRepository">Provides a method to update re-encrypted send data</param>
|
||||
/// <param name="emergencyAccessRepository">Provides a method to update re-encrypted emergency access data</param>
|
||||
/// <param name="organizationUserRepository">Provides a method to update re-encrypted organization user data</param>
|
||||
/// <param name="deviceRepository">Provides a method to update re-encrypted device keys</param>
|
||||
/// <param name="passwordHasher">Hashes the new master password</param>
|
||||
/// <param name="pushService">Logs out user from other devices after successful rotation</param>
|
||||
/// <param name="errors">Provides a password mismatch error if master password hash validation fails</param>
|
||||
/// <param name="credentialRepository">Provides a method to update re-encrypted WebAuthn keys</param>
|
||||
/// <param name="userSignatureKeyPairRepository">Provides a method to update re-encrypted signature keys</param>
|
||||
public RotateUserAccountKeysCommand(IUserService userService, IUserRepository userRepository,
|
||||
ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository,
|
||||
IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository,
|
||||
IDeviceRepository deviceRepository,
|
||||
IPasswordHasher<User> passwordHasher,
|
||||
IPushNotificationService pushService, IdentityErrorDescriber errors, IWebAuthnCredentialRepository credentialRepository,
|
||||
IUserSignatureKeyPairRepository userSignatureKeyPairRepository,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_userService = userService;
|
||||
@@ -60,6 +70,8 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
_identityErrorDescriber = errors;
|
||||
_credentialRepository = credentialRepository;
|
||||
_passwordHasher = passwordHasher;
|
||||
_userSignatureKeyPairRepository = userSignatureKeyPairRepository;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -80,50 +92,106 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
user.LastKeyRotationDate = now;
|
||||
user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
|
||||
if (
|
||||
!model.MasterPasswordUnlockData.ValidateForUser(user)
|
||||
)
|
||||
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = [];
|
||||
|
||||
await UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);
|
||||
UpdateUnlockMethods(model, user, saveEncryptedDataActions);
|
||||
UpdateUserData(model, user, saveEncryptedDataActions);
|
||||
|
||||
await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions);
|
||||
await _pushService.PushLogOutAsync(user.Id);
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
public async Task RotateV2AccountKeysAsync(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
{
|
||||
ValidateV2Encryption(model);
|
||||
await ValidateVerifyingKeyUnchangedAsync(model, user);
|
||||
|
||||
saveEncryptedDataActions.Add(_userSignatureKeyPairRepository.UpdateForKeyRotation(user.Id, model.AccountKeys.SignatureKeyPairData));
|
||||
user.SignedPublicKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey;
|
||||
user.SecurityState = model.AccountKeys.SecurityStateData!.SecurityState;
|
||||
user.SecurityVersion = model.AccountKeys.SecurityStateData.SecurityVersion;
|
||||
}
|
||||
|
||||
public void UpgradeV1ToV2Keys(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
{
|
||||
ValidateV2Encryption(model);
|
||||
saveEncryptedDataActions.Add(_userSignatureKeyPairRepository.SetUserSignatureKeyPair(user.Id, model.AccountKeys.SignatureKeyPairData));
|
||||
user.SignedPublicKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey;
|
||||
user.SecurityState = model.AccountKeys.SecurityStateData!.SecurityState;
|
||||
user.SecurityVersion = model.AccountKeys.SecurityStateData.SecurityVersion;
|
||||
}
|
||||
|
||||
public async Task UpdateAccountKeysAsync(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
{
|
||||
ValidatePublicKeyEncryptionKeyPairUnchanged(model, user);
|
||||
|
||||
if (IsV2EncryptionUserAsync(user))
|
||||
{
|
||||
throw new InvalidOperationException("The provided master password unlock data is not valid for this user.");
|
||||
await RotateV2AccountKeysAsync(model, user, saveEncryptedDataActions);
|
||||
}
|
||||
if (
|
||||
model.AccountPublicKey != user.PublicKey
|
||||
)
|
||||
else if (model.AccountKeys.SignatureKeyPairData != null)
|
||||
{
|
||||
throw new InvalidOperationException("The provided account public key does not match the user's current public key, and changing the account asymmetric keypair is currently not supported during key rotation.");
|
||||
UpgradeV1ToV2Keys(model, user, saveEncryptedDataActions);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.AesCbc256_HmacSha256_B64)
|
||||
{
|
||||
throw new InvalidOperationException("The provided account private key was not wrapped with AES-256-CBC-HMAC");
|
||||
}
|
||||
// V1 user to V1 user rotation needs to further changes, the private key was re-encrypted.
|
||||
}
|
||||
|
||||
user.Key = model.MasterPasswordUnlockData.MasterKeyEncryptedUserKey;
|
||||
user.PrivateKey = model.UserKeyEncryptedAccountPrivateKey;
|
||||
user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordUnlockData.MasterKeyAuthenticationHash);
|
||||
user.MasterPasswordHint = model.MasterPasswordUnlockData.MasterPasswordHint;
|
||||
// Private key is re-wrapped with new user key by client
|
||||
user.PrivateKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey;
|
||||
}
|
||||
|
||||
public void UpdateUserData(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
{
|
||||
// The revision date has to be updated so that de-synced clients don't accidentally post over the re-encrypted data
|
||||
// with an old-user key-encrypted copy
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new();
|
||||
if (model.Ciphers.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers));
|
||||
var ciphersWithUpdatedDate = model.Ciphers.ToList().Select(c => { c.RevisionDate = now; return c; });
|
||||
saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, ciphersWithUpdatedDate));
|
||||
}
|
||||
|
||||
if (model.Folders.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, model.Folders));
|
||||
var foldersWithUpdatedDate = model.Folders.ToList().Select(f => { f.RevisionDate = now; return f; });
|
||||
saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, foldersWithUpdatedDate));
|
||||
}
|
||||
|
||||
if (model.Sends.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, model.Sends));
|
||||
var sendsWithUpdatedDate = model.Sends.ToList().Select(s => { s.RevisionDate = now; return s; });
|
||||
saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, sendsWithUpdatedDate));
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateUnlockMethods(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
{
|
||||
if (!model.MasterPasswordUnlockData.ValidateForUser(user))
|
||||
{
|
||||
throw new InvalidOperationException("The provided master password unlock data is not valid for this user.");
|
||||
}
|
||||
// Update master password authentication & unlock
|
||||
user.Key = model.MasterPasswordUnlockData.MasterKeyEncryptedUserKey;
|
||||
user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordUnlockData.MasterKeyAuthenticationHash);
|
||||
user.MasterPasswordHint = model.MasterPasswordUnlockData.MasterPasswordHint;
|
||||
|
||||
if (model.EmergencyAccesses.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(
|
||||
_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses));
|
||||
saveEncryptedDataActions.Add(_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses));
|
||||
}
|
||||
|
||||
if (model.OrganizationUsers.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(
|
||||
_organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers));
|
||||
saveEncryptedDataActions.Add(_organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers));
|
||||
}
|
||||
|
||||
if (model.WebAuthnKeys.Any())
|
||||
@@ -135,9 +203,80 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
{
|
||||
saveEncryptedDataActions.Add(_deviceRepository.UpdateKeysForRotationAsync(user.Id, model.DeviceKeys));
|
||||
}
|
||||
}
|
||||
|
||||
await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions);
|
||||
await _pushService.PushLogOutAsync(user.Id);
|
||||
return IdentityResult.Success;
|
||||
private bool IsV2EncryptionUserAsync(User user)
|
||||
{
|
||||
// Returns whether the user is a V2 user based on the private key's encryption type.
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
var isPrivateKeyEncryptionV2 = GetEncryptionType(user.PrivateKey) == EncryptionType.XChaCha20Poly1305_B64;
|
||||
return isPrivateKeyEncryptionV2;
|
||||
}
|
||||
|
||||
private async Task ValidateVerifyingKeyUnchangedAsync(RotateUserAccountKeysData model, User user)
|
||||
{
|
||||
var currentSignatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id) ?? throw new InvalidOperationException("User does not have a signature key pair.");
|
||||
if (model.AccountKeys.SignatureKeyPairData.VerifyingKey != currentSignatureKeyPair!.VerifyingKey)
|
||||
{
|
||||
throw new InvalidOperationException("The provided verifying key does not match the user's current verifying key.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidatePublicKeyEncryptionKeyPairUnchanged(RotateUserAccountKeysData model, User user)
|
||||
{
|
||||
var publicKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey;
|
||||
if (publicKey != user.PublicKey)
|
||||
{
|
||||
throw new InvalidOperationException("The provided account public key does not match the user's current public key, and changing the account asymmetric key pair is currently not supported during key rotation.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateV2Encryption(RotateUserAccountKeysData model)
|
||||
{
|
||||
if (model.AccountKeys.SignatureKeyPairData == null)
|
||||
{
|
||||
throw new InvalidOperationException("Signature key pair data is required for V2 encryption.");
|
||||
}
|
||||
if (GetEncryptionType(model.AccountKeys.SignatureKeyPairData.WrappedSigningKey) != EncryptionType.XChaCha20Poly1305_B64)
|
||||
{
|
||||
throw new InvalidOperationException("The provided signing key data is not wrapped with XChaCha20-Poly1305.");
|
||||
}
|
||||
if (string.IsNullOrEmpty(model.AccountKeys.SignatureKeyPairData.VerifyingKey))
|
||||
{
|
||||
throw new InvalidOperationException("The provided signature key pair data does not contain a valid verifying key.");
|
||||
}
|
||||
|
||||
if (GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.XChaCha20Poly1305_B64)
|
||||
{
|
||||
throw new InvalidOperationException("The provided private key encryption key is not wrapped with XChaCha20-Poly1305.");
|
||||
}
|
||||
if (string.IsNullOrEmpty(model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey))
|
||||
{
|
||||
throw new InvalidOperationException("No signed public key provided, but the user already has a signature key pair.");
|
||||
}
|
||||
if (model.AccountKeys.SecurityStateData == null || string.IsNullOrEmpty(model.AccountKeys.SecurityStateData.SecurityState))
|
||||
{
|
||||
throw new InvalidOperationException("No signed security state provider for V2 user");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to convert an encryption type string to an enum value.
|
||||
/// </summary>
|
||||
private static EncryptionType GetEncryptionType(string encString)
|
||||
{
|
||||
var parts = encString.Split('.');
|
||||
if (parts.Length == 1)
|
||||
{
|
||||
throw new ArgumentException("Invalid encryption type string.");
|
||||
}
|
||||
if (byte.TryParse(parts[0], out var encryptionTypeNumber))
|
||||
{
|
||||
if (Enum.IsDefined(typeof(EncryptionType), encryptionTypeNumber))
|
||||
{
|
||||
return (EncryptionType)encryptionTypeNumber;
|
||||
}
|
||||
}
|
||||
throw new ArgumentException("Invalid encryption type string.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -45,6 +47,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
protected IUserService _userService { get; }
|
||||
protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }
|
||||
protected IPolicyRequirementQuery PolicyRequirementQuery { get; }
|
||||
protected IUserAccountKeysQuery _accountKeysQuery { get; }
|
||||
|
||||
public BaseRequestValidator(
|
||||
UserManager<User> userManager,
|
||||
@@ -63,7 +66,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IMailService mailService
|
||||
IMailService mailService,
|
||||
IUserAccountKeysQuery userAccountKeysQuery
|
||||
)
|
||||
{
|
||||
_userManager = userManager;
|
||||
@@ -83,6 +87,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
PolicyRequirementQuery = policyRequirementQuery;
|
||||
_authRequestRepository = authRequestRepository;
|
||||
_mailService = mailService;
|
||||
_accountKeysQuery = userAccountKeysQuery;
|
||||
}
|
||||
|
||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||
@@ -439,6 +444,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
|
||||
{
|
||||
customResponse.Add("PrivateKey", user.PrivateKey);
|
||||
var accountKeys = await _accountKeysQuery.Run(user);
|
||||
customResponse.Add("AccountKeys", new PrivateKeysResponseModel(accountKeys));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(user.Key))
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.Auth.Models.Api.Response;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -47,7 +48,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
IUpdateInstallationCommand updateInstallationCommand,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IMailService mailService)
|
||||
IMailService mailService,
|
||||
IUserAccountKeysQuery userAccountKeysQuery)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
@@ -65,7 +67,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
userDecryptionOptionsBuilder,
|
||||
policyRequirementQuery,
|
||||
authRequestRepository,
|
||||
mailService)
|
||||
mailService,
|
||||
userAccountKeysQuery)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_updateInstallationCommand = updateInstallationCommand;
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@@ -41,7 +42,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IMailService mailService)
|
||||
IMailService mailService,
|
||||
IUserAccountKeysQuery userAccountKeysQuery)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
@@ -59,7 +61,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
userDecryptionOptionsBuilder,
|
||||
policyRequirementQuery,
|
||||
authRequestRepository,
|
||||
mailService)
|
||||
mailService,
|
||||
userAccountKeysQuery)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_currentContext = currentContext;
|
||||
|
||||
@@ -12,6 +12,7 @@ using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@@ -50,7 +51,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IMailService mailService)
|
||||
IMailService mailService,
|
||||
IUserAccountKeysQuery userAccountKeysQuery)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
@@ -68,7 +70,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
userDecryptionOptionsBuilder,
|
||||
policyRequirementQuery,
|
||||
authRequestRepository,
|
||||
mailService)
|
||||
mailService,
|
||||
userAccountKeysQuery)
|
||||
{
|
||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||
|
||||
@@ -6,7 +6,7 @@ using Bit.Core.Auth.Utilities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Response;
|
||||
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
@@ -71,6 +71,7 @@ public static class DapperServiceCollectionExtensions
|
||||
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
|
||||
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
|
||||
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
|
||||
services.AddSingleton<IUserSignatureKeyPairRepository, UserSignatureKeyPairRepository>();
|
||||
services.AddSingleton<IOrganizationReportRepository, OrganizationReportRepository>();
|
||||
services.AddSingleton<IOrganizationApplicationRepository, OrganizationApplicationRepository>();
|
||||
services.AddSingleton<IOrganizationMemberBaseDetailRepository, OrganizationMemberBaseDetailRepository>();
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Data;
|
||||
using Bit.Core.KeyManagement.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Infrastructure.Dapper.Repositories;
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Bit.Infrastructure.Dapper.KeyManagement.Repositories;
|
||||
|
||||
public class UserSignatureKeyPairRepository : Repository<UserSignatureKeyPair, Guid>, IUserSignatureKeyPairRepository
|
||||
{
|
||||
public UserSignatureKeyPairRepository(GlobalSettings globalSettings)
|
||||
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
|
||||
{
|
||||
}
|
||||
|
||||
public UserSignatureKeyPairRepository(string connectionString, string readOnlyConnectionString) : base(
|
||||
connectionString, readOnlyConnectionString)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<SignatureKeyPairData?> GetByUserIdAsync(Guid userId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
return (await connection.QuerySingleOrDefaultAsync<UserSignatureKeyPair>(
|
||||
"[dbo].[UserSignatureKeyPair_ReadByUserId]",
|
||||
new
|
||||
{
|
||||
UserId = userId
|
||||
},
|
||||
commandType: CommandType.StoredProcedure))?.ToSignatureKeyPairData();
|
||||
}
|
||||
}
|
||||
|
||||
public UpdateEncryptedDataForKeyRotation SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signingKeys)
|
||||
{
|
||||
return async (SqlConnection connection, SqlTransaction transaction) =>
|
||||
{
|
||||
await connection.QueryAsync(
|
||||
"[dbo].[UserSignatureKeyPair_SetForRotation]",
|
||||
new
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb(),
|
||||
UserId = userId,
|
||||
SignatureAlgorithm = (byte)signingKeys.SignatureAlgorithm,
|
||||
SigningKey = signingKeys.WrappedSigningKey,
|
||||
VerifyingKey = signingKeys.VerifyingKey,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow
|
||||
},
|
||||
commandType: CommandType.StoredProcedure,
|
||||
transaction: transaction);
|
||||
};
|
||||
}
|
||||
|
||||
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signingKeys)
|
||||
{
|
||||
return async (SqlConnection connection, SqlTransaction transaction) =>
|
||||
{
|
||||
await connection.QueryAsync(
|
||||
"[dbo].[UserSignatureKeyPair_UpdateForRotation]",
|
||||
new
|
||||
{
|
||||
UserId = grantorId,
|
||||
SignatureAlgorithm = (byte)signingKeys.SignatureAlgorithm,
|
||||
SigningKey = signingKeys.WrappedSigningKey,
|
||||
VerifyingKey = signingKeys.VerifyingKey,
|
||||
RevisionDate = DateTime.UtcNow
|
||||
},
|
||||
commandType: CommandType.StoredProcedure,
|
||||
transaction: transaction);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,7 @@ public static class EntityFrameworkServiceCollectionExtensions
|
||||
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
|
||||
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
|
||||
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
|
||||
services.AddSingleton<IUserSignatureKeyPairRepository, UserSignatureKeyPairRepository>();
|
||||
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
|
||||
services.AddSingleton<IOrganizationReportRepository, OrganizationReportRepository>();
|
||||
services.AddSingleton<IOrganizationApplicationRepository, OrganizationApplicationRepository>();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Configurations;
|
||||
|
||||
public class UserSignatureKeyPairEntityTypeConfiguration : IEntityTypeConfiguration<UserSignatureKeyPair>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UserSignatureKeyPair> builder)
|
||||
{
|
||||
builder
|
||||
.Property(s => s.Id)
|
||||
.ValueGeneratedNever();
|
||||
|
||||
builder
|
||||
.HasIndex(s => s.UserId)
|
||||
.IsUnique()
|
||||
.IsClustered(false);
|
||||
|
||||
builder.ToTable(nameof(UserSignatureKeyPair));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using AutoMapper;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Models;
|
||||
|
||||
public class UserSignatureKeyPair : Core.KeyManagement.Entities.UserSignatureKeyPair
|
||||
{
|
||||
public virtual User User { get; set; }
|
||||
}
|
||||
|
||||
public class UserSignatureKeyPairMapperProfile : Profile
|
||||
{
|
||||
public UserSignatureKeyPairMapperProfile()
|
||||
{
|
||||
CreateMap<Core.KeyManagement.Entities.UserSignatureKeyPair, UserSignatureKeyPair>().ReverseMap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
|
||||
using AutoMapper;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.KeyManagement.Repositories;
|
||||
|
||||
public class UserSignatureKeyPairRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : Repository<Core.KeyManagement.Entities.UserSignatureKeyPair, Models.UserSignatureKeyPair, Guid>(serviceScopeFactory, mapper, context => context.UserSignatureKeyPairs), IUserSignatureKeyPairRepository
|
||||
{
|
||||
public async Task<SignatureKeyPairData?> GetByUserIdAsync(Guid userId)
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var signingKeys = await dbContext.UserSignatureKeyPairs.FirstOrDefaultAsync(x => x.UserId == userId);
|
||||
if (signingKeys == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return signingKeys.ToSignatureKeyPairData();
|
||||
}
|
||||
|
||||
public UpdateEncryptedDataForKeyRotation SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signingKeys)
|
||||
{
|
||||
return async (_, _) =>
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var entity = new Models.UserSignatureKeyPair
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb(),
|
||||
UserId = userId,
|
||||
SignatureAlgorithm = signingKeys.SignatureAlgorithm,
|
||||
SigningKey = signingKeys.WrappedSigningKey,
|
||||
VerifyingKey = signingKeys.VerifyingKey,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
};
|
||||
await dbContext.UserSignatureKeyPairs.AddAsync(entity);
|
||||
await dbContext.SaveChangesAsync();
|
||||
};
|
||||
}
|
||||
|
||||
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signingKeys)
|
||||
{
|
||||
return async (_, _) =>
|
||||
{
|
||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var entity = await dbContext.UserSignatureKeyPairs.FirstOrDefaultAsync(x => x.UserId == grantorId);
|
||||
if (entity != null)
|
||||
{
|
||||
entity.SignatureAlgorithm = signingKeys.SignatureAlgorithm;
|
||||
entity.SigningKey = signingKeys.WrappedSigningKey;
|
||||
entity.VerifyingKey = signingKeys.VerifyingKey;
|
||||
entity.RevisionDate = DateTime.UtcNow;
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,7 @@ public class DatabaseContext : DbContext
|
||||
public DbSet<TaxRate> TaxRates { get; set; }
|
||||
public DbSet<Transaction> Transactions { get; set; }
|
||||
public DbSet<User> Users { get; set; }
|
||||
public DbSet<UserSignatureKeyPair> UserSignatureKeyPairs { get; set; }
|
||||
public DbSet<AuthRequest> AuthRequests { get; set; }
|
||||
public DbSet<OrganizationDomain> OrganizationDomains { get; set; }
|
||||
public DbSet<WebAuthnCredential> WebAuthnCredentials { get; set; }
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE PROCEDURE [dbo].[UserSignatureKeyPair_ReadByUserId]
|
||||
@UserId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[UserSignatureKeyPairView]
|
||||
WHERE
|
||||
[UserId] = @UserId;
|
||||
END
|
||||
@@ -0,0 +1,33 @@
|
||||
CREATE PROCEDURE [dbo].[UserSignatureKeyPair_SetForRotation]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@SignatureAlgorithm TINYINT,
|
||||
@SigningKey VARCHAR(MAX),
|
||||
@VerifyingKey VARCHAR(MAX),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
INSERT INTO [dbo].[UserSignatureKeyPair]
|
||||
(
|
||||
[Id],
|
||||
[UserId],
|
||||
[SignatureAlgorithm],
|
||||
[SigningKey],
|
||||
[VerifyingKey],
|
||||
[CreationDate],
|
||||
[RevisionDate]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@UserId,
|
||||
@SignatureAlgorithm,
|
||||
@SigningKey,
|
||||
@VerifyingKey,
|
||||
@CreationDate,
|
||||
@RevisionDate
|
||||
)
|
||||
END
|
||||
@@ -0,0 +1,19 @@
|
||||
CREATE PROCEDURE [dbo].[UserSignatureKeyPair_UpdateForRotation]
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@SignatureAlgorithm TINYINT,
|
||||
@SigningKey VARCHAR(MAX),
|
||||
@VerifyingKey VARCHAR(MAX),
|
||||
@RevisionDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
UPDATE
|
||||
[dbo].[UserSignatureKeyPair]
|
||||
SET
|
||||
[SignatureAlgorithm] = @SignatureAlgorithm,
|
||||
[SigningKey] = @SigningKey,
|
||||
[VerifyingKey] = @VerifyingKey,
|
||||
[RevisionDate] = @RevisionDate
|
||||
WHERE
|
||||
[UserId] = @UserId;
|
||||
END
|
||||
16
src/Sql/dbo/KeyManagement/Tables/UserSignatureKeyPair.sql
Normal file
16
src/Sql/dbo/KeyManagement/Tables/UserSignatureKeyPair.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
CREATE TABLE [dbo].[UserSignatureKeyPair] (
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||
[UserId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[SignatureAlgorithm] TINYINT NOT NULL,
|
||||
[SigningKey] VARCHAR(MAX) NOT NULL,
|
||||
[VerifyingKey] VARCHAR(MAX) NOT NULL,
|
||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||
CONSTRAINT [PK_UserSignatureKeyPair] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_UserSignatureKeyPair_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE
|
||||
);
|
||||
GO
|
||||
|
||||
CREATE UNIQUE NONCLUSTERED INDEX [IX_UserSignatureKeyPair_UserId]
|
||||
ON [dbo].[UserSignatureKeyPair]([UserId] ASC);
|
||||
GO
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE VIEW [dbo].[UserSignatureKeyPairView]
|
||||
AS
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[UserSignatureKeyPair]
|
||||
@@ -41,7 +41,10 @@
|
||||
@LastKdfChangeDate DATETIME2(7) = NULL,
|
||||
@LastKeyRotationDate DATETIME2(7) = NULL,
|
||||
@LastEmailChangeDate DATETIME2(7) = NULL,
|
||||
@VerifyDevices BIT = 1
|
||||
@VerifyDevices BIT = 1,
|
||||
@SecurityState VARCHAR(MAX) = NULL,
|
||||
@SecurityVersion INT = NULL,
|
||||
@SignedPublicKey VARCHAR(MAX) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@@ -90,7 +93,10 @@ BEGIN
|
||||
[LastKdfChangeDate],
|
||||
[LastKeyRotationDate],
|
||||
[LastEmailChangeDate],
|
||||
[VerifyDevices]
|
||||
[VerifyDevices],
|
||||
[SecurityState],
|
||||
[SecurityVersion],
|
||||
[SignedPublicKey]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@@ -136,6 +142,9 @@ BEGIN
|
||||
@LastKdfChangeDate,
|
||||
@LastKeyRotationDate,
|
||||
@LastEmailChangeDate,
|
||||
@VerifyDevices
|
||||
@VerifyDevices,
|
||||
@SecurityState,
|
||||
@SecurityVersion,
|
||||
@SignedPublicKey
|
||||
)
|
||||
END
|
||||
|
||||
@@ -41,7 +41,10 @@
|
||||
@LastKdfChangeDate DATETIME2(7) = NULL,
|
||||
@LastKeyRotationDate DATETIME2(7) = NULL,
|
||||
@LastEmailChangeDate DATETIME2(7) = NULL,
|
||||
@VerifyDevices BIT = 1
|
||||
@VerifyDevices BIT = 1,
|
||||
@SecurityState VARCHAR(MAX) = NULL,
|
||||
@SecurityVersion INT = NULL,
|
||||
@SignedPublicKey VARCHAR(MAX) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@@ -90,7 +93,10 @@ BEGIN
|
||||
[LastKdfChangeDate] = @LastKdfChangeDate,
|
||||
[LastKeyRotationDate] = @LastKeyRotationDate,
|
||||
[LastEmailChangeDate] = @LastEmailChangeDate,
|
||||
[VerifyDevices] = @VerifyDevices
|
||||
[VerifyDevices] = @VerifyDevices,
|
||||
[SecurityState] = @SecurityState,
|
||||
[SecurityVersion] = @SecurityVersion,
|
||||
[SignedPublicKey] = @SignedPublicKey
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
[LastKeyRotationDate] DATETIME2 (7) NULL,
|
||||
[LastEmailChangeDate] DATETIME2 (7) NULL,
|
||||
[VerifyDevices] BIT DEFAULT ((1)) NOT NULL,
|
||||
[SecurityState] VARCHAR (MAX) NULL,
|
||||
[SecurityVersion] INT NULL,
|
||||
[SignedPublicKey] VARCHAR (MAX) NULL,
|
||||
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||
);
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Entities;
|
||||
using Bit.Core.KeyManagement.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Vault.Enums;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -24,6 +28,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
{
|
||||
private static readonly string _mockEncryptedString =
|
||||
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
|
||||
private static readonly string _mockEncryptedType7String = "7.AOs41Hd8OQiCPXjyJKCiDA==";
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
|
||||
@@ -34,6 +39,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly IPasswordHasher<User> _passwordHasher;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository;
|
||||
private string _ownerEmail = null!;
|
||||
|
||||
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
|
||||
@@ -49,6 +55,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
_organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
_passwordHasher = _factory.GetService<IPasswordHasher<User>>();
|
||||
_organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||
_userSignatureKeyPairRepository = _factory.GetService<IUserSignatureKeyPairRepository>();
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
@@ -200,6 +207,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
var password = _passwordHasher.HashPassword(user, "newMasterPassword");
|
||||
user.MasterPassword = password;
|
||||
user.PublicKey = "publicKey";
|
||||
user.PrivateKey = _mockEncryptedString;
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.KdfType = user.Kdf;
|
||||
@@ -209,6 +217,8 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.Email = user.Email;
|
||||
request.AccountKeys.AccountPublicKey = "publicKey";
|
||||
request.AccountKeys.UserKeyEncryptedAccountPrivateKey = _mockEncryptedString;
|
||||
request.AccountKeys.PublicKeyEncryptionKeyPair = null;
|
||||
request.AccountKeys.SignatureKeyPair = null;
|
||||
|
||||
request.OldMasterKeyAuthenticationHash = "newMasterPassword";
|
||||
|
||||
@@ -354,4 +364,196 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
|
||||
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateV2UserAccountKeysAsync_Success(RotateUserAccountKeysAndDataRequestModel request)
|
||||
{
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
var user = await _userRepository.GetByEmailAsync(_ownerEmail);
|
||||
if (user == null)
|
||||
{
|
||||
throw new InvalidOperationException("User not found.");
|
||||
}
|
||||
|
||||
var password = _passwordHasher.HashPassword(user, "newMasterPassword");
|
||||
user.MasterPassword = password;
|
||||
user.PublicKey = "publicKey";
|
||||
user.PrivateKey = _mockEncryptedType7String;
|
||||
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
await _userSignatureKeyPairRepository.CreateAsync(new UserSignatureKeyPair
|
||||
{
|
||||
UserId = user.Id,
|
||||
SignatureAlgorithm = SignatureAlgorithm.Ed25519,
|
||||
SigningKey = _mockEncryptedType7String,
|
||||
VerifyingKey = "verifyingKey",
|
||||
});
|
||||
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.KdfType = user.Kdf;
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations = user.KdfIterations;
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory = user.KdfMemory;
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism = user.KdfParallelism;
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.Email = user.Email;
|
||||
request.AccountKeys.AccountPublicKey = "publicKey";
|
||||
request.AccountKeys.UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String;
|
||||
request.AccountKeys.PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel
|
||||
{
|
||||
PublicKey = "publicKey",
|
||||
WrappedPrivateKey = _mockEncryptedType7String,
|
||||
SignedPublicKey = "signedPublicKey",
|
||||
};
|
||||
request.AccountKeys.SignatureKeyPair = new SignatureKeyPairRequestModel
|
||||
{
|
||||
SignatureAlgorithm = "ed25519",
|
||||
WrappedSigningKey = _mockEncryptedType7String,
|
||||
VerifyingKey = "verifyingKey",
|
||||
};
|
||||
|
||||
request.OldMasterKeyAuthenticationHash = "newMasterPassword";
|
||||
|
||||
request.AccountData.Ciphers =
|
||||
[
|
||||
new CipherWithIdRequestModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = CipherType.Login,
|
||||
Name = _mockEncryptedString,
|
||||
Login = new CipherLoginModel
|
||||
{
|
||||
Username = _mockEncryptedString,
|
||||
Password = _mockEncryptedString,
|
||||
},
|
||||
},
|
||||
];
|
||||
request.AccountData.Folders = [
|
||||
new FolderWithIdRequestModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = _mockEncryptedString,
|
||||
},
|
||||
];
|
||||
request.AccountData.Sends = [
|
||||
new SendWithIdRequestModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = _mockEncryptedString,
|
||||
Key = _mockEncryptedString,
|
||||
Disabled = false,
|
||||
DeletionDate = DateTime.UtcNow.AddDays(1),
|
||||
},
|
||||
];
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey = _mockEncryptedString;
|
||||
request.AccountUnlockData.PasskeyUnlockData = [];
|
||||
request.AccountUnlockData.DeviceKeyUnlockData = [];
|
||||
request.AccountUnlockData.EmergencyAccessUnlockData = [];
|
||||
request.AccountUnlockData.OrganizationAccountRecoveryUnlockData = [];
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/accounts/key-management/rotate-user-account-keys", request);
|
||||
var responseMessage = await response.Content.ReadAsStringAsync();
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail);
|
||||
Assert.NotNull(userNewState);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.Email, userNewState.Email);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfType, userNewState.Kdf);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations, userNewState.KdfIterations);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUpgradeToV2UserAccountKeysAsync_Success(RotateUserAccountKeysAndDataRequestModel request)
|
||||
{
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
var user = await _userRepository.GetByEmailAsync(_ownerEmail);
|
||||
if (user == null)
|
||||
{
|
||||
throw new InvalidOperationException("User not found.");
|
||||
}
|
||||
|
||||
var password = _passwordHasher.HashPassword(user, "newMasterPassword");
|
||||
user.MasterPassword = password;
|
||||
user.PublicKey = "publicKey";
|
||||
user.PrivateKey = _mockEncryptedString;
|
||||
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.KdfType = user.Kdf;
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations = user.KdfIterations;
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory = user.KdfMemory;
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism = user.KdfParallelism;
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.Email = user.Email;
|
||||
request.AccountKeys.AccountPublicKey = "publicKey";
|
||||
request.AccountKeys.UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String;
|
||||
request.AccountKeys.PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel
|
||||
{
|
||||
PublicKey = "publicKey",
|
||||
WrappedPrivateKey = _mockEncryptedType7String,
|
||||
SignedPublicKey = "signedPublicKey",
|
||||
};
|
||||
request.AccountKeys.SignatureKeyPair = new SignatureKeyPairRequestModel
|
||||
{
|
||||
SignatureAlgorithm = "ed25519",
|
||||
WrappedSigningKey = _mockEncryptedType7String,
|
||||
VerifyingKey = "verifyingKey",
|
||||
};
|
||||
request.AccountKeys.SecurityState = new SecurityStateModel
|
||||
{
|
||||
SecurityVersion = 2,
|
||||
SecurityState = "v2",
|
||||
};
|
||||
|
||||
request.OldMasterKeyAuthenticationHash = "newMasterPassword";
|
||||
|
||||
request.AccountData.Ciphers =
|
||||
[
|
||||
new CipherWithIdRequestModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = CipherType.Login,
|
||||
Name = _mockEncryptedString,
|
||||
Login = new CipherLoginModel
|
||||
{
|
||||
Username = _mockEncryptedString,
|
||||
Password = _mockEncryptedString,
|
||||
},
|
||||
},
|
||||
];
|
||||
request.AccountData.Folders = [
|
||||
new FolderWithIdRequestModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = _mockEncryptedString,
|
||||
},
|
||||
];
|
||||
request.AccountData.Sends = [
|
||||
new SendWithIdRequestModel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = _mockEncryptedString,
|
||||
Key = _mockEncryptedString,
|
||||
Disabled = false,
|
||||
DeletionDate = DateTime.UtcNow.AddDays(1),
|
||||
},
|
||||
];
|
||||
request.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey = _mockEncryptedString;
|
||||
request.AccountUnlockData.PasskeyUnlockData = [];
|
||||
request.AccountUnlockData.DeviceKeyUnlockData = [];
|
||||
request.AccountUnlockData.EmergencyAccessUnlockData = [];
|
||||
request.AccountUnlockData.OrganizationAccountRecoveryUnlockData = [];
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/accounts/key-management/rotate-user-account-keys", request);
|
||||
var responseMessage = await response.Content.ReadAsStringAsync();
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail);
|
||||
Assert.NotNull(userNewState);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.Email, userNewState.Email);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfType, userNewState.Kdf);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations, userNewState.KdfIterations);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory);
|
||||
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Kdf;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -33,10 +34,10 @@ public class AccountsControllerTests : IDisposable
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
||||
private readonly IChangeKdfCommand _changeKdfCommand;
|
||||
|
||||
|
||||
public AccountsControllerTests()
|
||||
{
|
||||
_userService = Substitute.For<IUserService>();
|
||||
@@ -48,6 +49,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
|
||||
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
|
||||
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
|
||||
_changeKdfCommand = Substitute.For<IChangeKdfCommand>();
|
||||
|
||||
@@ -61,6 +63,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_tdeOffboardingPasswordCommand,
|
||||
_twoFactorIsEnabledQuery,
|
||||
_featureService,
|
||||
_userAccountKeysQuery,
|
||||
_twoFactorEmailService,
|
||||
_changeKdfCommand
|
||||
);
|
||||
|
||||
@@ -110,6 +110,7 @@ public class AccountsKeyManagementControllerTests
|
||||
public async Task RotateUserAccountKeysSuccess(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data, User user)
|
||||
{
|
||||
data.AccountKeys.SignatureKeyPair = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Success);
|
||||
@@ -142,8 +143,60 @@ public class AccountsKeyManagementControllerTests
|
||||
&& d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash
|
||||
&& d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey
|
||||
|
||||
&& d.AccountPublicKey == data.AccountKeys.AccountPublicKey
|
||||
&& d.UserKeyEncryptedAccountPrivateKey == data.AccountKeys.UserKeyEncryptedAccountPrivateKey
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.PublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.PublicKey
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserAccountKeys_UserCryptoV2_Success_Async(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data, User user)
|
||||
{
|
||||
data.AccountKeys.SignatureKeyPair = new SignatureKeyPairRequestModel
|
||||
{
|
||||
SignatureAlgorithm = "ed25519",
|
||||
WrappedSigningKey = "wrappedSigningKey",
|
||||
VerifyingKey = "verifyingKey"
|
||||
};
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Success);
|
||||
await sutProvider.Sut.RotateUserAccountKeysAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.EmergencyAccessUnlockData));
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.OrganizationAccountRecoveryUnlockData));
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.PasskeyUnlockData));
|
||||
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Ciphers));
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Folders));
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Sends));
|
||||
|
||||
await sutProvider.GetDependency<IRotateUserAccountKeysCommand>().Received(1)
|
||||
.RotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<RotateUserAccountKeysData>(d =>
|
||||
d.OldMasterKeyAuthenticationHash == data.OldMasterKeyAuthenticationHash
|
||||
|
||||
&& d.MasterPasswordUnlockData.KdfType == data.AccountUnlockData.MasterPasswordUnlockData.KdfType
|
||||
&& d.MasterPasswordUnlockData.KdfIterations == data.AccountUnlockData.MasterPasswordUnlockData.KdfIterations
|
||||
&& d.MasterPasswordUnlockData.KdfMemory == data.AccountUnlockData.MasterPasswordUnlockData.KdfMemory
|
||||
&& d.MasterPasswordUnlockData.KdfParallelism == data.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism
|
||||
&& d.MasterPasswordUnlockData.Email == data.AccountUnlockData.MasterPasswordUnlockData.Email
|
||||
|
||||
&& d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash
|
||||
&& d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey
|
||||
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.PublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.PublicKey
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.SignedPublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.SignedPublicKey
|
||||
&& d.AccountKeys!.SignatureKeyPairData!.SignatureAlgorithm == Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519
|
||||
&& d.AccountKeys!.SignatureKeyPairData.WrappedSigningKey == data.AccountKeys.SignatureKeyPair!.WrappedSigningKey
|
||||
&& d.AccountKeys!.SignatureKeyPairData.VerifyingKey == data.AccountKeys.SignatureKeyPair!.VerifyingKey
|
||||
));
|
||||
}
|
||||
|
||||
@@ -153,6 +206,7 @@ public class AccountsKeyManagementControllerTests
|
||||
public async Task RotateUserKeyNoUser_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data)
|
||||
{
|
||||
data.AccountKeys.SignatureKeyPair = null;
|
||||
User? user = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
@@ -165,6 +219,7 @@ public class AccountsKeyManagementControllerTests
|
||||
public async Task RotateUserKeyWrongData_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data, User user, IdentityErrorDescriber _identityErrorDescriber)
|
||||
{
|
||||
data.AccountKeys.SignatureKeyPair = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()));
|
||||
|
||||
112
test/Api.Test/KeyManagement/Controllers/UsersControllerTests.cs
Normal file
112
test/Api.Test/KeyManagement/Controllers/UsersControllerTests.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
#nullable enable
|
||||
using Bit.Api.KeyManagement.Controllers;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.KeyManagement.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(UsersController))]
|
||||
[SutProviderCustomize]
|
||||
[JsonDocumentCustomize]
|
||||
public class UsersControllerTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetPublicKey_NotFound_ThrowsNotFoundException(
|
||||
SutProvider<UsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IUserRepository>().GetPublicKeyAsync(Arg.Any<Guid>()).ReturnsNull();
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetPublicKeyAsync(new Guid()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetPublicKey_ReturnsUserKeyResponseModel(
|
||||
SutProvider<UsersController> sutProvider,
|
||||
Guid userId)
|
||||
{
|
||||
var publicKey = "publicKey";
|
||||
sutProvider.GetDependency<IUserRepository>().GetPublicKeyAsync(userId).Returns(publicKey);
|
||||
|
||||
var result = await sutProvider.Sut.GetPublicKeyAsync(userId);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(userId, result.UserId);
|
||||
Assert.Equal(publicKey, result.PublicKey);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAccountKeys_UserNotFound_ThrowsNotFoundException(
|
||||
SutProvider<UsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).ReturnsNull();
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAccountKeysAsync(new Guid()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAccountKeys_ReturnsPublicUserKeysResponseModel(
|
||||
SutProvider<UsersController> sutProvider,
|
||||
Guid userId)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = userId,
|
||||
PublicKey = "publicKey",
|
||||
SignedPublicKey = "signedPublicKey",
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(userId).Returns(user);
|
||||
sutProvider.GetDependency<IUserAccountKeysQuery>()
|
||||
.Run(user)
|
||||
.Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("wrappedPrivateKey", "publicKey", "signedPublicKey"),
|
||||
SignatureKeyPairData = new SignatureKeyPairData(SignatureAlgorithm.Ed25519, "wrappedSigningKey", "verifyingKey"),
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.GetAccountKeysAsync(userId);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("publicKey", result.PublicKey);
|
||||
Assert.Equal("signedPublicKey", result.SignedPublicKey);
|
||||
Assert.Equal("verifyingKey", result.VerifyingKey);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAccountKeys_ReturnsPublicUserKeysResponseModel_WithNullVerifyingKey(
|
||||
SutProvider<UsersController> sutProvider,
|
||||
Guid userId)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = userId,
|
||||
PublicKey = "publicKey",
|
||||
SignedPublicKey = null,
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(userId).Returns(user);
|
||||
sutProvider.GetDependency<IUserAccountKeysQuery>()
|
||||
.Run(user)
|
||||
.Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("wrappedPrivateKey", "publicKey", null),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.GetAccountKeysAsync(userId);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("publicKey", result.PublicKey);
|
||||
Assert.Null(result.SignedPublicKey);
|
||||
Assert.Null(result.VerifyingKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Api.KeyManagement.Models.Requests;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.KeyManagement.Models.Request;
|
||||
|
||||
public class SignatureKeyPairRequestModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToSignatureKeyPairData_WrongAlgorithm_Rejects()
|
||||
{
|
||||
var model = new SignatureKeyPairRequestModel
|
||||
{
|
||||
SignatureAlgorithm = "abc",
|
||||
WrappedSigningKey = "wrappedKey",
|
||||
VerifyingKey = "verifyingKey"
|
||||
};
|
||||
|
||||
Assert.Throws<ArgumentException>(() => model.ToSignatureKeyPairData());
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -74,6 +76,7 @@ public class SyncControllerTests
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
||||
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
||||
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||
|
||||
// Adjust random data to match required formats / test intentions
|
||||
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
||||
@@ -98,6 +101,11 @@ public class SyncControllerTests
|
||||
|
||||
// Setup returns
|
||||
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
organizationUserRepository
|
||||
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
|
||||
@@ -127,7 +135,6 @@ public class SyncControllerTests
|
||||
// Execute GET
|
||||
var result = await sutProvider.Sut.Get();
|
||||
|
||||
|
||||
// Asserts
|
||||
// Assert that methods are called
|
||||
var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
|
||||
@@ -166,6 +173,7 @@ public class SyncControllerTests
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
||||
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
||||
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||
|
||||
// Adjust random data to match required formats / test intentions
|
||||
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
||||
@@ -189,6 +197,11 @@ public class SyncControllerTests
|
||||
|
||||
// Setup returns
|
||||
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
organizationUserRepository
|
||||
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
|
||||
@@ -256,6 +269,7 @@ public class SyncControllerTests
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
||||
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
||||
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||
|
||||
// Adjust random data to match required formats / test intentions
|
||||
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
||||
@@ -290,6 +304,12 @@ public class SyncControllerTests
|
||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);
|
||||
userService.HasPremiumFromOrganization(user).Returns(false);
|
||||
|
||||
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
// Execute GET
|
||||
var result = await sutProvider.Sut.Get();
|
||||
|
||||
@@ -327,6 +347,13 @@ public class SyncControllerTests
|
||||
|
||||
user.MasterPassword = null;
|
||||
|
||||
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||
|
||||
@@ -352,6 +379,13 @@ public class SyncControllerTests
|
||||
user.KdfMemory = kdfMemory;
|
||||
user.KdfParallelism = kdfParallelism;
|
||||
|
||||
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||
|
||||
|
||||
43
test/Core.Test/KeyManagement/Queries/UserAccountKeysQuery.cs
Normal file
43
test/Core.Test/KeyManagement/Queries/UserAccountKeysQuery.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.KeyManagement.Queries;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class UserAccountKeysQueryTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task V1User_Success(SutProvider<UserAccountKeysQuery> sutProvider, User user)
|
||||
{
|
||||
var result = await sutProvider.Sut.Run(user);
|
||||
Assert.Equal(user.GetPublicKeyEncryptionKeyPair().PublicKey, result.PublicKeyEncryptionKeyPairData.PublicKey);
|
||||
Assert.Equal(user.GetPublicKeyEncryptionKeyPair().WrappedPrivateKey, result.PublicKeyEncryptionKeyPairData.WrappedPrivateKey);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task V2User_Success(SutProvider<UserAccountKeysQuery> sutProvider, User user)
|
||||
{
|
||||
user.SecurityState = "v2";
|
||||
user.SecurityVersion = 2;
|
||||
var signatureKeyPairRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
signatureKeyPairRepository.GetByUserIdAsync(user.Id).Returns(new SignatureKeyPairData(Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519, "wrappedSigningKey", "verifyingKey"));
|
||||
var result = await sutProvider.Sut.Run(user);
|
||||
Assert.Equal(user.GetPublicKeyEncryptionKeyPair().PublicKey, result.PublicKeyEncryptionKeyPairData.PublicKey);
|
||||
Assert.Equal(user.GetPublicKeyEncryptionKeyPair().WrappedPrivateKey, result.PublicKeyEncryptionKeyPairData.WrappedPrivateKey);
|
||||
Assert.Equal(user.GetPublicKeyEncryptionKeyPair().SignedPublicKey, result.PublicKeyEncryptionKeyPairData.SignedPublicKey);
|
||||
|
||||
Assert.NotNull(result.SignatureKeyPairData);
|
||||
Assert.Equal("wrappedSigningKey", result.SignatureKeyPairData.WrappedSigningKey);
|
||||
Assert.Equal("verifyingKey", result.SignatureKeyPairData.VerifyingKey);
|
||||
|
||||
Assert.Equal(user.SecurityState, result.SecurityStateData.SecurityState);
|
||||
Assert.Equal(user.GetSecurityVersion(), result.SecurityStateData.SecurityVersion);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
using Bit.Core.KeyManagement.UserKey.Implementations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.KeyManagement.UserKey;
|
||||
@@ -14,7 +21,7 @@ namespace Bit.Core.Test.KeyManagement.UserKey;
|
||||
public class RotateUserAccountKeysCommandTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task RejectsWrongOldMasterPassword(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
public async Task RotateUserAccountKeysAsync_WrongOldMasterPassword_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
user.Email = model.MasterPasswordUnlockData.Email;
|
||||
@@ -25,41 +32,38 @@ public class RotateUserAccountKeysCommandTests
|
||||
|
||||
Assert.NotEqual(IdentityResult.Success, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ThrowsWhenUserIsNull(SutProvider<RotateUserAccountKeysCommand> sutProvider,
|
||||
public async Task RotateUserAccountKeysAsync_UserIsNull_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(null, model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RejectsEmailChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
public async Task RotateUserAccountKeysAsync_EmailChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
user.Kdf = Enums.KdfType.Argon2id;
|
||||
user.KdfIterations = 3;
|
||||
user.KdfMemory = 64;
|
||||
user.KdfParallelism = 4;
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
|
||||
model.MasterPasswordUnlockData.Email = user.Email + ".different-domain";
|
||||
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
|
||||
model.MasterPasswordUnlockData.KdfIterations = 3;
|
||||
model.MasterPasswordUnlockData.KdfMemory = 64;
|
||||
model.MasterPasswordUnlockData.KdfParallelism = 4;
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RejectsKdfChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
public async Task RotateUserAccountKeysAsync_KdfChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
user.Kdf = Enums.KdfType.Argon2id;
|
||||
user.KdfIterations = 3;
|
||||
user.KdfMemory = 64;
|
||||
user.KdfParallelism = 4;
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
|
||||
model.MasterPasswordUnlockData.Email = user.Email;
|
||||
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.PBKDF2_SHA256;
|
||||
model.MasterPasswordUnlockData.KdfIterations = 600000;
|
||||
model.MasterPasswordUnlockData.KdfMemory = null;
|
||||
@@ -71,22 +75,15 @@ public class RotateUserAccountKeysCommandTests
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RejectsPublicKeyChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
public async Task RotateUserAccountKeysAsync_PublicKeyChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
user.PublicKey = "old-public";
|
||||
user.Kdf = Enums.KdfType.Argon2id;
|
||||
user.KdfIterations = 3;
|
||||
user.KdfMemory = 64;
|
||||
user.KdfParallelism = 4;
|
||||
|
||||
model.AccountPublicKey = "new-public";
|
||||
model.MasterPasswordUnlockData.Email = user.Email;
|
||||
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
|
||||
model.MasterPasswordUnlockData.KdfIterations = 3;
|
||||
model.MasterPasswordUnlockData.KdfMemory = 64;
|
||||
model.MasterPasswordUnlockData.KdfParallelism = 4;
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey = "new-public";
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
|
||||
@@ -94,27 +91,350 @@ public class RotateUserAccountKeysCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotatesCorrectly(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
public async Task RotateUserAccountKeysAsync_V1_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
user.Kdf = Enums.KdfType.Argon2id;
|
||||
user.KdfIterations = 3;
|
||||
user.KdfMemory = 64;
|
||||
user.KdfParallelism = 4;
|
||||
|
||||
model.MasterPasswordUnlockData.Email = user.Email;
|
||||
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
|
||||
model.MasterPasswordUnlockData.KdfIterations = 3;
|
||||
model.MasterPasswordUnlockData.KdfMemory = 64;
|
||||
model.MasterPasswordUnlockData.KdfParallelism = 4;
|
||||
|
||||
model.AccountPublicKey = user.PublicKey;
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
|
||||
var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
|
||||
|
||||
Assert.Equal(IdentityResult.Success, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_UpgradeV1ToV2_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
|
||||
var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
|
||||
Assert.Equal(IdentityResult.Success, result);
|
||||
Assert.Equal(user.SecurityState, model.AccountKeys.SecurityStateData!.SecurityState);
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_PublicKeyChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey = "new-public";
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_V2User_PrivateKeyNotXChaCha20_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV2ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "2.xxx";
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_V1User_PrivateKeyNotAesCbcHmac_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "7.xxx";
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("The provided account private key was not wrapped with AES-256-CBC-HMAC", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_V1_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);
|
||||
Assert.Empty(saveEncryptedDataActions);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_V2_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV2ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);
|
||||
Assert.NotEmpty(saveEncryptedDataActions);
|
||||
Assert.Equal(user.SecurityState, model.AccountKeys.SecurityStateData!.SecurityState);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_V2User_VerifyingKeyMismatch_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV2ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.SignatureKeyPairData.VerifyingKey = "different-verifying-key";
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("The provided verifying key does not match the user's current verifying key.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_V2User_SignedPublicKeyNullOrEmpty_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV2ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null;
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("No signed public key provided, but the user already has a signature key pair.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_V2User_WrappedSigningKeyNotXChaCha20_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV2ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.SignatureKeyPairData.WrappedSigningKey = "2.xxx";
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("The provided signing key data is not wrapped with XChaCha20-Poly1305.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeys_UpgradeToV2_InvalidVerifyingKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.SignatureKeyPairData.VerifyingKey = "";
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("The provided signature key pair data does not contain a valid verifying key.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_UpgradeToV2_IncorrectlyWrappedPrivateKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "2.abc";
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("The provided private key encryption key is not wrapped with XChaCha20-Poly1305.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSignedPublicKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null;
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("No signed public key provided, but the user already has a signature key pair.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSecurityState_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.SecurityStateData = null;
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("No signed security state provider for V2 user", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_RotateV2_NoSignatureKeyPair_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV2ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.SignatureKeyPairData = null;
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("Signature key pair data is required for V2 encryption.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_GetEncryptionType_EmptyString_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "";
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("Invalid encryption type string.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_GetEncryptionType_InvalidString_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "9.xxx";
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("Invalid encryption type string.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateUserData_RevisionDateChanged_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
var oldDate = new DateTime(2017, 1, 1);
|
||||
|
||||
var cipher = Substitute.For<Cipher>();
|
||||
cipher.RevisionDate = oldDate;
|
||||
model.Ciphers = [cipher];
|
||||
|
||||
var folder = Substitute.For<Folder>();
|
||||
folder.RevisionDate = oldDate;
|
||||
model.Folders = [folder];
|
||||
|
||||
var send = Substitute.For<Send>();
|
||||
send.RevisionDate = oldDate;
|
||||
model.Sends = [send];
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
|
||||
sutProvider.Sut.UpdateUserData(model, user, saveEncryptedDataActions);
|
||||
foreach (var dataAction in saveEncryptedDataActions)
|
||||
{
|
||||
await dataAction.Invoke();
|
||||
}
|
||||
|
||||
var updatedCiphers = sutProvider.GetDependency<ICipherRepository>()
|
||||
.ReceivedCalls()
|
||||
.FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")?
|
||||
.GetArguments()[1] as IEnumerable<Cipher>;
|
||||
foreach (var updatedCipher in updatedCiphers!)
|
||||
{
|
||||
var oldCipher = model.Ciphers.FirstOrDefault(c => c.Id == updatedCipher.Id);
|
||||
Assert.NotEqual(oldDate, updatedCipher.RevisionDate);
|
||||
}
|
||||
|
||||
var updatedFolders = sutProvider.GetDependency<IFolderRepository>()
|
||||
.ReceivedCalls()
|
||||
.FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")?
|
||||
.GetArguments()[1] as IEnumerable<Folder>;
|
||||
foreach (var updatedFolder in updatedFolders!)
|
||||
{
|
||||
var oldFolder = model.Folders.FirstOrDefault(f => f.Id == updatedFolder.Id);
|
||||
Assert.NotEqual(oldDate, updatedFolder.RevisionDate);
|
||||
}
|
||||
|
||||
var updatedSends = sutProvider.GetDependency<ISendRepository>()
|
||||
.ReceivedCalls()
|
||||
.FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")?
|
||||
.GetArguments()[1] as IEnumerable<Send>;
|
||||
foreach (var updatedSend in updatedSends!)
|
||||
{
|
||||
var oldSend = model.Sends.FirstOrDefault(s => s.Id == updatedSend.Id);
|
||||
Assert.NotEqual(oldDate, updatedSend.RevisionDate);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions to set valid test parameters that match each other to the model and user.
|
||||
private static void SetTestKdfAndSaltForUserAndModel(User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
user.Kdf = Enums.KdfType.Argon2id;
|
||||
user.KdfIterations = 3;
|
||||
user.KdfMemory = 64;
|
||||
user.KdfParallelism = 4;
|
||||
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
|
||||
model.MasterPasswordUnlockData.KdfIterations = 3;
|
||||
model.MasterPasswordUnlockData.KdfMemory = 64;
|
||||
model.MasterPasswordUnlockData.KdfParallelism = 4;
|
||||
// The email is the salt for the KDF and is validated currently.
|
||||
user.Email = model.MasterPasswordUnlockData.Email;
|
||||
}
|
||||
|
||||
private static void SetV1ExistingUser(User user, IUserSignatureKeyPairRepository userSignatureKeyPairRepository)
|
||||
{
|
||||
user.PrivateKey = "2.abc";
|
||||
user.PublicKey = "public";
|
||||
user.SignedPublicKey = null;
|
||||
userSignatureKeyPairRepository.GetByUserIdAsync(user.Id).ReturnsNull();
|
||||
}
|
||||
|
||||
private static void SetV2ExistingUser(User user, IUserSignatureKeyPairRepository userSignatureKeyPairRepository)
|
||||
{
|
||||
user.PrivateKey = "7.abc";
|
||||
user.PublicKey = "public";
|
||||
user.SignedPublicKey = "signed-public";
|
||||
userSignatureKeyPairRepository.GetByUserIdAsync(user.Id).Returns(new SignatureKeyPairData(SignatureAlgorithm.Ed25519, "7.abc", "verifying-key"));
|
||||
}
|
||||
|
||||
private static void SetV1ModelUser(RotateUserAccountKeysData model)
|
||||
{
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("2.abc", "public", null);
|
||||
model.AccountKeys.SignatureKeyPairData = null;
|
||||
model.AccountKeys.SecurityStateData = null;
|
||||
}
|
||||
|
||||
private static void SetV2ModelUser(RotateUserAccountKeysData model)
|
||||
{
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("7.abc", "public", "signed-public");
|
||||
model.AccountKeys.SignatureKeyPairData = new SignatureKeyPairData(SignatureAlgorithm.Ed25519, "7.abc", "verifying-key");
|
||||
model.AccountKeys.SecurityStateData = new SecurityStateData
|
||||
{
|
||||
SecurityState = "abc",
|
||||
SecurityVersion = 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Response;
|
||||
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -51,6 +53,7 @@ public class BaseRequestValidatorTests
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IAuthRequestRepository _authRequestRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
|
||||
private readonly BaseRequestValidatorTestWrapper _sut;
|
||||
|
||||
@@ -73,6 +76,7 @@ public class BaseRequestValidatorTests
|
||||
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
|
||||
_authRequestRepository = Substitute.For<IAuthRequestRepository>();
|
||||
_mailService = Substitute.For<IMailService>();
|
||||
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
|
||||
|
||||
_sut = new BaseRequestValidatorTestWrapper(
|
||||
_userManager,
|
||||
@@ -91,7 +95,8 @@ public class BaseRequestValidatorTests
|
||||
_userDecryptionOptionsBuilder,
|
||||
_policyRequirementQuery,
|
||||
_authRequestRepository,
|
||||
_mailService);
|
||||
_mailService,
|
||||
_userAccountKeysQuery);
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
@@ -180,6 +185,13 @@ public class BaseRequestValidatorTests
|
||||
// 5 -> not legacy user
|
||||
_userService.IsLegacyUser(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||
"test-private-key",
|
||||
"test-public-key"
|
||||
)
|
||||
});
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
@@ -227,6 +239,13 @@ public class BaseRequestValidatorTests
|
||||
// 5 -> not legacy user
|
||||
_userService.IsLegacyUser(Arg.Any<string>())
|
||||
.Returns(false);
|
||||
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||
"test-private-key",
|
||||
"test-public-key"
|
||||
)
|
||||
});
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
@@ -460,6 +479,13 @@ public class BaseRequestValidatorTests
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||
.Returns(Task.FromResult(true));
|
||||
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||
"test-private-key",
|
||||
"test-public-key"
|
||||
)
|
||||
});
|
||||
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
@@ -495,6 +521,13 @@ public class BaseRequestValidatorTests
|
||||
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||
.Returns(Task.FromResult(true));
|
||||
context.ValidatedTokenRequest.ClientId = "web";
|
||||
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||
"test-private-key",
|
||||
"test-public-key"
|
||||
)
|
||||
});
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
@@ -529,6 +562,13 @@ public class BaseRequestValidatorTests
|
||||
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||
.Returns(Task.FromResult(true));
|
||||
context.ValidatedTokenRequest.ClientId = "web";
|
||||
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||
"test-private-key",
|
||||
"test-public-key"
|
||||
)
|
||||
});
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
@@ -591,6 +631,13 @@ public class BaseRequestValidatorTests
|
||||
HasMasterPassword = false,
|
||||
MasterPasswordUnlock = null
|
||||
}));
|
||||
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||
"test-private-key",
|
||||
"test-public-key"
|
||||
)
|
||||
});
|
||||
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
_sut.isValid = true;
|
||||
@@ -644,6 +691,14 @@ public class BaseRequestValidatorTests
|
||||
}
|
||||
}));
|
||||
|
||||
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||
"test-private-key",
|
||||
"test-public-key"
|
||||
)
|
||||
});
|
||||
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
_sut.isValid = true;
|
||||
|
||||
@@ -671,6 +726,152 @@ public class BaseRequestValidatorTests
|
||||
Assert.Equal("test@example.com", userDecryptionOptions.MasterPasswordUnlock.Salt);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_CustomResponse_ShouldIncludeAccountKeys(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var mockAccountKeys = new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||
"test-private-key",
|
||||
"test-public-key",
|
||||
"test-signed-public-key"
|
||||
),
|
||||
SignatureKeyPairData = new SignatureKeyPairData(
|
||||
Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519,
|
||||
"test-wrapped-signing-key",
|
||||
"test-verifying-key"
|
||||
),
|
||||
SecurityStateData = new SecurityStateData
|
||||
{
|
||||
SecurityState = "test-security-state",
|
||||
SecurityVersion = 2
|
||||
}
|
||||
};
|
||||
|
||||
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(mockAccountKeys);
|
||||
|
||||
_userDecryptionOptionsBuilder.ForUser(Arg.Any<User>()).Returns(_userDecryptionOptionsBuilder);
|
||||
_userDecryptionOptionsBuilder.WithDevice(Arg.Any<Device>()).Returns(_userDecryptionOptionsBuilder);
|
||||
_userDecryptionOptionsBuilder.WithSso(Arg.Any<SsoConfig>()).Returns(_userDecryptionOptionsBuilder);
|
||||
_userDecryptionOptionsBuilder.WithWebAuthnLoginCredential(Arg.Any<WebAuthnCredential>()).Returns(_userDecryptionOptionsBuilder);
|
||||
_userDecryptionOptionsBuilder.BuildAsync().Returns(Task.FromResult(new UserDecryptionOptions
|
||||
{
|
||||
HasMasterPassword = true,
|
||||
MasterPasswordUnlock = new MasterPasswordUnlockResponseModel
|
||||
{
|
||||
Kdf = new MasterPasswordUnlockKdfResponseModel
|
||||
{
|
||||
KdfType = KdfType.PBKDF2_SHA256,
|
||||
Iterations = 100000
|
||||
},
|
||||
MasterKeyEncryptedUserKey = _mockEncryptedString,
|
||||
Salt = "test@example.com"
|
||||
}
|
||||
}));
|
||||
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
_sut.isValid = true;
|
||||
|
||||
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.GrantResult.IsError);
|
||||
var customResponse = context.GrantResult.CustomResponse;
|
||||
|
||||
// Verify AccountKeys are included in response
|
||||
Assert.Contains("AccountKeys", customResponse);
|
||||
Assert.IsType<PrivateKeysResponseModel>(customResponse["AccountKeys"]);
|
||||
|
||||
var accountKeysResponse = (PrivateKeysResponseModel)customResponse["AccountKeys"];
|
||||
Assert.NotNull(accountKeysResponse.PublicKeyEncryptionKeyPair);
|
||||
Assert.Equal("test-public-key", accountKeysResponse.PublicKeyEncryptionKeyPair.PublicKey);
|
||||
Assert.Equal("test-private-key", accountKeysResponse.PublicKeyEncryptionKeyPair.WrappedPrivateKey);
|
||||
Assert.Equal("test-signed-public-key", accountKeysResponse.PublicKeyEncryptionKeyPair.SignedPublicKey);
|
||||
|
||||
Assert.NotNull(accountKeysResponse.SignatureKeyPair);
|
||||
Assert.Equal("test-wrapped-signing-key", accountKeysResponse.SignatureKeyPair.WrappedSigningKey);
|
||||
Assert.Equal("test-verifying-key", accountKeysResponse.SignatureKeyPair.VerifyingKey);
|
||||
|
||||
Assert.NotNull(accountKeysResponse.SecurityState);
|
||||
Assert.Equal("test-security-state", accountKeysResponse.SecurityState.SecurityState);
|
||||
Assert.Equal(2, accountKeysResponse.SecurityState.SecurityVersion);
|
||||
}
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_CustomResponse_AccountKeysQuery_SkippedWhenPrivateKeyIsNull(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
requestContext.User.PrivateKey = null;
|
||||
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
_sut.isValid = true;
|
||||
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.GrantResult.IsError);
|
||||
|
||||
// Verify that the account keys query wasn't called.
|
||||
await _userAccountKeysQuery.Received(0).Run(Arg.Any<User>());
|
||||
}
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_CustomResponse_AccountKeysQuery_CalledWithCorrectUser(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var expectedUser = requestContext.User;
|
||||
|
||||
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||
"test-private-key",
|
||||
"test-public-key"
|
||||
)
|
||||
});
|
||||
|
||||
_userDecryptionOptionsBuilder.ForUser(Arg.Any<User>()).Returns(_userDecryptionOptionsBuilder);
|
||||
_userDecryptionOptionsBuilder.WithDevice(Arg.Any<Device>()).Returns(_userDecryptionOptionsBuilder);
|
||||
_userDecryptionOptionsBuilder.WithSso(Arg.Any<SsoConfig>()).Returns(_userDecryptionOptionsBuilder);
|
||||
_userDecryptionOptionsBuilder.WithWebAuthnLoginCredential(Arg.Any<WebAuthnCredential>()).Returns(_userDecryptionOptionsBuilder);
|
||||
_userDecryptionOptionsBuilder.BuildAsync().Returns(Task.FromResult(new UserDecryptionOptions()));
|
||||
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
_sut.isValid = true;
|
||||
|
||||
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(context.GrantResult.IsError);
|
||||
|
||||
// Verify that the account keys query was called with the correct user
|
||||
await _userAccountKeysQuery.Received(1).Run(Arg.Is<User>(u => u.Id == expectedUser.Id));
|
||||
}
|
||||
|
||||
private BaseRequestValidationContextFake CreateContext(
|
||||
ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
|
||||
@@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@@ -64,7 +65,8 @@ IBaseRequestValidatorTestWrapper
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IMailService mailService) :
|
||||
IMailService mailService,
|
||||
IUserAccountKeysQuery userAccountKeysQuery) :
|
||||
base(
|
||||
userManager,
|
||||
userService,
|
||||
@@ -82,7 +84,8 @@ IBaseRequestValidatorTestWrapper
|
||||
userDecryptionOptionsBuilder,
|
||||
policyRequirementQuery,
|
||||
authRequestRepository,
|
||||
mailService)
|
||||
mailService,
|
||||
userAccountKeysQuery)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ public class EfRepositoryListBuilder<T> : ISpecimenBuilder where T : BaseEntityF
|
||||
cfg.AddProfile<TransactionMapperProfile>();
|
||||
cfg.AddProfile<UserMapperProfile>();
|
||||
cfg.AddProfile<PasswordHealthReportApplicationProfile>();
|
||||
cfg.AddProfile<UserSignatureKeyPairMapperProfile>();
|
||||
cfg.AddProfile<OrganizationReportProfile>();
|
||||
})
|
||||
.CreateMapper()));
|
||||
|
||||
@@ -29,7 +29,8 @@ public class UserCompare : IEqualityComparer<User>
|
||||
x.LicenseKey == y.LicenseKey &&
|
||||
x.ApiKey == y.ApiKey &&
|
||||
x.Kdf == y.Kdf &&
|
||||
x.KdfIterations == y.KdfIterations;
|
||||
x.KdfIterations == y.KdfIterations &&
|
||||
x.SignedPublicKey == y.SignedPublicKey;
|
||||
}
|
||||
|
||||
public int GetHashCode([DisallowNull] User obj)
|
||||
|
||||
396
util/Migrator/DbScripts/2025-10-13_00_UserCryptoV2.sql
Normal file
396
util/Migrator/DbScripts/2025-10-13_00_UserCryptoV2.sql
Normal file
@@ -0,0 +1,396 @@
|
||||
IF OBJECT_ID('[dbo].[UserSignatureKeyPair]') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[UserSignatureKeyPair]
|
||||
(
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||
[UserId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[SignatureAlgorithm] TINYINT NOT NULL,
|
||||
[SigningKey] VARCHAR(MAX) NOT NULL,
|
||||
[VerifyingKey] VARCHAR(MAX) NOT NULL,
|
||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||
CONSTRAINT [PK_UserSignatureKeyPair] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_UserSignatureKeyPair_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE
|
||||
);
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS(SELECT name
|
||||
FROM sys.indexes
|
||||
WHERE name = 'IX_UserSignatureKeyPair_UserId')
|
||||
BEGIN
|
||||
CREATE UNIQUE NONCLUSTERED INDEX [IX_UserSignatureKeyPair_UserId]
|
||||
ON [dbo].[UserSignatureKeyPair]([UserId] ASC);
|
||||
END
|
||||
GO
|
||||
|
||||
|
||||
CREATE OR ALTER VIEW [dbo].[UserSignatureKeyPairView]
|
||||
AS
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[UserSignatureKeyPair]
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[UserSignatureKeyPair_ReadByUserId]
|
||||
@UserId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[UserSignatureKeyPairView]
|
||||
WHERE
|
||||
[UserId] = @UserId;
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[UserSignatureKeyPair_UpdateForRotation]
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@SignatureAlgorithm TINYINT,
|
||||
@SigningKey VARCHAR(MAX),
|
||||
@VerifyingKey VARCHAR(MAX),
|
||||
@RevisionDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
UPDATE
|
||||
[dbo].[UserSignatureKeyPair]
|
||||
SET
|
||||
[SignatureAlgorithm] = @SignatureAlgorithm,
|
||||
[SigningKey] = @SigningKey,
|
||||
[VerifyingKey] = @VerifyingKey,
|
||||
[RevisionDate] = @RevisionDate
|
||||
WHERE
|
||||
[UserId] = @UserId;
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[UserSignatureKeyPair_SetForRotation]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@SignatureAlgorithm TINYINT,
|
||||
@SigningKey VARCHAR(MAX),
|
||||
@VerifyingKey VARCHAR(MAX),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
INSERT INTO [dbo].[UserSignatureKeyPair]
|
||||
(
|
||||
[Id],
|
||||
[UserId],
|
||||
[SignatureAlgorithm],
|
||||
[SigningKey],
|
||||
[VerifyingKey],
|
||||
[CreationDate],
|
||||
[RevisionDate]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@UserId,
|
||||
@SignatureAlgorithm,
|
||||
@SigningKey,
|
||||
@VerifyingKey,
|
||||
@CreationDate,
|
||||
@RevisionDate
|
||||
)
|
||||
END
|
||||
GO
|
||||
|
||||
IF COL_LENGTH('[dbo].[User]', 'SecurityState') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE
|
||||
[dbo].[User]
|
||||
ADD
|
||||
[SecurityState] VARCHAR(MAX) NULL;
|
||||
END
|
||||
GO
|
||||
|
||||
IF COL_LENGTH('[dbo].[User]', 'SecurityVersion') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE
|
||||
[dbo].[User]
|
||||
ADD
|
||||
[SecurityVersion] INT NULL;
|
||||
END
|
||||
GO
|
||||
|
||||
IF COL_LENGTH('[dbo].[User]', 'SignedPublicKey') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE
|
||||
[dbo].[User]
|
||||
ADD
|
||||
[SignedPublicKey] VARCHAR(MAX) NULL;
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[User_Create]
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@Name NVARCHAR(50),
|
||||
@Email NVARCHAR(256),
|
||||
@EmailVerified BIT,
|
||||
@MasterPassword NVARCHAR(300),
|
||||
@MasterPasswordHint NVARCHAR(50),
|
||||
@Culture NVARCHAR(10),
|
||||
@SecurityStamp NVARCHAR(50),
|
||||
@TwoFactorProviders NVARCHAR(MAX),
|
||||
@TwoFactorRecoveryCode NVARCHAR(32),
|
||||
@EquivalentDomains NVARCHAR(MAX),
|
||||
@ExcludedGlobalEquivalentDomains NVARCHAR(MAX),
|
||||
@AccountRevisionDate DATETIME2(7),
|
||||
@Key NVARCHAR(MAX),
|
||||
@PublicKey NVARCHAR(MAX),
|
||||
@PrivateKey NVARCHAR(MAX),
|
||||
@Premium BIT,
|
||||
@PremiumExpirationDate DATETIME2(7),
|
||||
@RenewalReminderDate DATETIME2(7),
|
||||
@Storage BIGINT,
|
||||
@MaxStorageGb SMALLINT,
|
||||
@Gateway TINYINT,
|
||||
@GatewayCustomerId VARCHAR(50),
|
||||
@GatewaySubscriptionId VARCHAR(50),
|
||||
@ReferenceData VARCHAR(MAX),
|
||||
@LicenseKey VARCHAR(100),
|
||||
@Kdf TINYINT,
|
||||
@KdfIterations INT,
|
||||
@KdfMemory INT = NULL,
|
||||
@KdfParallelism INT = NULL,
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@ApiKey VARCHAR(30),
|
||||
@ForcePasswordReset BIT = 0,
|
||||
@UsesKeyConnector BIT = 0,
|
||||
@FailedLoginCount INT = 0,
|
||||
@LastFailedLoginDate DATETIME2(7),
|
||||
@AvatarColor VARCHAR(7) = NULL,
|
||||
@LastPasswordChangeDate DATETIME2(7) = NULL,
|
||||
@LastKdfChangeDate DATETIME2(7) = NULL,
|
||||
@LastKeyRotationDate DATETIME2(7) = NULL,
|
||||
@LastEmailChangeDate DATETIME2(7) = NULL,
|
||||
@VerifyDevices BIT = 1,
|
||||
@SecurityState VARCHAR(MAX) = NULL,
|
||||
@SecurityVersion INT = NULL,
|
||||
@SignedPublicKey VARCHAR(MAX) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
INSERT INTO [dbo].[User]
|
||||
(
|
||||
[Id],
|
||||
[Name],
|
||||
[Email],
|
||||
[EmailVerified],
|
||||
[MasterPassword],
|
||||
[MasterPasswordHint],
|
||||
[Culture],
|
||||
[SecurityStamp],
|
||||
[TwoFactorProviders],
|
||||
[TwoFactorRecoveryCode],
|
||||
[EquivalentDomains],
|
||||
[ExcludedGlobalEquivalentDomains],
|
||||
[AccountRevisionDate],
|
||||
[Key],
|
||||
[PublicKey],
|
||||
[PrivateKey],
|
||||
[Premium],
|
||||
[PremiumExpirationDate],
|
||||
[RenewalReminderDate],
|
||||
[Storage],
|
||||
[MaxStorageGb],
|
||||
[Gateway],
|
||||
[GatewayCustomerId],
|
||||
[GatewaySubscriptionId],
|
||||
[ReferenceData],
|
||||
[LicenseKey],
|
||||
[Kdf],
|
||||
[KdfIterations],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[ApiKey],
|
||||
[ForcePasswordReset],
|
||||
[UsesKeyConnector],
|
||||
[FailedLoginCount],
|
||||
[LastFailedLoginDate],
|
||||
[AvatarColor],
|
||||
[KdfMemory],
|
||||
[KdfParallelism],
|
||||
[LastPasswordChangeDate],
|
||||
[LastKdfChangeDate],
|
||||
[LastKeyRotationDate],
|
||||
[LastEmailChangeDate],
|
||||
[VerifyDevices],
|
||||
[SecurityState],
|
||||
[SecurityVersion],
|
||||
[SignedPublicKey]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@Name,
|
||||
@Email,
|
||||
@EmailVerified,
|
||||
@MasterPassword,
|
||||
@MasterPasswordHint,
|
||||
@Culture,
|
||||
@SecurityStamp,
|
||||
@TwoFactorProviders,
|
||||
@TwoFactorRecoveryCode,
|
||||
@EquivalentDomains,
|
||||
@ExcludedGlobalEquivalentDomains,
|
||||
@AccountRevisionDate,
|
||||
@Key,
|
||||
@PublicKey,
|
||||
@PrivateKey,
|
||||
@Premium,
|
||||
@PremiumExpirationDate,
|
||||
@RenewalReminderDate,
|
||||
@Storage,
|
||||
@MaxStorageGb,
|
||||
@Gateway,
|
||||
@GatewayCustomerId,
|
||||
@GatewaySubscriptionId,
|
||||
@ReferenceData,
|
||||
@LicenseKey,
|
||||
@Kdf,
|
||||
@KdfIterations,
|
||||
@CreationDate,
|
||||
@RevisionDate,
|
||||
@ApiKey,
|
||||
@ForcePasswordReset,
|
||||
@UsesKeyConnector,
|
||||
@FailedLoginCount,
|
||||
@LastFailedLoginDate,
|
||||
@AvatarColor,
|
||||
@KdfMemory,
|
||||
@KdfParallelism,
|
||||
@LastPasswordChangeDate,
|
||||
@LastKdfChangeDate,
|
||||
@LastKeyRotationDate,
|
||||
@LastEmailChangeDate,
|
||||
@VerifyDevices,
|
||||
@SecurityState,
|
||||
@SecurityVersion,
|
||||
@SignedPublicKey
|
||||
)
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[User_Update]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@Name NVARCHAR(50),
|
||||
@Email NVARCHAR(256),
|
||||
@EmailVerified BIT,
|
||||
@MasterPassword NVARCHAR(300),
|
||||
@MasterPasswordHint NVARCHAR(50),
|
||||
@Culture NVARCHAR(10),
|
||||
@SecurityStamp NVARCHAR(50),
|
||||
@TwoFactorProviders NVARCHAR(MAX),
|
||||
@TwoFactorRecoveryCode NVARCHAR(32),
|
||||
@EquivalentDomains NVARCHAR(MAX),
|
||||
@ExcludedGlobalEquivalentDomains NVARCHAR(MAX),
|
||||
@AccountRevisionDate DATETIME2(7),
|
||||
@Key NVARCHAR(MAX),
|
||||
@PublicKey NVARCHAR(MAX),
|
||||
@PrivateKey NVARCHAR(MAX),
|
||||
@Premium BIT,
|
||||
@PremiumExpirationDate DATETIME2(7),
|
||||
@RenewalReminderDate DATETIME2(7),
|
||||
@Storage BIGINT,
|
||||
@MaxStorageGb SMALLINT,
|
||||
@Gateway TINYINT,
|
||||
@GatewayCustomerId VARCHAR(50),
|
||||
@GatewaySubscriptionId VARCHAR(50),
|
||||
@ReferenceData VARCHAR(MAX),
|
||||
@LicenseKey VARCHAR(100),
|
||||
@Kdf TINYINT,
|
||||
@KdfIterations INT,
|
||||
@KdfMemory INT = NULL,
|
||||
@KdfParallelism INT = NULL,
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@ApiKey VARCHAR(30),
|
||||
@ForcePasswordReset BIT = 0,
|
||||
@UsesKeyConnector BIT = 0,
|
||||
@FailedLoginCount INT,
|
||||
@LastFailedLoginDate DATETIME2(7),
|
||||
@AvatarColor VARCHAR(7),
|
||||
@LastPasswordChangeDate DATETIME2(7) = NULL,
|
||||
@LastKdfChangeDate DATETIME2(7) = NULL,
|
||||
@LastKeyRotationDate DATETIME2(7) = NULL,
|
||||
@LastEmailChangeDate DATETIME2(7) = NULL,
|
||||
@VerifyDevices BIT = 1,
|
||||
@SecurityState VARCHAR(MAX) = NULL,
|
||||
@SecurityVersion INT = NULL,
|
||||
@SignedPublicKey VARCHAR(MAX) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
[dbo].[User]
|
||||
SET
|
||||
[Name] = @Name,
|
||||
[Email] = @Email,
|
||||
[EmailVerified] = @EmailVerified,
|
||||
[MasterPassword] = @MasterPassword,
|
||||
[MasterPasswordHint] = @MasterPasswordHint,
|
||||
[Culture] = @Culture,
|
||||
[SecurityStamp] = @SecurityStamp,
|
||||
[TwoFactorProviders] = @TwoFactorProviders,
|
||||
[TwoFactorRecoveryCode] = @TwoFactorRecoveryCode,
|
||||
[EquivalentDomains] = @EquivalentDomains,
|
||||
[ExcludedGlobalEquivalentDomains] = @ExcludedGlobalEquivalentDomains,
|
||||
[AccountRevisionDate] = @AccountRevisionDate,
|
||||
[Key] = @Key,
|
||||
[PublicKey] = @PublicKey,
|
||||
[PrivateKey] = @PrivateKey,
|
||||
[Premium] = @Premium,
|
||||
[PremiumExpirationDate] = @PremiumExpirationDate,
|
||||
[RenewalReminderDate] = @RenewalReminderDate,
|
||||
[Storage] = @Storage,
|
||||
[MaxStorageGb] = @MaxStorageGb,
|
||||
[Gateway] = @Gateway,
|
||||
[GatewayCustomerId] = @GatewayCustomerId,
|
||||
[GatewaySubscriptionId] = @GatewaySubscriptionId,
|
||||
[ReferenceData] = @ReferenceData,
|
||||
[LicenseKey] = @LicenseKey,
|
||||
[Kdf] = @Kdf,
|
||||
[KdfIterations] = @KdfIterations,
|
||||
[KdfMemory] = @KdfMemory,
|
||||
[KdfParallelism] = @KdfParallelism,
|
||||
[CreationDate] = @CreationDate,
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[ApiKey] = @ApiKey,
|
||||
[ForcePasswordReset] = @ForcePasswordReset,
|
||||
[UsesKeyConnector] = @UsesKeyConnector,
|
||||
[FailedLoginCount] = @FailedLoginCount,
|
||||
[LastFailedLoginDate] = @LastFailedLoginDate,
|
||||
[AvatarColor] = @AvatarColor,
|
||||
[LastPasswordChangeDate] = @LastPasswordChangeDate,
|
||||
[LastKdfChangeDate] = @LastKdfChangeDate,
|
||||
[LastKeyRotationDate] = @LastKeyRotationDate,
|
||||
[LastEmailChangeDate] = @LastEmailChangeDate,
|
||||
[VerifyDevices] = @VerifyDevices,
|
||||
[SecurityState] = @SecurityState,
|
||||
[SecurityVersion] = @SecurityVersion,
|
||||
[SignedPublicKey] = @SignedPublicKey
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
GO
|
||||
|
||||
EXECUTE sp_refreshview 'dbo.UserView'
|
||||
EXECUTE sp_refreshview 'dbo.EmergencyAccessDetailsView'
|
||||
EXECUTE sp_refreshview 'dbo.OrganizationUserUserDetailsView'
|
||||
EXECUTE sp_refreshview 'dbo.ProviderUserUserDetailsView'
|
||||
EXECUTE sp_refreshview 'dbo.UserEmailDomainView'
|
||||
GO
|
||||
3338
util/MySqlMigrations/Migrations/20251013083703_UserCryptoV2.Designer.cs
generated
Normal file
3338
util/MySqlMigrations/Migrations/20251013083703_UserCryptoV2.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.MySqlMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class UserCryptoV2 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SecurityState",
|
||||
table: "User",
|
||||
type: "longtext",
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "SecurityVersion",
|
||||
table: "User",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SignedPublicKey",
|
||||
table: "User",
|
||||
type: "longtext",
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserSignatureKeyPair",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
SignatureAlgorithm = table.Column<byte>(type: "tinyint unsigned", nullable: false),
|
||||
VerifyingKey = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
SigningKey = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreationDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
RevisionDate = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserSignatureKeyPair", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserSignatureKeyPair_User_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "User",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserSignatureKeyPair_UserId",
|
||||
table: "UserSignatureKeyPair",
|
||||
column: "UserId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserSignatureKeyPair");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SecurityState",
|
||||
table: "User");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SecurityVersion",
|
||||
table: "User");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SignedPublicKey",
|
||||
table: "User");
|
||||
}
|
||||
}
|
||||
@@ -1873,6 +1873,15 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("SecurityState")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int?>("SecurityVersion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SignedPublicKey")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<long?>("Storage")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
@@ -1901,6 +1910,40 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
b.ToTable("User", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("RevisionDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<byte>("SignatureAlgorithm")
|
||||
.HasColumnType("tinyint unsigned");
|
||||
|
||||
b.Property<string>("SigningKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("VerifyingKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique()
|
||||
.HasAnnotation("SqlServer:Clustered", false);
|
||||
|
||||
b.ToTable("UserSignatureKeyPair", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -2948,6 +2991,17 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b =>
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||
|
||||
3344
util/PostgresMigrations/Migrations/20251013083502_UserCryptoV2.Designer.cs
generated
Normal file
3344
util/PostgresMigrations/Migrations/20251013083502_UserCryptoV2.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.PostgresMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class UserCryptoV2 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SecurityState",
|
||||
table: "User",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "SecurityVersion",
|
||||
table: "User",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SignedPublicKey",
|
||||
table: "User",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserSignatureKeyPair",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
SignatureAlgorithm = table.Column<byte>(type: "smallint", nullable: false),
|
||||
VerifyingKey = table.Column<string>(type: "text", nullable: false),
|
||||
SigningKey = table.Column<string>(type: "text", nullable: false),
|
||||
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
RevisionDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserSignatureKeyPair", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserSignatureKeyPair_User_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "User",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserSignatureKeyPair_UserId",
|
||||
table: "UserSignatureKeyPair",
|
||||
column: "UserId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserSignatureKeyPair");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SecurityState",
|
||||
table: "User");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SecurityVersion",
|
||||
table: "User");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SignedPublicKey",
|
||||
table: "User");
|
||||
}
|
||||
}
|
||||
@@ -1879,6 +1879,15 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("SecurityState")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("SecurityVersion")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("SignedPublicKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<long?>("Storage")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
@@ -1907,6 +1916,40 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
b.ToTable("User", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("RevisionDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<byte>("SignatureAlgorithm")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<string>("SigningKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("VerifyingKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique()
|
||||
.HasAnnotation("SqlServer:Clustered", false);
|
||||
|
||||
b.ToTable("UserSignatureKeyPair", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -2954,6 +2997,17 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b =>
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||
|
||||
3327
util/SqliteMigrations/Migrations/20251013083514_UserCryptoV2.Designer.cs
generated
Normal file
3327
util/SqliteMigrations/Migrations/20251013083514_UserCryptoV2.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.SqliteMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class UserCryptoV2 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SecurityState",
|
||||
table: "User",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "SecurityVersion",
|
||||
table: "User",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SignedPublicKey",
|
||||
table: "User",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserSignatureKeyPair",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
SignatureAlgorithm = table.Column<byte>(type: "INTEGER", nullable: false),
|
||||
VerifyingKey = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SigningKey = table.Column<string>(type: "TEXT", nullable: false),
|
||||
CreationDate = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
RevisionDate = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserSignatureKeyPair", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserSignatureKeyPair_User_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "User",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserSignatureKeyPair_UserId",
|
||||
table: "UserSignatureKeyPair",
|
||||
column: "UserId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserSignatureKeyPair");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SecurityState",
|
||||
table: "User");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SecurityVersion",
|
||||
table: "User");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SignedPublicKey",
|
||||
table: "User");
|
||||
}
|
||||
}
|
||||
@@ -1862,6 +1862,15 @@ namespace Bit.SqliteMigrations.Migrations
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SecurityState")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SecurityVersion")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SignedPublicKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("Storage")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -1890,6 +1899,40 @@ namespace Bit.SqliteMigrations.Migrations
|
||||
b.ToTable("User", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("RevisionDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte>("SignatureAlgorithm")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SigningKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("VerifyingKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique()
|
||||
.HasAnnotation("SqlServer:Clustered", false);
|
||||
|
||||
b.ToTable("UserSignatureKeyPair", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -2937,6 +2980,17 @@ namespace Bit.SqliteMigrations.Migrations
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b =>
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||
|
||||
Reference in New Issue
Block a user