mirror of
https://github.com/bitwarden/server
synced 2025-12-14 23:33:41 +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.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.KeyManagement.Kdf;
|
using Bit.Core.KeyManagement.Kdf;
|
||||||
|
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Core.Models.Api.Response;
|
using Bit.Core.Models.Api.Response;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -40,6 +41,7 @@ public class AccountsController : Controller
|
|||||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||||
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
||||||
private readonly IChangeKdfCommand _changeKdfCommand;
|
private readonly IChangeKdfCommand _changeKdfCommand;
|
||||||
|
|
||||||
@@ -53,6 +55,7 @@ public class AccountsController : Controller
|
|||||||
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
|
IUserAccountKeysQuery userAccountKeysQuery,
|
||||||
ITwoFactorEmailService twoFactorEmailService,
|
ITwoFactorEmailService twoFactorEmailService,
|
||||||
IChangeKdfCommand changeKdfCommand
|
IChangeKdfCommand changeKdfCommand
|
||||||
)
|
)
|
||||||
@@ -66,6 +69,7 @@ public class AccountsController : Controller
|
|||||||
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
|
_userAccountKeysQuery = userAccountKeysQuery;
|
||||||
_twoFactorEmailService = twoFactorEmailService;
|
_twoFactorEmailService = twoFactorEmailService;
|
||||||
_changeKdfCommand = changeKdfCommand;
|
_changeKdfCommand = changeKdfCommand;
|
||||||
}
|
}
|
||||||
@@ -332,7 +336,9 @@ public class AccountsController : Controller
|
|||||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
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,
|
providerUserOrganizationDetails, twoFactorEnabled,
|
||||||
hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
||||||
return response;
|
return response;
|
||||||
@@ -364,8 +370,9 @@ public class AccountsController : Controller
|
|||||||
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
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;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,8 +396,9 @@ public class AccountsController : Controller
|
|||||||
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
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;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using Bit.Api.Models.Request;
|
using Bit.Api.Models.Request;
|
||||||
using Bit.Api.Models.Request.Accounts;
|
using Bit.Api.Models.Request.Accounts;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
@@ -8,6 +9,7 @@ using Bit.Core.Billing.Models;
|
|||||||
using Bit.Core.Billing.Models.Business;
|
using Bit.Core.Billing.Models.Business;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@@ -21,7 +23,8 @@ namespace Bit.Api.Billing.Controllers;
|
|||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class AccountsController(
|
public class AccountsController(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
|
IUserAccountKeysQuery userAccountKeysQuery) : Controller
|
||||||
{
|
{
|
||||||
[HttpPost("premium")]
|
[HttpPost("premium")]
|
||||||
public async Task<PaymentResponseModel> PostPremiumAsync(
|
public async Task<PaymentResponseModel> PostPremiumAsync(
|
||||||
@@ -58,8 +61,9 @@ public class AccountsController(
|
|||||||
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
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);
|
userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
||||||
return new PaymentResponseModel
|
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,
|
OldMasterKeyAuthenticationHash = model.OldMasterKeyAuthenticationHash,
|
||||||
|
|
||||||
UserKeyEncryptedAccountPrivateKey = model.AccountKeys.UserKeyEncryptedAccountPrivateKey,
|
AccountKeys = model.AccountKeys.ToAccountKeysData(),
|
||||||
AccountPublicKey = model.AccountKeys.AccountPublicKey,
|
|
||||||
|
|
||||||
MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(),
|
MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(),
|
||||||
EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.AccountUnlockData.EmergencyAccessUnlockData),
|
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;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||||
@@ -7,4 +8,44 @@ public class AccountKeysRequestModel
|
|||||||
{
|
{
|
||||||
[EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { get; set; }
|
[EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { get; set; }
|
||||||
public required string AccountPublicKey { 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;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
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;
|
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;
|
||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
using Bit.Api.Auth.Models.Request.Accounts;
|
||||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
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;
|
using Bit.Api.Vault.Models.Request;
|
||||||
|
|
||||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
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.Api.AdminConsole.Models.Response.Providers;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.Entities;
|
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.Api;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
|
||||||
@@ -13,6 +15,7 @@ namespace Bit.Api.Models.Response;
|
|||||||
public class ProfileResponseModel : ResponseModel
|
public class ProfileResponseModel : ResponseModel
|
||||||
{
|
{
|
||||||
public ProfileResponseModel(User user,
|
public ProfileResponseModel(User user,
|
||||||
|
UserAccountKeysData userAccountKeysData,
|
||||||
IEnumerable<OrganizationUserOrganizationDetails> organizationsUserDetails,
|
IEnumerable<OrganizationUserOrganizationDetails> organizationsUserDetails,
|
||||||
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
|
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
|
||||||
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
|
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
|
||||||
@@ -35,6 +38,7 @@ public class ProfileResponseModel : ResponseModel
|
|||||||
TwoFactorEnabled = twoFactorEnabled;
|
TwoFactorEnabled = twoFactorEnabled;
|
||||||
Key = user.Key;
|
Key = user.Key;
|
||||||
PrivateKey = user.PrivateKey;
|
PrivateKey = user.PrivateKey;
|
||||||
|
AccountKeys = userAccountKeysData != null ? new PrivateKeysResponseModel(userAccountKeysData) : null;
|
||||||
SecurityStamp = user.SecurityStamp;
|
SecurityStamp = user.SecurityStamp;
|
||||||
ForcePasswordReset = user.ForcePasswordReset;
|
ForcePasswordReset = user.ForcePasswordReset;
|
||||||
UsesKeyConnector = user.UsesKeyConnector;
|
UsesKeyConnector = user.UsesKeyConnector;
|
||||||
@@ -60,7 +64,9 @@ public class ProfileResponseModel : ResponseModel
|
|||||||
public string Culture { get; set; }
|
public string Culture { get; set; }
|
||||||
public bool TwoFactorEnabled { get; set; }
|
public bool TwoFactorEnabled { get; set; }
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
|
[Obsolete("Use AccountKeys instead.")]
|
||||||
public string PrivateKey { get; set; }
|
public string PrivateKey { get; set; }
|
||||||
|
public PrivateKeysResponseModel AccountKeys { get; set; }
|
||||||
public string SecurityStamp { get; set; }
|
public string SecurityStamp { get; set; }
|
||||||
public bool ForcePasswordReset { get; set; }
|
public bool ForcePasswordReset { get; set; }
|
||||||
public bool UsesKeyConnector { get; set; }
|
public bool UsesKeyConnector { get; set; }
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ using Bit.Core.Context;
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
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;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -42,6 +44,7 @@ public class SyncController : Controller
|
|||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||||
|
|
||||||
public SyncController(
|
public SyncController(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
@@ -57,7 +60,8 @@ public class SyncController : Controller
|
|||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
|
IUserAccountKeysQuery userAccountKeysQuery)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_folderRepository = folderRepository;
|
_folderRepository = folderRepository;
|
||||||
@@ -73,6 +77,7 @@ public class SyncController : Controller
|
|||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
|
_userAccountKeysQuery = userAccountKeysQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
@@ -116,7 +121,14 @@ public class SyncController : Controller
|
|||||||
|
|
||||||
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
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,
|
organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
|
||||||
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ using Bit.Api.Tools.Models.Response;
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.Entities;
|
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.Api;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
@@ -24,6 +25,7 @@ public class SyncResponseModel() : ResponseModel("sync")
|
|||||||
public SyncResponseModel(
|
public SyncResponseModel(
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
User user,
|
User user,
|
||||||
|
UserAccountKeysData userAccountKeysData,
|
||||||
bool userTwoFactorEnabled,
|
bool userTwoFactorEnabled,
|
||||||
bool userHasPremiumFromOrganization,
|
bool userHasPremiumFromOrganization,
|
||||||
IDictionary<Guid, OrganizationAbility> organizationAbilities,
|
IDictionary<Guid, OrganizationAbility> organizationAbilities,
|
||||||
@@ -40,7 +42,7 @@ public class SyncResponseModel() : ResponseModel("sync")
|
|||||||
IEnumerable<Send> sends)
|
IEnumerable<Send> sends)
|
||||||
: this()
|
: this()
|
||||||
{
|
{
|
||||||
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
|
Profile = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails,
|
||||||
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);
|
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);
|
||||||
Folders = folders.Select(f => new FolderResponseModel(f));
|
Folders = folders.Select(f => new FolderResponseModel(f));
|
||||||
Ciphers = ciphers.Select(cipher =>
|
Ciphers = ciphers.Select(cipher =>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Bit.Core.KeyManagement.Models.Response;
|
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Models.Api.Response;
|
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 UserkeyRotationV2 = "userkey-rotation-v2";
|
||||||
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
||||||
public const string UserSdkForDecryption = "use-sdk-for-decryption";
|
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 PM17987_BlockType0 = "pm-17987-block-type-0";
|
||||||
public const string ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings";
|
public const string ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings";
|
||||||
public const string UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data";
|
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.Enums;
|
||||||
using Bit.Core.Auth.Models;
|
using Bit.Core.Auth.Models;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
@@ -21,6 +22,9 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
|||||||
[MaxLength(256)]
|
[MaxLength(256)]
|
||||||
public string Email { get; set; } = null!;
|
public string Email { get; set; } = null!;
|
||||||
public bool EmailVerified { get; set; }
|
public bool EmailVerified { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The server-side master-password hash
|
||||||
|
/// </summary>
|
||||||
[MaxLength(300)]
|
[MaxLength(300)]
|
||||||
public string? MasterPassword { get; set; }
|
public string? MasterPassword { get; set; }
|
||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
@@ -41,9 +45,30 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
|||||||
/// organization membership.
|
/// organization membership.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime AccountRevisionDate { get; set; } = DateTime.UtcNow;
|
public DateTime AccountRevisionDate { get; set; } = DateTime.UtcNow;
|
||||||
|
/// <summary>
|
||||||
|
/// The master-password-sealed user key.
|
||||||
|
/// </summary>
|
||||||
public string? Key { get; set; }
|
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; }
|
public string? PublicKey { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// User key wrapped private key.
|
||||||
|
/// </summary>
|
||||||
public string? PrivateKey { get; set; }
|
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 bool Premium { get; set; }
|
||||||
public DateTime? PremiumExpirationDate { get; set; }
|
public DateTime? PremiumExpirationDate { get; set; }
|
||||||
public DateTime? RenewalReminderDate { get; set; }
|
public DateTime? RenewalReminderDate { get; set; }
|
||||||
@@ -180,6 +205,12 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
|||||||
return Premium;
|
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>
|
/// <summary>
|
||||||
/// Serializes the C# object to the User.TwoFactorProviders property in JSON format.
|
/// Serializes the C# object to the User.TwoFactorProviders property in JSON format.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -243,4 +274,14 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
|||||||
{
|
{
|
||||||
return MasterPassword != null;
|
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.Commands.Interfaces;
|
||||||
using Bit.Core.KeyManagement.Kdf;
|
using Bit.Core.KeyManagement.Kdf;
|
||||||
using Bit.Core.KeyManagement.Kdf.Implementations;
|
using Bit.Core.KeyManagement.Kdf.Implementations;
|
||||||
|
using Bit.Core.KeyManagement.Queries;
|
||||||
|
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Bit.Core.KeyManagement;
|
namespace Bit.Core.KeyManagement;
|
||||||
@@ -11,6 +13,7 @@ public static class KeyManagementServiceCollectionExtensions
|
|||||||
public static void AddKeyManagementServices(this IServiceCollection services)
|
public static void AddKeyManagementServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddKeyManagementCommands();
|
services.AddKeyManagementCommands();
|
||||||
|
services.AddKeyManagementQueries();
|
||||||
services.AddSendPasswordServices();
|
services.AddSendPasswordServices();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,4 +22,9 @@ public static class KeyManagementServiceCollectionExtensions
|
|||||||
services.AddScoped<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>();
|
services.AddScoped<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>();
|
||||||
services.AddScoped<IChangeKdfCommand, ChangeKdfCommand>();
|
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.Enums;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.KeyManagement.Models.Response;
|
namespace Bit.Core.KeyManagement.Models.Api.Response;
|
||||||
|
|
||||||
public class MasterPasswordUnlockResponseModel
|
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
|
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.Entities;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
@@ -12,21 +10,19 @@ namespace Bit.Core.KeyManagement.Models.Data;
|
|||||||
public class RotateUserAccountKeysData
|
public class RotateUserAccountKeysData
|
||||||
{
|
{
|
||||||
// Authentication for this requests
|
// Authentication for this requests
|
||||||
public string OldMasterKeyAuthenticationHash { get; set; }
|
public required string OldMasterKeyAuthenticationHash { get; set; }
|
||||||
|
|
||||||
// Other keys encrypted by the userkey
|
public required UserAccountKeysData AccountKeys { get; set; }
|
||||||
public string UserKeyEncryptedAccountPrivateKey { get; set; }
|
|
||||||
public string AccountPublicKey { get; set; }
|
|
||||||
|
|
||||||
// All methods to get to the userkey
|
// All methods to get to the userkey
|
||||||
public MasterPasswordUnlockAndAuthenticationData MasterPasswordUnlockData { get; set; }
|
public required MasterPasswordUnlockAndAuthenticationData MasterPasswordUnlockData { get; set; }
|
||||||
public IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; }
|
public required IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; }
|
||||||
public IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
|
public required IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
|
||||||
public IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; }
|
public required IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; }
|
||||||
public IEnumerable<Device> DeviceKeys { get; set; }
|
public required IEnumerable<Device> DeviceKeys { get; set; }
|
||||||
|
|
||||||
// User vault data encrypted by the userkey
|
// User vault data encrypted by the userkey
|
||||||
public IEnumerable<Cipher> Ciphers { get; set; }
|
public required IEnumerable<Cipher> Ciphers { get; set; }
|
||||||
public IEnumerable<Folder> Folders { get; set; }
|
public required IEnumerable<Folder> Folders { get; set; }
|
||||||
public IReadOnlyList<Send> Sends { 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.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.KeyManagement.Models.Data;
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
|
using Bit.Core.KeyManagement.Repositories;
|
||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -25,6 +30,8 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
|||||||
private readonly IdentityErrorDescriber _identityErrorDescriber;
|
private readonly IdentityErrorDescriber _identityErrorDescriber;
|
||||||
private readonly IWebAuthnCredentialRepository _credentialRepository;
|
private readonly IWebAuthnCredentialRepository _credentialRepository;
|
||||||
private readonly IPasswordHasher<User> _passwordHasher;
|
private readonly IPasswordHasher<User> _passwordHasher;
|
||||||
|
private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Instantiates a new <see cref="RotateUserAccountKeysCommand"/>
|
/// 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="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="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="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="passwordHasher">Hashes the new master password</param>
|
||||||
/// <param name="pushService">Logs out user from other devices after successful rotation</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="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="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,
|
public RotateUserAccountKeysCommand(IUserService userService, IUserRepository userRepository,
|
||||||
ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository,
|
ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository,
|
||||||
IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository,
|
IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository,
|
||||||
IDeviceRepository deviceRepository,
|
IDeviceRepository deviceRepository,
|
||||||
IPasswordHasher<User> passwordHasher,
|
IPasswordHasher<User> passwordHasher,
|
||||||
IPushNotificationService pushService, IdentityErrorDescriber errors, IWebAuthnCredentialRepository credentialRepository,
|
IPushNotificationService pushService, IdentityErrorDescriber errors, IWebAuthnCredentialRepository credentialRepository,
|
||||||
|
IUserSignatureKeyPairRepository userSignatureKeyPairRepository,
|
||||||
IFeatureService featureService)
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
@@ -60,6 +70,8 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
|||||||
_identityErrorDescriber = errors;
|
_identityErrorDescriber = errors;
|
||||||
_credentialRepository = credentialRepository;
|
_credentialRepository = credentialRepository;
|
||||||
_passwordHasher = passwordHasher;
|
_passwordHasher = passwordHasher;
|
||||||
|
_userSignatureKeyPairRepository = userSignatureKeyPairRepository;
|
||||||
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -80,50 +92,106 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
|||||||
user.LastKeyRotationDate = now;
|
user.LastKeyRotationDate = now;
|
||||||
user.SecurityStamp = Guid.NewGuid().ToString();
|
user.SecurityStamp = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
if (
|
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = [];
|
||||||
!model.MasterPasswordUnlockData.ValidateForUser(user)
|
|
||||||
)
|
await UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);
|
||||||
{
|
UpdateUnlockMethods(model, user, saveEncryptedDataActions);
|
||||||
throw new InvalidOperationException("The provided master password unlock data is not valid for this user.");
|
UpdateUserData(model, user, saveEncryptedDataActions);
|
||||||
}
|
|
||||||
if (
|
await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions);
|
||||||
model.AccountPublicKey != user.PublicKey
|
await _pushService.PushLogOutAsync(user.Id);
|
||||||
)
|
return IdentityResult.Success;
|
||||||
{
|
|
||||||
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.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Key = model.MasterPasswordUnlockData.MasterKeyEncryptedUserKey;
|
public async Task RotateV2AccountKeysAsync(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||||
user.PrivateKey = model.UserKeyEncryptedAccountPrivateKey;
|
{
|
||||||
user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordUnlockData.MasterKeyAuthenticationHash);
|
ValidateV2Encryption(model);
|
||||||
user.MasterPasswordHint = model.MasterPasswordUnlockData.MasterPasswordHint;
|
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))
|
||||||
|
{
|
||||||
|
await RotateV2AccountKeysAsync(model, user, saveEncryptedDataActions);
|
||||||
|
}
|
||||||
|
else if (model.AccountKeys.SignatureKeyPairData != null)
|
||||||
|
{
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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())
|
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())
|
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())
|
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())
|
if (model.EmergencyAccesses.Any())
|
||||||
{
|
{
|
||||||
saveEncryptedDataActions.Add(
|
saveEncryptedDataActions.Add(_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses));
|
||||||
_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.OrganizationUsers.Any())
|
if (model.OrganizationUsers.Any())
|
||||||
{
|
{
|
||||||
saveEncryptedDataActions.Add(
|
saveEncryptedDataActions.Add(_organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers));
|
||||||
_organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.WebAuthnKeys.Any())
|
if (model.WebAuthnKeys.Any())
|
||||||
@@ -135,9 +203,80 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
|||||||
{
|
{
|
||||||
saveEncryptedDataActions.Add(_deviceRepository.UpdateKeysForRotationAsync(user.Id, model.DeviceKeys));
|
saveEncryptedDataActions.Add(_deviceRepository.UpdateKeysForRotationAsync(user.Id, model.DeviceKeys));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions);
|
private bool IsV2EncryptionUserAsync(User user)
|
||||||
await _pushService.PushLogOutAsync(user.Id);
|
{
|
||||||
return IdentityResult.Success;
|
// 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.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
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;
|
||||||
using Bit.Core.Models.Api.Response;
|
using Bit.Core.Models.Api.Response;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@@ -45,6 +47,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
protected IUserService _userService { get; }
|
protected IUserService _userService { get; }
|
||||||
protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }
|
protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }
|
||||||
protected IPolicyRequirementQuery PolicyRequirementQuery { get; }
|
protected IPolicyRequirementQuery PolicyRequirementQuery { get; }
|
||||||
|
protected IUserAccountKeysQuery _accountKeysQuery { get; }
|
||||||
|
|
||||||
public BaseRequestValidator(
|
public BaseRequestValidator(
|
||||||
UserManager<User> userManager,
|
UserManager<User> userManager,
|
||||||
@@ -63,7 +66,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IAuthRequestRepository authRequestRepository,
|
IAuthRequestRepository authRequestRepository,
|
||||||
IMailService mailService
|
IMailService mailService,
|
||||||
|
IUserAccountKeysQuery userAccountKeysQuery
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
@@ -83,6 +87,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
PolicyRequirementQuery = policyRequirementQuery;
|
PolicyRequirementQuery = policyRequirementQuery;
|
||||||
_authRequestRepository = authRequestRepository;
|
_authRequestRepository = authRequestRepository;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
|
_accountKeysQuery = userAccountKeysQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
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))
|
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
|
||||||
{
|
{
|
||||||
customResponse.Add("PrivateKey", user.PrivateKey);
|
customResponse.Add("PrivateKey", user.PrivateKey);
|
||||||
|
var accountKeys = await _accountKeysQuery.Run(user);
|
||||||
|
customResponse.Add("AccountKeys", new PrivateKeysResponseModel(accountKeys));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(user.Key))
|
if (!string.IsNullOrWhiteSpace(user.Key))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using Bit.Core.Auth.Models.Api.Response;
|
|||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Core.Platform.Installations;
|
using Bit.Core.Platform.Installations;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -47,7 +48,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
IUpdateInstallationCommand updateInstallationCommand,
|
IUpdateInstallationCommand updateInstallationCommand,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IAuthRequestRepository authRequestRepository,
|
IAuthRequestRepository authRequestRepository,
|
||||||
IMailService mailService)
|
IMailService mailService,
|
||||||
|
IUserAccountKeysQuery userAccountKeysQuery)
|
||||||
: base(
|
: base(
|
||||||
userManager,
|
userManager,
|
||||||
userService,
|
userService,
|
||||||
@@ -65,7 +67,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
userDecryptionOptionsBuilder,
|
userDecryptionOptionsBuilder,
|
||||||
policyRequirementQuery,
|
policyRequirementQuery,
|
||||||
authRequestRepository,
|
authRequestRepository,
|
||||||
mailService)
|
mailService,
|
||||||
|
userAccountKeysQuery)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_updateInstallationCommand = updateInstallationCommand;
|
_updateInstallationCommand = updateInstallationCommand;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Services;
|
|||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@@ -41,7 +42,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
ISsoConfigRepository ssoConfigRepository,
|
ISsoConfigRepository ssoConfigRepository,
|
||||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IMailService mailService)
|
IMailService mailService,
|
||||||
|
IUserAccountKeysQuery userAccountKeysQuery)
|
||||||
: base(
|
: base(
|
||||||
userManager,
|
userManager,
|
||||||
userService,
|
userService,
|
||||||
@@ -59,7 +61,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
userDecryptionOptionsBuilder,
|
userDecryptionOptionsBuilder,
|
||||||
policyRequirementQuery,
|
policyRequirementQuery,
|
||||||
authRequestRepository,
|
authRequestRepository,
|
||||||
mailService)
|
mailService,
|
||||||
|
userAccountKeysQuery)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using Bit.Core.Auth.Repositories;
|
|||||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@@ -50,7 +51,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
|||||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
|
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IAuthRequestRepository authRequestRepository,
|
IAuthRequestRepository authRequestRepository,
|
||||||
IMailService mailService)
|
IMailService mailService,
|
||||||
|
IUserAccountKeysQuery userAccountKeysQuery)
|
||||||
: base(
|
: base(
|
||||||
userManager,
|
userManager,
|
||||||
userService,
|
userService,
|
||||||
@@ -68,7 +70,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
|||||||
userDecryptionOptionsBuilder,
|
userDecryptionOptionsBuilder,
|
||||||
policyRequirementQuery,
|
policyRequirementQuery,
|
||||||
authRequestRepository,
|
authRequestRepository,
|
||||||
mailService)
|
mailService,
|
||||||
|
userAccountKeysQuery)
|
||||||
{
|
{
|
||||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using Bit.Core.Auth.Utilities;
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.KeyManagement.Models.Response;
|
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ public static class DapperServiceCollectionExtensions
|
|||||||
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
|
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
|
||||||
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
|
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
|
||||||
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
|
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
|
||||||
|
services.AddSingleton<IUserSignatureKeyPairRepository, UserSignatureKeyPairRepository>();
|
||||||
services.AddSingleton<IOrganizationReportRepository, OrganizationReportRepository>();
|
services.AddSingleton<IOrganizationReportRepository, OrganizationReportRepository>();
|
||||||
services.AddSingleton<IOrganizationApplicationRepository, OrganizationApplicationRepository>();
|
services.AddSingleton<IOrganizationApplicationRepository, OrganizationApplicationRepository>();
|
||||||
services.AddSingleton<IOrganizationMemberBaseDetailRepository, OrganizationMemberBaseDetailRepository>();
|
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<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
|
||||||
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
|
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
|
||||||
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
|
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
|
||||||
|
services.AddSingleton<IUserSignatureKeyPairRepository, UserSignatureKeyPairRepository>();
|
||||||
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
|
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
|
||||||
services.AddSingleton<IOrganizationReportRepository, OrganizationReportRepository>();
|
services.AddSingleton<IOrganizationReportRepository, OrganizationReportRepository>();
|
||||||
services.AddSingleton<IOrganizationApplicationRepository, OrganizationApplicationRepository>();
|
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<TaxRate> TaxRates { get; set; }
|
||||||
public DbSet<Transaction> Transactions { get; set; }
|
public DbSet<Transaction> Transactions { get; set; }
|
||||||
public DbSet<User> Users { get; set; }
|
public DbSet<User> Users { get; set; }
|
||||||
|
public DbSet<UserSignatureKeyPair> UserSignatureKeyPairs { get; set; }
|
||||||
public DbSet<AuthRequest> AuthRequests { get; set; }
|
public DbSet<AuthRequest> AuthRequests { get; set; }
|
||||||
public DbSet<OrganizationDomain> OrganizationDomains { get; set; }
|
public DbSet<OrganizationDomain> OrganizationDomains { get; set; }
|
||||||
public DbSet<WebAuthnCredential> WebAuthnCredentials { 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,
|
@LastKdfChangeDate DATETIME2(7) = NULL,
|
||||||
@LastKeyRotationDate DATETIME2(7) = NULL,
|
@LastKeyRotationDate DATETIME2(7) = NULL,
|
||||||
@LastEmailChangeDate 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
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@@ -90,7 +93,10 @@ BEGIN
|
|||||||
[LastKdfChangeDate],
|
[LastKdfChangeDate],
|
||||||
[LastKeyRotationDate],
|
[LastKeyRotationDate],
|
||||||
[LastEmailChangeDate],
|
[LastEmailChangeDate],
|
||||||
[VerifyDevices]
|
[VerifyDevices],
|
||||||
|
[SecurityState],
|
||||||
|
[SecurityVersion],
|
||||||
|
[SignedPublicKey]
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
@@ -136,6 +142,9 @@ BEGIN
|
|||||||
@LastKdfChangeDate,
|
@LastKdfChangeDate,
|
||||||
@LastKeyRotationDate,
|
@LastKeyRotationDate,
|
||||||
@LastEmailChangeDate,
|
@LastEmailChangeDate,
|
||||||
@VerifyDevices
|
@VerifyDevices,
|
||||||
|
@SecurityState,
|
||||||
|
@SecurityVersion,
|
||||||
|
@SignedPublicKey
|
||||||
)
|
)
|
||||||
END
|
END
|
||||||
|
|||||||
@@ -41,7 +41,10 @@
|
|||||||
@LastKdfChangeDate DATETIME2(7) = NULL,
|
@LastKdfChangeDate DATETIME2(7) = NULL,
|
||||||
@LastKeyRotationDate DATETIME2(7) = NULL,
|
@LastKeyRotationDate DATETIME2(7) = NULL,
|
||||||
@LastEmailChangeDate 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
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@@ -90,7 +93,10 @@ BEGIN
|
|||||||
[LastKdfChangeDate] = @LastKdfChangeDate,
|
[LastKdfChangeDate] = @LastKdfChangeDate,
|
||||||
[LastKeyRotationDate] = @LastKeyRotationDate,
|
[LastKeyRotationDate] = @LastKeyRotationDate,
|
||||||
[LastEmailChangeDate] = @LastEmailChangeDate,
|
[LastEmailChangeDate] = @LastEmailChangeDate,
|
||||||
[VerifyDevices] = @VerifyDevices
|
[VerifyDevices] = @VerifyDevices,
|
||||||
|
[SecurityState] = @SecurityState,
|
||||||
|
[SecurityVersion] = @SecurityVersion,
|
||||||
|
[SignedPublicKey] = @SignedPublicKey
|
||||||
WHERE
|
WHERE
|
||||||
[Id] = @Id
|
[Id] = @Id
|
||||||
END
|
END
|
||||||
|
|||||||
@@ -42,6 +42,9 @@
|
|||||||
[LastKeyRotationDate] DATETIME2 (7) NULL,
|
[LastKeyRotationDate] DATETIME2 (7) NULL,
|
||||||
[LastEmailChangeDate] DATETIME2 (7) NULL,
|
[LastEmailChangeDate] DATETIME2 (7) NULL,
|
||||||
[VerifyDevices] BIT DEFAULT ((1)) NOT 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)
|
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.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
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.Repositories;
|
||||||
using Bit.Core.Vault.Enums;
|
using Bit.Core.Vault.Enums;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
@@ -24,6 +28,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
|||||||
{
|
{
|
||||||
private static readonly string _mockEncryptedString =
|
private static readonly string _mockEncryptedString =
|
||||||
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
|
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
|
||||||
|
private static readonly string _mockEncryptedType7String = "7.AOs41Hd8OQiCPXjyJKCiDA==";
|
||||||
|
|
||||||
private readonly HttpClient _client;
|
private readonly HttpClient _client;
|
||||||
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
|
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
|
||||||
@@ -34,6 +39,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
|||||||
private readonly IDeviceRepository _deviceRepository;
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
private readonly IPasswordHasher<User> _passwordHasher;
|
private readonly IPasswordHasher<User> _passwordHasher;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository;
|
||||||
private string _ownerEmail = null!;
|
private string _ownerEmail = null!;
|
||||||
|
|
||||||
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
|
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
|
||||||
@@ -49,6 +55,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
|||||||
_organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
_organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||||
_passwordHasher = _factory.GetService<IPasswordHasher<User>>();
|
_passwordHasher = _factory.GetService<IPasswordHasher<User>>();
|
||||||
_organizationRepository = _factory.GetService<IOrganizationRepository>();
|
_organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||||
|
_userSignatureKeyPairRepository = _factory.GetService<IUserSignatureKeyPairRepository>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
@@ -200,6 +207,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
|||||||
var password = _passwordHasher.HashPassword(user, "newMasterPassword");
|
var password = _passwordHasher.HashPassword(user, "newMasterPassword");
|
||||||
user.MasterPassword = password;
|
user.MasterPassword = password;
|
||||||
user.PublicKey = "publicKey";
|
user.PublicKey = "publicKey";
|
||||||
|
user.PrivateKey = _mockEncryptedString;
|
||||||
await _userRepository.ReplaceAsync(user);
|
await _userRepository.ReplaceAsync(user);
|
||||||
|
|
||||||
request.AccountUnlockData.MasterPasswordUnlockData.KdfType = user.Kdf;
|
request.AccountUnlockData.MasterPasswordUnlockData.KdfType = user.Kdf;
|
||||||
@@ -209,6 +217,8 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
|||||||
request.AccountUnlockData.MasterPasswordUnlockData.Email = user.Email;
|
request.AccountUnlockData.MasterPasswordUnlockData.Email = user.Email;
|
||||||
request.AccountKeys.AccountPublicKey = "publicKey";
|
request.AccountKeys.AccountPublicKey = "publicKey";
|
||||||
request.AccountKeys.UserKeyEncryptedAccountPrivateKey = _mockEncryptedString;
|
request.AccountKeys.UserKeyEncryptedAccountPrivateKey = _mockEncryptedString;
|
||||||
|
request.AccountKeys.PublicKeyEncryptionKeyPair = null;
|
||||||
|
request.AccountKeys.SignatureKeyPair = null;
|
||||||
|
|
||||||
request.OldMasterKeyAuthenticationHash = "newMasterPassword";
|
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.RevisionDate, TimeSpan.FromMinutes(1));
|
||||||
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, 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.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.KeyManagement.Kdf;
|
using Bit.Core.KeyManagement.Kdf;
|
||||||
|
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
@@ -33,10 +34,10 @@ public class AccountsControllerTests : IDisposable
|
|||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||||
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
||||||
private readonly IChangeKdfCommand _changeKdfCommand;
|
private readonly IChangeKdfCommand _changeKdfCommand;
|
||||||
|
|
||||||
|
|
||||||
public AccountsControllerTests()
|
public AccountsControllerTests()
|
||||||
{
|
{
|
||||||
_userService = Substitute.For<IUserService>();
|
_userService = Substitute.For<IUserService>();
|
||||||
@@ -48,6 +49,7 @@ public class AccountsControllerTests : IDisposable
|
|||||||
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
|
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
|
||||||
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
|
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
|
||||||
_featureService = Substitute.For<IFeatureService>();
|
_featureService = Substitute.For<IFeatureService>();
|
||||||
|
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
|
||||||
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
|
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
|
||||||
_changeKdfCommand = Substitute.For<IChangeKdfCommand>();
|
_changeKdfCommand = Substitute.For<IChangeKdfCommand>();
|
||||||
|
|
||||||
@@ -61,6 +63,7 @@ public class AccountsControllerTests : IDisposable
|
|||||||
_tdeOffboardingPasswordCommand,
|
_tdeOffboardingPasswordCommand,
|
||||||
_twoFactorIsEnabledQuery,
|
_twoFactorIsEnabledQuery,
|
||||||
_featureService,
|
_featureService,
|
||||||
|
_userAccountKeysQuery,
|
||||||
_twoFactorEmailService,
|
_twoFactorEmailService,
|
||||||
_changeKdfCommand
|
_changeKdfCommand
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ public class AccountsKeyManagementControllerTests
|
|||||||
public async Task RotateUserAccountKeysSuccess(SutProvider<AccountsKeyManagementController> sutProvider,
|
public async Task RotateUserAccountKeysSuccess(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
RotateUserAccountKeysAndDataRequestModel data, User user)
|
RotateUserAccountKeysAndDataRequestModel data, User user)
|
||||||
{
|
{
|
||||||
|
data.AccountKeys.SignatureKeyPair = null;
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||||
.Returns(IdentityResult.Success);
|
.Returns(IdentityResult.Success);
|
||||||
@@ -142,8 +143,60 @@ public class AccountsKeyManagementControllerTests
|
|||||||
&& d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash
|
&& d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash
|
||||||
&& d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey
|
&& d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey
|
||||||
|
|
||||||
&& d.AccountPublicKey == data.AccountKeys.AccountPublicKey
|
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey
|
||||||
&& d.UserKeyEncryptedAccountPrivateKey == data.AccountKeys.UserKeyEncryptedAccountPrivateKey
|
&& 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,
|
public async Task RotateUserKeyNoUser_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
RotateUserAccountKeysAndDataRequestModel data)
|
RotateUserAccountKeysAndDataRequestModel data)
|
||||||
{
|
{
|
||||||
|
data.AccountKeys.SignatureKeyPair = null;
|
||||||
User? user = null;
|
User? user = null;
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
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,
|
public async Task RotateUserKeyWrongData_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||||
RotateUserAccountKeysAndDataRequestModel data, User user, IdentityErrorDescriber _identityErrorDescriber)
|
RotateUserAccountKeysAndDataRequestModel data, User user, IdentityErrorDescriber _identityErrorDescriber)
|
||||||
{
|
{
|
||||||
|
data.AccountKeys.SignatureKeyPair = null;
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||||
.Returns(IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()));
|
.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.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
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;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@@ -74,6 +76,7 @@ public class SyncControllerTests
|
|||||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||||
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
||||||
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
||||||
|
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||||
|
|
||||||
// Adjust random data to match required formats / test intentions
|
// Adjust random data to match required formats / test intentions
|
||||||
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
||||||
@@ -98,6 +101,11 @@ public class SyncControllerTests
|
|||||||
|
|
||||||
// Setup returns
|
// Setup returns
|
||||||
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||||
|
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||||
|
{
|
||||||
|
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||||
|
SignatureKeyPairData = null,
|
||||||
|
});
|
||||||
|
|
||||||
organizationUserRepository
|
organizationUserRepository
|
||||||
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
|
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
|
||||||
@@ -127,7 +135,6 @@ public class SyncControllerTests
|
|||||||
// Execute GET
|
// Execute GET
|
||||||
var result = await sutProvider.Sut.Get();
|
var result = await sutProvider.Sut.Get();
|
||||||
|
|
||||||
|
|
||||||
// Asserts
|
// Asserts
|
||||||
// Assert that methods are called
|
// Assert that methods are called
|
||||||
var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
|
var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
|
||||||
@@ -166,6 +173,7 @@ public class SyncControllerTests
|
|||||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||||
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
||||||
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
||||||
|
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||||
|
|
||||||
// Adjust random data to match required formats / test intentions
|
// Adjust random data to match required formats / test intentions
|
||||||
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
||||||
@@ -189,6 +197,11 @@ public class SyncControllerTests
|
|||||||
|
|
||||||
// Setup returns
|
// Setup returns
|
||||||
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||||
|
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||||
|
{
|
||||||
|
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||||
|
SignatureKeyPairData = null,
|
||||||
|
});
|
||||||
|
|
||||||
organizationUserRepository
|
organizationUserRepository
|
||||||
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
|
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
|
||||||
@@ -256,6 +269,7 @@ public class SyncControllerTests
|
|||||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||||
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
||||||
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
||||||
|
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||||
|
|
||||||
// Adjust random data to match required formats / test intentions
|
// Adjust random data to match required formats / test intentions
|
||||||
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
||||||
@@ -290,6 +304,12 @@ public class SyncControllerTests
|
|||||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);
|
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);
|
||||||
userService.HasPremiumFromOrganization(user).Returns(false);
|
userService.HasPremiumFromOrganization(user).Returns(false);
|
||||||
|
|
||||||
|
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||||
|
{
|
||||||
|
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||||
|
SignatureKeyPairData = null,
|
||||||
|
});
|
||||||
|
|
||||||
// Execute GET
|
// Execute GET
|
||||||
var result = await sutProvider.Sut.Get();
|
var result = await sutProvider.Sut.Get();
|
||||||
|
|
||||||
@@ -327,6 +347,13 @@ public class SyncControllerTests
|
|||||||
|
|
||||||
user.MasterPassword = null;
|
user.MasterPassword = null;
|
||||||
|
|
||||||
|
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||||
|
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||||
|
{
|
||||||
|
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||||
|
SignatureKeyPairData = null,
|
||||||
|
});
|
||||||
|
|
||||||
var userService = sutProvider.GetDependency<IUserService>();
|
var userService = sutProvider.GetDependency<IUserService>();
|
||||||
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||||
|
|
||||||
@@ -352,6 +379,13 @@ public class SyncControllerTests
|
|||||||
user.KdfMemory = kdfMemory;
|
user.KdfMemory = kdfMemory;
|
||||||
user.KdfParallelism = kdfParallelism;
|
user.KdfParallelism = kdfParallelism;
|
||||||
|
|
||||||
|
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||||
|
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||||
|
{
|
||||||
|
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||||
|
SignatureKeyPairData = null,
|
||||||
|
});
|
||||||
|
|
||||||
var userService = sutProvider.GetDependency<IUserService>();
|
var userService = sutProvider.GetDependency<IUserService>();
|
||||||
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
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.Entities;
|
||||||
|
using Bit.Core.KeyManagement.Enums;
|
||||||
using Bit.Core.KeyManagement.Models.Data;
|
using Bit.Core.KeyManagement.Models.Data;
|
||||||
|
using Bit.Core.KeyManagement.Repositories;
|
||||||
using Bit.Core.KeyManagement.UserKey.Implementations;
|
using Bit.Core.KeyManagement.UserKey.Implementations;
|
||||||
using Bit.Core.Services;
|
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;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
|
using NSubstitute.ReturnsExtensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Core.Test.KeyManagement.UserKey;
|
namespace Bit.Core.Test.KeyManagement.UserKey;
|
||||||
@@ -14,7 +21,7 @@ namespace Bit.Core.Test.KeyManagement.UserKey;
|
|||||||
public class RotateUserAccountKeysCommandTests
|
public class RotateUserAccountKeysCommandTests
|
||||||
{
|
{
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task RejectsWrongOldMasterPassword(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
public async Task RotateUserAccountKeysAsync_WrongOldMasterPassword_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||||
RotateUserAccountKeysData model)
|
RotateUserAccountKeysData model)
|
||||||
{
|
{
|
||||||
user.Email = model.MasterPasswordUnlockData.Email;
|
user.Email = model.MasterPasswordUnlockData.Email;
|
||||||
@@ -25,41 +32,38 @@ public class RotateUserAccountKeysCommandTests
|
|||||||
|
|
||||||
Assert.NotEqual(IdentityResult.Success, result);
|
Assert.NotEqual(IdentityResult.Success, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task ThrowsWhenUserIsNull(SutProvider<RotateUserAccountKeysCommand> sutProvider,
|
public async Task RotateUserAccountKeysAsync_UserIsNull_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider,
|
||||||
RotateUserAccountKeysData model)
|
RotateUserAccountKeysData model)
|
||||||
{
|
{
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(null, model));
|
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(null, model));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task RejectsEmailChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
public async Task RotateUserAccountKeysAsync_EmailChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||||
RotateUserAccountKeysData model)
|
RotateUserAccountKeysData model)
|
||||||
{
|
{
|
||||||
user.Kdf = Enums.KdfType.Argon2id;
|
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||||
user.KdfIterations = 3;
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||||
user.KdfMemory = 64;
|
SetV1ExistingUser(user, signatureRepository);
|
||||||
user.KdfParallelism = 4;
|
SetV1ModelUser(model);
|
||||||
|
|
||||||
model.MasterPasswordUnlockData.Email = user.Email + ".different-domain";
|
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)
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model));
|
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task RejectsKdfChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
public async Task RotateUserAccountKeysAsync_KdfChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||||
RotateUserAccountKeysData model)
|
RotateUserAccountKeysData model)
|
||||||
{
|
{
|
||||||
user.Kdf = Enums.KdfType.Argon2id;
|
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||||
user.KdfIterations = 3;
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||||
user.KdfMemory = 64;
|
SetV1ExistingUser(user, signatureRepository);
|
||||||
user.KdfParallelism = 4;
|
SetV1ModelUser(model);
|
||||||
|
|
||||||
model.MasterPasswordUnlockData.Email = user.Email;
|
|
||||||
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.PBKDF2_SHA256;
|
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.PBKDF2_SHA256;
|
||||||
model.MasterPasswordUnlockData.KdfIterations = 600000;
|
model.MasterPasswordUnlockData.KdfIterations = 600000;
|
||||||
model.MasterPasswordUnlockData.KdfMemory = null;
|
model.MasterPasswordUnlockData.KdfMemory = null;
|
||||||
@@ -71,22 +75,15 @@ public class RotateUserAccountKeysCommandTests
|
|||||||
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task RejectsPublicKeyChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
public async Task RotateUserAccountKeysAsync_PublicKeyChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||||
RotateUserAccountKeysData model)
|
RotateUserAccountKeysData model)
|
||||||
{
|
{
|
||||||
user.PublicKey = "old-public";
|
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||||
user.Kdf = Enums.KdfType.Argon2id;
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||||
user.KdfIterations = 3;
|
SetV1ExistingUser(user, signatureRepository);
|
||||||
user.KdfMemory = 64;
|
SetV1ModelUser(model);
|
||||||
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;
|
|
||||||
|
|
||||||
|
model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey = "new-public";
|
||||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
@@ -94,27 +91,350 @@ public class RotateUserAccountKeysCommandTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task RotatesCorrectly(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
public async Task RotateUserAccountKeysAsync_V1_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||||
RotateUserAccountKeysData model)
|
RotateUserAccountKeysData model)
|
||||||
{
|
{
|
||||||
user.Kdf = Enums.KdfType.Argon2id;
|
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||||
user.KdfIterations = 3;
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||||
user.KdfMemory = 64;
|
SetV1ExistingUser(user, signatureRepository);
|
||||||
user.KdfParallelism = 4;
|
SetV1ModelUser(model);
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
|
var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
|
||||||
|
|
||||||
Assert.Equal(IdentityResult.Success, result);
|
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.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
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.Models.Api;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -51,6 +53,7 @@ public class BaseRequestValidatorTests
|
|||||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
private readonly IAuthRequestRepository _authRequestRepository;
|
private readonly IAuthRequestRepository _authRequestRepository;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
|
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||||
|
|
||||||
private readonly BaseRequestValidatorTestWrapper _sut;
|
private readonly BaseRequestValidatorTestWrapper _sut;
|
||||||
|
|
||||||
@@ -73,6 +76,7 @@ public class BaseRequestValidatorTests
|
|||||||
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
|
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
|
||||||
_authRequestRepository = Substitute.For<IAuthRequestRepository>();
|
_authRequestRepository = Substitute.For<IAuthRequestRepository>();
|
||||||
_mailService = Substitute.For<IMailService>();
|
_mailService = Substitute.For<IMailService>();
|
||||||
|
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
|
||||||
|
|
||||||
_sut = new BaseRequestValidatorTestWrapper(
|
_sut = new BaseRequestValidatorTestWrapper(
|
||||||
_userManager,
|
_userManager,
|
||||||
@@ -91,7 +95,8 @@ public class BaseRequestValidatorTests
|
|||||||
_userDecryptionOptionsBuilder,
|
_userDecryptionOptionsBuilder,
|
||||||
_policyRequirementQuery,
|
_policyRequirementQuery,
|
||||||
_authRequestRepository,
|
_authRequestRepository,
|
||||||
_mailService);
|
_mailService,
|
||||||
|
_userAccountKeysQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logic path
|
/* Logic path
|
||||||
@@ -180,6 +185,13 @@ public class BaseRequestValidatorTests
|
|||||||
// 5 -> not legacy user
|
// 5 -> not legacy user
|
||||||
_userService.IsLegacyUser(Arg.Any<string>())
|
_userService.IsLegacyUser(Arg.Any<string>())
|
||||||
.Returns(false);
|
.Returns(false);
|
||||||
|
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||||
|
{
|
||||||
|
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||||
|
"test-private-key",
|
||||||
|
"test-public-key"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await _sut.ValidateAsync(context);
|
await _sut.ValidateAsync(context);
|
||||||
@@ -227,6 +239,13 @@ public class BaseRequestValidatorTests
|
|||||||
// 5 -> not legacy user
|
// 5 -> not legacy user
|
||||||
_userService.IsLegacyUser(Arg.Any<string>())
|
_userService.IsLegacyUser(Arg.Any<string>())
|
||||||
.Returns(false);
|
.Returns(false);
|
||||||
|
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||||
|
{
|
||||||
|
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||||
|
"test-private-key",
|
||||||
|
"test-public-key"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await _sut.ValidateAsync(context);
|
await _sut.ValidateAsync(context);
|
||||||
@@ -460,6 +479,13 @@ public class BaseRequestValidatorTests
|
|||||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||||
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||||
.Returns(Task.FromResult(true));
|
.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);
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
@@ -495,6 +521,13 @@ public class BaseRequestValidatorTests
|
|||||||
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||||
.Returns(Task.FromResult(true));
|
.Returns(Task.FromResult(true));
|
||||||
context.ValidatedTokenRequest.ClientId = "web";
|
context.ValidatedTokenRequest.ClientId = "web";
|
||||||
|
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||||
|
{
|
||||||
|
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||||
|
"test-private-key",
|
||||||
|
"test-public-key"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await _sut.ValidateAsync(context);
|
await _sut.ValidateAsync(context);
|
||||||
@@ -529,6 +562,13 @@ public class BaseRequestValidatorTests
|
|||||||
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||||
.Returns(Task.FromResult(true));
|
.Returns(Task.FromResult(true));
|
||||||
context.ValidatedTokenRequest.ClientId = "web";
|
context.ValidatedTokenRequest.ClientId = "web";
|
||||||
|
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||||
|
{
|
||||||
|
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||||
|
"test-private-key",
|
||||||
|
"test-public-key"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await _sut.ValidateAsync(context);
|
await _sut.ValidateAsync(context);
|
||||||
@@ -591,6 +631,13 @@ public class BaseRequestValidatorTests
|
|||||||
HasMasterPassword = false,
|
HasMasterPassword = false,
|
||||||
MasterPasswordUnlock = null
|
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);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
_sut.isValid = true;
|
_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);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
@@ -671,6 +726,152 @@ public class BaseRequestValidatorTests
|
|||||||
Assert.Equal("test@example.com", userDecryptionOptions.MasterPasswordUnlock.Salt);
|
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(
|
private BaseRequestValidationContextFake CreateContext(
|
||||||
ValidatedTokenRequest tokenRequest,
|
ValidatedTokenRequest tokenRequest,
|
||||||
CustomValidatorRequestContext requestContext,
|
CustomValidatorRequestContext requestContext,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Services;
|
|||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@@ -64,7 +65,8 @@ IBaseRequestValidatorTestWrapper
|
|||||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IAuthRequestRepository authRequestRepository,
|
IAuthRequestRepository authRequestRepository,
|
||||||
IMailService mailService) :
|
IMailService mailService,
|
||||||
|
IUserAccountKeysQuery userAccountKeysQuery) :
|
||||||
base(
|
base(
|
||||||
userManager,
|
userManager,
|
||||||
userService,
|
userService,
|
||||||
@@ -82,7 +84,8 @@ IBaseRequestValidatorTestWrapper
|
|||||||
userDecryptionOptionsBuilder,
|
userDecryptionOptionsBuilder,
|
||||||
policyRequirementQuery,
|
policyRequirementQuery,
|
||||||
authRequestRepository,
|
authRequestRepository,
|
||||||
mailService)
|
mailService,
|
||||||
|
userAccountKeysQuery)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ public class EfRepositoryListBuilder<T> : ISpecimenBuilder where T : BaseEntityF
|
|||||||
cfg.AddProfile<TransactionMapperProfile>();
|
cfg.AddProfile<TransactionMapperProfile>();
|
||||||
cfg.AddProfile<UserMapperProfile>();
|
cfg.AddProfile<UserMapperProfile>();
|
||||||
cfg.AddProfile<PasswordHealthReportApplicationProfile>();
|
cfg.AddProfile<PasswordHealthReportApplicationProfile>();
|
||||||
|
cfg.AddProfile<UserSignatureKeyPairMapperProfile>();
|
||||||
cfg.AddProfile<OrganizationReportProfile>();
|
cfg.AddProfile<OrganizationReportProfile>();
|
||||||
})
|
})
|
||||||
.CreateMapper()));
|
.CreateMapper()));
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ public class UserCompare : IEqualityComparer<User>
|
|||||||
x.LicenseKey == y.LicenseKey &&
|
x.LicenseKey == y.LicenseKey &&
|
||||||
x.ApiKey == y.ApiKey &&
|
x.ApiKey == y.ApiKey &&
|
||||||
x.Kdf == y.Kdf &&
|
x.Kdf == y.Kdf &&
|
||||||
x.KdfIterations == y.KdfIterations;
|
x.KdfIterations == y.KdfIterations &&
|
||||||
|
x.SignedPublicKey == y.SignedPublicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetHashCode([DisallowNull] User obj)
|
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)
|
.HasMaxLength(50)
|
||||||
.HasColumnType("varchar(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")
|
b.Property<long?>("Storage")
|
||||||
.HasColumnType("bigint");
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
@@ -1901,6 +1910,40 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
b.ToTable("User", (string)null);
|
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 =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -2948,6 +2991,17 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
b.Navigation("User");
|
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 =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
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)
|
.HasMaxLength(50)
|
||||||
.HasColumnType("character varying(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")
|
b.Property<long?>("Storage")
|
||||||
.HasColumnType("bigint");
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
@@ -1907,6 +1916,40 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
b.ToTable("User", (string)null);
|
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 =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -2954,6 +2997,17 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
b.Navigation("User");
|
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 =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
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)
|
.HasMaxLength(50)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityState")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("SecurityVersion")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("SignedPublicKey")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<long?>("Storage")
|
b.Property<long?>("Storage")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
@@ -1890,6 +1899,40 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
b.ToTable("User", (string)null);
|
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 =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -2937,6 +2980,17 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
b.Navigation("User");
|
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 =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||||
|
|||||||
Reference in New Issue
Block a user