Skip to content

Commit

Permalink
Issue mozilla-mobile#10140 - Part 3: Implement the new Autocomplete.S…
Browse files Browse the repository at this point in the history
…torageDelegate interface in GeckoStorageDelegateWrapper

- Uses the new GeckoView's `Autocomplete.StorageDelegate` interface in GeckoStorageDelegateWrapper
- Implements GeckoView's `Autocomplete.StorageDelegate.onCreditCardFetch`
  • Loading branch information
gabrielluong committed May 3, 2021
1 parent 0538e1d commit 8d9a131
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,58 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import mozilla.components.browser.engine.gecko.ext.toLogin
import mozilla.components.browser.engine.gecko.ext.toLoginEntry
import mozilla.components.concept.storage.Login
import mozilla.components.concept.storage.CreditCardsAddressesStorageDelegate
import mozilla.components.concept.storage.LoginStorageDelegate
import org.mozilla.geckoview.Autocomplete
import org.mozilla.geckoview.GeckoResult

/**
* This class exists only to convert incoming [LoginEntry] arguments into [Login]s, then forward
* them to [storageDelegate]. This allows us to avoid duplicating [LoginStorageDelegate] code
* between different versions of GeckoView, by duplicating this wrapper instead.
* Gecko credit card and login storage delegate that handles runtime storage requests. This allows
* the Gecko runtime to call the underlying storage to handle requests for fetching, saving and
* updating of autocomplete items in the storage.
*/
@Suppress("Deprecation")
// This will be addressed in https://github.com/mozilla-mobile/android-components/issues/10093
class GeckoLoginDelegateWrapper(private val storageDelegate: LoginStorageDelegate) :
Autocomplete.LoginStorageDelegate {
class GeckoStorageDelegateWrapper(
private val creditCardsAddressesStorageDelegate: CreditCardsAddressesStorageDelegate,
private val loginStorageDelegate: LoginStorageDelegate
) : Autocomplete.StorageDelegate {

override fun onCreditCardFetch(): GeckoResult<Array<Autocomplete.CreditCard>>? {
val result = GeckoResult<Array<Autocomplete.CreditCard>>()

GlobalScope.launch(IO) {
val creditCards = creditCardsAddressesStorageDelegate.onCreditCardsFetch().await()
.mapNotNull {
val plaintextCardNumber =
creditCardsAddressesStorageDelegate.decrypt(it.encryptedCardNumber)?.number

if (plaintextCardNumber == null) {
null
} else {
Autocomplete.CreditCard.Builder()
.guid(it.guid)
.name(it.billingName)
.number(plaintextCardNumber)
.expirationMonth(it.expiryMonth.toString())
.expirationYear(it.expiryYear.toString())
.build()
}
}
.toTypedArray()
result.complete(creditCards)
}

return result
}

override fun onLoginSave(login: Autocomplete.LoginEntry) {
storageDelegate.onLoginSave(login.toLogin())
loginStorageDelegate.onLoginSave(login.toLogin())
}

override fun onLoginFetch(domain: String): GeckoResult<Array<Autocomplete.LoginEntry>>? {
val result = GeckoResult<Array<Autocomplete.LoginEntry>>()

GlobalScope.launch(IO) {
val storedLogins = storageDelegate.onLoginFetch(domain)
val storedLogins = loginStorageDelegate.onLoginFetch(domain)

val logins = storedLogins.await()
.map { it.toLoginEntry() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,46 @@ interface CreditCardsAddressesStorage {
* @param guid Unique identifier for the desired address.
*/
suspend fun touchAddress(guid: String)

/**
* Returns an instance of [CreditCardCrypto] that knows how to encrypt and decrypt credit card
* numbers.
*
* @return [CreditCardCrypto] instance.
*/
fun getCreditCardCrypto(): CreditCardCrypto
}

/**
* An interface that defines methods for encrypting and decrypting a credit card number.
*/
interface CreditCardCrypto : KeyProvider {

/**
* Encrypt a [CreditCardNumber.Plaintext] using the provided key. A `null` result means a
* bad key was provided. In that case caller should obtain a new key and try again.
*
* @param key The encryption key to encrypt the plaintext credit card number.
* @param plaintextCardNumber A plaintext credit card number to be encrypted.
* @return An encrypted credit card number or `null` if a bad [key] was provided.
*/
fun encrypt(
key: ManagedKey,
plaintextCardNumber: CreditCardNumber.Plaintext
): CreditCardNumber.Encrypted?

/**
* Decrypt a [CreditCardNumber.Encrypted] using the provided key. A `null` result means a
* bad key was provided. In that case caller should obtain a new key and try again.
*
* @param key The encryption key to decrypt the decrypt credit card number.
* @param encryptedCardNumber An encrypted credit card number to be decrypted.
* @return A plaintext, non-encrypted credit card number or `null` if a bad [key] was provided.
*/
fun decrypt(
key: ManagedKey,
encryptedCardNumber: CreditCardNumber.Encrypted
): CreditCardNumber.Plaintext?
}

/**
Expand Down Expand Up @@ -273,6 +313,15 @@ data class UpdatableAddressFields(
*/
interface CreditCardsAddressesStorageDelegate {

/**
* Decrypt a [CreditCardNumber.Encrypted] into its plaintext equivalent or `null` if
* it fails to decrypt.
*
* @param encryptedCardNumber An encrypted credit card number to be decrypted.
* @return A plaintext, non-encrypted credit card number.
*/
fun decrypt(encryptedCardNumber: CreditCardNumber.Encrypted): CreditCardNumber.Plaintext?

/**
* Returns all stored addresses. This is called when the engine believes an address field
* should be autofilled.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.lib.dataprotect
package mozilla.components.concept.storage

/**
* Knows how to provide a [ManagedKey].
Expand Down
1 change: 1 addition & 0 deletions components/service/firefox-accounts/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ android {
dependencies {
// Types defined in concept-sync are part of the public API of this module.
api project(':concept-sync')
api project(':concept-storage')

// Parts of this dependency are typealiase'd or are otherwise part of this module's public API.
api Dependencies.mozilla_fxa
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

package mozilla.components.service.fxa.sync

import mozilla.components.concept.storage.KeyProvider
import mozilla.components.concept.sync.SyncableStore
import mozilla.components.lib.dataprotect.KeyProvider
import mozilla.components.service.fxa.SyncConfig
import mozilla.components.service.fxa.SyncEngine
import mozilla.components.service.fxa.manager.SyncEnginesStorage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import androidx.work.WorkManager
import androidx.work.WorkerParameters
import mozilla.appservices.syncmanager.SyncParams
import mozilla.appservices.syncmanager.SyncServiceStatus
import mozilla.components.concept.storage.KeyProvider
import mozilla.appservices.syncmanager.SyncManager as RustSyncManager
import mozilla.components.lib.dataprotect.KeyProvider
import mozilla.components.service.fxa.FxaDeviceSettingsCache
import mozilla.components.service.fxa.SyncAuthInfoCache
import mozilla.components.service.fxa.SyncConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import mozilla.components.concept.storage.Address
import mozilla.components.concept.storage.CreditCard
import mozilla.components.concept.storage.CreditCardNumber
import mozilla.components.concept.storage.CreditCardsAddressesStorage
import mozilla.components.concept.storage.KeyGenerationReason
import mozilla.components.concept.storage.KeyRecoveryHandler
import mozilla.components.concept.storage.NewCreditCardFields
import mozilla.components.concept.storage.UpdatableAddressFields
import mozilla.components.concept.storage.UpdatableCreditCardFields
import mozilla.components.concept.sync.SyncableStore
import mozilla.components.lib.dataprotect.KeyGenerationReason
import mozilla.components.lib.dataprotect.KeyRecoveryHandler
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
import mozilla.components.support.base.log.logger.Logger
import java.io.Closeable
Expand Down Expand Up @@ -157,6 +157,10 @@ class AutofillCreditCardsAddressesStorage(
conn.getStorage().touchAddress(guid)
}

override fun getCreditCardCrypto(): AutofillCrypto {
return crypto
}

override fun registerWithSyncManager() {
conn.getStorage().registerWithSyncManager()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import mozilla.appservices.autofill.ErrorException
import mozilla.appservices.autofill.createKey
import mozilla.appservices.autofill.decryptString
import mozilla.appservices.autofill.encryptString
import mozilla.components.concept.storage.CreditCardCrypto
import mozilla.components.concept.storage.CreditCardNumber
import mozilla.components.lib.dataprotect.KeyGenerationReason
import mozilla.components.lib.dataprotect.KeyProvider
import mozilla.components.lib.dataprotect.KeyRecoveryHandler
import mozilla.components.lib.dataprotect.ManagedKey
import mozilla.components.concept.storage.KeyGenerationReason
import mozilla.components.concept.storage.KeyRecoveryHandler
import mozilla.components.concept.storage.ManagedKey
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
import mozilla.components.support.base.log.logger.Logger

Expand All @@ -33,15 +33,14 @@ class AutofillCrypto(
private val context: Context,
private val securePrefs: SecureAbove22Preferences,
private val keyRecoveryHandler: KeyRecoveryHandler
) : KeyProvider {
) : CreditCardCrypto {
private val logger = Logger("AutofillCrypto")
private val plaintextPrefs by lazy { context.getSharedPreferences(AUTOFILL_PREFS, Context.MODE_PRIVATE) }

/**
* Encrypt a [CreditCardNumber.Plaintext] using provided key.
* A `null` result means a bad key was provided. In that case caller should obtain a new key and try again.
*/
fun encrypt(key: ManagedKey, plaintextCardNumber: CreditCardNumber.Plaintext): CreditCardNumber.Encrypted? {
override fun encrypt(
key: ManagedKey,
plaintextCardNumber: CreditCardNumber.Plaintext
): CreditCardNumber.Encrypted? {
return try {
CreditCardNumber.Encrypted(encrypt(key, plaintextCardNumber.number))
} catch (e: ErrorException.JsonError) {
Expand All @@ -53,11 +52,10 @@ class AutofillCrypto(
}
}

/**
* Decrypt a [CreditCardNumber.Encrypted] using provided key.
* A `null` result means a bad key was provided. In that case caller should obtain a new key and try again.
*/
fun decrypt(key: ManagedKey, encryptedCardNumber: CreditCardNumber.Encrypted): CreditCardNumber.Plaintext? {
override fun decrypt(
key: ManagedKey,
encryptedCardNumber: CreditCardNumber.Encrypted
): CreditCardNumber.Plaintext? {
return try {
CreditCardNumber.Plaintext(decrypt(key, encryptedCardNumber.number))
} catch (e: ErrorException.JsonError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import mozilla.components.concept.storage.Address
import mozilla.components.concept.storage.CreditCard
import mozilla.components.concept.storage.CreditCardNumber
import mozilla.components.concept.storage.CreditCardsAddressesStorage
import mozilla.components.concept.storage.CreditCardsAddressesStorageDelegate

Expand All @@ -21,6 +22,12 @@ class GeckoCreditCardsAddressesStorageDelegate(
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
) : CreditCardsAddressesStorageDelegate {

override fun decrypt(encryptedCardNumber: CreditCardNumber.Encrypted): CreditCardNumber.Plaintext? {
val crypto = storage.value.getCreditCardCrypto()
val key = crypto.key()
return crypto.decrypt(key, encryptedCardNumber)
}

override fun onAddressesFetch(): Deferred<List<Address>> {
return scope.async {
storage.value.getAllAddresses()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ package mozilla.components.service.sync.autofill

import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.concept.storage.CreditCardNumber
import mozilla.components.lib.dataprotect.KeyGenerationReason
import mozilla.components.lib.dataprotect.KeyRecoveryHandler
import mozilla.components.lib.dataprotect.ManagedKey
import mozilla.components.concept.storage.KeyGenerationReason
import mozilla.components.concept.storage.KeyRecoveryHandler
import mozilla.components.concept.storage.ManagedKey
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineScope
import mozilla.components.support.test.mock
import mozilla.components.concept.storage.CreditCardNumber
import mozilla.components.concept.storage.NewCreditCardFields
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -21,7 +24,8 @@ import org.mockito.Mockito.verify
@RunWith(AndroidJUnit4::class)
class GeckoCreditCardsAddressesStorageDelegateTest {

private val storage = AutofillCreditCardsAddressesStorage(testContext, mock())
private lateinit var storage: AutofillCreditCardsAddressesStorage
private lateinit var securePrefs: SecureAbove22Preferences
private lateinit var delegate: GeckoCreditCardsAddressesStorageDelegate
private lateinit var scope: TestCoroutineScope

Expand All @@ -32,9 +36,31 @@ class GeckoCreditCardsAddressesStorageDelegateTest {
@Before
fun before() = runBlocking {
scope = TestCoroutineScope()
// forceInsecure is set in the tests because a keystore wouldn't be configured in the test environment.
securePrefs = SecureAbove22Preferences(testContext, "autofill", forceInsecure = true)
storage = AutofillCreditCardsAddressesStorage(testContext, lazy { securePrefs })
delegate = GeckoCreditCardsAddressesStorageDelegate(lazy { storage }, scope)
}

@Test
fun `decrypt`() = runBlocking {
val plaintextNumber = CreditCardNumber.Plaintext("4111111111111111")
val creditCardFields = NewCreditCardFields(
billingName = "Jon Doe",
plaintextCardNumber = plaintextNumber,
cardNumberLast4 = "1111",
expiryMonth = 12,
expiryYear = 2028,
cardType = "amex"
)
val creditCard = storage.addCreditCard(creditCardFields)

assertEquals(
plaintextNumber,
delegate.decrypt(creditCard.encryptedCardNumber)
)
}

@Test
fun `onAddressFetch`() {
scope.launch {
Expand Down
9 changes: 5 additions & 4 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ permalink: /changelog/
* **browser-menu**:
* 🚒 Bug fixed [issue #10032](https://github.com/mozilla-mobile/android-components/issues/10032) - A BrowserMenuCompoundButton used in our BrowserMenu setup with a DynamicWidthRecyclerView is not clipped anymore.

* **browser-engine-gecko**:
* Implements the new GeckoView `Autocomplete.StorageDelegate` interface in `GeckoStorageDelegateWrapper`. This will replace the deprecated `GeckoLoginDelegateWrapper` and provide additional autocomplete support for credit cards. [#10140](https://github.com/mozilla-mobile/android-components/issues/10140)

* **feature-downloads**:
* ⚠️ **This is a breaking change**: `AbstractFetchDownloadService.openFile()` changed its signature from `AbstractFetchDownloadService.openFile(context: Context, filePath: String, contentType: String?)` to `AbstractFetchDownloadService.openFile(applicationContext: Context, download: DownloadState)`.
* 🚒 Bug fixed [issue #](https://github.com/mozilla-mobile/android-components/issues/10138) - The downloaded files cannot be seen.
Expand All @@ -37,15 +40,13 @@ permalink: /changelog/
* ⚠️ **This is a breaking change**: `CreditCard`'s number field changed to `encryptedCardNumber`, `cardNumberLast4` added.
* New `CreditCardNumber` class, which encapsulate either an encrypted or plaintext versions of credit cards.
* `AutofillCreditCardsAddressesStorage` reflects these breaking changes.
* Introduced a new `CreditCardCrypto` interface for for encrypting and decrypting a credit card number. [#10140](https://github.com/mozilla-mobile/android-components/issues/10140)
* 🌟️ New APIs for managing keys - `ManagedKey`, `KeyProvider` and `KeyRecoveryHandler`. `AutofillCreditCardsAddressesStorage` implements these APIs for managing keys for credit card storage.

* **service-firefox-accounts**
* 🌟️ When configuring syncable storage layers, `SyncManager` now takes an optional `KeyProvider` to handle encryption/decryption of protected values.
* 🌟️ Support for syncing Address and Credit Cards

* **lib-dataprotect**
* 🌟️ New APIs for managing keys - `ManagedKey`, `KeyProvider` and `KeyRecoveryHandler`.
* 🌟️ `AutofillCreditCardsAddressesStorage` implements these APIs for managing keys for credit card storage.

# 75.0.0

* [Commits](https://github.com/mozilla-mobile/android-components/compare/v74.0.0...v75.0.0)
Expand Down

0 comments on commit 8d9a131

Please sign in to comment.