Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: encrypt sensitive columns #1593

Merged
merged 1 commit into from
Jun 12, 2024
Merged

feat: encrypt sensitive columns #1593

merged 1 commit into from
Jun 12, 2024

Conversation

hf
Copy link
Contributor

@hf hf commented May 29, 2024

Adds support for encrypting sensitive columns like the MFA secret and password hash.

The goal with this encryption mechanism is to add yet another layer of security on top of the database permissions provided by Postgres. In the event that the database leaks or is accessed by malicious users or the database permissions are incorrectly defined, the encryption key would also be required to inspect this sensitive data.

Encryption is done using AES-GCM-256. Strings that are encrypted are converted into a JSON string with this shape:

{
  "key_id": "key identifier used for encryption",
  "alg": "aes-gcm-hkdf",
  "nonce": "GCM 12 byte nonce",
  "data": "Base64 standard encoding of the ciphertext"
}

As AES-GCM must not be used more than 2^32 times with a single symmetric key, and this is not that much -- imagine serving 100m users -- then this means that all users can only add 42 passwords or MFA verification factors before running into this hard limit. To fix this, a symmetric key is derived using HKDF with SHA256 such that the symmetric key is used together with the object ID (for passwords - the user ID, for TOTP secrets - the factor ID). This way there's a separate AES-GCM key per object, and additionally gives the security property that a malicious actor with write permissions to the database cannot swap passwords / TOTP secrets from Malice's account to Target's account. They would need to also change the UUIDs of these objects, which is likely to be hard.

To turn on encryption the following configs need to be added:

GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPT=true -- that turns on encryption for new objects.
GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPTION_KEY_ID=key-id -- ID of the encryption key, allowing to rotate keys easily.
GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPTION_KEY=key -- Base64 URL encoding of a 256 bit AES key

Once encryption has been turned on, in order to have the rows be readable for ever this config must be provided with all past and future keys:

GOTRUE_SECURITY_DB_ENCRYPTION_DECRYPTION_KEYS=key-id:key -- A map of key IDs and Base64 URL key encodings of the keys.

To retire keys, you should just move the old key to the decryption keys map, and advertise the new encryption key ID. On each successful sign in with password, or any MFA verification attempt, the latest key will be used to re-encrypt the column. This also applies for the non-encrypted-to-encrypted case.

@hf hf force-pushed the hf/encrypt-sensitive-columns branch 3 times, most recently from 81eb808 to 8a97e9d Compare June 4, 2024 11:38
Copy link
Contributor

github-actions bot commented Jun 4, 2024

Pull Request Test Coverage Report for Build 9366388885

Details

  • 100 of 151 (66.23%) changed or added relevant lines in 8 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.05%) to 57.638%

Changes Missing Coverage Covered Lines Changed/Added Lines %
internal/api/admin.go 1 2 50.0%
internal/api/mfa.go 14 18 77.78%
internal/models/user.go 16 20 80.0%
internal/conf/configuration.go 15 31 48.39%
internal/crypto/crypto.go 48 74 64.86%
Totals Coverage Status
Change from base Build 9361623090: 0.05%
Covered Lines: 8591
Relevant Lines: 14905

💛 - Coveralls

@hf hf force-pushed the hf/encrypt-sensitive-columns branch 2 times, most recently from 4e050a0 to 243cceb Compare June 4, 2024 13:39
@hf hf marked this pull request as ready for review June 4, 2024 13:48
@hf hf requested a review from a team as a code owner June 4, 2024 13:48
Copy link
Contributor

@J0 J0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for putting this together

internal/api/mfa.go Show resolved Hide resolved
internal/api/mfa.go Outdated Show resolved Hide resolved
internal/api/mfa_test.go Show resolved Hide resolved
internal/crypto/crypto.go Show resolved Hide resolved
internal/conf/configuration.go Show resolved Hide resolved
internal/conf/configuration.go Show resolved Hide resolved
internal/models/user.go Outdated Show resolved Hide resolved
internal/models/user.go Outdated Show resolved Hide resolved
@kangmingtay
Copy link
Member

kangmingtay commented Jun 5, 2024

@hf Some questions:

... such that the symmetric key is used together with the object ID (for passwords - the user ID, for TOTP secrets - the factor ID). This way there's a separate AES-GCM key per object, ...

just to clarify my understanding, the encryption key of an object is derived from the symmetric key + the object id?

