Skip to content

Commit 17b844d

Browse files
committed
[PM-30899] Store account keys on new user creation
This commit updates the `createAccountKeys` API call to handle and store the full response object, which includes the `accountKeys`. When a new SSO user is created, the `accountKeys` received from the `createAccountKeys` endpoint are now stored in `authDiskSource`. Additionally, during a soft logout, the user's `accountKeys` are preserved and restored along with other account data upon re-authentication. Specific changes include: - Modified `AccountsService` and related API definitions to expect and return a `CreateAccountKeysResponseJson` object instead of `Unit`. - Introduced the `CreateAccountKeysResponseJson` data class to model the API response. - Updated `AuthRepositoryImpl` to store the `accountKeys` from the response after creating a new SSO user. - Updated `UserLogoutManagerImpl` to ensure `accountKeys` are retained during a soft logout. - Adjusted associated tests to reflect these changes, including mocking the new response structure and verifying that `accountKeys` are stored correctly.
1 parent afc1ff4 commit 17b844d

File tree

9 files changed

+103
-9
lines changed

9 files changed

+103
-9
lines changed

app/src/main/kotlin/com/x8bit/bitwarden/data/auth/manager/UserLogoutManagerImpl.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class UserLogoutManagerImpl(
8686
val pinProtectedUserKeyEnvelope = authDiskSource.getPinProtectedUserKeyEnvelope(
8787
userId = userId,
8888
)
89+
val accountKeys = authDiskSource.getAccountKeys(userId = userId)
8990

9091
switchUserIfAvailable(
9192
currentUserId = userId,
@@ -114,6 +115,7 @@ class UserLogoutManagerImpl(
114115
userId = userId,
115116
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
116117
)
118+
storeAccountKeys(userId = userId, accountKeys = accountKeys)
117119
}
118120
}
119121

app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ class AuthRepositoryImpl(
439439
},
440440
)
441441

442+
@Suppress("LongMethod")
442443
override suspend fun createNewSsoUser(): NewSsoUserResult {
443444
val account = authDiskSource.userState?.activeAccount
444445
?: return NewSsoUserResult.Failure(error = NoActiveUserException())
@@ -465,6 +466,13 @@ class AuthRepositoryImpl(
465466
publicKey = keys.publicKey,
466467
encryptedPrivateKey = keys.privateKey,
467468
)
469+
.onSuccess { response ->
470+
authDiskSource
471+
.storeAccountKeys(
472+
userId = userId,
473+
accountKeys = response.accountKeys,
474+
)
475+
}
468476
.map { keys }
469477
}
470478
.flatMap { keys ->

app/src/test/kotlin/com/x8bit/bitwarden/data/auth/manager/UserLogoutManagerTest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.manager
22

33
import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager
44
import com.bitwarden.core.data.manager.toast.ToastManager
5+
import com.bitwarden.network.model.AccountKeysJson
56
import com.bitwarden.network.model.KdfTypeJson
67
import com.bitwarden.ui.platform.base.MainDispatcherExtension
78
import com.bitwarden.ui.platform.resource.BitwardenString
@@ -144,6 +145,7 @@ class UserLogoutManagerTest {
144145
val pinProtectedUserKey = "pinProtectedUserKey"
145146
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
146147
val encryptedPin = "encryptedPin"
148+
val accountKeys: AccountKeysJson = mockk()
147149

148150
every { authDiskSource.userState } returns MULTI_USER_STATE
149151
every {
@@ -172,6 +174,10 @@ class UserLogoutManagerTest {
172174
every {
173175
authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
174176
} just runs
177+
every { authDiskSource.getAccountKeys(userId = userId) } returns accountKeys
178+
every {
179+
authDiskSource.storeAccountKeys(userId = userId, accountKeys = accountKeys)
180+
} just runs
175181

176182
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.Timeout)
177183

@@ -204,6 +210,7 @@ class UserLogoutManagerTest {
204210
pinProtectedUserKey = pinProtectedUserKey,
205211
)
206212
authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
213+
authDiskSource.storeAccountKeys(userId = userId, accountKeys = accountKeys)
207214
}
208215
}
209216

@@ -215,6 +222,7 @@ class UserLogoutManagerTest {
215222
val pinProtectedUserKey = "pinProtectedUserKey"
216223
val pinProtectedUserKeyEnvelope = "pinProtectedUserKeyEnvelope"
217224
val encryptedPin = "encryptedPin"
225+
val accountKeys: AccountKeysJson = mockk()
218226

219227
every { authDiskSource.userState } returns MULTI_USER_STATE
220228
every {
@@ -243,6 +251,10 @@ class UserLogoutManagerTest {
243251
every {
244252
authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
245253
} just runs
254+
every { authDiskSource.getAccountKeys(userId = userId) } returns accountKeys
255+
every {
256+
authDiskSource.storeAccountKeys(userId = userId, accountKeys = accountKeys)
257+
} just runs
246258

247259
userLogoutManager.softLogout(userId = userId, reason = LogoutReason.Timeout)
248260

@@ -269,6 +281,7 @@ class UserLogoutManagerTest {
269281
pinProtectedUserKey = pinProtectedUserKey,
270282
)
271283
authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
284+
authDiskSource.storeAccountKeys(userId = userId, accountKeys = accountKeys)
272285
}
273286
}
274287

app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.bitwarden.data.datasource.disk.model.ServerConfig
2626
import com.bitwarden.data.datasource.disk.util.FakeConfigDiskSource
2727
import com.bitwarden.data.repository.model.Environment
2828
import com.bitwarden.network.model.ConfigResponseJson
29+
import com.bitwarden.network.model.CreateAccountKeysResponseJson
2930
import com.bitwarden.network.model.DeleteAccountResponseJson
3031
import com.bitwarden.network.model.GetTokenResponseJson
3132
import com.bitwarden.network.model.IdentityTokenAuthModel
@@ -1137,7 +1138,12 @@ class AuthRepositoryTest {
11371138
publicKey = userPublicKey,
11381139
encryptedPrivateKey = userPrivateKey,
11391140
)
1140-
} returns Unit.asSuccess()
1141+
} returns CreateAccountKeysResponseJson(
1142+
key = null,
1143+
publicKey = userPublicKey,
1144+
privateKey = userPrivateKey,
1145+
accountKeys = null,
1146+
).asSuccess()
11411147
coEvery {
11421148
organizationService.organizationResetPasswordEnroll(
11431149
organizationId = orgId,
@@ -1222,7 +1228,12 @@ class AuthRepositoryTest {
12221228
publicKey = userPublicKey,
12231229
encryptedPrivateKey = userPrivateKey,
12241230
)
1225-
} returns Unit.asSuccess()
1231+
} returns CreateAccountKeysResponseJson(
1232+
key = null,
1233+
publicKey = userPublicKey,
1234+
privateKey = userPrivateKey,
1235+
accountKeys = ACCOUNT_KEYS,
1236+
).asSuccess()
12261237
coEvery {
12271238
organizationService.organizationResetPasswordEnroll(
12281239
organizationId = orgId,
@@ -1236,6 +1247,7 @@ class AuthRepositoryTest {
12361247
val result = repository.createNewSsoUser()
12371248

12381249
fakeAuthDiskSource.assertPrivateKey(userId = USER_ID_1, privateKey = userPrivateKey)
1250+
fakeAuthDiskSource.assertAccountKeys(userId = USER_ID_1, accountKeys = ACCOUNT_KEYS)
12391251
assertEquals(NewSsoUserResult.Success, result)
12401252
coVerify(exactly = 1) {
12411253
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
@@ -1310,7 +1322,12 @@ class AuthRepositoryTest {
13101322
publicKey = userPublicKey,
13111323
encryptedPrivateKey = userPrivateKey,
13121324
)
1313-
} returns Unit.asSuccess()
1325+
} returns CreateAccountKeysResponseJson(
1326+
key = null,
1327+
publicKey = userPublicKey,
1328+
privateKey = userPrivateKey,
1329+
accountKeys = ACCOUNT_KEYS,
1330+
).asSuccess()
13141331
coEvery {
13151332
organizationService.organizationResetPasswordEnroll(
13161333
organizationId = orgId,
@@ -1330,6 +1347,7 @@ class AuthRepositoryTest {
13301347
val result = repository.createNewSsoUser()
13311348

13321349
fakeAuthDiskSource.assertPrivateKey(userId = USER_ID_1, privateKey = userPrivateKey)
1350+
fakeAuthDiskSource.assertAccountKeys(userId = USER_ID_1, accountKeys = ACCOUNT_KEYS)
13331351
assertEquals(NewSsoUserResult.Success, result)
13341352
coVerify(exactly = 1) {
13351353
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)

network/src/main/kotlin/com/bitwarden/network/api/AuthenticatedAccountsApi.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.bitwarden.network.api
22

33
import com.bitwarden.network.model.CreateAccountKeysRequest
4+
import com.bitwarden.network.model.CreateAccountKeysResponseJson
45
import com.bitwarden.network.model.DeleteAccountRequestJson
56
import com.bitwarden.network.model.NetworkResult
67
import com.bitwarden.network.model.ResetPasswordRequestJson
@@ -26,7 +27,9 @@ internal interface AuthenticatedAccountsApi {
2627
* Creates the keys for the current account.
2728
*/
2829
@POST("/accounts/keys")
29-
suspend fun createAccountKeys(@Body body: CreateAccountKeysRequest): NetworkResult<Unit>
30+
suspend fun createAccountKeys(
31+
@Body body: CreateAccountKeysRequest,
32+
): NetworkResult<CreateAccountKeysResponseJson>
3033

3134
/**
3235
* Deletes the current account.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.bitwarden.network.model
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
/**
7+
* Response object returned when creating account keys.
8+
*
9+
* @property key The user key (nullable).
10+
* @property publicKey The public key for the account.
11+
* @property privateKey The encrypted private key for the account.
12+
* @property accountKeys The account keys containing encryption key pairs and security state.
13+
*/
14+
@Serializable
15+
data class CreateAccountKeysResponseJson(
16+
@SerialName("key")
17+
val key: String?,
18+
19+
@SerialName("publicKey")
20+
val publicKey: String?,
21+
22+
@SerialName("privateKey")
23+
val privateKey: String?,
24+
25+
@SerialName("accountKeys")
26+
val accountKeys: AccountKeysJson?,
27+
)

network/src/main/kotlin/com/bitwarden/network/service/AccountsService.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.bitwarden.network.service
22

3+
import com.bitwarden.network.model.CreateAccountKeysResponseJson
34
import com.bitwarden.network.model.DeleteAccountResponseJson
45
import com.bitwarden.network.model.KeyConnectorKeyRequestJson
56
import com.bitwarden.network.model.KeyConnectorMasterKeyResponseJson
@@ -26,7 +27,10 @@ interface AccountsService {
2627
/**
2728
* Creates a new account's keys.
2829
*/
29-
suspend fun createAccountKeys(publicKey: String, encryptedPrivateKey: String): Result<Unit>
30+
suspend fun createAccountKeys(
31+
publicKey: String,
32+
encryptedPrivateKey: String,
33+
): Result<CreateAccountKeysResponseJson>
3034

3135
/**
3236
* Make delete account request.

network/src/main/kotlin/com/bitwarden/network/service/AccountsServiceImpl.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.bitwarden.network.api.AuthenticatedKeyConnectorApi
55
import com.bitwarden.network.api.UnauthenticatedAccountsApi
66
import com.bitwarden.network.api.UnauthenticatedKeyConnectorApi
77
import com.bitwarden.network.model.CreateAccountKeysRequest
8+
import com.bitwarden.network.model.CreateAccountKeysResponseJson
89
import com.bitwarden.network.model.DeleteAccountRequestJson
910
import com.bitwarden.network.model.DeleteAccountResponseJson
1011
import com.bitwarden.network.model.KeyConnectorKeyRequestJson
@@ -50,7 +51,7 @@ internal class AccountsServiceImpl(
5051
override suspend fun createAccountKeys(
5152
publicKey: String,
5253
encryptedPrivateKey: String,
53-
): Result<Unit> =
54+
): Result<CreateAccountKeysResponseJson> =
5455
authenticatedAccountsApi
5556
.createAccountKeys(
5657
body = CreateAccountKeysRequest(

network/src/test/kotlin/com/bitwarden/network/service/AccountsServiceTest.kt

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,10 @@ class AccountsServiceTest : BaseServiceTest() {
5353
}
5454

5555
@Test
56-
fun `createAccountKeys with empty response is success`() = runTest {
56+
fun `createAccountKeys success response should return Success`() = runTest {
5757
val publicKey = "publicKey"
5858
val encryptedPrivateKey = "encryptedPrivateKey"
59-
val json = ""
60-
val response = MockResponse().setBody(json)
59+
val response = MockResponse().setBody(CREATE_ACCOUNT_KEYS_REQUEST_RESPONSE)
6160
server.enqueue(response)
6261

6362
val result = service.createAccountKeys(
@@ -368,3 +367,22 @@ private val UPDATE_KDF_REQUEST = UpdateKdfJsonRequest(
368367
salt = "mockSalt",
369368
),
370369
)
370+
private val CREATE_ACCOUNT_KEYS_REQUEST_RESPONSE = """
371+
{
372+
"key": null,
373+
"publicKey": "mockPublicKey-1",
374+
"privateKey": "mockPrivateKey-1",
375+
"accountKeys": {
376+
"signatureKeyPair": null,
377+
"publicKeyEncryptionKeyPair": {
378+
"wrappedPrivateKey": "mockWrappedPrivateKey-1",
379+
"publicKey": "mockPublicKey-1",
380+
"signedPublicKey": null,
381+
"object": "publicKeyEncryptionKeyPair"
382+
},
383+
"securityState": null,
384+
"object": "privateKeys"
385+
},
386+
"object": "keys"
387+
}
388+
""".trimIndent()

0 commit comments

Comments
 (0)