Skip to content

Commit

Permalink
PM-10947: Handle key connector unlock for existing user (#842)
Browse files Browse the repository at this point in the history
  • Loading branch information
matt-livefront committed Aug 20, 2024
1 parent ffbd89a commit 9d00511
Show file tree
Hide file tree
Showing 22 changed files with 529 additions and 67 deletions.
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ identifier_name:

inclusive_language:
override_allowed_terms:
- masterKey
- masterPassword

custom_rules:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ extension IdentityTokenResponseModel {
kdfMemory: Int? = nil,
kdfParallelism: Int? = nil,
key: String? = "KEY",
keyConnectorUrl: String? = nil,
masterPasswordPolicy: MasterPasswordPolicyResponseModel? = nil,
privateKey: String? = "PRIVATE_KEY",
resetMasterPassword: Bool = false,
Expand All @@ -68,6 +69,7 @@ extension IdentityTokenResponseModel {
kdfMemory: kdfMemory,
kdfParallelism: kdfParallelism,
key: key,
keyConnectorUrl: keyConnectorUrl,
masterPasswordPolicy: masterPasswordPolicy,
privateKey: privateKey,
resetMasterPassword: resetMasterPassword,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ struct IdentityTokenResponseModel: Equatable, JSONResponse, KdfConfigProtocol {
/// The user's key.
let key: String?

/// The user's key connector URL.
let keyConnectorUrl: String?

/// Policies related to the user's master password.
let masterPasswordPolicy: MasterPasswordPolicyResponseModel?

Expand Down
19 changes: 19 additions & 0 deletions BitwardenShared/Core/Auth/Repositories/AuthRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ protocol AuthRepository: AnyObject {
///
func unlockVaultWithDeviceKey() async throws

/// Attempts to unlock the user's vault with the user's Key Connector key.
///
func unlockVaultWithKeyConnectorKey() async throws

/// Attempts to unlock the user's vault with the stored neverlock key.
///
func unlockVaultWithNeverlockKey() async throws
Expand Down Expand Up @@ -336,6 +340,9 @@ class DefaultAuthRepository {
/// The keychain service used by this repository.
private let keychainService: KeychainRepository

/// The service used by the application to manage Key Connector.
private let keyConnectorService: KeyConnectorService

/// The service used by the application to make organization-related API requests.
private let organizationAPIService: OrganizationAPIService

Expand Down Expand Up @@ -366,6 +373,7 @@ class DefaultAuthRepository {
/// - configService: The service to get server-specified configuration.
/// - environmentService: The service used by the application to manage the environment settings.
/// - keychainService: The keychain service used by the application.
/// - keyConnectorService: The service used by the application to manage Key Connector.
/// - organizationAPIService: The service used by the application to make organization-related API requests.
/// - organizationService: The service used to manage syncing and updates to the user's organizations.
/// - organizationUserAPIService: The service used by the application to make organization
Expand All @@ -382,6 +390,7 @@ class DefaultAuthRepository {
configService: ConfigService,
environmentService: EnvironmentService,
keychainService: KeychainRepository,
keyConnectorService: KeyConnectorService,
organizationAPIService: OrganizationAPIService,
organizationService: OrganizationService,
organizationUserAPIService: OrganizationUserAPIService,
Expand All @@ -396,6 +405,7 @@ class DefaultAuthRepository {
self.configService = configService
self.environmentService = environmentService
self.keychainService = keychainService
self.keyConnectorService = keyConnectorService
self.organizationAPIService = organizationAPIService
self.organizationService = organizationService
self.organizationUserAPIService = organizationUserAPIService
Expand Down Expand Up @@ -738,6 +748,15 @@ extension DefaultAuthRepository: AuthRepository {
))
}

func unlockVaultWithKeyConnectorKey() async throws {
let account = try await stateService.getActiveAccount()
let encryptionKeys = try await stateService.getAccountEncryptionKeys(userId: account.profile.userId)
guard let encryptedUserKey = encryptionKeys.encryptedUserKey else { throw StateServiceError.noEncUserKey }

let masterKey = try await keyConnectorService.getMasterKeyFromKeyConnector()
try await unlockVault(method: .keyConnector(masterKey: masterKey, userKey: encryptedUserKey))
}

func unlockVaultWithNeverlockKey() async throws {
let id = try await stateService.getActiveAccountId()
let key = KeychainItem.neverLock(userId: id)
Expand Down
53 changes: 53 additions & 0 deletions BitwardenShared/Core/Auth/Repositories/AuthRepositoryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
var clientService: MockClientService!
var configService: MockConfigService!
var environmentService: MockEnvironmentService!
var keyConnectorService: MockKeyConnectorService!
var keychainService: MockKeychainRepository!
var organizationService: MockOrganizationService!
var subject: DefaultAuthRepository!
Expand Down Expand Up @@ -89,6 +90,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
biometricsRepository = MockBiometricsRepository()
configService = MockConfigService()
environmentService = MockEnvironmentService()
keyConnectorService = MockKeyConnectorService()
keychainService = MockKeychainRepository()
organizationService = MockOrganizationService()
stateService = MockStateService()
Expand All @@ -103,6 +105,7 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
configService: configService,
environmentService: environmentService,
keychainService: keychainService,
keyConnectorService: keyConnectorService,
organizationAPIService: APIService(client: client),
organizationService: organizationService,
organizationUserAPIService: APIService(client: client),
Expand Down Expand Up @@ -1315,6 +1318,56 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
XCTAssertTrue(vaultTimeoutService.unlockVaultHadUserInteraction)
}

/// `unlockVaultWithKeyConnectorKey()` unlocks the user's vault with their key connector key.
func test_unlockVaultWithKeyConnectorKey() async {
clientService.mockCrypto.initializeUserCryptoResult = .success(())
keyConnectorService.getMasterKeyFromKeyConnectorResult = .success("key")
stateService.accountEncryptionKeys = [
"1": AccountEncryptionKeys(
encryptedPrivateKey: "private",
encryptedUserKey: "user"
),
]
stateService.activeAccount = .fixture()

await assertAsyncDoesNotThrow {
try await subject.unlockVaultWithKeyConnectorKey()
}

XCTAssertEqual(
clientService.mockCrypto.initializeUserCryptoRequest,
InitUserCryptoRequest(
kdfParams: KdfConfig().sdkKdf,
email: "user@bitwarden.com",
privateKey: "private",
method: .keyConnector(masterKey: "key", userKey: "user")
)
)
XCTAssertTrue(vaultTimeoutService.unlockVaultHadUserInteraction)
}

/// `unlockVaultWithKeyConnectorKey()` throws an error if the user is missing an encrypted user key.
func test_unlockVaultWithKeyConnectorKey_missingEncryptedUserKey() async {
stateService.activeAccount = .fixture()
stateService.accountEncryptionKeys = [
"1": AccountEncryptionKeys(
encryptedPrivateKey: "private",
encryptedUserKey: nil
),
]

await assertAsyncThrows(error: StateServiceError.noEncUserKey) {
try await subject.unlockVaultWithKeyConnectorKey()
}
}

/// `unlockVaultWithKeyConnectorKey()` throws an error if there's no active account.
func test_unlockVaultWithKeyConnectorKey_noActiveAccount() async {
await assertAsyncThrows(error: StateServiceError.noActiveAccount) {
try await subject.unlockVaultWithKeyConnectorKey()
}
}

/// `logout` throws an error with no accounts.
func test_logout_noAccounts() async {
stateService.accounts = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ class MockAuthRepository: AuthRepository { // swiftlint:disable:this type_body_l

var unlockVaultResult: Result<Void, Error> = .success(())
var unlockVaultWithBiometricsResult: Result<Void, Error> = .success(())
var unlockVaultWithDeviceKeyCalled = false
var unlockVaultWithDeviceKeyResult: Result<Void, Error> = .success(())
var unlockVaultWithKeyConnectorKeyCalled = false
var unlockVaultWithKeyConnectorKeyResult: Result<Void, Error> = .success(())
var unlockVaultWithNeverlockKeyCalled = false
var unlockVaultWithNeverlockResult: Result<Void, Error> = .success(())
Expand Down Expand Up @@ -252,10 +254,12 @@ class MockAuthRepository: AuthRepository { // swiftlint:disable:this type_body_l
}

func unlockVaultWithDeviceKey() async throws {
unlockVaultWithDeviceKeyCalled = true
try unlockVaultWithDeviceKeyResult.get()
}

func unlockVaultWithKeyConnectorKey() async throws {
unlockVaultWithKeyConnectorKeyCalled = true
try unlockVaultWithKeyConnectorKeyResult.get()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ extension APITestData {
)
static let identityTokenSuccessTwoFactorToken = loadFromJsonBundle(resource: "IdentityTokenSuccessTwoFactorToken")
static let identityTokenCaptchaError = loadFromJsonBundle(resource: "IdentityTokenCaptchaFailure")
static let identityTokenKeyConnector = loadFromJsonBundle(resource: "IdentityTokenKeyConnector")
static let identityTokenNoMasterPassword = loadFromJsonBundle(resource: "IdentityTokenNoMasterPassword")
static let identityTokenRefresh = loadFromJsonBundle(resource: "identityTokenRefresh")
static let identityTokenTrustedDevice = loadFromJsonBundle(resource: "IdentityTokenTrustedDevice")
static let identityTokenTwoFactorError = loadFromJsonBundle(resource: "IdentityTokenTwoFactorFailure")
static let preValidateSingleSignOn = loadFromJsonBundle(resource: "preValidateSingleSignOn")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTY5MDg4NzksInN1YiI6IjEzNTEyNDY3LTljZmUtNDNiMC05NjlmLTA3NTM0MDg0NzY0YiIsIm5hbWUiOiJCaXR3YXJkZW4gVXNlciIsImVtYWlsIjoidXNlckBiaXR3YXJkZW4uY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlhdCI6MTUxNjIzOTAyMiwicHJlbWl1bSI6ZmFsc2UsImFtciI6WyJBcHBsaWNhdGlvbiJdfQ.KDqC8kUaOAgBiUY8eeLa0a4xYWN8GmheXTFXmataFwM",
"expires_in": 3600,
"token_type": "Bearer",
"refresh_token": "REFRESH_TOKEN",
"scope": "api offline_access",
"PrivateKey": "PRIVATE_KEY",
"Key": "KEY",
"MasterPasswordPolicy": null,
"ForcePasswordReset": false,
"ResetMasterPassword": false,
"Kdf": 0,
"KdfIterations": 600000,
"KdfMemory": null,
"KdfParallelism": null,
"UserDecryptionOptions": {
"HasMasterPassword": false,
"KeyConnectorOption": {
"KeyConnectorUrl": "https://vault.bitwarden.com/key-connector"
},
"Object": "userDecryptionOptions"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTY5MDg4NzksInN1YiI6IjEzNTEyNDY3LTljZmUtNDNiMC05NjlmLTA3NTM0MDg0NzY0YiIsIm5hbWUiOiJCaXR3YXJkZW4gVXNlciIsImVtYWlsIjoidXNlckBiaXR3YXJkZW4uY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlhdCI6MTUxNjIzOTAyMiwicHJlbWl1bSI6ZmFsc2UsImFtciI6WyJBcHBsaWNhdGlvbiJdfQ.KDqC8kUaOAgBiUY8eeLa0a4xYWN8GmheXTFXmataFwM",
"expires_in": 3600,
"token_type": "Bearer",
"refresh_token": "REFRESH_TOKEN",
"scope": "api offline_access",
"MasterPasswordPolicy": null,
"ForcePasswordReset": false,
"ResetMasterPassword": false,
"Kdf": 0,
"KdfIterations": 600000,
"KdfMemory": null,
"KdfParallelism": null,
"UserDecryptionOptions": {
"HasMasterPassword": false,
"TrustedDeviceOption": {
"EncryptedPrivateKey": "private-key",
"EncryptedUserKey": "user-key",
"HasAdminApproval": false,
"HasLoginApprovingDevice": true,
"HasManageResetPasswordPermission": false
},
"Object": "userDecryptionOptions"
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import Foundation

// swiftlint:disable inclusive_language

// MARK: - KeyConnectorAPIService

/// A protocol for an API service used to make key connector requests.
Expand Down Expand Up @@ -38,5 +36,3 @@ extension APIService: KeyConnectorAPIService {
_ = try await service.send(PostKeyConnectorUserKeyRequest(body: body))
}
}

// swiftlint:enable inclusive_language
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import XCTest

@testable import BitwardenShared

// swiftlint:disable inclusive_language

class KeyConnectorAPIServiceTests: BitwardenTestCase {
// MARK: Properties

Expand Down Expand Up @@ -80,5 +78,3 @@ class KeyConnectorAPIServiceTests: BitwardenTestCase {
}
}
}

// swiftlint:enable inclusive_language
Loading

0 comments on commit 9d00511

Please sign in to comment.