8000 [PM-8352] Fido2 creation user verification by fedemkr · Pull Request #736 · bitwarden/ios · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

[PM-8352] Fido2 creation user verification #736

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

Merged
merged 48 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
342991d
PM-8533 Improved autofill vault item rows UI.
fedemkr Jun 10, 2024
2377e34
Merge branch 'main' into mobiletf/pm-8533/improve-autofill-cipher-ui
fedemkr Jun 10, 2024
98a9c06
Update GlobalTestHelpers/Support/ParametrizedTestRunner.swift
fedemkr Jun 11, 2024
0bfafac
Update BitwardenShared/UI/Vault/Views/VaultListItemRow/VaultListItemR…
fedemkr Jun 11, 2024
962bf44
Merge branch 'main' into mobiletf/pm-8533/improve-autofill-cipher-ui
fedemkr Jun 11, 2024
91f787a
PM-8533 Fixed mainFido2Credential docs.
fedemkr Jun 11, 2024
6a76499
Update BitwardenShared/Core/Vault/Models/Views/LoginView+Fido2.swift
fedemkr Jun 11, 2024
efa0f03
PM-8533 Address PR feedback. Renamed ParametrizedTestRunner to Combin…
fedemkr Jun 11, 2024
3d0e01b
Merge branch 'main' into mobiletf/pm-8533/improve-autofill-cipher-ui
fedemkr Jun 12, 2024
9b40a10
PM-8999 Updated SDK to revision 47ca291
fedemkr Jun 18, 2024
208f8ae
Merge branch 'main' into mobiletf/pm-8999/update-sdk-to-47ca291
fedemkr Jun 18, 2024
b9e2915
Merge branch 'mobiletf/pm-8999/update-sdk-to-47ca291' into mobiletf/p…
fedemkr Jun 18, 2024
398f579
PM-8533 Updated to use the latest SDK changes with the Fido2Credentia…
fedemkr Jun 19, 2024
e3591e9
Merge branch 'main' into mobiletf/pm-8533/improve-autofill-cipher-ui
fedemkr Jun 19, 2024
f737944
Merge branch 'main' into mobiletf/pm-8863/fido2-creation
fedemkr Jun 19, 2024
a26093a
Merge branch 'mobiletf/pm-8533/improve-autofill-cipher-ui' into mobil…
fedemkr Jun 19, 2024
468e01b
PM-8863 Start implementing Fido2 credential creation process
fedemkr Jun 19, 2024
5316c5a
PM-8863 Improve naming and code
fedemkr Jun 19, 2024
d6df160
Merge branch 'main' into mobiletf/pm-8863/fido2-creation
fedemkr Jun 24, 2024
8000
4912d0f
Merge branch 'main' into mobiletf/pm-8863/fido2-creation
fedemkr Jun 26, 2024
5c547df
Merge branch 'main' into mobiletf/pm-8863/fido2-creation
fedemkr Jul 1, 2024
97331c8
Merge branch 'main' into mobiletf/pm-8863/fido2-creation
fedemkr Jul 1, 2024
6181298
PM-8863 Implemented Fido2 creation flow for new customizable cipher
fedemkr Jul 1, 2024
1c77d8c
PM-8863 Added unit tests and some test helpers like InvocationMocker.
fedemkr Jul 3, 2024
c431905
PM-8863 Fix typo
fedemkr Jul 3, 2024
b106f95
PM-8863 Fix active cipher condition and added tests for Fido2Credenti…
fedemkr Jul 4, 2024
e41955d
PM-8863 Refactored the CredentialProviderContext so we don't need the…
fedemkr Jul 4, 2024
6ba6b56
PM-8863 Updated tests
fedemkr Jul 4, 2024
1a5b1b7
Merge branch 'main' into mobiletf/pm-8863/fido2-creation and adapted …
fedemkr Jul 9, 2024
0b7fa60
PM-8863 Addressed PR feedback: Move Uv initialization from AS object …
fedemkr Jul 9, 2024
ab95b40
PM-8863 Restored ASCredentialProviderExtensionShowsConfigurationUI so…
fedemkr Jul 9, 2024
530600e
PM-8863 Removed Task wrapper in initFido2State and rearrange the code…
fedemkr Jul 9, 2024
16077b9
PM-8863 Simplified ExtensionMode so we avoid having available usage, …
fedemkr Jul 10, 2024
1af7cb9
Merge branch 'main' into mobiletf/pm-8863/fido2-creation
fedemkr Jul 10, 2024
99fd9f8
Update BitwardenShared/Core/Autofill/Utilities/CredentialProviderCont…
fedemkr Jul 11, 2024
e907762
Merge branch 'main' into mobiletf/pm-8863/fido2-creation
fedemkr Jul 11, 2024
ac74ca1
PM-8863 Updated docs and fix typos.
fedemkr Jul 11, 2024
f991296
PM-8863 Changed mock Data for repeating bytes to use the Data(repeati…
fedemkr Jul 11, 2024
636b29e
Update BitwardenShared/UI/Autofill/Application/Fido2AppExtensionDeleg…
fedemkr Jul 11, 2024
1b9d3ac
Merge branch 'main' into mobiletf/pm-8352/fido2-creation-user-verific…
fedemkr Jul 11, 2024
086d8dc
Merge branch 'main' into mobiletf/pm-8352/fido2-creation-user-verific…
fedemkr Jul 12, 2024
788ed56
Merge branch 'main' into mobiletf/pm-8352/fido2-creation-user-verific…
fedemkr Jul 15, 2024
8473c81
PM-8352 Implemented user verification while creating Fido2 credential…
fedemkr Jul 15, 2024
068c6f8
PM-8352 Added missing unit tests.
fedemkr Jul 16, 2024
7228196
Merge branch 'main' into mobiletf/pm-8352/fido2-creation-user-verific…
fedemkr Jul 16, 2024
35b3322
Update BitwardenShared/UI/Auth/Utilities/UserVerificationHelperTests.…
fedemkr Jul 16, 2024
2d9ddb2
Merge branch 'main' into mobiletf/pm-8352/fido2-creation-user-verific…
fedemkr Jul 16, 2024
bc28702
PM-8352 Improved testing and modified TODO jira ticket reference.
fedemkr Jul 16, 2024
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
34 changes: 34 additions & 0 deletions BitwardenShared/Core/Auth/Repositories/AuthRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@ protocol AuthRepository: AnyObject {
///
func validatePassword(_ password: String) async throws -> Bool

/// Validates thes user's entered PIN.
/// - Parameter pin: Pin to validate.
/// - Returns: `true` if valid, `false` otherwise.
func validatePin(pin: String) async -> Bool

/// Verifies that the entered one-time password matches the one sent to the user.
///
/// - Parameter otp: The user's one-time password to verify.
Expand Down Expand Up @@ -774,6 +779,35 @@ extension DefaultAuthRepository: AuthRepository {
}
}

func validatePin(pin: String) async -> Bool {
guard let pinProtectedUserKey = try? await stateService.pinProtectedUserKey() else {
return false
}

// HACK: As the SDK doesn't provide a way to directly validate the pin yet, we have this method
// which just tries to initialize the user crypto and if it succeeds then the PIN is correct, otherwise
// the PIN is incorrect.

do {
let account = try await stateService.getActiveAccount()
let encryptionKeys = try await stateService.getAccountEncryptionKeys()

try await clientService.crypto().initializeUserCrypto(
req: InitUserCryptoRequest(
kdfParams: account.kdf.sdkKdf,
email: account.profile.email,
privateKey: encryptionKeys.encryptedPrivateKey,
method: .pin(pin: pin, pinProtectedUserKey: pinProtectedUserKey)
)
)
try await organizationService.initializeOrganizationCrypto()

return true
} catch {
return false
}
}

func verifyOtp(_ otp: String) async throws {
try await accountAPIService.verifyOtp(otp)
}
Expand Down
113 changes: 113 additions & 0 deletions BitwardenShared/Core/Auth/Repositories/AuthRepositoryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,119 @@ class AuthRepositoryTests: BitwardenTestCase { // swiftlint:disable:this type_bo
XCTAssertFalse(isValid)
}

/// `validatePin(_:)` returns `true` if the pin is valid when initializing the user crypto.
func test_validatePin() async throws {
let account = Account.fixture()
stateService.activeAccount = account

stateService.accountEncryptionKeys = [
"1": AccountEncryptionKeys(encryptedPrivateKey: "PRIVATE_KEY", encryptedUserKey: "USER_KEY"),
]

stateService.encryptedPinByUserId[account.profile.userId] = "123"
stateService.pinProtectedUserKeyValue[account.profile.userId] = "123"

let isPinValid = await subject.validatePin(pin: "123")

XCTAssertEqual(
clientService.mockCrypto.initializeUserCryptoRequest,
InitUserCryptoRequest(
kdfParams: .pbkdf2(iterations: UInt32(Constants.pbkdf2Iterations)),
email: "user@bitwarden.com",
privateKey: "PRIVATE_KEY",
method: .pin(pin: "123", pinProtectedUserKey: "123")
)
)
XCTAssertTrue(isPinValid)
}

/// `validatePin(_:)` returns `false` if the there is no active account.
func test_validatePin_noActiveAccount() async throws {
let account = Account.fixture()

stateService.accountEncryptionKeys = [
"1": AccountEncryptionKeys(encryptedPrivateKey: "PRIVATE_KEY", encryptedUserKey: "USER_KEY"),
]

stateService.encryptedPinByUserId[account.profile.userId] = "123"

let isPinValid = await subject.validatePin(pin: "123")

XCTAssertNotEqual(
clientService.mockCrypto.initializeUserCryptoRequest,
InitUserCryptoRequest(
kdfParams: .pbkdf2(iterations: UInt32(Constants.pbkdf2Iterations)),
email: "user@bitwarden.com",
privateKey: "PRIVATE_KEY",
method: .pin(pin: "123", pinProtectedUserKey: "123")
)
)
XCTAssertFalse(isPinValid)
}

/// `validatePin(_:)` returns `false` if the there is no pin protected user key.
func test_validatePin_noPinProtectedUserKey() async throws {
let account = Account.fixture()
stateService.activeAccount = account

stateService.accountEncryptionKeys = [
"1": AccountEncryptionKeys(encryptedPrivateKey: "PRIVATE_KEY", encryptedUserKey: "USER_KEY"),
]

stateService.encryptedPinByUserId[account.profile.userId] = "123"

let isPinValid = await subject.validatePin(pin: "123")

XCTAssertNotEqual(
clientService.mockCrypto.initializeUserCryptoRequest,
InitUserCryptoRequest(
kdfParams: .pbkdf2(iterations: UInt32(Constants.pbkdf2Iterations)),
email: "user@bitwarden.com",
privateKey: "PRIVATE_KEY",
method: .pin(pin: "123", pinProtectedUserKey: "123")
)
)
XCTAssertFalse(isPinValid)
}

/// `validatePin(_:)` returns `false` if initializing user crypto throws.
func test_validatePin_initializeUserCryptoThrows() async throws {
let account = Account.fixture()
stateService.activeAccount = account

stateService.accountEncryptionKeys = [
"1": AccountEncryptionKeys(encryptedPrivateKey: "PRIVATE_KEY", encryptedUserKey: "USER_KEY"),
]

stateService.encryptedPinByUserId[account.profile.userId] = "123"
stateService.pinProtectedUserKeyValue[account.profile.userId] = "123"

clientService.mockCrypto.initializeUserCryptoResult = .failure(BitwardenTestError.example)

let isPinValid = await subject.validatePin(pin: "123")

XCTAssertFalse(isPinValid)
}

/// `validatePin(_:)` returns `false` if initializing org crypto throws.
func test_validatePin_initializeOrgCryptoThrows() async throws {
let account = Account.fixture()
stateService.activeAccount = account

stateService.accountEncryptionKeys = [
"1": AccountEncryptionKeys(encryptedPrivateKey: "PRIVATE_KEY", encryptedUserKey: "USER_KEY"),
]

stateService.encryptedPinByUserId[account.profile.userId] = "123"
stateService.pinProtectedUserKeyValue[account.profile.userId] = "123"

organizationService.initializeOrganizationCryptoError = BitwardenTestError.example

let isPinValid = await subject.validatePin(pin: "123")

XCTAssertFalse(isPinValid)
}

/// `verifyOtp(_:)` makes an API request to verify an OTP code.
func test_verifyOtp() async throws {
client.result = .httpSuccess(testData: .emptyResponse)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ class MockAuthRepository: AuthRepository { // swiftlint:disable:this type_body_l
var validatePasswordPasswords = [String]()
var validatePasswordResult: Result<Bool, Error> = .success(true)

var validatePinResult: Bool = true

var vaultTimeout = [String: SessionTimeoutValue]()

func allowBioMetricUnlock(_ enabled: Bool) async throws {
Expand Down Expand Up @@ -303,6 +305,10 @@ class MockAuthRepository: AuthRepository { // swiftlint:disable:this type_body_l
return try validatePasswordResult.get()
}

func validatePin(pin: String) async -> Bool {
validatePinResult
}

func verifyOtp(_ otp: String) async throws {
verifyOtpOpt = otp
try verifyOtpResult.get()
Expand Down
15 changes: 12 additions & 3 deletions BitwardenShared/UI/Auth/Utilities/UserVerificationHelper.swift
F438
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,18 @@ extension DefaultUserVerificationHelper: UserVerificationHelper {
continuation.resume(throwing: UserVerificationError.cancelled)
},
settingUp: false,
completion: { _ in
// TODO: PM-8388 Perform PIN verification when method available from SDK
continuation.resume(returning: .notVerified)
completion: { pin in
guard await self.authRepository.validatePin(pin: pin) else {
self.userVerificationDelegate?.showAlert(
.defaultAlert(title: Localizations.invalidPIN),
onDismissed: {
continuation.resume(returning: .notVerified)
}
)
return
}

continuation.resume(returning: .verified)
}
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,26 +219,102 @@ class UserVerificationHelperTests: BitwardenTestCase {
}

/// `verifyPin()` unable to perform when auth repository pin unlock is not available.
func test_verifyPin_unableToPerformR3c0rdables() async throws {
func test_verifyPin_unableToPerform() async throws {
authRepository.isPinUnlockAvailableResult = .success(false)

let result = try await subject.verifyPin()

XCTAssertEqual(result, .unableToPerform)
}

// TODO: PM-8388 Add more tests for `verifyPin`
/// `verifyPin()` with cancelled verification.
func test_verifyPin_cancelled() async throws {
authRepository.isPinUnlockAvailableResult = .success(true)
let task = Task {
try await self.subject.verifyPin()
}

try await waitForAsync {
!self.userVerificationDelegate.alertShown.isEmpty
}

let alert = try XCTUnwrap(userVerificationDelegate.alertShown.last)
try await alert.tapAction(title: Localizations.cancel)

await assertAsyncThrows(error: UserVerificationError.cancelled) {
_ = try await task.value
}
}

/// `verifyPin()` with verified PIN.
func test_verifyPin_verified() async throws {
authRepository.isPinUnlockAvailableResult = .success(true)
authRepository.validatePinResult = true

let task = Task {
try await self.subject.verifyPin()
}

try await waitForAsync {
!self.userVerificationDelegate.alertShown.isEmpty
}

try await enterPinInAlertAndSubmit()

let result = try await task.value

XCTAssertEqual(result, .verified)
}

/// `verifyPin()` with not verified PIN.
func test_verifyPin_notVerified() async throws {
authRepository.isPinUnlockAvailableResult = .success(true)
authRepository.validatePinResult = false

let task = Task {
try await self.subject.verifyPin()
}

try await waitForAsync {
!self.userVerificationDelegate.alertShown.isEmpty
}

try await enterPinInAlertAndSubmit()

try await waitForAsync {
self.userVerificationDelegate.alertShown
.last?.title == Localizations.invalidPIN
}

let alert = try XCTUnwrap(userVerificationDelegate.alertShown.last)

XCTAssertEqual(alert, .defaultAlert(title: Localizations.invalidPIN))

try await alert.tapAction(title: Localizations.ok)

userVerificationDelegate.alertOnDismissed?()

let result = try await task.value

XCTAssertEqual(result, .notVerified)
}

// MARK: Private

private func enterMasterPasswordInAlertAndSubmit() async throws {
let alert = try XCTUnwrap(userVerificationDelegate.alertShown.last)

XCTAssertEqual(alert, .masterPasswordPrompt { _ in })
var textField = try XCTUnwrap(alert.alertTextFields.first)
textField = AlertTextField(id: "password", text: "password")

try await alert.tapAction(title: Localizations.submit, alertTextFields: [textField])
try alert.setText("password", forTextFieldWithId: "password")
try await alert.tapAction(title: Localizations.submit)
}

private func enterPinInAlertAndSubmit() async throws {
let alert = try XCTUnwrap(userVerificationDelegate.alertShown.last)
XCTAssertEqual(alert, .enterPINCode(settingUp: false) { _ in })

try alert.setText("pin", forTextFieldWithId: "pin")
try await alert.tapAction(title: Localizations.submit)
}
}

Expand Down
Loading
Loading
0