Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PM-10947: Handle key connector unlock for existing user #842

Merged
merged 2 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading