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

BITAU-98 Add EncryptionUtils helper functions to the bridge SDK #3888

Merged
merged 16 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import kotlinx.serialization.Serializable
* @param totpUri A TOTP code URI to be added to the Bitwarden app.
*/
@Serializable
data class AddTotpLoginItemDataJson(
internal data class AddTotpLoginItemDataJson(
Copy link
Collaborator Author

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 ๐Ÿ˜Ž

@SerialName("totpUri")
val totpUri: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import java.time.Instant
* @param accounts The list of shared accounts.
*/
@Serializable
data class SharedAccountDataJson(
internal data class SharedAccountDataJson(
@SerialName("accounts")
val accounts: List<AccountJson>,
) {
Expand Down
207 changes: 207 additions & 0 deletions bridge/src/main/java/com/bitwarden/bridge/util/EncryptionUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package com.bitwarden.bridge.util
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah yep, who knows why my brain used that order ๐Ÿ˜†

4fd92d7

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm a bit torn...do we need to refer to Bitwarden app and Bitwarden Authenticator app or would referring to the IBridgeServiceCallback be enough? This is probably especially relevant to think through because once we add the "coroutines wrapper layer" it will actually be the SDK code that calls these and not the Authenticator app directly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 syncAccounts, we need the entire data layer of the main Bitwarden app, so it makes sense this logic would be in the BItwarden app not the SDK.

Maybe I'm missing something though.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 decrypt call internal.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks, Brian!

147c632

*
* @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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Still a reference to Bitwarden Authenticator app here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

๐Ÿ‘ missed this encrypt, good call:

b33e6d1

* 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,
)
Loading