mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 05:43:41 +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:
@@ -14,40 +14,59 @@ void runSync(void* context, NSDictionary *params) {
|
|||||||
|
|
||||||
// Map credentials to ASPasswordCredential objects
|
// Map credentials to ASPasswordCredential objects
|
||||||
NSMutableArray *mappedCredentials = [NSMutableArray arrayWithCapacity:credentials.count];
|
NSMutableArray *mappedCredentials = [NSMutableArray arrayWithCapacity:credentials.count];
|
||||||
|
|
||||||
for (NSDictionary *credential in credentials) {
|
for (NSDictionary *credential in credentials) {
|
||||||
NSString *type = credential[@"type"];
|
@try {
|
||||||
|
NSString *type = credential[@"type"];
|
||||||
if ([type isEqualToString:@"password"]) {
|
|
||||||
NSString *cipherId = credential[@"cipherId"];
|
if ([type isEqualToString:@"password"]) {
|
||||||
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"]) {
|
|
||||||
NSString *cipherId = credential[@"cipherId"];
|
NSString *cipherId = credential[@"cipherId"];
|
||||||
NSString *rpId = credential[@"rpId"];
|
NSString *uri = credential[@"uri"];
|
||||||
NSString *userName = credential[@"userName"];
|
NSString *username = credential[@"username"];
|
||||||
NSData *credentialId = decodeBase64URL(credential[@"credentialId"]);
|
|
||||||
NSData *userHandle = decodeBase64URL(credential[@"userHandle"]);
|
// 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");
|
ASCredentialServiceIdentifier *serviceId = [[ASCredentialServiceIdentifier alloc]
|
||||||
id credential = [[passkeyCredentialIdentityClass alloc]
|
initWithIdentifier:uri type:ASCredentialServiceIdentifierTypeURL];
|
||||||
initWithRelyingPartyIdentifier:rpId
|
ASPasswordCredentialIdentity *passwordIdentity = [[ASPasswordCredentialIdentity alloc]
|
||||||
userName:userName
|
initWithServiceIdentifier:serviceId user:username recordIdentifier:cipherId];
|
||||||
credentialID:credentialId
|
|
||||||
userHandle:userHandle
|
|
||||||
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])}));
|
_return(context, _success(@{@"added": @([mappedCredentials count])}));
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
@@ -332,6 +332,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
ConfigService,
|
ConfigService,
|
||||||
Fido2AuthenticatorServiceAbstraction,
|
Fido2AuthenticatorServiceAbstraction,
|
||||||
AccountService,
|
AccountService,
|
||||||
|
AuthService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { Injectable, OnDestroy } from "@angular/core";
|
|||||||
import { autofill } from "desktop_native/napi";
|
import { autofill } from "desktop_native/napi";
|
||||||
import {
|
import {
|
||||||
Subject,
|
Subject,
|
||||||
|
combineLatest,
|
||||||
|
debounceTime,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
filter,
|
filter,
|
||||||
firstValueFrom,
|
firstValueFrom,
|
||||||
@@ -12,6 +14,8 @@ import {
|
|||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
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 { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||||
@@ -52,6 +56,7 @@ export class DesktopAutofillService implements OnDestroy {
|
|||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction<NativeWindowObject>,
|
private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction<NativeWindowObject>,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
|
private authService: AuthService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@@ -59,21 +64,39 @@ export class DesktopAutofillService implements OnDestroy {
|
|||||||
.getFeatureFlag$(FeatureFlag.MacOsNativeCredentialSync)
|
.getFeatureFlag$(FeatureFlag.MacOsNativeCredentialSync)
|
||||||
.pipe(
|
.pipe(
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
switchMap((enabled) => {
|
//filter((enabled) => enabled === true), // Only proceed if feature is enabled
|
||||||
return this.accountService.activeAccount$.pipe(
|
switchMap(() => {
|
||||||
map((account) => account?.id),
|
return combineLatest([
|
||||||
filter((userId): userId is UserId => userId != null),
|
this.accountService.activeAccount$.pipe(
|
||||||
switchMap((userId) => this.cipherService.cipherViews$(userId)),
|
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
|
debounceTime(100), // just a precaution to not spam the sync if there are multiple changes (we typically observe a null change)
|
||||||
// when the account locks. We should instead explicilty clear the credentials
|
// No filter for empty arrays here - we want to sync even if there are 0 items
|
||||||
// when the user logs out. Maybe by subscribing to the encrypted ciphers observable instead.
|
filter((cipherViewMap) => cipherViewMap !== null),
|
||||||
|
|
||||||
mergeMap((cipherViewMap) => this.sync(Object.values(cipherViewMap ?? []))),
|
mergeMap((cipherViewMap) => this.sync(Object.values(cipherViewMap ?? []))),
|
||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
)
|
)
|
||||||
.subscribe();
|
.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();
|
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>({
|
const syncResult = await ipc.autofill.runCommand<NativeAutofillSyncCommand>({
|
||||||
namespace: "autofill",
|
namespace: "autofill",
|
||||||
command: "sync",
|
command: "sync",
|
||||||
|
|||||||
Reference in New Issue
Block a user