Add TOTP code generation (#687)

This commit is contained in:
Oleg Semenenko 2024-01-19 16:40:56 -06:00 committed by Álison Fernandes
parent 88e4b45f7d
commit a877897a19
7 changed files with 132 additions and 0 deletions

View file

@ -5,6 +5,7 @@ import com.bitwarden.core.CipherListView
import com.bitwarden.core.CipherView
import com.bitwarden.core.Collection
import com.bitwarden.core.CollectionView
import com.bitwarden.core.DateTime
import com.bitwarden.core.DerivePinKeyResponse
import com.bitwarden.core.Folder
import com.bitwarden.core.FolderView
@ -15,6 +16,7 @@ import com.bitwarden.core.PasswordHistory
import com.bitwarden.core.PasswordHistoryView
import com.bitwarden.core.Send
import com.bitwarden.core.SendView
import com.bitwarden.core.TotpResponse
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
/**
@ -251,4 +253,13 @@ interface VaultSdkSource {
userId: String,
passwordHistoryList: List<PasswordHistory>,
): Result<List<PasswordHistoryView>>
/**
* Generate a verification code and the period using the totp code.
*/
suspend fun generateTotp(
userId: String,
totp: String,
time: DateTime,
): Result<TotpResponse>
}

View file

@ -5,6 +5,7 @@ import com.bitwarden.core.CipherListView
import com.bitwarden.core.CipherView
import com.bitwarden.core.Collection
import com.bitwarden.core.CollectionView
import com.bitwarden.core.DateTime
import com.bitwarden.core.DerivePinKeyResponse
import com.bitwarden.core.Folder
import com.bitwarden.core.FolderView
@ -14,6 +15,7 @@ import com.bitwarden.core.PasswordHistory
import com.bitwarden.core.PasswordHistoryView
import com.bitwarden.core.Send
import com.bitwarden.core.SendView
import com.bitwarden.core.TotpResponse
import com.bitwarden.sdk.BitwardenException
import com.bitwarden.sdk.Client
import com.bitwarden.sdk.ClientVault
@ -252,6 +254,19 @@ class VaultSdkSourceImpl(
.decryptList(passwordHistoryList)
}
override suspend fun generateTotp(
userId: String,
totp: String,
time: DateTime,
): Result<TotpResponse> = runCatching {
getClient(userId = userId)
.vault()
.generateTotp(
key = totp,
time = time,
)
}
private fun getClient(
userId: String,
): Client = sdkClientManager.getOrCreateClient(userId = userId)

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.repository
import android.net.Uri
import com.bitwarden.core.CipherView
import com.bitwarden.core.CollectionView
import com.bitwarden.core.DateTime
import com.bitwarden.core.FolderView
import com.bitwarden.core.SendType
import com.bitwarden.core.SendView
@ -13,6 +14,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult
@ -198,6 +200,11 @@ interface VaultRepository : VaultLockManager {
*/
suspend fun removePasswordSend(sendId: String): RemovePasswordSendResult
/**
* Attempt to get the verification code and the period.
*/
suspend fun generateTotp(totpCode: String, time: DateTime): GenerateTotpResult
/**
* Attempt to delete a send.
*/

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.repository
import android.net.Uri
import com.bitwarden.core.CipherView
import com.bitwarden.core.CollectionView
import com.bitwarden.core.DateTime
import com.bitwarden.core.FolderView
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod
@ -37,6 +38,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult
@ -593,6 +595,27 @@ class VaultRepositoryImpl(
)
}
override suspend fun generateTotp(
totpCode: String,
time: DateTime,
): GenerateTotpResult {
val userId = requireNotNull(activeUserId)
return vaultSdkSource.generateTotp(
time = time,
userId = userId,
totp = totpCode,
)
.fold(
onSuccess = {
GenerateTotpResult.Success(
code = it.code,
periodSeconds = it.period.toInt(),
)
},
onFailure = { GenerateTotpResult.Error },
)
}
/**
* Checks if the given [userId] has an associated encrypted PIN key but not a pin-protected user
* key. This indicates a scenario in which a user has requested PIN unlocking but requires

View file

@ -0,0 +1,20 @@
package com.x8bit.bitwarden.data.vault.repository.model
/**
* Models the result of generating a totp code.
*/
sealed class GenerateTotpResult {
/**
* The code was generated successfully.
*/
data class Success(
val code: String,
val periodSeconds: Int,
) : GenerateTotpResult()
/**
* An error occurred while generating the code.
*/
data object Error : GenerateTotpResult()
}

View file

@ -5,6 +5,7 @@ import com.bitwarden.core.CipherListView
import com.bitwarden.core.CipherView
import com.bitwarden.core.Collection
import com.bitwarden.core.CollectionView
import com.bitwarden.core.DateTime
import com.bitwarden.core.DerivePinKeyResponse
import com.bitwarden.core.Folder
import com.bitwarden.core.FolderView
@ -14,6 +15,7 @@ import com.bitwarden.core.PasswordHistory
import com.bitwarden.core.PasswordHistoryView
import com.bitwarden.core.Send
import com.bitwarden.core.SendView
import com.bitwarden.core.TotpResponse
import com.bitwarden.sdk.BitwardenException
import com.bitwarden.sdk.Client
import com.bitwarden.sdk.ClientCrypto
@ -31,6 +33,7 @@ import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@ -616,4 +619,32 @@ class VaultSdkSourceTest {
}
verify { sdkClientManager.getOrCreateClient(userId = userId) }
}
@Test
fun `generateTotp should call SDK and return a Result with correct data`() =
runTest {
val userId = "userId"
val totpResponse = TotpResponse("TestCode", 30u)
coEvery { clientVault.generateTotp(any(), any()) } returns totpResponse
val time = DateTime.now()
val result = vaultSdkSource.generateTotp(
userId = userId,
totp = "Totp",
time = time,
)
assertEquals(
Result.success(totpResponse),
result,
)
coVerify {
clientVault.generateTotp(
key = "Totp",
time = time,
)
}
verify { sdkClientManager.getOrCreateClient(userId = userId) }
}
}

View file

@ -4,12 +4,14 @@ import android.net.Uri
import app.cash.turbine.test
import com.bitwarden.core.CipherView
import com.bitwarden.core.CollectionView
import com.bitwarden.core.DateTime
import com.bitwarden.core.FolderView
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.core.SendType
import com.bitwarden.core.SendView
import com.bitwarden.core.TotpResponse
import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
@ -57,6 +59,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult
@ -2107,6 +2110,28 @@ class VaultRepositoryTest {
)
}
@Test
fun `generateTotp should return a success result on getting a code`() = runTest {
val totpResponse = TotpResponse("Testcode", 30u)
coEvery { vaultSdkSource.generateTotp(any(), any(), any()) } returns Result.success(
totpResponse,
)
fakeAuthDiskSource.userState = MOCK_USER_STATE
val result = vaultRepository.generateTotp(
totpCode = "testCode",
time = DateTime.now(),
)
assertEquals(
GenerateTotpResult.Success(
code = totpResponse.code,
periodSeconds = totpResponse.period.toInt(),
),
result,
)
}
//region Helper functions
/**