1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

[PM-21034] Feature Branch - "User Crypto V2" (#5982)

* [PM-21034] Database changes for signature keypairs (#5906)

* Add signing key repositories, models, and sql migration scripts

* Rename UserSigningKeys table to UserSigningKey

* Rename signedpublickeyownershipclaim to signedpublickey

* Move signedPublicKey to last parameter

* Add newline at end of file

* Rename to signature key pair

* Further rename to signaturekeypair

* Rename to UserSignatureKeyPairRepository

* Add newline

* Rename more instances to UserSignatureKeyPair

* Update parameter order

* Fix order

* Add more renames

* Cleanup

* Fix sql

* Add ef migrations

* Fix difference in SQL SP compared to migration SP

* Fix difference in SQL SP vs migration

* Fix difference in SQL SP vs migration

* Attempt to fix sql

* Rename migration to start later

* Address feedback

* Move UserSignatureKeyPair to KM codeownership

* Fix build

* Fix build

* Fix build

* Move out entitytypeconfiguration

* Use view for reading usersignaturekeypairs

* Fix migration script

* Fix migration script

* Drop view if exists

* Enable nullable

* Replace with create or alter view

* Switch go generatecomb

* Switch to generatecomb

* Move signature algorithm

* Move useresignaturekeypairentitytypeconfiguration to km ownership

* Move userSignatureKeyPair model

* Unswap file names

* Move sql files to km ownership

* Add index on userid for signature keys

* Fix wrong filename

* Remove string length limit

* Regenerate EF migrations

* Undo changes to program.cs

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Rename dbset to plural

* Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* [PM-21034] Implement api changes to retreive signing keys (#5932)

* Add signing key repositories, models, and sql migration scripts

* Rename UserSigningKeys table to UserSigningKey

* Rename signedpublickeyownershipclaim to signedpublickey

* Move signedPublicKey to last parameter

* Add newline at end of file

* Rename to signature key pair

* Further rename to signaturekeypair

* Rename to UserSignatureKeyPairRepository

* Add newline

* Rename more instances to UserSignatureKeyPair

* Update parameter order

* Fix order

* Add more renames

* Cleanup

* Fix sql

* Add ef migrations

* Fix difference in SQL SP compared to migration SP

* Fix difference in SQL SP vs migration

* Fix difference in SQL SP vs migration

* Attempt to fix sql

* Rename migration to start later

* Address feedback

* Move UserSignatureKeyPair to KM codeownership

* Fix build

* Fix build

* Fix build

* Move out entitytypeconfiguration

* Use view for reading usersignaturekeypairs

* Fix migration script

* Fix migration script

* Add initial get keys endpoint

* Add sync response

* Cleanup

* Add query and fix types

* Add tests and cleanup

* Fix test

* Drop view if exists

* Add km queries

* Cleanup

* Enable nullable

* Cleanup

* Cleanup

* Enable nullable

* Fix incorrect namespace

* Remove unused using

* Fix test build

* Fix build error

* Fix build

* Attempt to fix tests

* Attempt to fix tests

* Replace with create or alter view

* Attempt to fix tests

* Attempt to fix build

* Rename to include async suffix

* Fix test

* Rename repo

* Attempt to fix tests

* Cleanup

* Test

* Undo test

* Fix tests

* Fix test

* Switch go generatecomb

* Switch to generatecomb

* Move signature algorithm

* Move useresignaturekeypairentitytypeconfiguration to km ownership

* Move userSignatureKeyPair model

* Unswap file names

* Move sql files to km ownership

* Add index on userid for signature keys

* Fix wrong filename

* Fix build

* Remove string length limit

* Regenerate EF migrations

* Undo changes to program.cs

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Rename dbset to plural

* Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Controllers/UsersController.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Cleanup and move query to core

* Fix test

* Fix build

* Fix tests

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Switch away from primary constructor

* Use argumentNullException

* Add test

* Pass user account keys directly to profileresponsemodel

* Move registration to core

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Remove empty line

* Apply suggestions

* Fix tests

* Fix tests

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* [PM-22384] Implement key-rotation based enrollment to user-crypto v2 (#5934)

* Add signing key repositories, models, and sql migration scripts

* Rename UserSigningKeys table to UserSigningKey

* Rename signedpublickeyownershipclaim to signedpublickey

* Move signedPublicKey to last parameter

* Add newline at end of file

* Rename to signature key pair

* Further rename to signaturekeypair

* Rename to UserSignatureKeyPairRepository

* Add newline

* Rename more instances to UserSignatureKeyPair

* Update parameter order

* Fix order

* Add more renames

* Cleanup

* Fix sql

* Add ef migrations

* Fix difference in SQL SP compared to migration SP

* Fix difference in SQL SP vs migration

* Fix difference in SQL SP vs migration

* Attempt to fix sql

* Rename migration to start later

* Address feedback

* Move UserSignatureKeyPair to KM codeownership

* Fix build

* Fix build

* Fix build

* Move out entitytypeconfiguration

* Use view for reading usersignaturekeypairs

* Fix migration script

* Fix migration script

* Add initial get keys endpoint

* Add sync response

* Cleanup

* Add query and fix types

* Add tests and cleanup

* Fix test

* Drop view if exists

* Add km queries

* Cleanup

* Enable nullable

* Cleanup

* Cleanup

* Enable nullable

* Fix incorrect namespace

* Remove unused using

* Fix test build

* Fix build error

* Fix build

* Attempt to fix tests

* Attempt to fix tests

* Replace with create or alter view

* Attempt to fix tests

* Attempt to fix build

* Rename to include async suffix

* Fix test

* Rename repo

* Attempt to fix tests

* Cleanup

* Test

* Undo test

* Fix tests

* Fix test

* Switch go generatecomb

* Switch to generatecomb

* Move signature algorithm

* Move useresignaturekeypairentitytypeconfiguration to km ownership

* Move userSignatureKeyPair model

* Unswap file names

* Move sql files to km ownership

* Add index on userid for signature keys

* Fix wrong filename

* Fix build

* Remove string length limit

* Regenerate EF migrations

* Undo changes to program.cs

* Cleanup

* Add migration to user encryption v2

* Fix build

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Rename dbset to plural

* Cleanup

* Cleanup

* Fix build

* Fix test

* Add validation

* Fix test

* Apply fixes

* Fix tests

* Improve tests

* Add tests

* Add error message validation

* Fix tests

* Fix tests

* Fix test

* Add test

* Fix tests and errors

* Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Controllers/UsersController.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Cleanup and move query to core

* Fix test

* Fix build

* Fix tests

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Switch away from primary constructor

* Use argumentNullException

* Add test

* Pass user account keys directly to profileresponsemodel

* Fix build

* Fix namespace

* Make signedpublickey optional

* Remove unused file

* Fix cases for request data conversion

* Revert constructor change

* Undo comments change

* Apply fixes

* Move registration to core

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Remove empty line

* Apply suggestions

* Fix tests

* Fix tests

* Fix build of integration tests

* Attempt to fix tests

* Add test

* Move v2 encryption user async below public functions

* Add todo

* Rename to have async suffix

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Address feedback

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test coverage

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Split up validation from rotation

* Fix tests

* Increase test coverage

* Rename tests

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test for no signature keypair data

* Fix build

* Enable nullable

* Fix build

* Clean up data model

* Fix tests

* Cleanup

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Fix build

* [PM-22862] Account security version (#5995)

* Add signing key repositories, models, and sql migration scripts

* Rename UserSigningKeys table to UserSigningKey

* Rename signedpublickeyownershipclaim to signedpublickey

* Move signedPublicKey to last parameter

* Add newline at end of file

* Rename to signature key pair

* Further rename to signaturekeypair

* Rename to UserSignatureKeyPairRepository

* Add newline

* Rename more instances to UserSignatureKeyPair

* Update parameter order

* Fix order

* Add more renames

* Cleanup

* Fix sql

* Add ef migrations

* Fix difference in SQL SP compared to migration SP

* Fix difference in SQL SP vs migration

* Fix difference in SQL SP vs migration

* Attempt to fix sql

* Rename migration to start later

* Address feedback

* Move UserSignatureKeyPair to KM codeownership

* Fix build

* Fix build

* Fix build

* Move out entitytypeconfiguration

* Use view for reading usersignaturekeypairs

* Fix migration script

* Fix migration script

* Add initial get keys endpoint

* Add sync response

* Cleanup

* Add query and fix types

* Add tests and cleanup

* Fix test

* Drop view if exists

* Add km queries

* Cleanup

* Enable nullable

* Cleanup

* Cleanup

* Enable nullable

* Fix incorrect namespace

* Remove unused using

* Fix test build

* Fix build error

* Fix build

* Attempt to fix tests

* Attempt to fix tests

* Replace with create or alter view

* Attempt to fix tests

* Attempt to fix build

* Rename to include async suffix

* Fix test

* Rename repo

* Attempt to fix tests

* Cleanup

* Test

* Undo test

* Fix tests

* Fix test

* Switch go generatecomb

* Switch to generatecomb

* Move signature algorithm

* Move useresignaturekeypairentitytypeconfiguration to km ownership

* Move userSignatureKeyPair model

* Unswap file names

* Move sql files to km ownership

* Add index on userid for signature keys

* Fix wrong filename

* Fix build

* Remove string length limit

* Regenerate EF migrations

* Undo changes to program.cs

* Cleanup

* Add migration to user encryption v2

* Fix build

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Rename dbset to plural

* Cleanup

* Cleanup

* Fix build

* Fix test

* Add validation

* Fix test

* Apply fixes

* Fix tests

* Improve tests

* Add tests

* Add error message validation

* Fix tests

* Fix tests

* Fix test

* Add test

* Fix tests and errors

* Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Controllers/UsersController.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Cleanup and move query to core

* Fix test

* Fix build

* Fix tests

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Switch away from primary constructor

* Use argumentNullException

* Add test

* Pass user account keys directly to profileresponsemodel

* Fix build

* Fix namespace

* Make signedpublickey optional

* Remove unused file

* Fix cases for request data conversion

* Revert constructor change

* Undo comments change

* Apply fixes

* Move registration to core

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Remove empty line

* Apply suggestions

* Fix tests

* Fix tests

* Fix build of integration tests

* Attempt to fix tests

* Add test

* Move v2 encryption user async below public functions

* Add todo

* Rename to have async suffix

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Address feedback

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test coverage

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Split up validation from rotation

* Fix tests

* Increase test coverage

* Rename tests

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test for no signature keypair data

* Fix build

* Enable nullable

* Fix build

* Clean up data model

* Fix tests

* Merge branch 'km/signing-upgrade-rotation' into km/account-security-version

* Add security state to rotation

* Update tests

* Update tests and check for security state in v2 model

* Cleanup

* Add tests

* Add security state data to integration test

* Re-sort and remove limit

* Update migrations

* Fix sql

* Fix sql

* Fix sql

* Fix fixture

* Fix test

* Fix test

* Fix test

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* [PM-22853] Add feature flag (#6090)

* Add signing key repositories, models, and sql migration scripts

* Rename UserSigningKeys table to UserSigningKey

* Rename signedpublickeyownershipclaim to signedpublickey

* Move signedPublicKey to last parameter

* Add newline at end of file

* Rename to signature key pair

* Further rename to signaturekeypair

* Rename to UserSignatureKeyPairRepository

* Add newline

* Rename more instances to UserSignatureKeyPair

* Update parameter order

* Fix order

* Add more renames

* Cleanup

* Fix sql

* Add ef migrations

* Fix difference in SQL SP compared to migration SP

* Fix difference in SQL SP vs migration

* Fix difference in SQL SP vs migration

* Attempt to fix sql

* Rename migration to start later

* Address feedback

* Move UserSignatureKeyPair to KM codeownership

* Fix build

* Fix build

* Fix build

* Move out entitytypeconfiguration

* Use view for reading usersignaturekeypairs

* Fix migration script

* Fix migration script

* Add initial get keys endpoint

* Add sync response

* Cleanup

* Add query and fix types

* Add tests and cleanup

* Fix test

* Drop view if exists

* Add km queries

* Cleanup

* Enable nullable

* Cleanup

* Cleanup

* Enable nullable

* Fix incorrect namespace

* Remove unused using

* Fix test build

* Fix build error

* Fix build

* Attempt to fix tests

* Attempt to fix tests

* Replace with create or alter view

* Attempt to fix tests

* Attempt to fix build

* Rename to include async suffix

* Fix test

* Rename repo

* Attempt to fix tests

* Cleanup

* Test

* Undo test

* Fix tests

* Fix test

* Switch go generatecomb

* Switch to generatecomb

* Move signature algorithm

* Move useresignaturekeypairentitytypeconfiguration to km ownership

* Move userSignatureKeyPair model

* Unswap file names

* Move sql files to km ownership

* Add index on userid for signature keys

* Fix wrong filename

* Fix build

* Remove string length limit

* Regenerate EF migrations

* Undo changes to program.cs

* Cleanup

* Add migration to user encryption v2

* Fix build

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Rename dbset to plural

* Cleanup

* Cleanup

* Fix build

* Fix test

* Add validation

* Fix test

* Apply fixes

* Fix tests

* Improve tests

* Add tests

* Add error message validation

* Fix tests

* Fix tests

* Fix test

* Add test

* Fix tests and errors

* Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Controllers/UsersController.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Cleanup and move query to core

* Fix test

* Fix build

* Fix tests

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Switch away from primary constructor

* Use argumentNullException

* Add test

* Pass user account keys directly to profileresponsemodel

* Fix build

* Fix namespace

* Make signedpublickey optional

* Remove unused file

* Fix cases for request data conversion

* Revert constructor change

* Undo comments change

* Apply fixes

* Move registration to core

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Remove empty line

* Apply suggestions

* Fix tests

* Fix tests

* Fix build of integration tests

* Attempt to fix tests

* Add test

* Move v2 encryption user async below public functions

* Add todo

* Rename to have async suffix

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Address feedback

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test coverage

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Split up validation from rotation

* Fix tests

* Increase test coverage

* Rename tests

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test for no signature keypair data

* Fix build

* Enable nullable

* Fix build

* Clean up data model

* Fix tests

* Merge branch 'km/signing-upgrade-rotation' into km/account-security-version

* Add security state to rotation

* Update tests

* Add feature flag

* Update tests and check for security state in v2 model

* Cleanup

* Add tests

* Add security state data to integration test

* Re-sort and remove limit

* Update migrations

* Fix sql

* Fix sql

* Fix sql

* Fix fixture

* Fix test

* Fix test

* Fix test

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* [PM-23222] Update revision date on key rotation (#6038)

* Add signing key repositories, models, and sql migration scripts

* Rename UserSigningKeys table to UserSigningKey

* Rename signedpublickeyownershipclaim to signedpublickey

* Move signedPublicKey to last parameter

* Add newline at end of file

* Rename to signature key pair

* Further rename to signaturekeypair

* Rename to UserSignatureKeyPairRepository

* Add newline

* Rename more instances to UserSignatureKeyPair

* Update parameter order

* Fix order

* Add more renames

* Cleanup

* Fix sql

* Add ef migrations

* Fix difference in SQL SP compared to migration SP

* Fix difference in SQL SP vs migration

* Fix difference in SQL SP vs migration

* Attempt to fix sql

* Rename migration to start later

* Address feedback

* Move UserSignatureKeyPair to KM codeownership

* Fix build

* Fix build

* Fix build

* Move out entitytypeconfiguration

* Use view for reading usersignaturekeypairs

* Fix migration script

* Fix migration script

* Add initial get keys endpoint

* Add sync response

* Cleanup

* Add query and fix types

* Add tests and cleanup

* Fix test

* Drop view if exists

* Add km queries

* Cleanup

* Enable nullable

* Cleanup

* Cleanup

* Enable nullable

* Fix incorrect namespace

* Remove unused using

* Fix test build

* Fix build error

* Fix build

* Attempt to fix tests

* Attempt to fix tests

* Replace with create or alter view

* Attempt to fix tests

* Attempt to fix build

* Rename to include async suffix

* Fix test

* Rename repo

* Attempt to fix tests

* Cleanup

* Test

* Undo test

* Fix tests

* Fix test

* Switch go generatecomb

* Switch to generatecomb

* Move signature algorithm

* Move useresignaturekeypairentitytypeconfiguration to km ownership

* Move userSignatureKeyPair model

* Unswap file names

* Move sql files to km ownership

* Add index on userid for signature keys

* Fix wrong filename

* Fix build

* Remove string length limit

* Regenerate EF migrations

* Undo changes to program.cs

* Cleanup

* Add migration to user encryption v2

* Fix build

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Rename dbset to plural

* Cleanup

* Cleanup

* Fix build

* Fix test

* Add validation

* Fix test

* Apply fixes

* Fix tests

* Improve tests

* Add tests

* Add error message validation

* Fix tests

* Fix tests

* Fix test

* Add test

* Fix tests and errors

* Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Controllers/UsersController.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Cleanup and move query to core

* Fix test

* Fix build

* Fix tests

* Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Switch away from primary constructor

* Use argumentNullException

* Add test

* Pass user account keys directly to profileresponsemodel

* Fix build

* Fix namespace

* Make signedpublickey optional

* Remove unused file

* Fix cases for request data conversion

* Revert constructor change

* Undo comments change

* Apply fixes

* Move registration to core

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/Startup.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Remove empty line

* Apply suggestions

* Fix tests

* Fix tests

* Fix build of integration tests

* Attempt to fix tests

* Add test

* Move v2 encryption user async below public functions

* Add todo

* Rename to have async suffix

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Address feedback

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test coverage

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Split up validation from rotation

* Fix tests

* Increase test coverage

* Rename tests

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Add test for no signature keypair data

* Fix build

* Enable nullable

* Fix build

* Clean up data model

* Fix tests

* Merge branch 'km/signing-upgrade-rotation' into km/account-security-version

* Add security state to rotation

* Update tests

* Update revision date on key rotation

* Update tests and check for security state in v2 model

* Cleanup

* Add tests

* Add security state data to integration test

* Re-sort and remove limit

* Update migrations

* Fix sql

* Fix sql

* Fix sql

* Fix fixture

* Fix test

* Fix test

* Fix test

* Add test for change date

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Fix signing keys

* Update sql migrations

* Fix tests

* Add keys to identity token response

* Fix tests

* Fix tests

* Fix formatting

* Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Controllers/UsersController.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Models/Requests/SignatureKeyPairRequestModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Api/KeyManagement/Models/Requests/PublicKeyEncryptionKeyPairRequestModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Repositories/IUserSignatureKeyPairRepository.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Data/PublicKeyEncryptionKeyPairData.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Data/SignatureKeyPairData.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Data/SecurityStateData.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Request/SecurityStateModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Response/PrivateKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Response/PublicKeysResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Response/PublicKeyEncryptionKeyPairResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Queries/Interfaces/IUserAcountKeysQuery.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update src/Core/KeyManagement/Models/Response/SignatureKeyPairResponseModel.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Remove unnecessary file

* Add eof spacing

* Move models

* Fix build

* Move models to API subdirectory

* Rename model

* Remove migrations

* Add new ef migrations

* Remove empty line

* Only query account keys if the user has keys

* Dotnet format

* Fix test

* Update test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Apply suggestion

* Fix whitespace

* Force camel case on response models

* Address feedback for sql files

* Fix build

* Make index unique

* Add contstraints

* Fix sql

* Fix order

* Cleanup

* Fix build

* Update migrations

* Update EF migrations

* Change parameters to nvarchar

* Update to Varchar

* Apply feedback

* Move refresh view

* Attempt to fix build

* Undo sql changes

* Apply feedback about varchar

* Apply feedback about refresh view

* Apply feedback about new lines

* Address SQL feedback

* Re-sort columns

* Fix build

* Fix order

* Fix build

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>
This commit is contained in:
Bernd Schoolmann
2025-10-20 12:51:08 +02:00
committed by GitHub
parent a68e2b9eb5
commit 4bf7cf956b
79 changed files with 12918 additions and 161 deletions

View File

@@ -18,6 +18,7 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Kdf;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Models.Api.Response;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -40,6 +41,7 @@ public class AccountsController : Controller
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
private readonly ITwoFactorEmailService _twoFactorEmailService;
private readonly IChangeKdfCommand _changeKdfCommand;
@@ -53,6 +55,7 @@ public class AccountsController : Controller
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService,
IUserAccountKeysQuery userAccountKeysQuery,
ITwoFactorEmailService twoFactorEmailService,
IChangeKdfCommand changeKdfCommand
)
@@ -66,6 +69,7 @@ public class AccountsController : Controller
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService;
_userAccountKeysQuery = userAccountKeysQuery;
_twoFactorEmailService = twoFactorEmailService;
_changeKdfCommand = changeKdfCommand;
}
@@ -332,7 +336,9 @@ public class AccountsController : Controller
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
var accountKeys = await _userAccountKeysQuery.Run(user);
var response = new ProfileResponseModel(user, accountKeys, organizationUserDetails, providerUserDetails,
providerUserOrganizationDetails, twoFactorEnabled,
hasPremiumFromOrg, organizationIdsClaimingActiveUser);
return response;
@@ -364,8 +370,9 @@ public class AccountsController : Controller
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
var userAccountKeys = await _userAccountKeysQuery.Run(user);
var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser);
var response = new ProfileResponseModel(user, userAccountKeys, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser);
return response;
}
@@ -389,8 +396,9 @@ public class AccountsController : Controller
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
var accountKeys = await _userAccountKeysQuery.Run(user);
var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
var response = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
return response;
}

View File

@@ -1,4 +1,5 @@
#nullable enable
using Bit.Api.Models.Request;
using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Response;
@@ -8,6 +9,7 @@ using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Business;
using Bit.Core.Billing.Services;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Models.Business;
using Bit.Core.Services;
using Bit.Core.Settings;
@@ -21,7 +23,8 @@ namespace Bit.Api.Billing.Controllers;
[Authorize("Application")]
public class AccountsController(
IUserService userService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IUserAccountKeysQuery userAccountKeysQuery) : Controller
{
[HttpPost("premium")]
public async Task<PaymentResponseModel> PostPremiumAsync(
@@ -58,8 +61,9 @@ public class AccountsController(
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
var accountKeys = await userAccountKeysQuery.Run(user);
var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled,
var profile = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled,
userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
return new PaymentResponseModel
{

View File

@@ -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);
}
}

View File

@@ -106,8 +106,7 @@ public class AccountsKeyManagementController : Controller
{
OldMasterKeyAuthenticationHash = model.OldMasterKeyAuthenticationHash,
UserKeyEncryptedAccountPrivateKey = model.AccountKeys.UserKeyEncryptedAccountPrivateKey,
AccountPublicKey = model.AccountKeys.AccountPublicKey,
AccountKeys = model.AccountKeys.ToAccountKeysData(),
MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(),
EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.AccountUnlockData.EmergencyAccessUnlockData),

View 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);
}
}

