mirror of
https://github.com/bitwarden/android.git
synced 2026-04-28 11:58:40 -05:00
BITAU-172 Rename Authenticator Bridge SDK (#3959)
This commit is contained in:
1
authenticatorbridge/.gitignore
vendored
Normal file
1
authenticatorbridge/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
8
authenticatorbridge/CHANGELOG.md
Normal file
8
authenticatorbridge/CHANGELOG.md
Normal file
@@ -0,0 +1,8 @@
|
||||
v0.1.0 (pending)
|
||||
--------
|
||||
|
||||
### API Changes
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
### Bug Fixes
|
||||
36
authenticatorbridge/CHANGELOG_FORMAT.MD
Normal file
36
authenticatorbridge/CHANGELOG_FORMAT.MD
Normal file
@@ -0,0 +1,36 @@
|
||||
# CHANGELOG Format
|
||||
|
||||
## Contents
|
||||
|
||||
* [Template](#template)
|
||||
* [Sections](#sections)
|
||||
|
||||
## Template
|
||||
|
||||
Each new version of the SDK should be accompanied by a new `CHANGELOG` entry with the following template:
|
||||
|
||||
```
|
||||
vX.Y.0 (pending)
|
||||
--------
|
||||
|
||||
### API Changes
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
```
|
||||
|
||||
The `(pending)` label, as well as any unused sections, should be removed prior to creating a release.
|
||||
|
||||
## Sections
|
||||
|
||||
One or more `CHANGELOG` entries should be added for each PR that makes a change that either adds / updates the exposed API of the SDK or changes code internally in a way that is detectable as a behavior change by external consumers. These entries should fall into one of the following sections:
|
||||
|
||||
- **API Changes**: Any update to existing code that affects the exposed API or behavior of the SDK in a backwards-compatible way should be included in this section.
|
||||
|
||||
- **Breaking Changes**: Any code introduced in a **backwards-incompatible** manner that could in principle affect compilation of existing code should be included in this section. Examples of this include changing return types, changing function / constructor parameter lists, and changing function / class names. Each entry in this list should include detailed instructions where necessary for how a caller should address any related compilation issues.
|
||||
|
||||
- **Bug Fixes**: Any update that is performed in order to fix a known bug should be included in this section.
|
||||
|
||||
If an PR exhibits multiple kinds of changes then there should be multiple corresponding entries.
|
||||
79
authenticatorbridge/README.md
Normal file
79
authenticatorbridge/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Authenticator Bridge SDK
|
||||
|
||||
## Contents
|
||||
|
||||
- [Compatibility](#compatibility)
|
||||
- [Building](#building)
|
||||
- [Versioning](#versioning)
|
||||
- [Dependencies](#dependencies)
|
||||
|
||||
## Other Documents
|
||||
|
||||
- [Changelog](CHANGELOG.md)
|
||||
- [Changelog Format Guide](CHANGELOG_FORMAT.MD)
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **Minimum SDK**: 28
|
||||
- **Target SDK**: 34
|
||||
|
||||
## Building
|
||||
|
||||
To build an AAR for inclusion in consumer applications, run:
|
||||
|
||||
```sh
|
||||
$ ./gradlew authenticatorbridge:assembleRelease
|
||||
```
|
||||
|
||||
## Versioning
|
||||
This repository conforms to the following versioning convention:
|
||||
|
||||
**v[MAJOR].[MINOR].[PATCH]**
|
||||
|
||||
```
|
||||
where [RELEASE] is incremented to represent major milestones that indicate a significant change in the library.
|
||||
|
||||
[MINOR] is incremented when any standard changes (breaking or otherwise) are introduced to the library.
|
||||
|
||||
[PATCH] is incremented when a hot-fix patch is required to an existing minor
|
||||
release due to a bug introduced in that release.
|
||||
```
|
||||
|
||||
Some things to note:
|
||||
|
||||
- All updates should have a corresponding `CHANGELOG.md` entry that at a high-level describes what is being newly introduced in it. For more info, see [Changelog Format Guide](CHANGELOG_FORMAT.MD)
|
||||
|
||||
- When incrementing a level any lower-levels should always reset to 0.
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Application Dependencies
|
||||
|
||||
The following is a list of all third-party dependencies required by the SDK.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The SDK does not come packaged with these dependencies, so consumers of the SDK must provide them.
|
||||
|
||||
- **kotlinx.serialization**
|
||||
- https://github.com/Kotlin/kotlinx.serialization/
|
||||
- Purpose: JSON serialization library for Kotlin.
|
||||
- License: Apache 2.0
|
||||
|
||||
### Development Environment Dependencies
|
||||
|
||||
The following is a list of additional third-party dependencies used as part of the local development environment. This includes test-related artifacts as well as tools related to code quality and linting. These are not present in the final packaged SDK.
|
||||
|
||||
- **JUnit 5**
|
||||
- https://github.com/junit-team/junit5
|
||||
- Purpose: Unit Testing framework for testing SDK code.
|
||||
- License: Eclipse Public License 2.0
|
||||
|
||||
- **MockK**
|
||||
- https://github.com/mockk/mockk
|
||||
- Purpose: Kotlin-friendly mocking library.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Turbine**
|
||||
- https://github.com/cashapp/turbine
|
||||
- Purpose: A small testing library for kotlinx.coroutine's Flow.
|
||||
- License: Apache 2.0
|
||||
75
authenticatorbridge/build.gradle.kts
Normal file
75
authenticatorbridge/build.gradle.kts
Normal file
@@ -0,0 +1,75 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
// For more info on versioning, see the README.
|
||||
val version = "0.1.0"
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.bitwarden.authenticatorbridge"
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
// This min value is selected to accommodate known consumers
|
||||
minSdk = 28
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
buildConfigField("String", "VERSION", "\"$version\"")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility(libs.versions.jvmTarget.get())
|
||||
targetCompatibility(libs.versions.jvmTarget.get())
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
aidl = true
|
||||
}
|
||||
// Add version name to the output .aar file:
|
||||
libraryVariants.all {
|
||||
val variant = this
|
||||
outputs
|
||||
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
val outputFileName = "authenticatorbridge-${version}-SNAPSHOT-${variant.baseName}.aar"
|
||||
output.outputFileName = outputFileName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// SDK dependencies:
|
||||
implementation(libs.kotlinx.serialization)
|
||||
|
||||
// Test environment dependencies:
|
||||
testImplementation(libs.junit.junit5)
|
||||
testImplementation(libs.mockk.mockk)
|
||||
testImplementation(libs.square.turbine)
|
||||
}
|
||||
|
||||
tasks {
|
||||
withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
0
authenticatorbridge/consumer-rules.pro
Normal file
0
authenticatorbridge/consumer-rules.pro
Normal file
21
authenticatorbridge/proguard-rules.pro
vendored
Normal file
21
authenticatorbridge/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
4
authenticatorbridge/src/main/AndroidManifest.xml
Normal file
4
authenticatorbridge/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.bitwarden.authenticatorbridge;
|
||||
|
||||
import com.bitwarden.authenticatorbridge.model.EncryptedAddTotpLoginItemData;
|
||||
import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyData;
|
||||
import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyFingerprintData;
|
||||
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeServiceCallback;
|
||||
|
||||
interface IAuthenticatorBridgeService {
|
||||
// ==============
|
||||
// Configuration
|
||||
// ==============
|
||||
|
||||
// Returns the version number string of the Authenticator Bridge SDK. This is useful so that
|
||||
// callers can compare the version of their Authenticator Bridge SDK with this value and
|
||||
// ensure that the two are compatible.
|
||||
//
|
||||
// For more info about versioning the Authenticator Bridge SDK, see the README.
|
||||
String getVersionNumber();
|
||||
|
||||
// Returns true when the given symmetric fingerprint data matches that contained by the SDK.
|
||||
boolean checkSymmetricEncryptionKeyFingerprint(in SymmetricEncryptionKeyFingerprintData symmetricKeyFingerprint);
|
||||
|
||||
// Returns a symmetric key that will be used for encypting all IPC traffic.
|
||||
//
|
||||
// Consumers should only call this function once to limit the number of times this key is
|
||||
// sent via IPC. Additionally, once the key is shared, checkSymmetricEncryptionKeyFingerprint
|
||||
// should be used to safely confirm that the key is valid.
|
||||
@nullable SymmetricEncryptionKeyData getSymmetricEncryptionKeyData();
|
||||
|
||||
// ==============
|
||||
// Registration
|
||||
// ==============
|
||||
|
||||
// Register the given callback to receive updates after syncAccounts is called.
|
||||
void registerBridgeServiceCallback(IAuthenticatorBridgeServiceCallback callback);
|
||||
|
||||
// Unregister the given callback from receiving updates.
|
||||
void unregisterBridgeServiceCallback(IAuthenticatorBridgeServiceCallback callback);
|
||||
|
||||
// ==============
|
||||
// Data Syncing
|
||||
// ==============
|
||||
|
||||
// Sync available accounts. Callers should register a callback via
|
||||
// registerBridgeServiceCallback before calling this function.
|
||||
void syncAccounts();
|
||||
|
||||
// ==============
|
||||
// Add TOTP Item
|
||||
// ==============
|
||||
|
||||
// Returns an intent that can be launched to navigate the user to the add Totp item flow
|
||||
// in the main password manager app.
|
||||
Intent createAddTotpLoginItemIntent();
|
||||
|
||||
// Give the given TOTP item data to the main Bitwarden app before launching the add TOTP
|
||||
// item flow. This should be called before launching the intent returned from
|
||||
// createAddTotpLoginItemIntent().
|
||||
void setPendingAddTotpLoginItemData(in EncryptedAddTotpLoginItemData data);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.bitwarden.authenticatorbridge;
|
||||
|
||||
import com.bitwarden.authenticatorbridge.model.EncryptedSharedAccountData;
|
||||
|
||||
interface IAuthenticatorBridgeServiceCallback {
|
||||
|
||||
// This function will be called when there is updated shared account data.
|
||||
void onAccountsSync(in EncryptedSharedAccountData data);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.bitwarden.authenticatorbridge.model;
|
||||
|
||||
parcelable EncryptedAddTotpLoginItemData;
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.bitwarden.authenticatorbridge.model;
|
||||
|
||||
parcelable EncryptedSharedAccountData;
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.bitwarden.authenticatorbridge.model;
|
||||
|
||||
parcelable SymmetricEncryptionKeyData;
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.bitwarden.authenticatorbridge.model;
|
||||
|
||||
parcelable SymmetricEncryptionKeyFingerprintData;
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.bitwarden.authenticatorbridge.model
|
||||
|
||||
/**
|
||||
* Domain level model for a TOTP item to be added to the Bitwarden app.
|
||||
*
|
||||
* @param totpUri A TOTP code URI to be added to the Bitwarden app.
|
||||
*/
|
||||
data class AddTotpLoginItemData(
|
||||
val totpUri: String,
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.bitwarden.authenticatorbridge.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Serializable model for a TOTP item to be added to the Bitwarden app. For domain level
|
||||
* model, see [AddTotpLoginItemData].
|
||||
*
|
||||
* @param totpUri A TOTP code URI to be added to the Bitwarden app.
|
||||
*/
|
||||
@Serializable
|
||||
internal data class AddTotpLoginItemDataJson(
|
||||
@SerialName("totpUri")
|
||||
val totpUri: String,
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.bitwarden.authenticatorbridge.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Wraps a [ByteArray] and implements [equals], [hashCode], and [Parcelable] so that it can more
|
||||
* easily be included in [Parcelable] models.
|
||||
*
|
||||
* @param byteArray Wrapped byte array
|
||||
*/
|
||||
@Parcelize
|
||||
data class ByteArrayContainer(
|
||||
val byteArray: ByteArray,
|
||||
) : Parcelable {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ByteArrayContainer
|
||||
|
||||
return byteArray.contentEquals(other.byteArray)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return byteArray.contentHashCode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for converting [ByteArray] to [ByteArrayContainer].
|
||||
*/
|
||||
fun ByteArray.toByteArrayContainer(): ByteArrayContainer =
|
||||
ByteArrayContainer(this)
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.bitwarden.authenticatorbridge.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Models an encrypted totp item to be added to the Bitwarden app.
|
||||
*
|
||||
* @param initializationVector Cryptographic initialization vector.
|
||||
* @param encryptedTotpUriJson Encrypted JSON string containing TOTP URI info. See
|
||||
* [AddTotpLoginItemDataJson] for the json structure of the string.
|
||||
*/
|
||||
@Parcelize
|
||||
data class EncryptedAddTotpLoginItemData(
|
||||
val initializationVector: ByteArrayContainer,
|
||||
val encryptedTotpUriJson: ByteArrayContainer,
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.bitwarden.authenticatorbridge.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Models data that will be sent to calling application via IPC.
|
||||
*
|
||||
* @param initializationVector Cryptographic initialization vector.
|
||||
* @param encryptedAccountsJson Encrypted JSON blob containing shared account data. See
|
||||
* [SharedAccountDataJson] For the serializable model contained in the blob.
|
||||
*/
|
||||
@Parcelize
|
||||
data class EncryptedSharedAccountData(
|
||||
val initializationVector: ByteArrayContainer,
|
||||
val encryptedAccountsJson: ByteArrayContainer,
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.bitwarden.authenticatorbridge.model
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Domain level model representing shared account data.
|
||||
*
|
||||
* @param accounts The list of shared accounts.
|
||||
*/
|
||||
data class SharedAccountData(
|
||||
val accounts: List<Account>,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Models a single shared account.
|
||||
*
|
||||
* @param userId user ID tied to the account.
|
||||
* @param name name associated with the account.
|
||||
* @param email email associated with the account.
|
||||
* @param environmentLabel environment associated with the account.
|
||||
* @param totpUris list of totp URIs associated with the account.
|
||||
* @param lastSyncTime the last time the account was synced by the main Bitwarden app.
|
||||
*/
|
||||
data class Account(
|
||||
val userId: String,
|
||||
val name: String?,
|
||||
val email: String,
|
||||
val environmentLabel: String,
|
||||
val totpUris: List<String>,
|
||||
val lastSyncTime: Instant,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.bitwarden.authenticatorbridge.model
|
||||
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Models a serializable list of shared accounts to be shared with other applications.
|
||||
*
|
||||
* For domain level model, see [SharedAccountData].
|
||||
*
|
||||
* @param accounts The list of shared accounts.
|
||||
*/
|
||||
@Serializable
|
||||
internal data class SharedAccountDataJson(
|
||||
@SerialName("accounts")
|
||||
val accounts: List<AccountJson>,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Models a single shared account in a serializable format.
|
||||
*
|
||||
* @param userId user ID tied to the account.
|
||||
* @param name name associated with the account.
|
||||
* @param email email associated with the account.
|
||||
* @param environmentLabel environment associated with the account.
|
||||
* @param totpUris list of totp URIs associated with the account.
|
||||
* @param lastSyncTime the last time the account was synced by the main Bitwarden app.
|
||||
*/
|
||||
@Serializable
|
||||
data class AccountJson(
|
||||
@SerialName("userId")
|
||||
val userId: String,
|
||||
|
||||
@SerialName("name")
|
||||
val name: String?,
|
||||
|
||||
@SerialName("email")
|
||||
val email: String,
|
||||
|
||||
@SerialName("environmentLabel")
|
||||
val environmentLabel: String,
|
||||
|
||||
@SerialName("totpUris")
|
||||
val totpUris: List<String>,
|
||||
|
||||
@SerialName("lastSyncTime")
|
||||
@Contextual
|
||||
val lastSyncTime: Instant,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.bitwarden.authenticatorbridge.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Wrapper for a symmetric encryption key.
|
||||
*
|
||||
* @param symmetricEncryptionKey The symmetric encryption key.
|
||||
*/
|
||||
@Parcelize
|
||||
data class SymmetricEncryptionKeyData(
|
||||
val symmetricEncryptionKey: ByteArrayContainer,
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.bitwarden.authenticatorbridge.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Wrapper for a symmetric encryption fingerprint (hash) that can be used
|
||||
* to verify if callers have the correct symmetric key for encrypting/decrypting IPC data.
|
||||
*
|
||||
* @param symmetricEncryptionKeyFingerprint The fingerprint of the symmetric encryption key.
|
||||
*/
|
||||
@Parcelize
|
||||
data class SymmetricEncryptionKeyFingerprintData(
|
||||
val symmetricEncryptionKeyFingerprint: ByteArrayContainer,
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.bitwarden.authenticatorbridge.util
|
||||
|
||||
import com.bitwarden.authenticatorbridge.BuildConfig
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.contextual
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Version of the Authenticator Bridge SDK.
|
||||
*/
|
||||
const val AUTHENTICATOR_BRIDGE_SDK_VERSION = BuildConfig.VERSION
|
||||
|
||||
/**
|
||||
* Common instance of [Json] that should be used throughout the SDK.
|
||||
*/
|
||||
internal val JSON = Json {
|
||||
// If there are keys returned by the server not modeled by a serializable class,
|
||||
// ignore them.
|
||||
// This makes additive server changes non-breaking.
|
||||
ignoreUnknownKeys = true
|
||||
|
||||
// We allow for nullable values to have keys missing in the JSON response.
|
||||
explicitNulls = false
|
||||
|
||||
// Add serializer for Instant serialization.
|
||||
serializersModule = SerializersModule {
|
||||
contextual(InstantSerializer)
|
||||
}
|
||||
|
||||
// Respect model default property values.
|
||||
coerceInputValues = true
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple serializer for serializing [Instant].
|
||||
*/
|
||||
private object InstantSerializer : KSerializer<Instant> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor("java.time.Instant", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Instant) =
|
||||
encoder.encodeString(value.toString())
|
||||
|
||||
override fun deserialize(decoder: Decoder): Instant = Instant.parse(decoder.decodeString())
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package com.bitwarden.authenticatorbridge.util
|
||||
|
||||
import android.security.keystore.KeyProperties
|
||||
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeService
|
||||
import com.bitwarden.authenticatorbridge.model.AddTotpLoginItemData
|
||||
import com.bitwarden.authenticatorbridge.model.AddTotpLoginItemDataJson
|
||||
import com.bitwarden.authenticatorbridge.model.EncryptedAddTotpLoginItemData
|
||||
import com.bitwarden.authenticatorbridge.model.EncryptedSharedAccountData
|
||||
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||
import com.bitwarden.authenticatorbridge.model.SharedAccountDataJson
|
||||
import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyData
|
||||
import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyFingerprintData
|
||||
import com.bitwarden.authenticatorbridge.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
|
||||
* [IAuthenticatorBridgeService.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
|
||||
* [IAuthenticatorBridgeService.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<SymmetricEncryptionKeyFingerprintData> =
|
||||
runCatching {
|
||||
val messageDigest = MessageDigest.getInstance(KeyProperties.DIGEST_SHA256)
|
||||
messageDigest.reset()
|
||||
messageDigest.update(this.symmetricEncryptionKey.byteArray)
|
||||
SymmetricEncryptionKeyFingerprintData(messageDigest.digest().toByteArrayContainer())
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt [SharedAccountData].
|
||||
*
|
||||
* This is intended to be used by the main Bitwarden app during a
|
||||
* [IAuthenticatorBridgeService.syncAccounts] call.
|
||||
*
|
||||
* @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].
|
||||
*
|
||||
* @param symmetricEncryptionKeyData Symmetric key used for encryption.
|
||||
*/
|
||||
internal 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,
|
||||
)
|
||||
@@ -0,0 +1,192 @@
|
||||
package com.bitwarden.authenticatorbridge.util
|
||||
|
||||
import android.security.keystore.KeyProperties
|
||||
import com.bitwarden.authenticatorbridge.model.AddTotpLoginItemData
|
||||
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||
import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyData
|
||||
import com.bitwarden.authenticatorbridge.model.toByteArrayContainer
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.time.Instant
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
|
||||
class EncryptionUtilTest {
|
||||
|
||||
@Test
|
||||
fun `generateSecretKey should return success when there are no internal exceptions`() {
|
||||
val secretKey = generateSecretKey()
|
||||
assertTrue(secretKey.isSuccess)
|
||||
assertNotNull(secretKey.getOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generateSecretKey should return failure when KeyGenerator getInstance throws`() {
|
||||
mockkStatic(KeyGenerator::class)
|
||||
every { KeyGenerator.getInstance("AES") } throws NoSuchAlgorithmException()
|
||||
val secretKey = generateSecretKey()
|
||||
assertTrue(secretKey.isFailure)
|
||||
unmockkStatic(KeyGenerator::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toFingerprint should return success when there are no internal exceptions`() {
|
||||
val keyData = SymmetricEncryptionKeyData(
|
||||
symmetricEncryptionKey = generateSecretKey().getOrThrow().encoded.toByteArrayContainer()
|
||||
)
|
||||
val result = keyData.toFingerprint()
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toFingerprint should return failure when MessageDigest getInstance fails`() {
|
||||
mockkStatic(MessageDigest::class)
|
||||
every { MessageDigest.getInstance("SHA-256") } throws NoSuchAlgorithmException()
|
||||
val keyData = SymmetricEncryptionKeyData(
|
||||
symmetricEncryptionKey = generateSecretKey().getOrThrow().encoded.toByteArrayContainer()
|
||||
)
|
||||
val result = keyData.toFingerprint()
|
||||
assertTrue(result.isFailure)
|
||||
unmockkStatic(MessageDigest::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encrypt SharedAccountData should return success when there are no internal exceptions`() {
|
||||
val result = SHARED_ACCOUNT_DATA.encrypt(SYMMETRIC_KEY)
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encrypt SharedAccountData should return failure when generateCipher fails`() {
|
||||
mockkStatic(Cipher::class)
|
||||
every {
|
||||
Cipher.getInstance(CIPHER_TRANSFORMATION)
|
||||
} throws NoSuchAlgorithmException()
|
||||
val result = SHARED_ACCOUNT_DATA.encrypt(SYMMETRIC_KEY)
|
||||
assertTrue(result.isFailure)
|
||||
unmockkStatic(Cipher::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `decrypt EncryptedSharedAccountData should return success when there are no internal exceptions`() {
|
||||
val result = ENCRYPTED_SHARED_ACCOUNT_DATA.decrypt(SYMMETRIC_KEY)
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(SHARED_ACCOUNT_DATA, result.getOrThrow())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decrypt EncryptedSharedAccountData should return failure when generateCipher fails`() {
|
||||
mockkStatic(Cipher::class)
|
||||
every {
|
||||
Cipher.getInstance(CIPHER_TRANSFORMATION)
|
||||
} throws NoSuchAlgorithmException()
|
||||
val result = ENCRYPTED_SHARED_ACCOUNT_DATA.decrypt(SYMMETRIC_KEY)
|
||||
assertTrue(result.isFailure)
|
||||
unmockkStatic(Cipher::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encrypting and decrypting SharedAccountData should leave the data untransformed`() {
|
||||
val result = SHARED_ACCOUNT_DATA
|
||||
.encrypt(SYMMETRIC_KEY)
|
||||
.getOrThrow()
|
||||
.decrypt(SYMMETRIC_KEY)
|
||||
assertEquals(
|
||||
SHARED_ACCOUNT_DATA,
|
||||
result.getOrThrow()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `encrypt AddTotpLoginItemData should return success wwhen there are no internal exceptions`() {
|
||||
val result = ADD_TOTP_ITEM.encrypt(SYMMETRIC_KEY)
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encrypt AddTotpLoginItemData should return failure when generateCipher fails`() {
|
||||
mockkStatic(Cipher::class)
|
||||
every {
|
||||
Cipher.getInstance(CIPHER_TRANSFORMATION)
|
||||
} throws NoSuchAlgorithmException()
|
||||
val result = ADD_TOTP_ITEM.encrypt(SYMMETRIC_KEY)
|
||||
assertTrue(result.isFailure)
|
||||
unmockkStatic(Cipher::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLIneLength")
|
||||
fun `decrypt EncryptedAddTotpLoginItemData should return success when there are no internal exceptions`() {
|
||||
val result = ENCRYPTED_ADD_TOTP_ITEM.decrypt(SYMMETRIC_KEY)
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decrypt EncryptedAddTotpLoginItemData should return failure when generateCipher fails`() {
|
||||
mockkStatic(Cipher::class)
|
||||
every {
|
||||
Cipher.getInstance(CIPHER_TRANSFORMATION)
|
||||
} throws NoSuchAlgorithmException()
|
||||
val result = ENCRYPTED_ADD_TOTP_ITEM.decrypt(SYMMETRIC_KEY)
|
||||
assertTrue(result.isFailure)
|
||||
unmockkStatic(Cipher::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encrypting and decrypting AddTotpLoginItemData should leave the data untransformed`() {
|
||||
val result = ADD_TOTP_ITEM
|
||||
.encrypt(SYMMETRIC_KEY)
|
||||
.getOrThrow()
|
||||
.decrypt(SYMMETRIC_KEY)
|
||||
assertEquals(
|
||||
ADD_TOTP_ITEM,
|
||||
result.getOrThrow()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSymmetricEncryptionKeyData should wrap the given ByteArray`() {
|
||||
val sourceArray = generateSecretKey().getOrThrow().encoded
|
||||
val wrappedArray = sourceArray.toSymmetricEncryptionKeyData()
|
||||
assertTrue(sourceArray.contentEquals(wrappedArray.symmetricEncryptionKey.byteArray))
|
||||
}
|
||||
}
|
||||
|
||||
private val SHARED_ACCOUNT_DATA = SharedAccountData(
|
||||
accounts = listOf(
|
||||
SharedAccountData.Account(
|
||||
userId = "userId",
|
||||
name = "Johnny Appleseed",
|
||||
email = "johnyapples@test.com",
|
||||
environmentLabel = "bitwarden.com",
|
||||
totpUris = listOf("test.com"),
|
||||
lastSyncTime = Instant.parse("2024-09-10T10:15:30.00Z")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
private val ADD_TOTP_ITEM = AddTotpLoginItemData(
|
||||
totpUri = "test.com"
|
||||
)
|
||||
|
||||
private val SYMMETRIC_KEY = SymmetricEncryptionKeyData(
|
||||
symmetricEncryptionKey = generateSecretKey().getOrThrow().encoded.toByteArrayContainer()
|
||||
)
|
||||
|
||||
private val ENCRYPTED_SHARED_ACCOUNT_DATA =
|
||||
SHARED_ACCOUNT_DATA.encrypt(SYMMETRIC_KEY).getOrThrow()
|
||||
|
||||
private val ENCRYPTED_ADD_TOTP_ITEM = ADD_TOTP_ITEM.encrypt(SYMMETRIC_KEY).getOrThrow()
|
||||
|
||||
private const val CIPHER_TRANSFORMATION = KeyProperties.KEY_ALGORITHM_AES + "/" +
|
||||
KeyProperties.BLOCK_MODE_CBC + "/" +
|
||||
"PKCS5PADDING"
|
||||
Reference in New Issue
Block a user