PM-40154: feat: Create a new Shared Preferences that encrypts data via the AndroidKeystore#7158
PM-40154: feat: Create a new Shared Preferences that encrypts data via the AndroidKeystore#7158david-livefront wants to merge 2 commits into
Conversation
d7c24bb to
aa9e67c
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #7158 +/- ##
===========================================
- Coverage 85.70% 27.11% -58.59%
===========================================
Files 1047 1023 -24
Lines 67560 66041 -1519
Branches 9789 9616 -173
===========================================
- Hits 57900 17907 -39993
- Misses 6147 47004 +40857
+ Partials 3513 1130 -2383
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
18d2899 to
0405c99
Compare
0405c99 to
ffcb25d
Compare
ffcb25d to
bafe9cf
Compare
quexten
left a comment
There was a problem hiding this comment.
If we had a tech breakdown beforehand, I'd recommend implementing this in the SDK based on our modern encryption primitives instead. Given the stage, I'll comment the minimum to make this reasonably secure. I recommend dropping the separate MAC key, and switching from CBC to GCM. Currently, the API is very easy to misuse, and you generally do not want to expose separate authentication and encryption (without authentication) APIs.
| alias: String, | ||
| bytes: ByteArray, | ||
| ): Result<ByteArray> = keystoreManager | ||
| .getOrCreateKey(alias = KeyAlias.Aes(name = alias)) |
There was a problem hiding this comment.
In general, it is dangerous cryptographic design to offer separate encrypt and sign (actually, this should be authenticate, sign usually is used in asymmetric contexts) primitives. You should only ever offer authenticated encryption that does both.
| key: String, | ||
| value: ByteArray, | ||
| ): SharedPreferences.Editor { | ||
| val encryptedKey = encryptionManager |
There was a problem hiding this comment.
Oh.. are you deliberately not encrypting the keys, but just want to auth them, but the values should be encrypted and authenticated?
There was a problem hiding this comment.
@david-livefront Could you clarify what the intent of protecting the "Key" entries here is? The values should be encrypted and authenticated because they contain secret values, and tampering with them may affect the security of the app, so much is clear. Thus we use authenticated encryption for them. But what about the keys? Is there a perceived threat here?
Depending on what the answer to this is I would recommend either:
- Use AES-GCM for values as authenticated encryption, do not encrypt or authenticate the key entries whatsoever
(if you decide that the keys are worth protecting integrity of)
2. Use AES-GCM for the values, put the key into the authenticated additional data of AES-GCM, when encrypting the value
- This binds together the value + key and makes it not possible to swap between different value + key pairs.
However, so far I believe 1. to be the correct path forward unless I'm missing context.
There was a problem hiding this comment.
@quexten We are encrypting the keys for two main reasons.
The previous implementation encrypted the keys and I was attempting to match behavior . They used AES256_SIV for this because it is required for the keys to be deterministically encrypted (otherwise we can never retrieve the data). AES256_SIV is not a compliant option, so I had to come up with something else.
The only threat model I can think of if that if someone were to gain access to the file and attempted to brute force the data. The keys provide information on what each value is, and therefore, which values to brute force first. If this is not a concern, then I can easily drop addition logic and simplify everything.
CC @vvolkgang
There was a problem hiding this comment.
Right.
The only threat model I can think of if that if someone were to gain access to the file and attempted to brute force the data. The keys provide information on what each value is, and therefore, which values to brute force first. If this is not a concern, then I can easily drop addition logic and simplify everything.
- Since we are encrypting with a high-entropy key stored in the key-chain, we can assume brute-force is not possible for an aes256-gcm key.
- With the current design, you would still know what key is which, since we are authenticating but not encrypting them
-> I would recommend dropping the mac on the key and leaving it plaintext, and encrypting the values only. Ideally, we would context bind them so they cannot be swapped between keys, but doing so here would further complicate this PR / it is not blocking from my perspective, and would increase the scope, so I would recommend not doing so here.
There was a problem hiding this comment.
Thanks @quexten I have updated the PR.
- The
signfunction is completely gone - We leave the keys as plaintext
- We now use
AES/GCM/NoPaddingfor encrypting the values
e7abca5 to
4eb424e
Compare
| alias: String, | ||
| bytes: ByteArray, | ||
| ): Result<ByteArray> = keystoreManager | ||
| .getOrCreateKey(alias = alias) |
There was a problem hiding this comment.
I don't understand the kotlin syntax too well here. Is the IV generated by the javax.crypto.cipher api?
There was a problem hiding this comment.
I don't have a kotlin dev environment set up currently. @david-livefront could you confirm that the java crypto API here samples a random fresh IV on every invocation? If so I'm happy to approve the PR.
(IV / in gcm's case "nonce" re-use leads to a catastrophic cryptographic break, so we want to ensure nonces are correctly randomly sampled).
There was a problem hiding this comment.
That is correct, by default a new random IV is generated everytime.
| ) | ||
| } | ||
|
|
||
| is InitCipher.Encrypt -> init(Cipher.ENCRYPT_MODE, this@generateCipher) |
There was a problem hiding this comment.
@quexten Here where we initialize the cipher to encrypt data, it is creating a new IV since we are not providing one.
Above, on line 73, we provide the stored IV for decryption.
4eb424e to
d90db9e
Compare
d90db9e to
1249224
Compare
🎟️ Tracking
PM-40154
📔 Objective
This PR creates a new
KeystoreEncryptedSharedPreferencesthat utilizes theAndroidKeystoreto encrypt the data stored.Notable changes:
EncryptedSharedPreferenceswhich is deprecated and is not FIPS compliant.Strings(this is the only type we need at the moment).