View File

@@ -1,4 +1,5 @@
#nullable enable
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.KeyManagement.Models.Requests;
@@ -7,4 +8,44 @@ public class AccountKeysRequestModel
{
[EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { get; set; }
public required string AccountPublicKey { get; set; }
public PublicKeyEncryptionKeyPairRequestModel? PublicKeyEncryptionKeyPair { get; set; }
public SignatureKeyPairRequestModel? SignatureKeyPair { get; set; }
public SecurityStateModel? SecurityState { get; set; }
public UserAccountKeysData ToAccountKeysData()
{
// This will be cleaned up, after a compatibility period, at which point PublicKeyEncryptionKeyPair and SignatureKeyPair will be required.
// TODO: https://bitwarden.atlassian.net/browse/PM-23751
if (PublicKeyEncryptionKeyPair == null)
{
return new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData
(
UserKeyEncryptedAccountPrivateKey,
AccountPublicKey
),
};
}
else
{
if (SignatureKeyPair == null || SecurityState == null)
{
return new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = PublicKeyEncryptionKeyPair.ToPublicKeyEncryptionKeyPairData(),
};
}
else
{
return new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = PublicKeyEncryptionKeyPair.ToPublicKeyEncryptionKeyPairData(),
SignatureKeyPairData = SignatureKeyPair.ToSignatureKeyPairData(),
SecurityStateData = SecurityState.ToSecurityState()
};
}
}
}
}