To retire keys, you should just move the old key to the decryption keys map, and advertise the new encryption key ID.

The old key will already be in the decryption keys map because we had to use it previously to decrypt the objects right? So we actually just need to update the encryption key and encryption key id?

the latest key will be used to re-encrypt the column.

This gives me the impression that GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPTION_KEY is an array of keys but it is actually just 1 key right?

@hf
Copy link
Contributor Author

hf commented Jun 6, 2024

... such that the symmetric key is used together with the object ID (for passwords - the user ID, for TOTP secrets - the factor ID). This way there's a separate AES-GCM key per object, ...

just to clarify my understanding, the encryption key of an object is derived from the symmetric key + the object id?

Yes, the ..._ENCRYPTION_KEY is really a key-derivation-key. The encryption key for a specific row is derived from this KDK + the row's UUID.

To retire keys, you should just move the old key to the decryption keys map, and advertise the new encryption key ID.

The old key will already be in the decryption keys map because we had to use it previously to decrypt the objects right? So we actually just need to update the encryption key and encryption key id?

Yeah, I'll change the wording I see how it's confusing.

the latest key will be used to re-encrypt the column.

This gives me the impression that GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPTION_KEY is an array of keys but it is actually just 1 key right?

Yes, it's just one key.

@hf hf force-pushed the hf/encrypt-sensitive-columns branch from 243cceb to 92afa8c Compare June 7, 2024 09:41
@hf hf requested a review from kangmingtay June 7, 2024 09:42
@hf hf dismissed kangmingtay’s stale review June 8, 2024 08:43

Addressed the comments.

@hf hf force-pushed the hf/encrypt-sensitive-columns branch from 92afa8c to 3362518 Compare June 8, 2024 08:45
@hf hf merged commit e4a4758 into master Jun 12, 2024
2 checks passed
@hf hf deleted the hf/encrypt-sensitive-columns branch June 12, 2024 11:33
J0 pushed a commit that referenced this pull request Jun 14, 2024
🤖 I have created a release *beep* *boop*
---


##
[2.154.0](v2.153.0...v2.154.0)
(2024-06-12)


### Features

