1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

Improve MacOS Syncing

This changes the behaviour to react to logoff, but not to account locks. It also adds better error handling on the native side.
This commit is contained in:
Anders Åberg
2025-04-14 15:36:14 +02:00
parent 8d598c33ed
commit 6426dc51e6
3 changed files with 86 additions and 38 deletions

View File

@@ -14,40 +14,59 @@ void runSync(void* context, NSDictionary *params) {
// Map credentials to ASPasswordCredential objects
NSMutableArray *mappedCredentials = [NSMutableArray arrayWithCapacity:credentials.count];
for (NSDictionary *credential in credentials) {
NSString *type = credential[@"type"];
if ([type isEqualToString:@"password"]) {
NSString *cipherId = credential[@"cipherId"];
NSString *uri = credential[@"uri"];
NSString *username = credential[@"username"];
ASCredentialServiceIdentifier *serviceId = [[ASCredentialServiceIdentifier alloc]
initWithIdentifier:uri type:ASCredentialServiceIdentifierTypeURL];
ASPasswordCredentialIdentity *credential = [[ASPasswordCredentialIdentity alloc]
initWithServiceIdentifier:serviceId user:username recordIdentifier:cipherId];
[mappedCredentials addObject:credential];
}
if (@available(macos 14, *)) {
if ([type isEqualToString:@"fido2"]) {
@try {
NSString *type = credential[@"type"];
if ([type isEqualToString:@"password"]) {
NSString *cipherId = credential[@"cipherId"];
NSString *rpId = credential[@"rpId"];
NSString *userName = credential[@"userName"];
NSData *credentialId = decodeBase64URL(credential[@"credentialId"]);
NSData *userHandle = decodeBase64URL(credential[@"userHandle"]);
NSString *uri = credential[@"uri"];
NSString *username = credential[@"username"];
// Skip credentials with null username
if ([username isKindOfClass:[NSNull class]] || username.length == 0) {
NSLog(@"Skipping credential, username is empty: %@", credential);
continue;
}
Class passkeyCredentialIdentityClass = NSClassFromString(@"ASPasskeyCredentialIdentity");
id credential = [[passkeyCredentialIdentityClass alloc]
initWithRelyingPartyIdentifier:rpId
userName:userName
credentialID:credentialId
userHandle:userHandle
recordIdentifier:cipherId];
ASCredentialServiceIdentifier *serviceId = [[ASCredentialServiceIdentifier alloc]
initWithIdentifier:uri type:ASCredentialServiceIdentifierTypeURL];
ASPasswordCredentialIdentity *passwordIdentity = [[ASPasswordCredentialIdentity alloc]
initWithServiceIdentifier:serviceId user:username recordIdentifier:cipherId];
[mappedCredentials addObject:credential];
[mappedCredentials addObject:passwordIdentity];
}
else if (@available(macos 14, *)) {
if ([type isEqualToString:@"fido2"]) {
NSString *cipherId = credential[@"cipherId"];
NSString *rpId = credential[@"rpId"];
NSString *userName = credential[@"userName"];
// Skip credentials with null userName
if ([userName isKindOfClass:[NSNull class]] || userName.length == 0) {
NSLog(@"Skipping credential, username is empty: %@", credential);
continue;
}
NSData *credentialId = decodeBase64URL(credential[@"credentialId"]);
NSData *userHandle = decodeBase64URL(credential[@"userHandle"]);
Class passkeyCredentialIdentityClass = NSClassFromString(@"ASPasskeyCredentialIdentity");
id passkeyIdentity = [[passkeyCredentialIdentityClass alloc]
initWithRelyingPartyIdentifier:rpId
userName:userName
credentialID:credentialId
userHandle:userHandle
recordIdentifier:cipherId];
[mappedCredentials addObject:passkeyIdentity];
}
}
} @catch (NSException *exception) {
// Silently skip any credential that causes an exception
NSLog(@"ERROR: Exception processing credential: %@ - %@", exception.name, exception.reason);
continue;
}
}
@@ -59,4 +78,4 @@ void runSync(void* context, NSDictionary *params) {
_return(context, _success(@{@"added": @([mappedCredentials count])}));
}];
}
}

View File

@@ -332,6 +332,7 @@ const safeProviders: SafeProvider[] = [
ConfigService,
Fido2AuthenticatorServiceAbstraction,
AccountService,
AuthService,
],
}),
safeProvider({

View File

@@ -2,6 +2,8 @@ import { Injectable, OnDestroy } from "@angular/core";
import { autofill } from "desktop_native/napi";
import {
Subject,
combineLatest,
debounceTime,
distinctUntilChanged,
filter,
firstValueFrom,
@@ -12,6 +14,8 @@ import {
} from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
@@ -52,6 +56,7 @@ export class DesktopAutofillService implements OnDestroy {
private configService: ConfigService,
private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction<NativeWindowObject>,
private accountService: AccountService,
private authService: AuthService,
) {}
async init() {
@@ -59,21 +64,39 @@ export class DesktopAutofillService implements OnDestroy {
.getFeatureFlag$(FeatureFlag.MacOsNativeCredentialSync)
.pipe(
distinctUntilChanged(),
switchMap((enabled) => {
return this.accountService.activeAccount$.pipe(
map((account) => account?.id),
filter((userId): userId is UserId => userId != null),
switchMap((userId) => this.cipherService.cipherViews$(userId)),
//filter((enabled) => enabled === true), // Only proceed if feature is enabled
switchMap(() => {
return combineLatest([
this.accountService.activeAccount$.pipe(
map((account) => account?.id),
filter((userId): userId is UserId => userId != null),
),
this.authService.activeAccountStatus$,
]).pipe(
// Only proceed when the vault is unlocked
filter(([, status]) => status === AuthenticationStatus.Unlocked),
// Then get cipher views
switchMap(([userId]) => this.cipherService.cipherViews$(userId)),
);
}),
// TODO: This will unset all the autofill credentials on the OS
// when the account locks. We should instead explicilty clear the credentials
// when the user logs out. Maybe by subscribing to the encrypted ciphers observable instead.
debounceTime(100), // just a precaution to not spam the sync if there are multiple changes (we typically observe a null change)
// No filter for empty arrays here - we want to sync even if there are 0 items
filter((cipherViewMap) => cipherViewMap !== null),
mergeMap((cipherViewMap) => this.sync(Object.values(cipherViewMap ?? []))),
takeUntil(this.destroy$),
)
.subscribe();
// Listen for sign out to clear credentials?
this.authService.activeAccountStatus$
.pipe(
filter((status) => status === AuthenticationStatus.LoggedOut),
mergeMap(() => this.sync([])), // sync an empty array
takeUntil(this.destroy$),
)
.subscribe();
this.listenIpc();
}
@@ -131,6 +154,11 @@ export class DesktopAutofillService implements OnDestroy {
}));
}
this.logService.warning("Syncing autofill credentials", {
fido2Credentials,
passwordCredentials,
});
const syncResult = await ipc.autofill.runCommand<NativeAutofillSyncCommand>({
namespace: "autofill",
command: "sync",