View File

@@ -1,5 +1,4 @@
#nullable enable
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.KeyManagement.Models.Requests;

View File

@@ -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
);
}
}

View File

@@ -1,5 +1,4 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.KeyManagement.Models.Requests;

View File

@@ -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
);
}
}

View File

@@ -1,5 +1,4 @@
#nullable enable
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Request.WebAuthn;

View File

@@ -1,5 +1,4 @@
#nullable enable
using Bit.Api.Tools.Models.Request;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models.Request;
namespace Bit.Api.KeyManagement.Models.Requests;

View File

@@ -5,6 +5,8 @@ using Bit.Api.AdminConsole.Models.Response;
using Bit.Api.AdminConsole.Models.Response.Providers;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Api.Response;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
@@ -13,6 +15,7 @@ namespace Bit.Api.Models.Response;
public class ProfileResponseModel : ResponseModel
{
public ProfileResponseModel(User user,
UserAccountKeysData userAccountKeysData,
IEnumerable<OrganizationUserOrganizationDetails> organizationsUserDetails,
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
@@ -35,6 +38,7 @@ public class ProfileResponseModel : ResponseModel
TwoFactorEnabled = twoFactorEnabled;
Key = user.Key;
PrivateKey = user.PrivateKey;
AccountKeys = userAccountKeysData != null ? new PrivateKeysResponseModel(userAccountKeysData) : null;
SecurityStamp = user.SecurityStamp;
ForcePasswordReset = user.ForcePasswordReset;
UsesKeyConnector = user.UsesKeyConnector;
@@ -60,7 +64,9 @@ public class ProfileResponseModel : ResponseModel
public string Culture { get; set; }
public bool TwoFactorEnabled { get; set; }
public string Key { get; set; }
[Obsolete("Use AccountKeys instead.")]
public string PrivateKey { get; set; }
public PrivateKeysResponseModel AccountKeys { get; set; }
public string SecurityStamp { get; set; }
public bool ForcePasswordReset { get; set; }
public bool UsesKeyConnector { get; set; }

View File

@@ -11,6 +11,8 @@ using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -42,6 +44,7 @@ public class SyncController : Controller
private readonly IFeatureService _featureService;
private readonly IApplicationCacheService _applicationCacheService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
public SyncController(
IUserService userService,
@@ -57,7 +60,8 @@ public class SyncController : Controller
ICurrentContext currentContext,
IFeatureService featureService,
IApplicationCacheService applicationCacheService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IUserAccountKeysQuery userAccountKeysQuery)
{
_userService = userService;
_folderRepository = folderRepository;
@@ -73,6 +77,7 @@ public class SyncController : Controller
_featureService = featureService;
_applicationCacheService = applicationCacheService;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_userAccountKeysQuery = userAccountKeysQuery;
}
[HttpGet("")]
@@ -116,7 +121,14 @@ public class SyncController : Controller
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
UserAccountKeysData userAccountKeys = null;
// JIT TDE users and some broken/old users may not have a private key.
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
{
userAccountKeys = await _userAccountKeysQuery.Run(user);
}
var response = new SyncResponseModel(_globalSettings, user, userAccountKeys, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
return response;

View File

@@ -7,7 +7,8 @@ using Bit.Api.Tools.Models.Response;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Response;
using Bit.Core.KeyManagement.Models.Api.Response;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations;
@@ -24,6 +25,7 @@ public class SyncResponseModel() : ResponseModel("sync")
public SyncResponseModel(
GlobalSettings globalSettings,
User user,
UserAccountKeysData userAccountKeysData,
bool userTwoFactorEnabled,
bool userHasPremiumFromOrganization,
IDictionary<Guid, OrganizationAbility> organizationAbilities,
@@ -40,7 +42,7 @@ public class SyncResponseModel() : ResponseModel("sync")
IEnumerable<Send> sends)
: this()
{
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
Profile = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails,
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);
Folders = folders.Select(f => new FolderResponseModel(f));
Ciphers = ciphers.Select(cipher =>

View File

@@ -1,5 +1,5 @@
using System.Text.Json.Serialization;
using Bit.Core.KeyManagement.Models.Response;
using Bit.Core.KeyManagement.Models.Api.Response;
using Bit.Core.Models.Api;
namespace Bit.Core.Auth.Models.Api.Response;

View File

@@ -192,6 +192,7 @@ public static class FeatureFlagKeys
public const string UserkeyRotationV2 = "userkey-rotation-v2";
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
public const string UserSdkForDecryption = "use-sdk-for-decryption";
public const string EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation";
public const string PM17987_BlockType0 = "pm-17987-block-type-0";
public const string ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings";
public const string UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data";

View File

@@ -3,6 +3,7 @@ using System.Text.Json;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Identity;
@@ -21,6 +22,9 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
[MaxLength(256)]
public string Email { get; set; } = null!;
public bool EmailVerified { get; set; }
/// <summary>
/// The server-side master-password hash
/// </summary>
[MaxLength(300)]
public string? MasterPassword { get; set; }
[MaxLength(50)]
@@ -41,9 +45,30 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
/// organization membership.
/// </summary>
public DateTime AccountRevisionDate { get; set; } = DateTime.UtcNow;
/// <summary>
/// The master-password-sealed user key.
/// </summary>
public string? Key { get; set; }
/// <summary>
/// The raw public key, without a signature from the user's signature key.
/// </summary>
public string? PublicKey { get; set; }
/// <summary>
/// User key wrapped private key.
/// </summary>
public string? PrivateKey { get; set; }
/// <summary>
/// The public key, signed by the user's signature key.
/// </summary>
public string? SignedPublicKey { get; set; }
/// <summary>
/// The security version is included in the security state, but needs COSE parsing
/// </summary>
public int? SecurityVersion { get; set; }
/// <summary>
/// The security state is a signed object attesting to the version of the user's account.
/// </summary>
public string? SecurityState { get; set; }
public bool Premium { get; set; }
public DateTime? PremiumExpirationDate { get; set; }
public DateTime? RenewalReminderDate { get; set; }
@@ -180,6 +205,12 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
return Premium;
}
public int GetSecurityVersion()
{
// If no security version is set, it is version 1. The minimum initialized version is 2.
return SecurityVersion ?? 1;
}
/// <summary>
/// Serializes the C# object to the User.TwoFactorProviders property in JSON format.
/// </summary>
@@ -243,4 +274,14 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
{
return MasterPassword != null;
}
public PublicKeyEncryptionKeyPairData GetPublicKeyEncryptionKeyPair()
{
if (string.IsNullOrWhiteSpace(PrivateKey) || string.IsNullOrWhiteSpace(PublicKey))
{
throw new InvalidOperationException("User public key encryption key pair is not fully initialized.");
}
return new PublicKeyEncryptionKeyPairData(PrivateKey, PublicKey, SignedPublicKey);
}
}

View 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);
}
}

View 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
}

View File

@@ -2,6 +2,8 @@
using Bit.Core.KeyManagement.Commands.Interfaces;
using Bit.Core.KeyManagement.Kdf;
using Bit.Core.KeyManagement.Kdf.Implementations;
using Bit.Core.KeyManagement.Queries;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.KeyManagement;
@@ -11,6 +13,7 @@ public static class KeyManagementServiceCollectionExtensions
public static void AddKeyManagementServices(this IServiceCollection services)
{
services.AddKeyManagementCommands();
services.AddKeyManagementQueries();
services.AddSendPasswordServices();
}
@@ -19,4 +22,9 @@ public static class KeyManagementServiceCollectionExtensions
services.AddScoped<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>();
services.AddScoped<IChangeKdfCommand, ChangeKdfCommand>();
}
private static void AddKeyManagementQueries(this IServiceCollection services)
{
services.AddScoped<IUserAccountKeysQuery, UserAccountKeysQuery>();
}
}

View File

@@ -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
};
}
}

View File

@@ -2,7 +2,7 @@
using Bit.Core.Enums;
using Bit.Core.Utilities;
namespace Bit.Core.KeyManagement.Models.Response;
namespace Bit.Core.KeyManagement.Models.Api.Response;
public class MasterPasswordUnlockResponseModel
{

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -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));
}
}

View File

@@ -1,4 +1,4 @@
namespace Bit.Core.KeyManagement.Models.Response;
namespace Bit.Core.KeyManagement.Models.Api.Response;
public class UserDecryptionResponseModel
{

View File

@@ -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;
}
}

View File

