-
Notifications
You must be signed in to change notification settings - Fork 844
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
BITAU-98 Add EncryptionUtils helper functions to the bridge SDK #3888
Changes from 15 commits
5f14e2b
e377d57
048af58
85e3e70
60ad525
8ca7123
aaed2fd
d3191a8
af046f9
d503887
5643141
147c632
6cf52ec
f7c34af
f14779e
b33e6d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
package com.bitwarden.bridge.util | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. S/O to @brian-livefront for originally writing all this code in his POC. I'm standing on the shoulders of a giant! |
||
|
||
import android.security.keystore.KeyProperties | ||
import com.bitwarden.bridge.IBridgeService | ||
import com.bitwarden.bridge.model.AddTotpLoginItemData | ||
import com.bitwarden.bridge.model.AddTotpLoginItemDataJson | ||
import com.bitwarden.bridge.model.EncryptedAddTotpLoginItemData | ||
import com.bitwarden.bridge.model.EncryptedSharedAccountData | ||
import com.bitwarden.bridge.model.SharedAccountData | ||
import com.bitwarden.bridge.model.SharedAccountDataJson | ||
import com.bitwarden.bridge.model.SymmetricEncryptionKeyData | ||
import com.bitwarden.bridge.model.toByteArrayContainer | ||
import kotlinx.serialization.encodeToString | ||
import java.security.MessageDigest | ||
import javax.crypto.Cipher | ||
import javax.crypto.KeyGenerator | ||
import javax.crypto.SecretKey | ||
import javax.crypto.spec.IvParameterSpec | ||
import javax.crypto.spec.SecretKeySpec | ||
|
||
/** | ||
* Generate a symmetric [SecretKey] that will used for encrypting IPC traffic. | ||
* | ||
* This is intended to be used for implementing [IBridgeService.getSymmetricEncryptionKeyData]. | ||
*/ | ||
fun generateSecretKey(): Result<SecretKey> = runCatching { | ||
val keygen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES) | ||
keygen.init(256) | ||
keygen.generateKey() | ||
} | ||
|
||
/** | ||
* Generate a fingerprint for the given symmetric key. | ||
* | ||
* This is intended to be used for implementing | ||
* [IBridgeService.checkSymmetricEncryptionKeyFingerprint], which allows callers of the service | ||
* to verify that they have the correct symmetric key without actually having to send the key. | ||
*/ | ||
fun SymmetricEncryptionKeyData.toFingerprint(): Result<ByteArray> = runCatching { | ||
val messageDigest = MessageDigest.getInstance(KeyProperties.DIGEST_SHA256) | ||
messageDigest.reset() | ||
messageDigest.update(this.symmetricEncryptionKey.byteArray) | ||
messageDigest.digest() | ||
} | ||
|
||
/** | ||
* Encrypt [SharedAccountData]. | ||
* | ||
* This is intended to be used by the main Bitwarden app during a [IBridgeService.syncAccounts] call. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would think these lines might need to come before all the @param in these There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yep, who knows why my brain used that order ๐ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit torn...do we need to refer to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like leaving this breadcrumb to try and help out any future reader/editor with more context. Re: the coroutines wrapping layer... I was imagining that layer only helping out with the Authenticator side of things (connecting to the service, registering callbacks). Here's the coroutines wrapper ticket, nothing there about helping out with the PMA side of things. To me, this makes sense, because in order to perform Maybe I'm missing something though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We spoke offline. We'll leave references to "the Bitwarden app" but remove those to "the Bitwarden Authenticator app", mainly since we're thinking we can make the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, Brian! |
||
* | ||
* @param symmetricEncryptionKeyData Symmetric key used for encryption. | ||
*/ | ||
fun SharedAccountData.encrypt( | ||
symmetricEncryptionKeyData: SymmetricEncryptionKeyData, | ||
): Result<EncryptedSharedAccountData> = runCatching { | ||
val encodedKey = symmetricEncryptionKeyData.symmetricEncryptionKey.byteArray | ||
val key = encodedKey.toSecretKey() | ||
val cipher = generateCipher() | ||
cipher.init(Cipher.ENCRYPT_MODE, key) | ||
val jsonString = JSON.encodeToString(this.toJsonModel()) | ||
val encryptedJsonString = cipher.doFinal(jsonString.encodeToByteArray()).toByteArrayContainer() | ||
|
||
EncryptedSharedAccountData( | ||
initializationVector = cipher.iv.toByteArrayContainer(), | ||
encryptedAccountsJson = encryptedJsonString, | ||
) | ||
} | ||
|
||
/** | ||
* Decrypt [EncryptedSharedAccountData]. | ||
* | ||
* @param symmetricEncryptionKeyData Symmetric key used for decryption. | ||
*/ | ||
internal fun EncryptedSharedAccountData.decrypt( | ||
symmetricEncryptionKeyData: SymmetricEncryptionKeyData, | ||
): Result<SharedAccountData> = runCatching { | ||
val encodedKey = symmetricEncryptionKeyData | ||
.symmetricEncryptionKey | ||
.byteArray | ||
val key = encodedKey.toSecretKey() | ||
|
||
val iv = IvParameterSpec(this.initializationVector.byteArray) | ||
val cipher = generateCipher() | ||
cipher.init(Cipher.DECRYPT_MODE, key, iv) | ||
val decryptedModel = JSON.decodeFromString<SharedAccountDataJson>( | ||
cipher.doFinal(this.encryptedAccountsJson.byteArray).decodeToString() | ||
) | ||
decryptedModel.toDomainModel() | ||
} | ||
|
||
/** | ||
* Encrypt [AddTotpLoginItemData]. | ||
* | ||
* This is intended to be used by the Bitwarden Authenticator app before requesting a new TOTP | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still a reference to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ missed this |
||
* item be added to the main Bitwarden app. | ||
* | ||
* @param symmetricEncryptionKeyData Symmetric key used for encryption. | ||
*/ | ||
fun AddTotpLoginItemData.encrypt( | ||
symmetricEncryptionKeyData: SymmetricEncryptionKeyData, | ||
): Result<EncryptedAddTotpLoginItemData> = runCatching { | ||
val encodedKey = symmetricEncryptionKeyData.symmetricEncryptionKey.byteArray | ||
val key = encodedKey.toSecretKey() | ||
val cipher = generateCipher() | ||
cipher.init(Cipher.ENCRYPT_MODE, key) | ||
val encryptedJsonString = | ||
cipher.doFinal(JSON.encodeToString(this.toJsonModel()).encodeToByteArray()) | ||
|
||
EncryptedAddTotpLoginItemData( | ||
initializationVector = cipher.iv.toByteArrayContainer(), | ||
encryptedTotpUriJson = encryptedJsonString.toByteArrayContainer(), | ||
) | ||
} | ||
|
||
/** | ||
* Decrypt [EncryptedSharedAccountData]. | ||
* | ||
* @param symmetricEncryptionKeyData Symmetric key used for decryption. | ||
*/ | ||
internal fun EncryptedAddTotpLoginItemData.decrypt( | ||
symmetricEncryptionKeyData: SymmetricEncryptionKeyData, | ||
): Result<AddTotpLoginItemData> = runCatching { | ||
val encodedKey = symmetricEncryptionKeyData | ||
.symmetricEncryptionKey | ||
.byteArray | ||
val key = encodedKey.toSecretKey() | ||
|
||
val iv = IvParameterSpec(this.initializationVector.byteArray) | ||
val cipher = generateCipher() | ||
cipher.init(Cipher.DECRYPT_MODE, key, iv) | ||
val decryptedModel = JSON.decodeFromString<AddTotpLoginItemDataJson>( | ||
cipher.doFinal(this.encryptedTotpUriJson.byteArray).decodeToString() | ||
) | ||
decryptedModel.toDomainModel() | ||
} | ||
|
||
/** | ||
* Helper function for converting a [ByteArray] to a type safe [SymmetricEncryptionKeyData]. | ||
* | ||
* This is useful since callers may be storing encryption key data as a [ByteArray] under the hood | ||
* and must convert to a [SymmetricEncryptionKeyData] to use the SDK's encryption APIs. | ||
*/ | ||
fun ByteArray.toSymmetricEncryptionKeyData(): SymmetricEncryptionKeyData = | ||
SymmetricEncryptionKeyData(toByteArrayContainer()) | ||
|
||
/** | ||
* Convert the given [ByteArray] to a [SecretKey]. | ||
*/ | ||
private fun ByteArray.toSecretKey(): SecretKey = | ||
SecretKeySpec(this, 0, this.size, KeyProperties.KEY_ALGORITHM_AES) | ||
|
||
/** | ||
* Helper function for generating a [Cipher] that can be used for encrypting/decrypting using | ||
* [SymmetricEncryptionKeyData]. | ||
*/ | ||
private fun generateCipher(): Cipher = | ||
Cipher.getInstance( | ||
KeyProperties.KEY_ALGORITHM_AES + "/" + | ||
KeyProperties.BLOCK_MODE_CBC + "/" + | ||
"PKCS5PADDING" | ||
) | ||
|
||
/** | ||
* Helper function for converting [SharedAccountData] to a serializable [SharedAccountDataJson]. | ||
*/ | ||
private fun SharedAccountData.toJsonModel() = SharedAccountDataJson( | ||
accounts = this.accounts.map { account -> | ||
SharedAccountDataJson.AccountJson( | ||
userId = account.userId, | ||
name = account.name, | ||
environmentLabel = account.environmentLabel, | ||
email = account.email, | ||
totpUris = account.totpUris, | ||
lastSyncTime = account.lastSyncTime | ||
) | ||
} | ||
) | ||
|
||
/** | ||
* Helper function for converting [SharedAccountDataJson] to a [SharedAccountData]. | ||
*/ | ||
private fun SharedAccountDataJson.toDomainModel() = SharedAccountData( | ||
accounts = this.accounts.map { account -> | ||
SharedAccountData.Account( | ||
userId = account.userId, | ||
name = account.name, | ||
environmentLabel = account.environmentLabel, | ||
email = account.email, | ||
totpUris = account.totpUris, | ||
lastSyncTime = account.lastSyncTime | ||
) | ||
} | ||
) | ||
|
||
/** | ||
* Helper function for converting [AddTotpLoginItemDataJson] to a [AddTotpLoginItemData]. | ||
*/ | ||
private fun AddTotpLoginItemDataJson.toDomainModel() = AddTotpLoginItemData( | ||
totpUri = totpUri, | ||
) | ||
|
||
/** | ||
* Helper function for converting [AddTotpLoginItemData] to a serializable [AddTotpLoginItemDataJson]. | ||
*/ | ||
private fun AddTotpLoginItemData.toJsonModel() = AddTotpLoginItemDataJson( | ||
totpUri = totpUri, | ||
) |
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 realized when working on this PR that these are just an implementation detail of the encryption.
I love being able to use
internal
๐