-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
Add field-level encryption to API Keys #5046
Add field-level encryption to API Keys #5046
Conversation
Things I was not sure about, these and could not validate with Richard because he is on vacation 🎉 Are the notes regarding authService.apiKeyAuthenticate and authService.getUserData redundant? because i could not find those functions. Why generate a hash from the unencrypted apiKey to act as the cache key in the @CachedEntity builder for authService.getUserData Remove the apiKey from the JWT payload in authService.getApiSignedToken |
export interface IApiKeyDto { | ||
key: string; | ||
_userId: string; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can not use the DAL Entity because the interfaces are different
}) | ||
apiKeys: IApiKey[]; | ||
@ApiPropertyOptional() | ||
apiKeys?: IApiKeyDto[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional because get-environment.usecase does not return the api keys, i wonder if we need to return them at all under environment-response.dto. I would suggest removing it at altogether and providing the keys only under GET /environments/api-keys
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GET /environments
does use that interface and returns the apiKeys
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, that is why I left it as optional because GET /environments/me
do not return it.
|
||
export const NOVU_SUB_MASK = 'nvsk.'; | ||
|
||
export type EncryptedSecret = `${typeof NOVU_SUB_MASK}${string}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kudos to Richard for helping me to be more familiar with Template Literal Types.
This small type highlighted some potential issues we could have in compilation time while developing this feature, and will probably prevent future issues.
export function encryptApiKey(apiKey: string): EncryptedSecret { | ||
if (novuEncrypted(apiKey)) { | ||
return apiKey; | ||
} | ||
|
||
return encryptSecret(apiKey); | ||
} | ||
|
||
export function decryptApiKey(apiKey: string): string { | ||
if (novuEncrypted(apiKey)) { | ||
return decryptSecret(apiKey); | ||
} | ||
|
||
return apiKey; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
those are middleware for the encryption-decryption that will help us make sure that we won't encrypt/decrypt twice for no reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't we have these checks actually in the encryptSecret, decryptSecret
functions? because I see that they are exposed and used in other places.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is a good point but i did not want to merge the base functions encryptSecret, decryptSecret
that at the moment responsible for adding and removing the prefix. as it could affect the integration credentials as well.
}) | ||
apiKeys: IApiKey[]; | ||
@ApiPropertyOptional() | ||
apiKeys?: IApiKeyDto[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GET /environments
does use that interface and returns the apiKeys
.
...api/src/app/environments/usecases/generate-unique-api-key/generate-unique-api-key.usecase.ts
Outdated
Show resolved
Hide resolved
export function encryptApiKey(apiKey: string): EncryptedSecret { | ||
if (novuEncrypted(apiKey)) { | ||
return apiKey; | ||
} | ||
|
||
return encryptSecret(apiKey); | ||
} | ||
|
||
export function decryptApiKey(apiKey: string): string { | ||
if (novuEncrypted(apiKey)) { | ||
return decryptSecret(apiKey); | ||
} | ||
|
||
return apiKey; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't we have these checks actually in the encryptSecret, decryptSecret
functions? because I see that they are exposed and used in other places.
packages/application-generic/src/services/cache/key-builders/entities.ts
Outdated
Show resolved
Hide resolved
@@ -1,9 +1,10 @@ | |||
import { nanoid } from 'nanoid'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you please also cover this functionality with e2e tests for all use-cases touched?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was not sure what would be the best way to test it as the API key is i the code of our testing environment. i wanted to revisit the tests tomorrow once again.
small note, our test env organically simulates backward compatibility because we do not store an encrypted api key by the @novu/test service on sesstion init. i could not add the encryption because we do not propose application genetic in test lib.
meaning that we created not encrypted api keys in the DB on session init and the application (e2e tests) works as expected (backward compatibility).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, we should have in the EnvironmentService
the same encryption ideally by reusing the utils encryptApiKey, decryptApiKey
.
Theoretically, both packages are built to CJS, so we might include @novu/application-generic
in @novu/testing
, but not suggesting that 😅 Another way would be to have another lib that is pure for reusable utils and without app-related logic, which is headache 🤕 so the only easy thing would be to duplicate the code?
…el-encryption-to-api-keys # Conflicts: # packages/node/src/lib/changes/changes.spec.ts
apps/api/migrations/encrypt-api-keys/encrypt-api-keys-migration.spec.ts
Outdated
Show resolved
Hide resolved
apps/api/src/app/environments/usecases/create-environment/create-environment.e2e.ts
Outdated
Show resolved
Hide resolved
apps/api/src/app/environments/usecases/create-environment/create-environment.e2e.ts
Outdated
Show resolved
Hide resolved
apps/api/src/app/environments/usecases/create-environment/create-environment.usecase.ts
Show resolved
Hide resolved
apps/api/src/app/environments/usecases/create-environment/create-environment.usecase.ts
Outdated
Show resolved
Hide resolved
packages/application-generic/src/encryption/encrypt-provider.ts
Outdated
Show resolved
Hide resolved
packages/application-generic/src/encryption/encrypt-provider.ts
Outdated
Show resolved
Hide resolved
Yes, they are redundant - the API Key auth flow was recently simplified and these functions were removed.
The approach you have taken is much better. Nice work.
Yes, they were removed in the API Key auth flow simplification mentioned above. |
✅ Deploy Preview for dev-web-novu ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
✅ Deploy Preview for dev-web-novu ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
…on-to-api-keys' into nv-3172-add-field-level-encryption-to-api-keys
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work @djabarovgeorge . Great security posture improvement 🦾
What change does this PR introduce?
Remove the apiKey from return of authService.getUserData
Remove the apiKey from JWT payload in authService.getApiSignedToken
Why was this change needed?
The API key is not currently encrypted at rest, this poses a vulnerability if the raw contents of the database or the cache are available to a bad actor who may use the api key with bad intentions. We need to obfuscate the value of the api key at rest to prevent direct use of the key in the event of a database breach
We want to be able to show the API key to users from the dashboard to simplify and ease product adoption, therefore hashing of the API key in the database is not an option. We must therefore implement encryption prior to writes and decryption during reads to support the necessary obfuscation.
Definition of Done
https://docs.novu.co/additional-resources/data-migrations has the new migration listed