1
0
mirror of https://github.com/bitwarden/server synced 2026-01-06 02:23:51 +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)
);