mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 17:23:37 +00:00
[PM-9035] desktop build logic to provide credentials to os on sync (#10181)
* feat: scaffold desktop_objc * feat: rename fido2 to autofill * feat: scaffold electron autofill * feat: auto call hello world on init * feat: scaffold call to basic objc function * feat: simple log that checks if autofill is enabled * feat: adding some availability guards * feat: scaffold services and allow calls from inspector * feat: create custom type for returning strings across rust/objc boundary * chore: clean up comments * feat: enable ARC * feat: add util function `c_string_to_nsstring` * chore: refactor and rename to `run_command` * feat: add try-catch around command execution * feat: properly implement command calling Add static typing. Add proper error handling. * feat: add autoreleasepool to avoid memory leaks * chore: change objc names to camelCase * fix: error returning * feat: extract some helper functions into utils class * feat: scaffold status command * feat: implement status command * feat: implement password credential mapping * wip: implement sync command This crashes because we are not properly handling the fact that `saveCredentialIdentities` uses callbacks, resulting in a race condition where we try to access a variable (result) that has already gotten dealloc'd. * feat: first version of callback * feat: make run_command async * feat: functioning callback returns * chore: refactor to make objc code easier to read and use * feat: refactor everything to use new callback return method * feat: re-implement status command with callback * fix: warning about CommandContext not being FFI-safe * feat: implement sync command using callbacks * feat: implement manual password credential sync * feat: add auto syncing * docs: add todo * feat: add support for passkeys * chore: move desktop autofill service to init service * feat: auto-add all .m files to builder * fix: native build on unix and windows * fix: unused compiler warnings * fix: napi type exports * feat: add corresponding dist command * feat: comment signing profile until we fix signing * fix: build breaking on non-macOS platforms * chore: cargo lock update * chore: revert accidental version change * feat: put sync behind feature flag * chore: put files in autofill folder * fix: obj-c code not recompiling on changes * feat: add `namespace` to commands * fix: linting complaining about flag * feat: add autofill as owner of their objc code * chore: make autofill owner of run_command in core crate * fix: re-add napi annotation * fix: remove dev bypass
This commit is contained in:
@@ -20,6 +20,7 @@ import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/va
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService as KeyServiceAbstraction } from "@bitwarden/key-management";
|
||||
|
||||
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service";
|
||||
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
||||
import { SshAgentService } from "../../platform/services/ssh-agent.service";
|
||||
import { VersionService } from "../../platform/services/version.service";
|
||||
@@ -45,6 +46,7 @@ export class InitService {
|
||||
private accountService: AccountService,
|
||||
private versionService: VersionService,
|
||||
private sshAgentService: SshAgentService,
|
||||
private autofillService: DesktopAutofillService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
@@ -82,6 +84,8 @@ export class InitService {
|
||||
|
||||
const containerService = new ContainerService(this.keyService, this.encryptService);
|
||||
containerService.attachToGlobal(this.win);
|
||||
|
||||
await this.autofillService.init();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
@@ -91,6 +92,7 @@ import {
|
||||
import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service";
|
||||
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service";
|
||||
import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service";
|
||||
import { flagEnabled } from "../../platform/flags";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
@@ -301,6 +303,10 @@ const safeProviders: SafeProvider[] = [
|
||||
provide: DesktopAutofillSettingsService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DesktopAutofillService,
|
||||
deps: [LogService, CipherServiceAbstraction, ConfigService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: NativeMessagingManifestService,
|
||||
useClass: NativeMessagingManifestService,
|
||||
|
||||
9
apps/desktop/src/autofill/preload.ts
Normal file
9
apps/desktop/src/autofill/preload.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
import { Command } from "../platform/main/autofill/command";
|
||||
import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main";
|
||||
|
||||
export default {
|
||||
runCommand: <C extends Command>(params: RunCommandParams<C>): Promise<RunCommandResult<C>> =>
|
||||
ipcRenderer.invoke("autofill.runCommand", params),
|
||||
};
|
||||
121
apps/desktop/src/autofill/services/desktop-autofill.service.ts
Normal file
121
apps/desktop/src/autofill/services/desktop-autofill.service.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Injectable, OnDestroy } from "@angular/core";
|
||||
import { EMPTY, Subject, distinctUntilChanged, mergeMap, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { getCredentialsForAutofill } from "@bitwarden/common/platform/services/fido2/fido2-autofill-utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { NativeAutofillStatusCommand } from "../../platform/main/autofill/status.command";
|
||||
import {
|
||||
NativeAutofillFido2Credential,
|
||||
NativeAutofillPasswordCredential,
|
||||
NativeAutofillSyncCommand,
|
||||
} from "../../platform/main/autofill/sync.command";
|
||||
|
||||
@Injectable()
|
||||
export class DesktopAutofillService implements OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private cipherService: CipherService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.MacOsNativeCredentialSync)
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((enabled) => {
|
||||
if (!enabled) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return this.cipherService.cipherViews$;
|
||||
}),
|
||||
// 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.
|
||||
mergeMap((cipherViewMap) => this.sync(Object.values(cipherViewMap ?? []))),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
/** Give metadata about all available credentials in the users vault */
|
||||
async sync(cipherViews: CipherView[]) {
|
||||
const status = await this.status();
|
||||
if (status.type === "error") {
|
||||
return this.logService.error("Error getting autofill status", status.error);
|
||||
}
|
||||
|
||||
if (!status.value.state.enabled) {
|
||||
// Autofill is disabled
|
||||
return;
|
||||
}
|
||||
|
||||
let fido2Credentials: NativeAutofillFido2Credential[];
|
||||
let passwordCredentials: NativeAutofillPasswordCredential[];
|
||||
|
||||
if (status.value.support.password) {
|
||||
passwordCredentials = cipherViews
|
||||
.filter(
|
||||
(cipher) =>
|
||||
cipher.type === CipherType.Login &&
|
||||
cipher.login.uris?.length > 0 &&
|
||||
cipher.login.uris.some((uri) => uri.match !== UriMatchStrategy.Never) &&
|
||||
cipher.login.uris.some((uri) => !Utils.isNullOrWhitespace(uri.uri)) &&
|
||||
!Utils.isNullOrWhitespace(cipher.login.username),
|
||||
)
|
||||
.map((cipher) => ({
|
||||
type: "password",
|
||||
cipherId: cipher.id,
|
||||
uri: cipher.login.uris.find((uri) => uri.match !== UriMatchStrategy.Never).uri,
|
||||
username: cipher.login.username,
|
||||
}));
|
||||
}
|
||||
|
||||
if (status.value.support.fido2) {
|
||||
fido2Credentials = (await getCredentialsForAutofill(cipherViews)).map((credential) => ({
|
||||
type: "fido2",
|
||||
...credential,
|
||||
}));
|
||||
}
|
||||
|
||||
const syncResult = await ipc.autofill.runCommand<NativeAutofillSyncCommand>({
|
||||
namespace: "autofill",
|
||||
command: "sync",
|
||||
params: {
|
||||
credentials: [...fido2Credentials, ...passwordCredentials],
|
||||
},
|
||||
});
|
||||
|
||||
if (syncResult.type === "error") {
|
||||
return this.logService.error("Error syncing autofill credentials", syncResult.error);
|
||||
}
|
||||
|
||||
this.logService.debug(`Synced ${syncResult.value.added} autofill credentials`);
|
||||
}
|
||||
|
||||
/** Get autofill status from OS */
|
||||
private status() {
|
||||
// TODO: Investigate why this type needs to be explicitly set
|
||||
return ipc.autofill.runCommand<NativeAutofillStatusCommand>({
|
||||
namespace: "autofill",
|
||||
command: "status",
|
||||
params: {},
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import { PowerMonitorMain } from "./main/power-monitor.main";
|
||||
import { TrayMain } from "./main/tray.main";
|
||||
import { UpdaterMain } from "./main/updater.main";
|
||||
import { WindowMain } from "./main/window.main";
|
||||
import { NativeAutofillMain } from "./platform/main/autofill/native-autofill.main";
|
||||
import { ClipboardMain } from "./platform/main/clipboard.main";
|
||||
import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener";
|
||||
import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service";
|
||||
@@ -72,6 +73,7 @@ export class Main {
|
||||
biometricsService: DesktopBiometricsService;
|
||||
nativeMessagingMain: NativeMessagingMain;
|
||||
clipboardMain: ClipboardMain;
|
||||
nativeAutofillMain: NativeAutofillMain;
|
||||
desktopAutofillSettingsService: DesktopAutofillSettingsService;
|
||||
versionMain: VersionMain;
|
||||
sshAgentService: MainSshAgentService;
|
||||
@@ -256,6 +258,9 @@ export class Main {
|
||||
|
||||
new EphemeralValueStorageService();
|
||||
new SSOLocalhostCallbackService(this.environmentService, this.messagingService);
|
||||
|
||||
this.nativeAutofillMain = new NativeAutofillMain(this.logService);
|
||||
void this.nativeAutofillMain.init();
|
||||
}
|
||||
|
||||
bootstrap() {
|
||||
|
||||
23
apps/desktop/src/platform/main/autofill/command.ts
Normal file
23
apps/desktop/src/platform/main/autofill/command.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NativeAutofillStatusCommand } from "./status.command";
|
||||
import { NativeAutofillSyncCommand } from "./sync.command";
|
||||
|
||||
export type CommandDefinition = {
|
||||
namespace: string;
|
||||
name: string;
|
||||
input: Record<string, unknown>;
|
||||
output: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type CommandOutput<SuccessOutput> =
|
||||
| {
|
||||
type: "error";
|
||||
error: string;
|
||||
}
|
||||
| { type: "success"; value: SuccessOutput };
|
||||
|
||||
export type IpcCommandInvoker<C extends CommandDefinition> = (
|
||||
params: C["input"],
|
||||
) => Promise<CommandOutput<C["output"]>>;
|
||||
|
||||
/** A list of all available commands */
|
||||
export type Command = NativeAutofillSyncCommand | NativeAutofillStatusCommand;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { autofill } from "@bitwarden/desktop-napi";
|
||||
|
||||
import { CommandDefinition } from "./command";
|
||||
|
||||
export type RunCommandParams<C extends CommandDefinition> = {
|
||||
namespace: C["namespace"];
|
||||
command: C["name"];
|
||||
params: C["input"];
|
||||
};
|
||||
|
||||
export type RunCommandResult<C extends CommandDefinition> = C["output"];
|
||||
|
||||
export class NativeAutofillMain {
|
||||
constructor(private logService: LogService) {}
|
||||
|
||||
async init() {
|
||||
ipcMain.handle(
|
||||
"autofill.runCommand",
|
||||
<C extends CommandDefinition>(
|
||||
_event: any,
|
||||
params: RunCommandParams<C>,
|
||||
): Promise<RunCommandResult<C>> => {
|
||||
return this.runCommand(params);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async runCommand<C extends CommandDefinition>(
|
||||
command: RunCommandParams<C>,
|
||||
): Promise<RunCommandResult<C>> {
|
||||
try {
|
||||
const result = await autofill.runCommand(JSON.stringify(command));
|
||||
const parsed = JSON.parse(result) as RunCommandResult<C>;
|
||||
|
||||
if (parsed.type === "error") {
|
||||
this.logService.error(`Error running autofill command '${command.command}':`, parsed.error);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
this.logService.error(`Error running autofill command '${command.command}':`, e);
|
||||
|
||||
if (e instanceof Error) {
|
||||
return { type: "error", error: e.stack ?? String(e) } as RunCommandResult<C>;
|
||||
}
|
||||
|
||||
return { type: "error", error: String(e) } as RunCommandResult<C>;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
apps/desktop/src/platform/main/autofill/status.command.ts
Normal file
20
apps/desktop/src/platform/main/autofill/status.command.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { CommandDefinition, CommandOutput } from "./command";
|
||||
|
||||
export interface NativeAutofillStatusCommand extends CommandDefinition {
|
||||
name: "status";
|
||||
input: NativeAutofillStatusParams;
|
||||
output: NativeAutofillStatusResult;
|
||||
}
|
||||
|
||||
export type NativeAutofillStatusParams = Record<string, never>;
|
||||
|
||||
export type NativeAutofillStatusResult = CommandOutput<{
|
||||
support: {
|
||||
fido2: boolean;
|
||||
password: boolean;
|
||||
incrementalUpdates: boolean;
|
||||
};
|
||||
state: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}>;
|
||||
37
apps/desktop/src/platform/main/autofill/sync.command.ts
Normal file
37
apps/desktop/src/platform/main/autofill/sync.command.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { CommandDefinition, CommandOutput } from "./command";
|
||||
|
||||
export interface NativeAutofillSyncCommand extends CommandDefinition {
|
||||
name: "sync";
|
||||
input: NativeAutofillSyncParams;
|
||||
output: NativeAutofillSyncResult;
|
||||
}
|
||||
|
||||
export type NativeAutofillSyncParams = {
|
||||
credentials: NativeAutofillCredential[];
|
||||
};
|
||||
|
||||
export type NativeAutofillCredential =
|
||||
| NativeAutofillFido2Credential
|
||||
| NativeAutofillPasswordCredential;
|
||||
|
||||
export type NativeAutofillFido2Credential = {
|
||||
type: "fido2";
|
||||
cipherId: string;
|
||||
rpId: string;
|
||||
userName: string;
|
||||
/** Should be Base64URL-encoded binary data */
|
||||
credentialId: string;
|
||||
/** Should be Base64URL-encoded binary data */
|
||||
userHandle: string;
|
||||
};
|
||||
|
||||
export type NativeAutofillPasswordCredential = {
|
||||
type: "password";
|
||||
cipherId: string;
|
||||
uri: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type NativeAutofillSyncResult = CommandOutput<{
|
||||
added: number;
|
||||
}>;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { contextBridge } from "electron";
|
||||
|
||||
import auth from "./auth/preload";
|
||||
import autofill from "./autofill/preload";
|
||||
import keyManagement from "./key-management/preload";
|
||||
import platform from "./platform/preload";
|
||||
|
||||
@@ -17,6 +18,7 @@ import platform from "./platform/preload";
|
||||
// Each team owns a subspace of the `ipc` global variable in the renderer.
|
||||
export const ipc = {
|
||||
auth,
|
||||
autofill,
|
||||
platform,
|
||||
keyManagement,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user