@@ -1,6 +1,4 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
@@ -12,21 +10,19 @@ namespace Bit.Core.KeyManagement.Models.Data;
public class RotateUserAccountKeysData
{
// Authentication for this requests
public string OldMasterKeyAuthenticationHash { get; set; }
public required string OldMasterKeyAuthenticationHash { get; set; }
// Other keys encrypted by the userkey
public string UserKeyEncryptedAccountPrivateKey { get; set; }
public string AccountPublicKey { get; set; }
public required UserAccountKeysData AccountKeys { get; set; }
// All methods to get to the userkey
public MasterPasswordUnlockAndAuthenticationData MasterPasswordUnlockData { get; set; }
public IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; }
public IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
public IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; }
public IEnumerable<Device> DeviceKeys { get; set; }
public required MasterPasswordUnlockAndAuthenticationData MasterPasswordUnlockData { get; set; }
public required IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; }
public required IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
public required IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; }
public required IEnumerable<Device> DeviceKeys { get; set; }
// User vault data encrypted by the userkey
public IEnumerable<Cipher> Ciphers { get; set; }
public IEnumerable<Folder> Folders { get; set; }
public IReadOnlyList<Send> Sends { get; set; }
public required IEnumerable<Cipher> Ciphers { get; set; }
public required IEnumerable<Folder> Folders { get; set; }
public required IReadOnlyList<Send> Sends { get; set; }
}

View 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; }
}

View 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));
}
}

View File

@@ -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; }
}

View File

@@ -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);
}

View 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(),
}
};
}
}
}

View File

@@ -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);
}

View File

@@ -1,6 +1,11 @@
using Bit.Core.Auth.Repositories;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using Bit.Core.Auth.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -25,6 +30,8 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
private readonly IdentityErrorDescriber _identityErrorDescriber;
private readonly IWebAuthnCredentialRepository _credentialRepository;
private readonly IPasswordHasher<User> _passwordHasher;
private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository;
private readonly IFeatureService _featureService;
/// <summary>
/// Instantiates a new <see cref="RotateUserAccountKeysCommand"/>
@@ -36,16 +43,19 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
/// <param name="sendRepository">Provides a method to update re-encrypted send data</param>
/// <param name="emergencyAccessRepository">Provides a method to update re-encrypted emergency access data</param>
/// <param name="organizationUserRepository">Provides a method to update re-encrypted organization user data</param>
/// <param name="deviceRepository">Provides a method to update re-encrypted device keys</param>
/// <param name="passwordHasher">Hashes the new master password</param>
/// <param name="pushService">Logs out user from other devices after successful rotation</param>
/// <param name="errors">Provides a password mismatch error if master password hash validation fails</param>
/// <param name="credentialRepository">Provides a method to update re-encrypted WebAuthn keys</param>
/// <param name="userSignatureKeyPairRepository">Provides a method to update re-encrypted signature keys</param>
public RotateUserAccountKeysCommand(IUserService userService, IUserRepository userRepository,
ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository,
IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository,
IDeviceRepository deviceRepository,
IPasswordHasher<User> passwordHasher,
IPushNotificationService pushService, IdentityErrorDescriber errors, IWebAuthnCredentialRepository credentialRepository,
IUserSignatureKeyPairRepository userSignatureKeyPairRepository,
IFeatureService featureService)
{
_userService = userService;
@@ -60,6 +70,8 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
_identityErrorDescriber = errors;
_credentialRepository = credentialRepository;
_passwordHasher = passwordHasher;
_userSignatureKeyPairRepository = userSignatureKeyPairRepository;
_featureService = featureService;
}
/// <inheritdoc />
@@ -80,50 +92,106 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
user.LastKeyRotationDate = now;
user.SecurityStamp = Guid.NewGuid().ToString();
if (
!model.MasterPasswordUnlockData.ValidateForUser(user)
)
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = [];
await UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);
UpdateUnlockMethods(model, user, saveEncryptedDataActions);
UpdateUserData(model, user, saveEncryptedDataActions);
await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions);
await _pushService.PushLogOutAsync(user.Id);
return IdentityResult.Success;
}
public async Task RotateV2AccountKeysAsync(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
{
ValidateV2Encryption(model);
await ValidateVerifyingKeyUnchangedAsync(model, user);
saveEncryptedDataActions.Add(_userSignatureKeyPairRepository.UpdateForKeyRotation(user.Id, model.AccountKeys.SignatureKeyPairData));
user.SignedPublicKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey;
user.SecurityState = model.AccountKeys.SecurityStateData!.SecurityState;
user.SecurityVersion = model.AccountKeys.SecurityStateData.SecurityVersion;
}
public void UpgradeV1ToV2Keys(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
{
ValidateV2Encryption(model);
saveEncryptedDataActions.Add(_userSignatureKeyPairRepository.SetUserSignatureKeyPair(user.Id, model.AccountKeys.SignatureKeyPairData));
user.SignedPublicKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey;
user.SecurityState = model.AccountKeys.SecurityStateData!.SecurityState;
user.SecurityVersion = model.AccountKeys.SecurityStateData.SecurityVersion;
}
public async Task UpdateAccountKeysAsync(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
{
ValidatePublicKeyEncryptionKeyPairUnchanged(model, user);
if (IsV2EncryptionUserAsync(user))
{
throw new InvalidOperationException("The provided master password unlock data is not valid for this user.");
await RotateV2AccountKeysAsync(model, user, saveEncryptedDataActions);
}
if (
model.AccountPublicKey != user.PublicKey
)
else if (model.AccountKeys.SignatureKeyPairData != null)
{
throw new InvalidOperationException("The provided account public key does not match the user's current public key, and changing the account asymmetric keypair is currently not supported during key rotation.");
UpgradeV1ToV2Keys(model, user, saveEncryptedDataActions);
}
else
{
if (GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.AesCbc256_HmacSha256_B64)
{
throw new InvalidOperationException("The provided account private key was not wrapped with AES-256-CBC-HMAC");
}
// V1 user to V1 user rotation needs to further changes, the private key was re-encrypted.
}
user.Key = model.MasterPasswordUnlockData.MasterKeyEncryptedUserKey;
user.PrivateKey = model.UserKeyEncryptedAccountPrivateKey;
user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordUnlockData.MasterKeyAuthenticationHash);
user.MasterPasswordHint = model.MasterPasswordUnlockData.MasterPasswordHint;
// Private key is re-wrapped with new user key by client
user.PrivateKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey;
}
public void UpdateUserData(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
{
// The revision date has to be updated so that de-synced clients don't accidentally post over the re-encrypted data
// with an old-user key-encrypted copy
var now = DateTime.UtcNow;
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new();
if (model.Ciphers.Any())
{
saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers));
var ciphersWithUpdatedDate = model.Ciphers.ToList().Select(c => { c.RevisionDate = now; return c; });
saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, ciphersWithUpdatedDate));
}
if (model.Folders.Any())
{
saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, model.Folders));
var foldersWithUpdatedDate = model.Folders.ToList().Select(f => { f.RevisionDate = now; return f; });
saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, foldersWithUpdatedDate));
}
if (model.Sends.Any())
{
saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, model.Sends));
var sendsWithUpdatedDate = model.Sends.ToList().Select(s => { s.RevisionDate = now; return s; });
saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, sendsWithUpdatedDate));
}
}
void UpdateUnlockMethods(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
{
if (!model.MasterPasswordUnlockData.ValidateForUser(user))
{
throw new InvalidOperationException("The provided master password unlock data is not valid for this user.");
}
// Update master password authentication & unlock
user.Key = model.MasterPasswordUnlockData.MasterKeyEncryptedUserKey;
user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordUnlockData.MasterKeyAuthenticationHash);
user.MasterPasswordHint = model.MasterPasswordUnlockData.MasterPasswordHint;
if (model.EmergencyAccesses.Any())
{
saveEncryptedDataActions.Add(
_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses));
saveEncryptedDataActions.Add(_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses));
}
if (model.OrganizationUsers.Any())
{
saveEncryptedDataActions.Add(
_organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers));
saveEncryptedDataActions.Add(_organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers));
}
if (model.WebAuthnKeys.Any())
@@ -135,9 +203,80 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
{
saveEncryptedDataActions.Add(_deviceRepository.UpdateKeysForRotationAsync(user.Id, model.DeviceKeys));
}
}
await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions);
await _pushService.PushLogOutAsync(user.Id);
return IdentityResult.Success;
private bool IsV2EncryptionUserAsync(User user)
{
// Returns whether the user is a V2 user based on the private key's encryption type.
ArgumentNullException.ThrowIfNull(user);
var isPrivateKeyEncryptionV2 = GetEncryptionType(user.PrivateKey) == EncryptionType.XChaCha20Poly1305_B64;
return isPrivateKeyEncryptionV2;
}
private async Task ValidateVerifyingKeyUnchangedAsync(RotateUserAccountKeysData model, User user)
{
var currentSignatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id) ?? throw new InvalidOperationException("User does not have a signature key pair.");
if (model.AccountKeys.SignatureKeyPairData.VerifyingKey != currentSignatureKeyPair!.VerifyingKey)
{
throw new InvalidOperationException("The provided verifying key does not match the user's current verifying key.");
}
}
private static void ValidatePublicKeyEncryptionKeyPairUnchanged(RotateUserAccountKeysData model, User user)
{
var publicKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey;
if (publicKey != user.PublicKey)
{
throw new InvalidOperationException("The provided account public key does not match the user's current public key, and changing the account asymmetric key pair is currently not supported during key rotation.");
}
}
private static void ValidateV2Encryption(RotateUserAccountKeysData model)
{
if (model.AccountKeys.SignatureKeyPairData == null)
{
throw new InvalidOperationException("Signature key pair data is required for V2 encryption.");
}
if (GetEncryptionType(model.AccountKeys.SignatureKeyPairData.WrappedSigningKey) != EncryptionType.XChaCha20Poly1305_B64)
{
throw new InvalidOperationException("The provided signing key data is not wrapped with XChaCha20-Poly1305.");
}
if (string.IsNullOrEmpty(model.AccountKeys.SignatureKeyPairData.VerifyingKey))
{
throw new InvalidOperationException("The provided signature key pair data does not contain a valid verifying key.");
}
if (GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.XChaCha20Poly1305_B64)
{
throw new InvalidOperationException("The provided private key encryption key is not wrapped with XChaCha20-Poly1305.");
}
if (string.IsNullOrEmpty(model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey))
{
throw new InvalidOperationException("No signed public key provided, but the user already has a signature key pair.");
}
if (model.AccountKeys.SecurityStateData == null || string.IsNullOrEmpty(model.AccountKeys.SecurityStateData.SecurityState))
{
throw new InvalidOperationException("No signed security state provider for V2 user");
}
}
/// <summary>
/// Helper method to convert an encryption type string to an enum value.
/// </summary>
private static EncryptionType GetEncryptionType(string encString)
{
var parts = encString.Split('.');
if (parts.Length == 1)
{
throw new ArgumentException("Invalid encryption type string.");
}
if (byte.TryParse(parts[0], out var encryptionTypeNumber))
{
if (Enum.IsDefined(typeof(EncryptionType), encryptionTypeNumber))
{
return (EncryptionType)encryptionTypeNumber;
}
}
throw new ArgumentException("Invalid encryption type string.");
}
}

View File

@@ -14,6 +14,8 @@ using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Api.Response;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Models.Api;
using Bit.Core.Models.Api.Response;
using Bit.Core.Repositories;
@@ -45,6 +47,7 @@ public abstract class BaseRequestValidator<T> where T : class
protected IUserService _userService { get; }
protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }
protected IPolicyRequirementQuery PolicyRequirementQuery { get; }
protected IUserAccountKeysQuery _accountKeysQuery { get; }
public BaseRequestValidator(
UserManager<User> userManager,
@@ -63,7 +66,8 @@ public abstract class BaseRequestValidator<T> where T : class
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IPolicyRequirementQuery policyRequirementQuery,
IAuthRequestRepository authRequestRepository,
IMailService mailService
IMailService mailService,
IUserAccountKeysQuery userAccountKeysQuery
)
{
_userManager = userManager;
@@ -83,6 +87,7 @@ public abstract class BaseRequestValidator<T> where T : class
PolicyRequirementQuery = policyRequirementQuery;
_authRequestRepository = authRequestRepository;
_mailService = mailService;
_accountKeysQuery = userAccountKeysQuery;
}
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
@@ -439,6 +444,8 @@ public abstract class BaseRequestValidator<T> where T : class
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
{
customResponse.Add("PrivateKey", user.PrivateKey);
var accountKeys = await _accountKeysQuery.Run(user);
customResponse.Add("AccountKeys", new PrivateKeysResponseModel(accountKeys));
}
if (!string.IsNullOrWhiteSpace(user.Key))

View File

@@ -8,6 +8,7 @@ using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Platform.Installations;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -47,7 +48,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
IUpdateInstallationCommand updateInstallationCommand,
IPolicyRequirementQuery policyRequirementQuery,
IAuthRequestRepository authRequestRepository,
IMailService mailService)
IMailService mailService,
IUserAccountKeysQuery userAccountKeysQuery)
: base(
userManager,
userService,
@@ -65,7 +67,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
userDecryptionOptionsBuilder,
policyRequirementQuery,
authRequestRepository,
mailService)
mailService,
userAccountKeysQuery)
{
_userManager = userManager;
_updateInstallationCommand = updateInstallationCommand;

View File

@@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@@ -41,7 +42,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
ISsoConfigRepository ssoConfigRepository,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IPolicyRequirementQuery policyRequirementQuery,
IMailService mailService)
IMailService mailService,
IUserAccountKeysQuery userAccountKeysQuery)
: base(
userManager,
userService,
@@ -59,7 +61,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
userDecryptionOptionsBuilder,
policyRequirementQuery,
authRequestRepository,
mailService)
mailService,
userAccountKeysQuery)
{
_userManager = userManager;
_currentContext = currentContext;

View File

@@ -12,6 +12,7 @@ using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@@ -50,7 +51,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
IPolicyRequirementQuery policyRequirementQuery,
IAuthRequestRepository authRequestRepository,
IMailService mailService)
IMailService mailService,
IUserAccountKeysQuery userAccountKeysQuery)
: base(
userManager,
userService,
@@ -68,7 +70,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
userDecryptionOptionsBuilder,
policyRequirementQuery,
authRequestRepository,
mailService)
mailService,
userAccountKeysQuery)
{
_assertionOptionsDataProtector = assertionOptionsDataProtector;
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;

View File

@@ -6,7 +6,7 @@ using Bit.Core.Auth.Utilities;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Response;
using Bit.Core.KeyManagement.Models.Api.Response;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;

View File

@@ -71,6 +71,7 @@ public static class DapperServiceCollectionExtensions
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
services.AddSingleton<IUserSignatureKeyPairRepository, UserSignatureKeyPairRepository>();
services.AddSingleton<IOrganizationReportRepository, OrganizationReportRepository>();
services.AddSingleton<IOrganizationApplicationRepository, OrganizationApplicationRepository>();
services.AddSingleton<IOrganizationMemberBaseDetailRepository, OrganizationMemberBaseDetailRepository>();

View File

@@ -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);
};
}
}

View File

@@ -108,6 +108,7 @@ public static class EntityFrameworkServiceCollectionExtensions
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
services.AddSingleton<IUserSignatureKeyPairRepository, UserSignatureKeyPairRepository>();
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
services.AddSingleton<IOrganizationReportRepository, OrganizationReportRepository>();
services.AddSingleton<IOrganizationApplicationRepository, OrganizationApplicationRepository>();

View File

@@ -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));
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
};
}
}

View File

@@ -74,6 +74,7 @@ public class DatabaseContext : DbContext
public DbSet<TaxRate> TaxRates { get; set; }
public DbSet<Transaction> Transactions { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<UserSignatureKeyPair> UserSignatureKeyPairs { get; set; }
public DbSet<AuthRequest> AuthRequests { get; set; }
public DbSet<OrganizationDomain> OrganizationDomains { get; set; }
public DbSet<WebAuthnCredential> WebAuthnCredentials { get; set; }

View File

@@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[UserSignatureKeyPair_ReadByUserId]
@UserId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON;
SELECT
*
FROM
[dbo].[UserSignatureKeyPairView]
WHERE
[UserId] = @UserId;
END

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -0,0 +1,6 @@
CREATE VIEW [dbo].[UserSignatureKeyPairView]
AS
SELECT
*
FROM
[dbo].[UserSignatureKeyPair]

View File

@@ -41,7 +41,10 @@
@LastKdfChangeDate DATETIME2(7) = NULL,
@LastKeyRotationDate DATETIME2(7) = NULL,
@LastEmailChangeDate DATETIME2(7) = NULL,
@VerifyDevices BIT = 1
@VerifyDevices BIT = 1,
@SecurityState VARCHAR(MAX) = NULL,
@SecurityVersion INT = NULL,
@SignedPublicKey VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
@@ -90,7 +93,10 @@ BEGIN
[LastKdfChangeDate],
[LastKeyRotationDate],
[LastEmailChangeDate],
[VerifyDevices]
[VerifyDevices],
[SecurityState],
[SecurityVersion],
[SignedPublicKey]
)
VALUES
(
@@ -136,6 +142,9 @@ BEGIN
@LastKdfChangeDate,
@LastKeyRotationDate,
@LastEmailChangeDate,
@VerifyDevices
@VerifyDevices,
@SecurityState,
@SecurityVersion,
@SignedPublicKey
)
END

View File

@@ -41,7 +41,10 @@
@LastKdfChangeDate DATETIME2(7) = NULL,
@LastKeyRotationDate DATETIME2(7) = NULL,
@LastEmailChangeDate DATETIME2(7) = NULL,
@VerifyDevices BIT = 1
@VerifyDevices BIT = 1,
@SecurityState VARCHAR(MAX) = NULL,
@SecurityVersion INT = NULL,
@SignedPublicKey VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
@@ -90,7 +93,10 @@ BEGIN
[LastKdfChangeDate] = @LastKdfChangeDate,
[LastKeyRotationDate] = @LastKeyRotationDate,
[LastEmailChangeDate] = @LastEmailChangeDate,
[VerifyDevices] = @VerifyDevices
[VerifyDevices] = @VerifyDevices,
[SecurityState] = @SecurityState,
[SecurityVersion] = @SecurityVersion,
[SignedPublicKey] = @SignedPublicKey
WHERE
[Id] = @Id
END

View File

@@ -42,6 +42,9 @@
[LastKeyRotationDate] DATETIME2 (7) NULL,
[LastEmailChangeDate] DATETIME2 (7) NULL,
[VerifyDevices] BIT DEFAULT ((1)) NOT NULL,
[SecurityState] VARCHAR (MAX) NULL,
[SecurityVersion] INT NULL,
[SignedPublicKey] VARCHAR (MAX) NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
);

View File

@@ -12,6 +12,10 @@ using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Entities;
using Bit.Core.KeyManagement.Enums;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.Repositories;
using Bit.Core.Vault.Enums;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -24,6 +28,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
{
private static readonly string _mockEncryptedString =
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
private static readonly string _mockEncryptedType7String = "7.AOs41Hd8OQiCPXjyJKCiDA==";
private readonly HttpClient _client;
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
@@ -34,6 +39,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
private readonly IDeviceRepository _deviceRepository;
private readonly IPasswordHasher<User> _passwordHasher;
private readonly IOrganizationRepository _organizationRepository;
private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository;
private string _ownerEmail = null!;
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
@@ -49,6 +55,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
_organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
_passwordHasher = _factory.GetService<IPasswordHasher<User>>();
_organizationRepository = _factory.GetService<IOrganizationRepository>();
_userSignatureKeyPairRepository = _factory.GetService<IUserSignatureKeyPairRepository>();
}
public async Task InitializeAsync()
@@ -200,6 +207,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
var password = _passwordHasher.HashPassword(user, "newMasterPassword");
user.MasterPassword = password;
user.PublicKey = "publicKey";
user.PrivateKey = _mockEncryptedString;
await _userRepository.ReplaceAsync(user);
request.AccountUnlockData.MasterPasswordUnlockData.KdfType = user.Kdf;
@@ -209,6 +217,8 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
request.AccountUnlockData.MasterPasswordUnlockData.Email = user.Email;
request.AccountKeys.AccountPublicKey = "publicKey";
request.AccountKeys.UserKeyEncryptedAccountPrivateKey = _mockEncryptedString;
request.AccountKeys.PublicKeyEncryptionKeyPair = null;
request.AccountKeys.SignatureKeyPair = null;
request.OldMasterKeyAuthenticationHash = "newMasterPassword";
@@ -354,4 +364,196 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
}
[Theory]
[BitAutoData]
public async Task RotateV2UserAccountKeysAsync_Success(RotateUserAccountKeysAndDataRequestModel request)
{
await _loginHelper.LoginAsync(_ownerEmail);
var user = await _userRepository.GetByEmailAsync(_ownerEmail);
if (user == null)
{
throw new InvalidOperationException("User not found.");
}
var password = _passwordHasher.HashPassword(user, "newMasterPassword");
user.MasterPassword = password;
user.PublicKey = "publicKey";
user.PrivateKey = _mockEncryptedType7String;
await _userRepository.ReplaceAsync(user);
await _userSignatureKeyPairRepository.CreateAsync(new UserSignatureKeyPair
{
UserId = user.Id,
SignatureAlgorithm = SignatureAlgorithm.Ed25519,
SigningKey = _mockEncryptedType7String,
VerifyingKey = "verifyingKey",
});
request.AccountUnlockData.MasterPasswordUnlockData.KdfType = user.Kdf;
request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations = user.KdfIterations;
request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory = user.KdfMemory;
request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism = user.KdfParallelism;
request.AccountUnlockData.MasterPasswordUnlockData.Email = user.Email;
request.AccountKeys.AccountPublicKey = "publicKey";
request.AccountKeys.UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String;
request.AccountKeys.PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel
{
PublicKey = "publicKey",
WrappedPrivateKey = _mockEncryptedType7String,
SignedPublicKey = "signedPublicKey",
};
request.AccountKeys.SignatureKeyPair = new SignatureKeyPairRequestModel
{
SignatureAlgorithm = "ed25519",
WrappedSigningKey = _mockEncryptedType7String,
VerifyingKey = "verifyingKey",
};
request.OldMasterKeyAuthenticationHash = "newMasterPassword";
request.AccountData.Ciphers =
[
new CipherWithIdRequestModel
{
Id = Guid.NewGuid(),
Type = CipherType.Login,
Name = _mockEncryptedString,
Login = new CipherLoginModel
{
Username = _mockEncryptedString,
Password = _mockEncryptedString,
},
},
];
request.AccountData.Folders = [
new FolderWithIdRequestModel
{
Id = Guid.NewGuid(),
Name = _mockEncryptedString,
},
];
request.AccountData.Sends = [
new SendWithIdRequestModel
{
Id = Guid.NewGuid(),
Name = _mockEncryptedString,
Key = _mockEncryptedString,
Disabled = false,
DeletionDate = DateTime.UtcNow.AddDays(1),
},
];
request.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey = _mockEncryptedString;
request.AccountUnlockData.PasskeyUnlockData = [];
request.AccountUnlockData.DeviceKeyUnlockData = [];
request.AccountUnlockData.EmergencyAccessUnlockData = [];
request.AccountUnlockData.OrganizationAccountRecoveryUnlockData = [];
var response = await _client.PostAsJsonAsync("/accounts/key-management/rotate-user-account-keys", request);
var responseMessage = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();
var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail);
Assert.NotNull(userNewState);
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.Email, userNewState.Email);
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfType, userNewState.Kdf);
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations, userNewState.KdfIterations);
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory);
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism);
}
[Theory]
[BitAutoData]
public async Task RotateUpgradeToV2UserAccountKeysAsync_Success(RotateUserAccountKeysAndDataRequestModel request)
{
await _loginHelper.LoginAsync(_ownerEmail);
var user = await _userRepository.GetByEmailAsync(_ownerEmail);
if (user == null)
{
throw new InvalidOperationException("User not found.");
}
var password = _passwordHasher.HashPassword(user, "newMasterPassword");
user.MasterPassword = password;
user.PublicKey = "publicKey";
user.PrivateKey = _mockEncryptedString;
await _userRepository.ReplaceAsync(user);
request.AccountUnlockData.MasterPasswordUnlockData.KdfType = user.Kdf;
request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations = user.KdfIterations;
request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory = user.KdfMemory;
request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism = user.KdfParallelism;
request.AccountUnlockData.MasterPasswordUnlockData.Email = user.Email;
request.AccountKeys.AccountPublicKey = "publicKey";
request.AccountKeys.UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String;
request.AccountKeys.PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel
{
PublicKey = "publicKey",
WrappedPrivateKey = _mockEncryptedType7String,
SignedPublicKey = "signedPublicKey",
};
request.AccountKeys.SignatureKeyPair = new SignatureKeyPairRequestModel
{
SignatureAlgorithm = "ed25519",
WrappedSigningKey = _mockEncryptedType7String,
VerifyingKey = "verifyingKey",
};
request.AccountKeys.SecurityState = new SecurityStateModel
{
SecurityVersion = 2,
SecurityState = "v2",
};
request.OldMasterKeyAuthenticationHash = "newMasterPassword";
request.AccountData.Ciphers =
[
new CipherWithIdRequestModel
{
Id = Guid.NewGuid(),
Type = CipherType.Login,
Name = _mockEncryptedString,
Login = new CipherLoginModel
{
Username = _mockEncryptedString,
Password = _mockEncryptedString,
},
},
];
request.AccountData.Folders = [
new FolderWithIdRequestModel
{
Id = Guid.NewGuid(),
Name = _mockEncryptedString,
},
];
request.AccountData.Sends = [
new SendWithIdRequestModel
{
Id = Guid.NewGuid(),
Name = _mockEncryptedString,
Key = _mockEncryptedString,
Disabled = false,
DeletionDate = DateTime.UtcNow.AddDays(1),
},
];
request.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey = _mockEncryptedString;
request.AccountUnlockData.PasskeyUnlockData = [];
request.AccountUnlockData.DeviceKeyUnlockData = [];
request.AccountUnlockData.EmergencyAccessUnlockData = [];
request.AccountUnlockData.OrganizationAccountRecoveryUnlockData = [];
var response = await _client.PostAsJsonAsync("/accounts/key-management/rotate-user-account-keys", request);
var responseMessage = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();
var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail);
Assert.NotNull(userNewState);
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.Email, userNewState.Email);
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfType, userNewState.Kdf);
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations, userNewState.KdfIterations);
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory);
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism);
}
}

View File

@@ -11,6 +11,7 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Kdf;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -33,10 +34,10 @@ public class AccountsControllerTests : IDisposable
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly IFeatureService _featureService;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
private readonly ITwoFactorEmailService _twoFactorEmailService;
private readonly IChangeKdfCommand _changeKdfCommand;
public AccountsControllerTests()
{
_userService = Substitute.For<IUserService>();
@@ -48,6 +49,7 @@ public class AccountsControllerTests : IDisposable
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
_featureService = Substitute.For<IFeatureService>();
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
_changeKdfCommand = Substitute.For<IChangeKdfCommand>();
@@ -61,6 +63,7 @@ public class AccountsControllerTests : IDisposable
_tdeOffboardingPasswordCommand,
_twoFactorIsEnabledQuery,
_featureService,
_userAccountKeysQuery,
_twoFactorEmailService,
_changeKdfCommand
);

View File

@@ -110,6 +110,7 @@ public class AccountsKeyManagementControllerTests
public async Task RotateUserAccountKeysSuccess(SutProvider<AccountsKeyManagementController> sutProvider,
RotateUserAccountKeysAndDataRequestModel data, User user)
{
data.AccountKeys.SignatureKeyPair = null;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
.Returns(IdentityResult.Success);
@@ -142,8 +143,60 @@ public class AccountsKeyManagementControllerTests
&& d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash
&& d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey
&& d.AccountPublicKey == data.AccountKeys.AccountPublicKey
&& d.UserKeyEncryptedAccountPrivateKey == data.AccountKeys.UserKeyEncryptedAccountPrivateKey
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.PublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.PublicKey
));
}
[Theory]
[BitAutoData]
public async Task RotateUserAccountKeys_UserCryptoV2_Success_Async(SutProvider<AccountsKeyManagementController> sutProvider,
RotateUserAccountKeysAndDataRequestModel data, User user)
{
data.AccountKeys.SignatureKeyPair = new SignatureKeyPairRequestModel
{
SignatureAlgorithm = "ed25519",
WrappedSigningKey = "wrappedSigningKey",
VerifyingKey = "verifyingKey"
};
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
.Returns(IdentityResult.Success);
await sutProvider.Sut.RotateUserAccountKeysAsync(data);
await sutProvider.GetDependency<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>>().Received(1)
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.EmergencyAccessUnlockData));
await sutProvider.GetDependency<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>>().Received(1)
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.OrganizationAccountRecoveryUnlockData));
await sutProvider.GetDependency<IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>>().Received(1)
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.PasskeyUnlockData));
await sutProvider.GetDependency<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>>().Received(1)
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Ciphers));
await sutProvider.GetDependency<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>>().Received(1)
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Folders));
await sutProvider.GetDependency<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>>().Received(1)
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Sends));
await sutProvider.GetDependency<IRotateUserAccountKeysCommand>().Received(1)
.RotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<RotateUserAccountKeysData>(d =>
d.OldMasterKeyAuthenticationHash == data.OldMasterKeyAuthenticationHash
&& d.MasterPasswordUnlockData.KdfType == data.AccountUnlockData.MasterPasswordUnlockData.KdfType
&& d.MasterPasswordUnlockData.KdfIterations == data.AccountUnlockData.MasterPasswordUnlockData.KdfIterations
&& d.MasterPasswordUnlockData.KdfMemory == data.AccountUnlockData.MasterPasswordUnlockData.KdfMemory
&& d.MasterPasswordUnlockData.KdfParallelism == data.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism
&& d.MasterPasswordUnlockData.Email == data.AccountUnlockData.MasterPasswordUnlockData.Email
&& d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash
&& d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.PublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.PublicKey
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.SignedPublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.SignedPublicKey
&& d.AccountKeys!.SignatureKeyPairData!.SignatureAlgorithm == Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519
&& d.AccountKeys!.SignatureKeyPairData.WrappedSigningKey == data.AccountKeys.SignatureKeyPair!.WrappedSigningKey
&& d.AccountKeys!.SignatureKeyPairData.VerifyingKey == data.AccountKeys.SignatureKeyPair!.VerifyingKey
));
}
@@ -153,6 +206,7 @@ public class AccountsKeyManagementControllerTests
public async Task RotateUserKeyNoUser_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
RotateUserAccountKeysAndDataRequestModel data)
{
data.AccountKeys.SignatureKeyPair = null;
User? user = null;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
@@ -165,6 +219,7 @@ public class AccountsKeyManagementControllerTests
public async Task RotateUserKeyWrongData_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
RotateUserAccountKeysAndDataRequestModel data, User user, IdentityErrorDescriber _identityErrorDescriber)
{
data.AccountKeys.SignatureKeyPair = null;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
.Returns(IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()));

View 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);
}
}

View File

@@ -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());
}
}

View File

@@ -12,6 +12,8 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
@@ -74,6 +76,7 @@ public class SyncControllerTests
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
// Adjust random data to match required formats / test intentions
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
@@ -98,6 +101,11 @@ public class SyncControllerTests
// Setup returns
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
SignatureKeyPairData = null,
});
organizationUserRepository
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
@@ -127,7 +135,6 @@ public class SyncControllerTests
// Execute GET
var result = await sutProvider.Sut.Get();
// Asserts
// Assert that methods are called
var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
@@ -166,6 +173,7 @@ public class SyncControllerTests
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
// Adjust random data to match required formats / test intentions
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
@@ -189,6 +197,11 @@ public class SyncControllerTests
// Setup returns
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
SignatureKeyPairData = null,
});
organizationUserRepository
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
@@ -256,6 +269,7 @@ public class SyncControllerTests
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
// Adjust random data to match required formats / test intentions
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
@@ -290,6 +304,12 @@ public class SyncControllerTests
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);
userService.HasPremiumFromOrganization(user).Returns(false);
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
SignatureKeyPairData = null,
});
// Execute GET
var result = await sutProvider.Sut.Get();
@@ -327,6 +347,13 @@ public class SyncControllerTests
user.MasterPassword = null;
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
SignatureKeyPairData = null,
});
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
@@ -352,6 +379,13 @@ public class SyncControllerTests
user.KdfMemory = kdfMemory;
user.KdfParallelism = kdfParallelism;
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
SignatureKeyPairData = null,
});
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);

View 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);
}
}

View File

@@ -1,11 +1,18 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.KeyManagement.UserKey.Implementations;
using Bit.Core.Services;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Repositories;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Core.Test.KeyManagement.UserKey;
@@ -14,7 +21,7 @@ namespace Bit.Core.Test.KeyManagement.UserKey;
public class RotateUserAccountKeysCommandTests
{
[Theory, BitAutoData]
public async Task RejectsWrongOldMasterPassword(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
public async Task RotateUserAccountKeysAsync_WrongOldMasterPassword_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
RotateUserAccountKeysData model)
{
user.Email = model.MasterPasswordUnlockData.Email;
@@ -25,41 +32,38 @@ public class RotateUserAccountKeysCommandTests
Assert.NotEqual(IdentityResult.Success, result);
}
[Theory, BitAutoData]
public async Task ThrowsWhenUserIsNull(SutProvider<RotateUserAccountKeysCommand> sutProvider,
public async Task RotateUserAccountKeysAsync_UserIsNull_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider,
RotateUserAccountKeysData model)
{
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(null, model));
}
[Theory, BitAutoData]
public async Task RejectsEmailChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
public async Task RotateUserAccountKeysAsync_EmailChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
RotateUserAccountKeysData model)
{
user.Kdf = Enums.KdfType.Argon2id;
user.KdfIterations = 3;
user.KdfMemory = 64;
user.KdfParallelism = 4;
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
model.MasterPasswordUnlockData.Email = user.Email + ".different-domain";
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
model.MasterPasswordUnlockData.KdfIterations = 3;
model.MasterPasswordUnlockData.KdfMemory = 64;
model.MasterPasswordUnlockData.KdfParallelism = 4;
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
.Returns(true);
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model));
}
[Theory, BitAutoData]
public async Task RejectsKdfChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
public async Task RotateUserAccountKeysAsync_KdfChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
RotateUserAccountKeysData model)
{
user.Kdf = Enums.KdfType.Argon2id;
user.KdfIterations = 3;
user.KdfMemory = 64;
user.KdfParallelism = 4;
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
model.MasterPasswordUnlockData.Email = user.Email;
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.PBKDF2_SHA256;
model.MasterPasswordUnlockData.KdfIterations = 600000;
model.MasterPasswordUnlockData.KdfMemory = null;
@@ -71,22 +75,15 @@ public class RotateUserAccountKeysCommandTests
[Theory, BitAutoData]
public async Task RejectsPublicKeyChange(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
public async Task RotateUserAccountKeysAsync_PublicKeyChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
RotateUserAccountKeysData model)
{
user.PublicKey = "old-public";
user.Kdf = Enums.KdfType.Argon2id;
user.KdfIterations = 3;
user.KdfMemory = 64;
user.KdfParallelism = 4;
model.AccountPublicKey = "new-public";
model.MasterPasswordUnlockData.Email = user.Email;
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
model.MasterPasswordUnlockData.KdfIterations = 3;
model.MasterPasswordUnlockData.KdfMemory = 64;
model.MasterPasswordUnlockData.KdfParallelism = 4;
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey = "new-public";
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
.Returns(true);
@@ -94,27 +91,350 @@ public class RotateUserAccountKeysCommandTests
}
[Theory, BitAutoData]
public async Task RotatesCorrectly(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
public async Task RotateUserAccountKeysAsync_V1_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
RotateUserAccountKeysData model)
{
user.Kdf = Enums.KdfType.Argon2id;
user.KdfIterations = 3;
user.KdfMemory = 64;
user.KdfParallelism = 4;
model.MasterPasswordUnlockData.Email = user.Email;
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
model.MasterPasswordUnlockData.KdfIterations = 3;
model.MasterPasswordUnlockData.KdfMemory = 64;
model.MasterPasswordUnlockData.KdfParallelism = 4;
model.AccountPublicKey = user.PublicKey;
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
.Returns(true);
var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
Assert.Equal(IdentityResult.Success, result);
}
[Theory, BitAutoData]
public async Task RotateUserAccountKeysAsync_UpgradeV1ToV2_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
.Returns(true);
var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
Assert.Equal(IdentityResult.Success, result);
Assert.Equal(user.SecurityState, model.AccountKeys.SecurityStateData!.SecurityState);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_PublicKeyChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey = "new-public";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_V2User_PrivateKeyNotXChaCha20_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV2ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "2.xxx";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_V1User_PrivateKeyNotAesCbcHmac_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "7.xxx";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("The provided account private key was not wrapped with AES-256-CBC-HMAC", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_V1_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);
Assert.Empty(saveEncryptedDataActions);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_V2_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV2ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);
Assert.NotEmpty(saveEncryptedDataActions);
Assert.Equal(user.SecurityState, model.AccountKeys.SecurityStateData!.SecurityState);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_V2User_VerifyingKeyMismatch_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV2ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.SignatureKeyPairData.VerifyingKey = "different-verifying-key";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("The provided verifying key does not match the user's current verifying key.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_V2User_SignedPublicKeyNullOrEmpty_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV2ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null;
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("No signed public key provided, but the user already has a signature key pair.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_V2User_WrappedSigningKeyNotXChaCha20_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV2ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.SignatureKeyPairData.WrappedSigningKey = "2.xxx";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("The provided signing key data is not wrapped with XChaCha20-Poly1305.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeys_UpgradeToV2_InvalidVerifyingKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.SignatureKeyPairData.VerifyingKey = "";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("The provided signature key pair data does not contain a valid verifying key.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_UpgradeToV2_IncorrectlyWrappedPrivateKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "2.abc";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("The provided private key encryption key is not wrapped with XChaCha20-Poly1305.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSignedPublicKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null;
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("No signed public key provided, but the user already has a signature key pair.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSecurityState_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.SecurityStateData = null;
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("No signed security state provider for V2 user", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_RotateV2_NoSignatureKeyPair_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV2ExistingUser(user, signatureRepository);
SetV2ModelUser(model);
model.AccountKeys.SignatureKeyPairData = null;
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("Signature key pair data is required for V2 encryption.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_GetEncryptionType_EmptyString_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("Invalid encryption type string.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateAccountKeysAsync_GetEncryptionType_InvalidString_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
SetTestKdfAndSaltForUserAndModel(user, model);
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
SetV1ExistingUser(user, signatureRepository);
SetV1ModelUser(model);
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "9.xxx";
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
Assert.Equal("Invalid encryption type string.", ex.Message);
}
[Theory, BitAutoData]
public async Task UpdateUserData_RevisionDateChanged_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
{
var oldDate = new DateTime(2017, 1, 1);
var cipher = Substitute.For<Cipher>();
cipher.RevisionDate = oldDate;
model.Ciphers = [cipher];
var folder = Substitute.For<Folder>();
folder.RevisionDate = oldDate;
model.Folders = [folder];
var send = Substitute.For<Send>();
send.RevisionDate = oldDate;
model.Sends = [send];
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
sutProvider.Sut.UpdateUserData(model, user, saveEncryptedDataActions);
foreach (var dataAction in saveEncryptedDataActions)
{
await dataAction.Invoke();
}
var updatedCiphers = sutProvider.GetDependency<ICipherRepository>()
.ReceivedCalls()
.FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")?
.GetArguments()[1] as IEnumerable<Cipher>;
foreach (var updatedCipher in updatedCiphers!)
{
var oldCipher = model.Ciphers.FirstOrDefault(c => c.Id == updatedCipher.Id);
Assert.NotEqual(oldDate, updatedCipher.RevisionDate);
}
var updatedFolders = sutProvider.GetDependency<IFolderRepository>()
.ReceivedCalls()
.FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")?
.GetArguments()[1] as IEnumerable<Folder>;
foreach (var updatedFolder in updatedFolders!)
{
var oldFolder = model.Folders.FirstOrDefault(f => f.Id == updatedFolder.Id);
Assert.NotEqual(oldDate, updatedFolder.RevisionDate);
}
var updatedSends = sutProvider.GetDependency<ISendRepository>()
.ReceivedCalls()
.FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")?
.GetArguments()[1] as IEnumerable<Send>;
foreach (var updatedSend in updatedSends!)
{
var oldSend = model.Sends.FirstOrDefault(s => s.Id == updatedSend.Id);
Assert.NotEqual(oldDate, updatedSend.RevisionDate);
}
}
// Helper functions to set valid test parameters that match each other to the model and user.
private static void SetTestKdfAndSaltForUserAndModel(User user, RotateUserAccountKeysData model)
{
user.Kdf = Enums.KdfType.Argon2id;
user.KdfIterations = 3;
user.KdfMemory = 64;
user.KdfParallelism = 4;
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
model.MasterPasswordUnlockData.KdfIterations = 3;
model.MasterPasswordUnlockData.KdfMemory = 64;
model.MasterPasswordUnlockData.KdfParallelism = 4;
// The email is the salt for the KDF and is validated currently.
user.Email = model.MasterPasswordUnlockData.Email;
}
private static void SetV1ExistingUser(User user, IUserSignatureKeyPairRepository userSignatureKeyPairRepository)
{
user.PrivateKey = "2.abc";
user.PublicKey = "public";
user.SignedPublicKey = null;
userSignatureKeyPairRepository.GetByUserIdAsync(user.Id).ReturnsNull();
}
private static void SetV2ExistingUser(User user, IUserSignatureKeyPairRepository userSignatureKeyPairRepository)
{
user.PrivateKey = "7.abc";
user.PublicKey = "public";
user.SignedPublicKey = "signed-public";
userSignatureKeyPairRepository.GetByUserIdAsync(user.Id).Returns(new SignatureKeyPairData(SignatureAlgorithm.Ed25519, "7.abc", "verifying-key"));
}
private static void SetV1ModelUser(RotateUserAccountKeysData model)
{
model.AccountKeys.PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("2.abc", "public", null);
model.AccountKeys.SignatureKeyPairData = null;
model.AccountKeys.SecurityStateData = null;
}
private static void SetV2ModelUser(RotateUserAccountKeysData model)
{
model.AccountKeys.PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("7.abc", "public", "signed-public");
model.AccountKeys.SignatureKeyPairData = new SignatureKeyPairData(SignatureAlgorithm.Ed25519, "7.abc", "verifying-key");
model.AccountKeys.SecurityStateData = new SecurityStateData
{
SecurityState = "abc",
SecurityVersion = 2,
};
}
}

View File

@@ -10,7 +10,9 @@ using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Response;
using Bit.Core.KeyManagement.Models.Api.Response;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Models.Api;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -51,6 +53,7 @@ public class BaseRequestValidatorTests
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IAuthRequestRepository _authRequestRepository;
private readonly IMailService _mailService;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
private readonly BaseRequestValidatorTestWrapper _sut;
@@ -73,6 +76,7 @@ public class BaseRequestValidatorTests
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
_authRequestRepository = Substitute.For<IAuthRequestRepository>();
_mailService = Substitute.For<IMailService>();
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
_sut = new BaseRequestValidatorTestWrapper(
_userManager,
@@ -91,7 +95,8 @@ public class BaseRequestValidatorTests
_userDecryptionOptionsBuilder,
_policyRequirementQuery,
_authRequestRepository,
_mailService);
_mailService,
_userAccountKeysQuery);
}
/* Logic path
@@ -180,6 +185,13 @@ public class BaseRequestValidatorTests
// 5 -> not legacy user
_userService.IsLegacyUser(Arg.Any<string>())
.Returns(false);
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
"test-private-key",
"test-public-key"
)
});
// Act
await _sut.ValidateAsync(context);
@@ -227,6 +239,13 @@ public class BaseRequestValidatorTests
// 5 -> not legacy user
_userService.IsLegacyUser(Arg.Any<string>())
.Returns(false);
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
"test-private-key",
"test-public-key"
)
});
// Act
await _sut.ValidateAsync(context);
@@ -460,6 +479,13 @@ public class BaseRequestValidatorTests
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
.Returns(Task.FromResult(true));
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
"test-private-key",
"test-public-key"
)
});
await _sut.ValidateAsync(context);
@@ -495,6 +521,13 @@ public class BaseRequestValidatorTests
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
.Returns(Task.FromResult(true));
context.ValidatedTokenRequest.ClientId = "web";
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
"test-private-key",
"test-public-key"
)
});
// Act
await _sut.ValidateAsync(context);
@@ -529,6 +562,13 @@ public class BaseRequestValidatorTests
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
.Returns(Task.FromResult(true));
context.ValidatedTokenRequest.ClientId = "web";
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
"test-private-key",
"test-public-key"
)
});
// Act
await _sut.ValidateAsync(context);
@@ -591,6 +631,13 @@ public class BaseRequestValidatorTests
HasMasterPassword = false,
MasterPasswordUnlock = null
}));
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
"test-private-key",
"test-public-key"
)
});
var context = CreateContext(tokenRequest, requestContext, grantResult);
_sut.isValid = true;
@@ -644,6 +691,14 @@ public class BaseRequestValidatorTests
}
}));
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
"test-private-key",
"test-public-key"
)
});
var context = CreateContext(tokenRequest, requestContext, grantResult);
_sut.isValid = true;
@@ -671,6 +726,152 @@ public class BaseRequestValidatorTests
Assert.Equal("test@example.com", userDecryptionOptions.MasterPasswordUnlock.Salt);
}
[Theory, BitAutoData]
public async Task ValidateAsync_CustomResponse_ShouldIncludeAccountKeys(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
var mockAccountKeys = new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
"test-private-key",
"test-public-key",
"test-signed-public-key"
),
SignatureKeyPairData = new SignatureKeyPairData(
Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519,
"test-wrapped-signing-key",
"test-verifying-key"
),
SecurityStateData = new SecurityStateData
{
SecurityState = "test-security-state",
SecurityVersion = 2
}
};
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(mockAccountKeys);
_userDecryptionOptionsBuilder.ForUser(Arg.Any<User>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithDevice(Arg.Any<Device>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithSso(Arg.Any<SsoConfig>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithWebAuthnLoginCredential(Arg.Any<WebAuthnCredential>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.BuildAsync().Returns(Task.FromResult(new UserDecryptionOptions
{
HasMasterPassword = true,
MasterPasswordUnlock = new MasterPasswordUnlockResponseModel
{
Kdf = new MasterPasswordUnlockKdfResponseModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = 100000
},
MasterKeyEncryptedUserKey = _mockEncryptedString,
Salt = "test@example.com"
}
}));
var context = CreateContext(tokenRequest, requestContext, grantResult);
_sut.isValid = true;
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
.Returns(Task.FromResult(true));
// Act
await _sut.ValidateAsync(context);
// Assert
Assert.False(context.GrantResult.IsError);
var customResponse = context.GrantResult.CustomResponse;
// Verify AccountKeys are included in response
Assert.Contains("AccountKeys", customResponse);
Assert.IsType<PrivateKeysResponseModel>(customResponse["AccountKeys"]);
var accountKeysResponse = (PrivateKeysResponseModel)customResponse["AccountKeys"];
Assert.NotNull(accountKeysResponse.PublicKeyEncryptionKeyPair);
Assert.Equal("test-public-key", accountKeysResponse.PublicKeyEncryptionKeyPair.PublicKey);
Assert.Equal("test-private-key", accountKeysResponse.PublicKeyEncryptionKeyPair.WrappedPrivateKey);
Assert.Equal("test-signed-public-key", accountKeysResponse.PublicKeyEncryptionKeyPair.SignedPublicKey);
Assert.NotNull(accountKeysResponse.SignatureKeyPair);
Assert.Equal("test-wrapped-signing-key", accountKeysResponse.SignatureKeyPair.WrappedSigningKey);
Assert.Equal("test-verifying-key", accountKeysResponse.SignatureKeyPair.VerifyingKey);
Assert.NotNull(accountKeysResponse.SecurityState);
Assert.Equal("test-security-state", accountKeysResponse.SecurityState.SecurityState);
Assert.Equal(2, accountKeysResponse.SecurityState.SecurityVersion);
}
[Theory, BitAutoData]
public async Task ValidateAsync_CustomResponse_AccountKeysQuery_SkippedWhenPrivateKeyIsNull(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
requestContext.User.PrivateKey = null;
var context = CreateContext(tokenRequest, requestContext, grantResult);
_sut.isValid = true;
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
.Returns(Task.FromResult(true));
// Act
await _sut.ValidateAsync(context);
// Assert
Assert.False(context.GrantResult.IsError);
// Verify that the account keys query wasn't called.
await _userAccountKeysQuery.Received(0).Run(Arg.Any<User>());
}
[Theory, BitAutoData]
public async Task ValidateAsync_CustomResponse_AccountKeysQuery_CalledWithCorrectUser(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
var expectedUser = requestContext.User;
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
"test-private-key",
"test-public-key"
)
});
_userDecryptionOptionsBuilder.ForUser(Arg.Any<User>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithDevice(Arg.Any<Device>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithSso(Arg.Any<SsoConfig>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.WithWebAuthnLoginCredential(Arg.Any<WebAuthnCredential>()).Returns(_userDecryptionOptionsBuilder);
_userDecryptionOptionsBuilder.BuildAsync().Returns(Task.FromResult(new UserDecryptionOptions()));
var context = CreateContext(tokenRequest, requestContext, grantResult);
_sut.isValid = true;
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
.Returns(Task.FromResult(true));
// Act
await _sut.ValidateAsync(context);
// Assert
Assert.False(context.GrantResult.IsError);
// Verify that the account keys query was called with the correct user
await _userAccountKeysQuery.Received(1).Run(Arg.Is<User>(u => u.Id == expectedUser.Id));
}
private BaseRequestValidationContextFake CreateContext(
ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,

View File

@@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@@ -64,7 +65,8 @@ IBaseRequestValidatorTestWrapper
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IPolicyRequirementQuery policyRequirementQuery,
IAuthRequestRepository authRequestRepository,
IMailService mailService) :
IMailService mailService,
IUserAccountKeysQuery userAccountKeysQuery) :
base(
userManager,
userService,
@@ -82,7 +84,8 @@ IBaseRequestValidatorTestWrapper
userDecryptionOptionsBuilder,
policyRequirementQuery,
authRequestRepository,
mailService)
mailService,
userAccountKeysQuery)
{
}

View File

@@ -92,6 +92,7 @@ public class EfRepositoryListBuilder<T> : ISpecimenBuilder where T : BaseEntityF
cfg.AddProfile<TransactionMapperProfile>();
cfg.AddProfile<UserMapperProfile>();
cfg.AddProfile<PasswordHealthReportApplicationProfile>();
cfg.AddProfile<UserSignatureKeyPairMapperProfile>();
cfg.AddProfile<OrganizationReportProfile>();
})
.CreateMapper()));

View File

@@ -29,7 +29,8 @@ public class UserCompare : IEqualityComparer<User>
x.LicenseKey == y.LicenseKey &&
x.ApiKey == y.ApiKey &&
x.Kdf == y.Kdf &&
x.KdfIterations == y.KdfIterations;
x.KdfIterations == y.KdfIterations &&
x.SignedPublicKey == y.SignedPublicKey;
}
public int GetHashCode([DisallowNull] User obj)

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}

View File

@@ -1873,6 +1873,15 @@ namespace Bit.MySqlMigrations.Migrations
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<string>("SecurityState")
.HasColumnType("longtext");
b.Property<int?>("SecurityVersion")
.HasColumnType("int");
b.Property<string>("SignedPublicKey")
.HasColumnType("longtext");
b.Property<long?>("Storage")
.HasColumnType("bigint");
@@ -1901,6 +1910,40 @@ namespace Bit.MySqlMigrations.Migrations
b.ToTable("User", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b =>
{
b.Property<Guid>("Id")
.HasColumnType("char(36)");
b.Property<DateTime>("CreationDate")
.HasColumnType("datetime(6)");
b.Property<DateTime>("RevisionDate")
.HasColumnType("datetime(6)");
b.Property<byte>("SignatureAlgorithm")
.HasColumnType("tinyint unsigned");
b.Property<string>("SigningKey")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("UserId")
.HasColumnType("char(36)");
b.Property<string>("VerifyingKey")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique()
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("UserSignatureKeyPair", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
{
b.Property<Guid>("Id")
@@ -2948,6 +2991,17 @@ namespace Bit.MySqlMigrations.Migrations
b.Navigation("User");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}

View File

@@ -1879,6 +1879,15 @@ namespace Bit.PostgresMigrations.Migrations
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("SecurityState")
.HasColumnType("text");
b.Property<int?>("SecurityVersion")
.HasColumnType("integer");
b.Property<string>("SignedPublicKey")
.HasColumnType("text");
b.Property<long?>("Storage")
.HasColumnType("bigint");
@@ -1907,6 +1916,40 @@ namespace Bit.PostgresMigrations.Migrations
b.ToTable("User", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<DateTime>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("RevisionDate")
.HasColumnType("timestamp with time zone");
b.Property<byte>("SignatureAlgorithm")
.HasColumnType("smallint");
b.Property<string>("SigningKey")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<string>("VerifyingKey")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique()
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("UserSignatureKeyPair", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
{
b.Property<Guid>("Id")
@@ -2954,6 +2997,17 @@ namespace Bit.PostgresMigrations.Migrations
b.Navigation("User");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}

View File

@@ -1862,6 +1862,15 @@ namespace Bit.SqliteMigrations.Migrations
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("SecurityState")
.HasColumnType("TEXT");
b.Property<int?>("SecurityVersion")
.HasColumnType("INTEGER");
b.Property<string>("SignedPublicKey")
.HasColumnType("TEXT");
b.Property<long?>("Storage")
.HasColumnType("INTEGER");
@@ -1890,6 +1899,40 @@ namespace Bit.SqliteMigrations.Migrations
b.ToTable("User", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT");
b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");
b.Property<DateTime>("RevisionDate")
.HasColumnType("TEXT");
b.Property<byte>("SignatureAlgorithm")
.HasColumnType("INTEGER");
b.Property<string>("SigningKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<string>("VerifyingKey")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique()
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("UserSignatureKeyPair", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
{
b.Property<Guid>("Id")
@@ -2937,6 +2980,17 @@ namespace Bit.SqliteMigrations.Migrations
b.Navigation("User");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")