Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ class AutofillCredentialServiceAppExtensionTests: BitwardenTestCase { // swiftli
return identityStore.saveCredentialIdentitiesCalled
}

XCTAssertTrue(stateService.doesActiveAccountHavePremiumCalled)
XCTAssertTrue(identityStore.saveCredentialIdentitiesCalled)
XCTAssertEqual(
identityStore.saveCredentialIdentitiesIdentities,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ protocol AutofillCredentialService: AnyObject {
/// A default implementation of an `AutofillCredentialService`.
///
class DefaultAutofillCredentialService {

// MARK: Private Properties

/// Helper to know about the app context.
Expand Down Expand Up @@ -339,7 +338,7 @@ class DefaultAutofillCredentialService {
///
/// - Parameter userId: The ID of the user whose ciphers and device auth key should be added to the identity store.
///
private func replaceAllIdentities(userId: String) async { // swiftlint:disable:this function_body_length
private func replaceAllIdentities(userId: String) async {
guard await identityStore.state().isEnabled else { return }

do {
Expand All @@ -355,8 +354,12 @@ class DefaultAutofillCredentialService {

if #available(iOS 17, *) {
var identities = [ASCredentialIdentity]()
let accountHasPremium = await stateService.doesActiveAccountHavePremium()
for cipher in decryptedCiphers {
let newIdentities = await credentialIdentityFactory.createCredentialIdentities(from: cipher)
let newIdentities = await credentialIdentityFactory.createCredentialIdentities(
from: cipher,
accountHasPremium: accountHasPremium,
)
identities.append(contentsOf: newIdentities)
}

Expand Down Expand Up @@ -582,6 +585,10 @@ extension DefaultAutofillCredentialService: AutofillCredentialService {
throw ASExtensionError(.userInteractionRequired)
}

guard await totpService.isTotpAuthorized(for: cipher) else {
throw ASExtensionError(.credentialIdentityNotFound)
}

guard let vault = try? await clientService.vault(),
let code = try? vault.generateTOTPCode(for: totpKey, date: timeProvider.presentTime) else {
throw ASExtensionError(.credentialIdentityNotFound)
Expand Down Expand Up @@ -636,7 +643,11 @@ extension DefaultAutofillCredentialService: AutofillCredentialService {
var identities = [ASCredentialIdentity]()
let decryptedCipher = try await clientService.vault().ciphers().decrypt(cipher: cipher)

let newIdentities = await credentialIdentityFactory.createCredentialIdentities(from: decryptedCipher)
let accountHasPremium = await stateService.doesActiveAccountHavePremium()
let newIdentities = await credentialIdentityFactory.createCredentialIdentities(
from: decryptedCipher,
accountHasPremium: accountHasPremium,
)
identities.append(contentsOf: newIdentities)

let fido2Identities = try await clientService.platform().fido2()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -694,8 +694,10 @@ class AutofillCredentialServiceTests: BitwardenTestCase { // swiftlint:disable:t

/// `provideFido2Credential(for:autofillCredentialServiceDelegate:fido2UserVerificationMediatorDelegate:)`
/// succeeds with device auth key.
@available(iOS 18.0, *)
func test_provideFido2Credential_succeeds_deviceAuthKey() async throws {
guard #available(iOS 18.0, *) else {
throw XCTSkip("Skipped on iOS < 18.0")
}
stateService.activeAccount = .fixture()
configService.featureFlagsBool[.deviceAuthKey] = true
let passkeyIdentity = ASPasskeyCredentialIdentity.fixture(
Expand Down Expand Up @@ -742,8 +744,10 @@ class AutofillCredentialServiceTests: BitwardenTestCase { // swiftlint:disable:t

/// `provideFido2Credential(for:autofillCredentialServiceDelegate:fido2UserVerificationMediatorDelegate:)`
/// skips device auth key logic if the feature flag is off.
@available(iOS 18.0, *)
func test_provideFido2Credential_skips_deviceAuthKey_featureFlagOff() async throws {
guard #available(iOS 18.0, *) else {
throw XCTSkip("Skipped on iOS < 18.0")
}
stateService.activeAccount = .fixture()
configService.featureFlagsBool[.deviceAuthKey] = false
let passkeyIdentity = ASPasskeyCredentialIdentity.fixture(
Expand Down Expand Up @@ -776,8 +780,10 @@ class AutofillCredentialServiceTests: BitwardenTestCase { // swiftlint:disable:t

/// `provideOTPCredential(for:autofillCredentialServiceDelegate:repromptPasswordValidated:)`
/// returns the credential containing the TOTP code for the specified ID.
@available(iOS 18.0, *)
func test_provideOTPCredential() async throws {
guard #available(iOS 18.0, *) else {
throw XCTSkip("Skipped on iOS < 18.0")
}
cipherService.fetchCipherResult = .success(
.fixture(login: .fixture(totp: "totpKey")),
)
Expand All @@ -796,8 +802,10 @@ class AutofillCredentialServiceTests: BitwardenTestCase { // swiftlint:disable:t

/// `provideOTPCredential(for:autofillCredentialServiceDelegate:repromptPasswordValidated:)`
/// throws an error if the cipher with the specified ID doesn't have a totp.
@available(iOS 18.0, *)
func test_provideOTPCredential_cipherMissingTOTP() async {
func test_provideOTPCredential_cipherMissingTOTP() async throws {
guard #available(iOS 18.0, *) else {
throw XCTSkip("Skipped on iOS < 18.0")
}
stateService.activeAccount = .fixture()
vaultTimeoutService.isClientLocked["1"] = false

Expand Down Expand Up @@ -831,8 +839,10 @@ class AutofillCredentialServiceTests: BitwardenTestCase { // swiftlint:disable:t

/// `provideOTPCredential(for:autofillCredentialServiceDelegate:repromptPasswordValidated:)`
/// throws an error if a cipher with the specified ID doesn't exist.
@available(iOS 18.0, *)
func test_provideOTPCredential_cipherNotFound() async {
func test_provideOTPCredential_cipherNotFound() async throws {
guard #available(iOS 18.0, *) else {
throw XCTSkip("Skipped on iOS < 18.0")
}
stateService.activeAccount = .fixture()
vaultTimeoutService.isClientLocked["1"] = false

Expand All @@ -847,8 +857,10 @@ class AutofillCredentialServiceTests: BitwardenTestCase { // swiftlint:disable:t

/// `provideOTPCredential(for:autofillCredentialServiceDelegate:repromptPasswordValidated:)`
/// unlocks the user's vault if they use never lock.
@available(iOS 18.0, *)
func test_provideOTPCredential_neverLock() async throws {
guard #available(iOS 18.0, *) else {
throw XCTSkip("Skipped on iOS < 18.0")
}
autofillCredentialServiceDelegate.unlockVaultWithNaverlockHandler = { [weak self] in
self?.vaultTimeoutService.isClientLocked["1"] = false
}
Expand All @@ -872,8 +884,10 @@ class AutofillCredentialServiceTests: BitwardenTestCase { // swiftlint:disable:t

/// `provideOTPCredential(for:autofillCredentialServiceDelegate:repromptPasswordValidated:)`
/// doesn't unlock the user's vault if they use never lock but it has been manually locked.
@available(iOS 18.0, *)
func test_provideOTPCredential_neverLockManuallyLocked() async throws {
guard #available(iOS 18.0, *) else {
throw XCTSkip("Skipped on iOS < 18.0")
}
autofillCredentialServiceDelegate.unlockVaultWithNaverlockHandler = { [weak self] in
self?.vaultTimeoutService.isClientLocked["1"] = false
}
Expand All @@ -897,8 +911,10 @@ class AutofillCredentialServiceTests: BitwardenTestCase { // swiftlint:disable:t

/// `provideOTPCredential(for:autofillCredentialServiceDelegate:repromptPasswordValidated:)`
/// throws an error if reprompt is required.
@available(iOS 18.0, *)
func test_provideOTPCredential_repromptRequired() async throws {
guard #available(iOS 18.0, *) else {
throw XCTSkip("Skipped on iOS < 18.0")
}
stateService.activeAccount = .fixture()
vaultTimeoutService.isClientLocked["1"] = false

Expand All @@ -919,10 +935,57 @@ class AutofillCredentialServiceTests: BitwardenTestCase { // swiftlint:disable:t
}
}

/// `provideOTPCredential(for:autofillCredentialServiceDelegate:repromptPasswordValidated:)`
/// succeeds when the user is authorized to use TOTP.
func test_provideOTPCredential_totpAuthorized() async throws {
guard #available(iOS 18.0, *) else {
throw XCTSkip("Skipped on iOS < 18.0")
}
cipherService.fetchCipherResult = .success(
.fixture(login: .fixture(totp: "totpKey")),
)
stateService.activeAccount = .fixture()
vaultTimeoutService.isClientLocked["1"] = false
clientService.mockVault.generateTOTPCodeResult = .success("123456")
totpService.isTotpAuthorizedResult = true

let credential = try await subject.provideOTPCredential(
for: "1",
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
repromptPasswordValidated: false,
)

XCTAssertEqual(credential.code, "123456")
}

/// `provideOTPCredential(for:autofillCredentialServiceDelegate:repromptPasswordValidated:)`
/// throws an error if the user is not authorized to use TOTP.
func test_provideOTPCredential_totpNotAuthorized() async throws {
guard #available(iOS 18.0, *) else {
throw XCTSkip("Skipped on iOS < 18.0")
}
cipherService.fetchCipherResult = .success(
.fixture(login: .fixture(totp: "totpKey")),
)
stateService.activeAccount = .fixture()
vaultTimeoutService.isClientLocked["1"] = false
totpService.isTotpAuthorizedResult = false

await assertAsyncThrows(error: ASExtensionError(.credentialIdentityNotFound)) {
_ = try await subject.provideOTPCredential(
for: "1",
autofillCredentialServiceDelegate: autofillCredentialServiceDelegate,
repromptPasswordValidated: false,
)
}
}

/// `provideOTPCredential(for:autofillCredentialServiceDelegate:repromptPasswordValidated:)`
/// throws an error if the user's vault is locked.
@available(iOS 18.0, *)
func test_provideOTPCredential_vaultLocked() async {
func test_provideOTPCredential_vaultLocked() async throws {
guard #available(iOS 18.0, *) else {
throw XCTSkip("Skipped on iOS < 18.0")
}
stateService.activeAccount = .fixture()
vaultTimeoutService.isClientLocked["1"] = true

Expand All @@ -937,8 +1000,10 @@ class AutofillCredentialServiceTests: BitwardenTestCase { // swiftlint:disable:t

/// `provideOTPCredential(for:autofillCredentialServiceDelegate:repromptPasswordValidated:)`
/// throws when generating TOTP code.
@available(iOS 18.0, *)
func test_provideOTPCredential_throwsGeneratingTOTPCode() async throws {
guard #available(iOS 18.0, *) else {
throw XCTSkip("Skipped on iOS < 18.0")
}
cipherService.fetchCipherResult = .success(
.fixture(login: .fixture(totp: "totpKey")),
)
Expand Down Expand Up @@ -1008,6 +1073,7 @@ class AutofillCredentialServiceTests: BitwardenTestCase { // swiftlint:disable:t
vaultTimeoutService.vaultLockStatusSubject.send(VaultLockStatus(isVaultLocked: false, userId: "1"))
waitFor(identityStore.replaceCredentialIdentitiesIdentities != nil)

XCTAssertTrue(stateService.doesActiveAccountHavePremiumCalled)
XCTAssertEqual(
identityStore.replaceCredentialIdentitiesIdentities,
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import BitwardenSdk
/// Protocol of the factory to create credential identities.
protocol CredentialIdentityFactory {
/// Creates the `ASCredentialIdentity` array from a `CipherView` (it may return empty).
/// - Parameter cipher: The cipher to get the identities from.
/// - Parameters:
/// - cipher: The cipher to get the identities from.
/// - accountHasPremium: Whether the active account has premium access.
/// - Returns: An array of `ASCredentialIdentity` (password or one time code)
@available(iOS 17.0, *)
func createCredentialIdentities(from cipher: CipherView) async -> [ASCredentialIdentity]
func createCredentialIdentities(from cipher: CipherView, accountHasPremium: Bool) async -> [ASCredentialIdentity]

/// Tries to create a `ASPasswordCredentialIdentity` from the given `cipher`
/// - Parameter cipher: CIpher to create the password identity.
Expand All @@ -18,10 +20,11 @@ protocol CredentialIdentityFactory {
/// Default implementation of `CredentialIdentityFactory` to create credential identities.
struct DefaultCredentialIdentityFactory: CredentialIdentityFactory {
@available(iOS 17.0, *)
func createCredentialIdentities(from cipher: CipherView) async -> [ASCredentialIdentity] {
func createCredentialIdentities(from cipher: CipherView, accountHasPremium: Bool) async -> [ASCredentialIdentity] {
var identities = [ASCredentialIdentity]()

if let oneTimeCodeIdentity = tryCreateOneTimeCodeIdentity(from: cipher) {
let isTotpAuthorized = accountHasPremium || cipher.organizationUseTotp
if isTotpAuthorized, let oneTimeCodeIdentity = tryCreateOneTimeCodeIdentity(from: cipher) {
identities.append(oneTimeCodeIdentity)
}

Expand Down
Loading
Loading