* add max length check for email
([#1508](#1508))
([f9c13c0](f9c13c0))
* add support for Slack OAuth V2
([#1591](#1591))
([bb99251](bb99251))
* encrypt sensitive columns
([#1593](#1593))
([e4a4758](e4a4758))
* upgrade otel to v1.26
([#1585](#1585))
([cdd13ad](cdd13ad))
* use largest avatar from spotify instead
([#1210](#1210))
([4f9994b](4f9994b)),
closes [#1209](#1209)


### Bug Fixes

* define search path in auth functions
([#1616](#1616))
([357bda2](357bda2))
* enable rls & update grants for auth tables
([#1617](#1617))
([28967aa](28967aa))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
uxodb pushed a commit to uxodb/auth that referenced this pull request Nov 13, 2024
Adds support for encrypting sensitive columns like the MFA secret and
password hash.

The goal with this encryption mechanism is to add yet another layer of
security on top of the database permissions provided by Postgres. In the
event that the database leaks or is accessed by malicious users or the
database permissions are incorrectly defined, the encryption key would
also be required to inspect this sensitive data.

Encryption is done using AES-GCM-256. Strings that are encrypted are
converted into a JSON string with this shape:

```json
{
  "key_id": "key identifier used for encryption",
  "alg": "aes-gcm-hkdf",
  "nonce": "GCM 12 byte nonce",
  "data": "Base64 standard encoding of the ciphertext"
}
```

As AES-GCM must not be used more than 2^32 times with a single symmetric
key, and this is not that much -- imagine serving 100m users -- then
this means that all users can only add 42 passwords or MFA verification
factors before running into this hard limit. To fix this, a symmetric
key is derived using
[HKDF](https://datatracker.ietf.org/doc/html/rfc5869) with SHA256 such
that the symmetric key is used together with the object ID (for
passwords - the user ID, for TOTP secrets - the factor ID). This way
there's a separate AES-GCM key per object, and additionally gives the
security property that a malicious actor with write permissions to the
database cannot swap passwords / TOTP secrets from Malice's account to
Target's account. They would need to also change the UUIDs of these
objects, which is likely to be hard.

To turn on encryption the following configs need to be added:

`GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPT=true` -- that turns on encryption
for new objects.
`GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPTION_KEY_ID=key-id` -- ID of the
encryption key, allowing to rotate keys easily.
`GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPTION_KEY=key` -- Base64 URL
encoding of a 256 bit AES key

Once encryption has been turned on, in order to have the rows be
readable **for ever** this config must be provided with all past and
future keys:

`GOTRUE_SECURITY_DB_ENCRYPTION_DECRYPTION_KEYS=key-id:key` -- A map of
key IDs and Base64 URL key encodings of the keys.

To retire keys, you should just move the old key to the decryption keys
map, and advertise the new encryption key ID. On each successful sign in
with password, or any MFA verification attempt, the latest key will be
used to re-encrypt the column. This also applies for the
non-encrypted-to-encrypted case.
uxodb pushed a commit to uxodb/auth that referenced this pull request Nov 13, 2024
🤖 I have created a release *beep* *boop*
---


##
[2.154.0](supabase/auth@v2.153.0...v2.154.0)
(2024-06-12)


### Features

* add max length check for email
([supabase#1508](supabase#1508))
([f9c13c0](supabase@f9c13c0))
* add support for Slack OAuth V2
([supabase#1591](supabase#1591))
([bb99251](supabase@bb99251))
* encrypt sensitive columns
([supabase#1593](supabase#1593))
([e4a4758](supabase@e4a4758))
* upgrade otel to v1.26
([supabase#1585](supabase#1585))
([cdd13ad](supabase@cdd13ad))
* use largest avatar from spotify instead
([supabase#1210](supabase#1210))
([4f9994b](supabase@4f9994b)),
closes [supabase#1209](supabase#1209)


### Bug Fixes

* define search path in auth functions
([supabase#1616](supabase#1616))
([357bda2](supabase@357bda2))
* enable rls & update grants for auth tables
([supabase#1617](supabase#1617))
([28967aa](supabase@28967aa))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
LashaJini pushed a commit to LashaJini/auth that referenced this pull request Nov 13, 2024
Adds support for encrypting sensitive columns like the MFA secret and
password hash.

The goal with this encryption mechanism is to add yet another layer of
security on top of the database permissions provided by Postgres. In the
event that the database leaks or is accessed by malicious users or the
database permissions are incorrectly defined, the encryption key would
also be required to inspect this sensitive data.

Encryption is done using AES-GCM-256. Strings that are encrypted are
converted into a JSON string with this shape:

```json
{
  "key_id": "key identifier used for encryption",
  "alg": "aes-gcm-hkdf",
  "nonce": "GCM 12 byte nonce",
  "data": "Base64 standard encoding of the ciphertext"
}
```

As AES-GCM must not be used more than 2^32 times with a single symmetric
key, and this is not that much -- imagine serving 100m users -- then
this means that all users can only add 42 passwords or MFA verification
factors before running into this hard limit. To fix this, a symmetric
key is derived using
[HKDF](https://datatracker.ietf.org/doc/html/rfc5869) with SHA256 such
that the symmetric key is used together with the object ID (for
passwords - the user ID, for TOTP secrets - the factor ID). This way
there's a separate AES-GCM key per object, and additionally gives the
security property that a malicious actor with write permissions to the
database cannot swap passwords / TOTP secrets from Malice's account to
Target's account. They would need to also change the UUIDs of these
objects, which is likely to be hard.

To turn on encryption the following configs need to be added:

`GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPT=true` -- that turns on encryption
for new objects.
`GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPTION_KEY_ID=key-id` -- ID of the
encryption key, allowing to rotate keys easily.
`GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPTION_KEY=key` -- Base64 URL
encoding of a 256 bit AES key

Once encryption has been turned on, in order to have the rows be
readable **for ever** this config must be provided with all past and
future keys:

`GOTRUE_SECURITY_DB_ENCRYPTION_DECRYPTION_KEYS=key-id:key` -- A map of
key IDs and Base64 URL key encodings of the keys.

To retire keys, you should just move the old key to the decryption keys
map, and advertise the new encryption key ID. On each successful sign in
with password, or any MFA verification attempt, the latest key will be
used to re-encrypt the column. This also applies for the
non-encrypted-to-encrypted case.
LashaJini pushed a commit to LashaJini/auth that referenced this pull request Nov 13, 2024
🤖 I have created a release *beep* *boop*
---


##
[2.154.0](supabase/auth@v2.153.0...v2.154.0)
(2024-06-12)


### Features

* add max length check for email
([supabase#1508](supabase#1508))
([f9c13c0](supabase@f9c13c0))
* add support for Slack OAuth V2
([supabase#1591](supabase#1591))
([bb99251](supabase@bb99251))
* encrypt sensitive columns
([supabase#1593](supabase#1593))
([e4a4758](supabase@e4a4758))
* upgrade otel to v1.26
([supabase#1585](supabase#1585))
([cdd13ad](supabase@cdd13ad))
* use largest avatar from spotify instead
([supabase#1210](supabase#1210))
([4f9994b](supabase@4f9994b)),
closes [supabase#1209](supabase#1209)


### Bug Fixes

* define search path in auth functions
([supabase#1616](supabase#1616))
([357bda2](supabase@357bda2))
* enable rls & update grants for auth tables
([supabase#1617](supabase#1617))
([28967aa](supabase@28967aa))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
LashaJini pushed a commit to LashaJini/auth that referenced this pull request Nov 15, 2024
Adds support for encrypting sensitive columns like the MFA secret and
password hash.

The goal with this encryption mechanism is to add yet another layer of
security on top of the database permissions provided by Postgres. In the
event that the database leaks or is accessed by malicious users or the
database permissions are incorrectly defined, the encryption key would
also be required to inspect this sensitive data.

Encryption is done using AES-GCM-256. Strings that are encrypted are
converted into a JSON string with this shape:

```json
{
  "key_id": "key identifier used for encryption",
  "alg": "aes-gcm-hkdf",
  "nonce": "GCM 12 byte nonce",
  "data": "Base64 standard encoding of the ciphertext"
}
```

As AES-GCM must not be used more than 2^32 times with a single symmetric
key, and this is not that much -- imagine serving 100m users -- then
this means that all users can only add 42 passwords or MFA verification
factors before running into this hard limit. To fix this, a symmetric
key is derived using
[HKDF](https://datatracker.ietf.org/doc/html/rfc5869) with SHA256 such
that the symmetric key is used together with the object ID (for
passwords - the user ID, for TOTP secrets - the factor ID). This way
there's a separate AES-GCM key per object, and additionally gives the
security property that a malicious actor with write permissions to the
database cannot swap passwords / TOTP secrets from Malice's account to
Target's account. They would need to also change the UUIDs of these
objects, which is likely to be hard.

To turn on encryption the following configs need to be added:

`GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPT=true` -- that turns on encryption
for new objects.
`GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPTION_KEY_ID=key-id` -- ID of the
encryption key, allowing to rotate keys easily.
`GOTRUE_SECURITY_DB_ENCRYPTION_ENCRYPTION_KEY=key` -- Base64 URL
encoding of a 256 bit AES key

Once encryption has been turned on, in order to have the rows be
readable **for ever** this config must be provided with all past and
future keys:

`GOTRUE_SECURITY_DB_ENCRYPTION_DECRYPTION_KEYS=key-id:key` -- A map of
key IDs and Base64 URL key encodings of the keys.

To retire keys, you should just move the old key to the decryption keys
map, and advertise the new encryption key ID. On each successful sign in
with password, or any MFA verification attempt, the latest key will be
used to re-encrypt the column. This also applies for the
non-encrypted-to-encrypted case.
LashaJini pushed a commit to LashaJini/auth that referenced this pull request Nov 15, 2024
🤖 I have created a release *beep* *boop*
---


##
[2.154.0](supabase/auth@v2.153.0...v2.154.0)
(2024-06-12)


### Features

* add max length check for email
([supabase#1508](supabase#1508))
([f9c13c0](supabase@f9c13c0))
* add support for Slack OAuth V2
([supabase#1591](supabase#1591))
([bb99251](supabase@bb99251))
* encrypt sensitive columns
([supabase#1593](supabase#1593))
([e4a4758](supabase@e4a4758))
* upgrade otel to v1.26
([supabase#1585](supabase#1585))
([cdd13ad](supabase@cdd13ad))
* use largest avatar from spotify instead
([supabase#1210](supabase#1210))
([4f9994b](supabase@4f9994b)),
closes [supabase#1209](supabase#1209)


### Bug Fixes

* define search path in auth functions
([supabase#1616](supabase#1616))
([357bda2](supabase@357bda2))
* enable rls & update grants for auth tables
([supabase#1617](supabase#1617))
([28967aa](supabase@28967aa))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants