1
0
mirror of https://github.com/bitwarden/directory-connector synced 2026-02-25 00:52:53 +00:00

Compare commits

...

5 Commits

27 changed files with 7473 additions and 834 deletions

View File

@@ -62,31 +62,25 @@ jobs:
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Update NPM
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Set up system dependencies
run: |
npm install -g node-gyp
node-gyp install "$(node -v)"
- name: Keytar
run: |
keytarVersion=$(cat package.json | jq -r '.dependencies.keytar')
keytarTar="keytar-v$keytarVersion-napi-v3-linux-x64.tar"
keytarTarGz="$keytarTar.gz"
keytarUrl="https://github.com/atom/node-keytar/releases/download/v$keytarVersion/$keytarTarGz"
mkdir -p ./keytar/linux
wget "$keytarUrl" -O "./keytar/linux/$keytarTarGz"
tar -xvf "./keytar/linux/$keytarTarGz" -C ./keytar/linux
sudo apt-get update
sudo apt-get -y install libdbus-1-dev libsecret-1-dev pkg-config
- name: Install
run: npm install
- name: Build native module
run: npm run build:native:release
- name: Package CLI
run: npm run dist:cli:lin
- name: Zip
run: zip -j "dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip" "dist-cli/linux/bwdc" "keytar/linux/build/Release/keytar.node"
run: zip -j "dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip" "dist-cli/linux/bwdc" "node_modules/dc-native/dc_native.linux-x64-gnu.node"
- name: Version Test
run: |
@@ -140,31 +134,20 @@ jobs:
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install "$(node -v)"
- name: Keytar
run: |
keytarVersion=$(cat package.json | jq -r '.dependencies.keytar')
keytarTar="keytar-v$keytarVersion-napi-v3-darwin-x64.tar"
keytarTarGz="$keytarTar.gz"
keytarUrl="https://github.com/atom/node-keytar/releases/download/v$keytarVersion/$keytarTarGz"
mkdir -p ./keytar/macos
wget "$keytarUrl" -O "./keytar/macos/$keytarTarGz"
tar -xvf "./keytar/macos/$keytarTarGz" -C ./keytar/macos
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Install
run: npm install
- name: Build native module
run: npm run build:native:release
- name: Package CLI
run: npm run dist:cli:mac
- name: Zip
run: zip -j "dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip" "dist-cli/macos/bwdc" "keytar/macos/build/Release/keytar.node"
run: zip -j "dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip" "dist-cli/macos/bwdc" "node_modules/dc-native/dc_native.darwin-x64.node"
- name: Version Test
run: |
@@ -215,36 +198,23 @@ jobs:
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install $(node -v)
- name: Keytar
shell: pwsh
run: |
$keytarVersion = (Get-Content -Raw -Path ./package.json | ConvertFrom-Json).dependencies.keytar
$keytarTar = "keytar-v${keytarVersion}-napi-v3-{0}-x64.tar"
$keytarTarGz = "${keytarTar}.gz"
$keytarUrl = "https://github.com/atom/node-keytar/releases/download/v${keytarVersion}/${keytarTarGz}"
New-Item -ItemType directory -Path ./keytar/windows | Out-Null
Invoke-RestMethod -Uri $($keytarUrl -f "win32") -OutFile "./keytar/windows/$($keytarTarGz -f "win32")"
7z e "./keytar/windows/$($keytarTarGz -f "win32")" -o"./keytar/windows"
7z e "./keytar/windows/$($keytarTar -f "win32")" -o"./keytar/windows"
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Install
run: npm install
- name: Build native module
run: npm run build:native:release
- name: Package CLI
run: npm run dist:cli:win
- name: Zip
shell: cmd
run: 7z a .\dist-cli\bwdc-windows-%_PACKAGE_VERSION%.zip .\dist-cli\windows\bwdc.exe .\keytar\windows\keytar.node
run: 7z a .\dist-cli\bwdc-windows-%_PACKAGE_VERSION%.zip .\dist-cli\windows\bwdc.exe .\node_modules\dc-native\dc_native.win32-x64-msvc.node
- name: Version Test
shell: pwsh
@@ -290,10 +260,10 @@ jobs:
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install $(node -v)
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Print environment
run: |
@@ -390,15 +360,13 @@ jobs:
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install "$(node -v)"
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Set up environment
run: |
sudo apt-get update
sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev
sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev libdbus-1-dev
sudo apt-get -y install rpm
- name: NPM Install
@@ -450,10 +418,8 @@ jobs:
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install "$(node -v)"
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Print environment
run: |

4
.gitignore vendored
View File

@@ -32,6 +32,10 @@ build
build-cli
.angular/cache
# Rust build artifacts
native/target
native/*.node
# Testing
coverage*
junit.xml*

2
.nvmrc
View File

@@ -1 +1 @@
v20
v22

View File

@@ -11,6 +11,7 @@
"app": "build"
},
"afterSign": "scripts/notarize.js",
"asarUnpack": ["node_modules/dc-native/*.node"],
"mac": {
"artifactName": "Bitwarden-Connector-${version}-mac.${ext}",
"category": "public.app-category.productivity",

View File

@@ -23,6 +23,7 @@ export default [
"eslint.config.mjs",
"scripts/**/*.js",
"**/node_modules/**",
"native/**",
],
},

View File

@@ -3,5 +3,6 @@ export enum StateVersion {
Two = 2, // Move to a typed State object
Three = 3, // Fix migration of users' premium status
Four = 4, // Fix 'Never Lock' option by removing stale data
Latest = Four,
Five = 5, // Migrate Windows keychain credentials from keytar (UTF-8) to desktop_core (UTF-16)
Latest = Five,
}

View File

@@ -1,43 +1,53 @@
import { ipcRenderer } from "electron";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions";
import { passwords } from "dc-native";
const APPLICATION_NAME = "Bitwarden Directory Connector";
export class ElectronRendererSecureStorageService implements StorageService {
async get<T>(key: string, options?: StorageOptions): Promise<T> {
const val = ipcRenderer.sendSync("keytar", {
action: "getPassword",
key: key,
keySuffix: options?.keySuffix ?? "",
});
return Promise.resolve(val != null ? (JSON.parse(val) as T) : null);
return passwords
.getPassword(this.buildServiceName(options), key)
.then((val: string) => JSON.parse(val) as T)
.catch((e: Error): T => {
if (e.message === passwords.PASSWORD_NOT_FOUND) {
return null;
}
throw e;
});
}
async has(key: string, options?: StorageOptions): Promise<boolean> {
const val = ipcRenderer.sendSync("keytar", {
action: "hasPassword",
key: key,
keySuffix: options?.keySuffix ?? "",
});
return Promise.resolve(!!val);
return (await this.get(key, options)) != null;
}
async save(key: string, obj: any, options?: StorageOptions): Promise<any> {
ipcRenderer.sendSync("keytar", {
action: "setPassword",
key: key,
keySuffix: options?.keySuffix ?? "",
value: JSON.stringify(obj),
});
return Promise.resolve();
if (!obj) {
return this.remove(key, options);
}
return passwords.setPassword(
this.buildServiceName(options),
key,
JSON.stringify(obj),
);
}
async remove(key: string, options?: StorageOptions): Promise<any> {
ipcRenderer.sendSync("keytar", {
action: "deletePassword",
key: key,
keySuffix: options?.keySuffix ?? "",
return passwords.deletePassword(this.buildServiceName(options), key).catch((e: Error) => {
if (e.message === passwords.PASSWORD_NOT_FOUND) {
return;
}
throw e;
});
return Promise.resolve();
}
private buildServiceName(options?: StorageOptions): string {
const suffix = options?.keySuffix;
if (suffix) {
return `${APPLICATION_NAME}_${suffix}`;
}
return APPLICATION_NAME;
}
}

3504
native/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

27
native/Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "dc_native"
version = "0.1.0"
edition = "2021"
description = "Native keychain bindings for Bitwarden Directory Connector"
license = "GPL-3.0"
[lib]
crate-type = ["cdylib"]
name = "dc_native"
[dependencies]
anyhow = "=1.0.100"
desktop_core = { git = "https://github.com/bitwarden/clients", rev = "00cf24972d944638bbd1adc00a0ae3eeabb6eb9a", package = "desktop_core" }
napi = { version = "2", features = ["async"] }
napi-derive = "2"
[target.'cfg(windows)'.dependencies]
scopeguard = "=1.2.0"
widestring = "=1.2.0"
windows = { version = "=0.61.1", features = [
"Win32_Foundation",
"Win32_Security_Credentials",
] }
[build-dependencies]
napi-build = "1"

5
native/build.rs Normal file
View File

@@ -0,0 +1,5 @@
extern crate napi_build;
fn main() {
napi_build::setup();
}

28
native/index.d.ts vendored Normal file
View File

@@ -0,0 +1,28 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
export declare namespace passwords {
/**
* Delete the stored password from the keychain.
* Throws an Error with message PASSWORD_NOT_FOUND if the password does not exist.
*/
export function deletePassword(service: string, account: string): Promise<void>;
/**
* Fetch the stored password from the keychain.
* Throws an Error with message PASSWORD_NOT_FOUND if the password does not exist.
*/
export function getPassword(service: string, account: string): Promise<string>;
/** Check if OS secure storage is available. */
export function isAvailable(): Promise<boolean>;
/**
* Migrate a credential that was stored by keytar (UTF-8 blob) to the new UTF-16 format
* used by desktop_core on Windows. No-ops on non-Windows platforms.
*
* Returns true if a migration was performed, false if the credential was already in the
* correct format or does not exist.
*/
export function migrateKeytarPassword(service: string, account: string): Promise<boolean>;
/** The error message returned when a password is not found during retrieval or deletion. */
export const PASSWORD_NOT_FOUND: string;
/** Save the password to the keychain. Adds an entry if none exists, otherwise updates it. */
export function setPassword(service: string, account: string, password: string): Promise<void>;
}

82
native/index.js Normal file
View File

@@ -0,0 +1,82 @@
const { existsSync } = require("fs");
const { join } = require("path");
const { platform, arch } = process;
let nativeBinding = null;
let loadError = null;
function loadFirstAvailable(localFiles) {
for (const localFile of localFiles) {
const filePath = join(__dirname, localFile);
if (existsSync(filePath)) {
return require(filePath);
}
}
throw new Error(
`Could not find dc-native binary for ${platform}-${arch}. Run 'npm run build:native' to compile it.`,
);
}
switch (platform) {
case "win32":
switch (arch) {
case "x64":
nativeBinding = loadFirstAvailable(["dc_native.win32-x64-msvc.node"]);
break;
case "arm64":
nativeBinding = loadFirstAvailable(["dc_native.win32-arm64-msvc.node"]);
break;
default:
throw new Error(`Unsupported architecture on Windows: ${arch}`);
}
break;
case "darwin":
switch (arch) {
case "x64":
nativeBinding = loadFirstAvailable(["dc_native.darwin-x64.node"]);
break;
case "arm64":
nativeBinding = loadFirstAvailable(["dc_native.darwin-arm64.node"]);
break;
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`);
}
break;
case "linux":
switch (arch) {
case "x64":
nativeBinding = loadFirstAvailable(["dc_native.linux-x64-gnu.node"]);
break;
case "arm64":
nativeBinding = loadFirstAvailable(["dc_native.linux-arm64-gnu.node"]);
break;
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`);
}
break;
default:
throw new Error(`Unsupported platform: ${platform}, architecture: ${arch}`);
}
if (!nativeBinding) {
if (loadError) {
throw loadError;
}
throw new Error(`Failed to load dc-native binding`);
}
// Re-export flat native symbols as the `passwords` namespace so all
// TypeScript callers (and the existing index.d.ts declarations) work unchanged.
module.exports = {
passwords: {
getPassword: (service, account) => nativeBinding.getPassword(service, account),
setPassword: (service, account, password) =>
nativeBinding.setPassword(service, account, password),
deletePassword: (service, account) => nativeBinding.deletePassword(service, account),
isAvailable: () => nativeBinding.isAvailable(),
migrateKeytarPassword: (service, account) =>
nativeBinding.migrateKeytarPassword(service, account),
PASSWORD_NOT_FOUND: "Password not found.",
},
};

1823
native/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
native/package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "dc-native",
"version": "1.0.0",
"description": "Native keychain bindings for Bitwarden Directory Connector",
"main": "index.js",
"types": "index.d.ts",
"license": "GPL-3.0",
"napi": {
"binaryName": "dc_native"
},
"scripts": {
"build": "napi build --platform --no-js",
"build:release": "napi build --platform --release --no-js"
},
"devDependencies": {
"@napi-rs/cli": "^3.0.0"
}
}

64
native/src/lib.rs Normal file
View File

@@ -0,0 +1,64 @@
#[macro_use]
extern crate napi_derive;
/// Fetch the stored password from the keychain.
/// Throws an Error with message PASSWORD_NOT_FOUND if the password does not exist.
#[napi]
pub async fn get_password(service: String, account: String) -> napi::Result<String> {
desktop_core::password::get_password(&service, &account)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Save the password to the keychain. Adds an entry if none exists, otherwise updates it.
#[napi]
pub async fn set_password(
service: String,
account: String,
password: String,
) -> napi::Result<()> {
desktop_core::password::set_password(&service, &account, &password)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Delete the stored password from the keychain.
/// Throws an Error with message PASSWORD_NOT_FOUND if the password does not exist.
#[napi]
pub async fn delete_password(service: String, account: String) -> napi::Result<()> {
desktop_core::password::delete_password(&service, &account)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Check if OS secure storage is available.
#[napi]
pub async fn is_available() -> napi::Result<bool> {
desktop_core::password::is_available()
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Migrate a credential that was stored by keytar (UTF-8 blob) to the new UTF-16 format
/// used by desktop_core on Windows. No-ops on non-Windows platforms.
///
/// Returns true if a migration was performed, false if the credential was already in the
/// correct format or does not exist.
#[napi]
pub async fn migrate_keytar_password(service: String, account: String) -> napi::Result<bool> {
#[cfg(windows)]
{
migration::migrate_keytar_password(&service, &account)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[cfg(not(windows))]
{
let _ = (service, account);
Ok(false)
}
}
#[cfg(windows)]
mod migration;

67
native/src/migration.rs Normal file
View File

@@ -0,0 +1,67 @@
/// Windows-only: migrates credentials stored by keytar (UTF-8 blob via CredWriteA) to the
/// UTF-16 format expected by desktop_core (CredWriteW).
///
/// Keytar used CredWriteA on Windows, which stored the credential blob as raw UTF-8 bytes.
/// desktop_core uses CredWriteW with a UTF-16 encoded blob. Reading old keytar credentials
/// through desktop_core's get_password produces garbled output because the UTF-8 bytes are
/// reinterpreted as UTF-16.
///
/// This function detects the old format by checking whether the raw blob bytes are valid UTF-8
/// without null bytes (UTF-16 LE encoding of ASCII always contains null bytes). If so, it
/// re-saves the credential using desktop_core's set_password (UTF-16 encoding).
use anyhow::{anyhow, Result};
use widestring::U16CString;
use windows::{
core::PCWSTR,
Win32::Security::Credentials::{CredFree, CredReadW, CRED_TYPE_GENERIC},
};
pub async fn migrate_keytar_password(service: &str, account: &str) -> Result<bool> {
let target = format!("{}/{}", service, account);
let target_wide = U16CString::from_str(&target)?;
let mut credential = std::ptr::null_mut();
let result = unsafe {
CredReadW(
PCWSTR(target_wide.as_ptr()),
CRED_TYPE_GENERIC,
None,
&mut credential,
)
};
scopeguard::defer! {{
unsafe { CredFree(credential as *mut _) };
}};
if result.is_err() {
// Credential does not exist; nothing to migrate.
return Ok(false);
}
let blob_bytes: Vec<u8> = unsafe {
let blob_ptr = (*credential).CredentialBlob;
let blob_size = (*credential).CredentialBlobSize as usize;
if blob_ptr.is_null() || blob_size == 0 {
return Ok(false);
}
std::slice::from_raw_parts(blob_ptr, blob_size).to_vec()
};
// UTF-16 LE encoding of ASCII always contains null bytes (e.g. 'A' → 0x41 0x00).
// Keytar stored raw UTF-8 bytes which will never contain null bytes for valid JSON.
// If the blob is valid UTF-8 and contains no null bytes, it was written by keytar.
let blob_is_utf8 = std::str::from_utf8(&blob_bytes)
.map(|s| !s.contains('\0'))
.unwrap_or(false);
if !blob_is_utf8 {
// Already UTF-16 or unrecognised format; no migration needed.
return Ok(false);
}
let utf8_value = String::from_utf8(blob_bytes).map_err(|e| anyhow!(e))?;
desktop_core::password::set_password(service, account, &utf8_value).await?;
Ok(true)
}

2334
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,15 +26,16 @@
"symlink:win": "rm -rf ./jslib && cmd /c mklink /J .\\jslib ..\\jslib",
"symlink:mac": "npm run symlink:lin",
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
"rebuild": "electron-rebuild",
"reset": "rimraf --glob ./node_modules/keytar/* && npm install",
"build:native": "cd native && npm install && npm run build",
"build:native:release": "cd native && npm install && npm run build:release",
"rebuild": "npm run build:native:release",
"lint": "eslint . && prettier --check .",
"lint:fix": "eslint . --fix",
"build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"",
"build:main": "webpack --config webpack.main.cjs",
"build:renderer": "webpack --config webpack.renderer.cjs",
"build:renderer:watch": "webpack --config webpack.renderer.cjs --watch",
"build:dist": "npm run reset && npm run rebuild && npm run build",
"build:dist": "npm run rebuild && npm run build",
"build:cli": "webpack --config webpack.cli.cjs",
"build:cli:watch": "webpack --config webpack.cli.cjs --watch",
"build:cli:prod": "cross-env NODE_ENV=production webpack --config webpack.cli.cjs",
@@ -78,7 +79,6 @@
"@angular/build": "21.1.2",
"@angular/compiler-cli": "21.1.1",
"@electron/notarize": "2.5.0",
"@electron/rebuild": "4.0.1",
"@microsoft/microsoft-graph-types": "2.43.1",
"@ngtools/webpack": "21.1.2",
"@types/inquirer": "8.2.10",
@@ -162,7 +162,7 @@
"googleapis": "149.0.0",
"https-proxy-agent": "7.0.6",
"inquirer": "8.2.6",
"keytar": "7.9.0",
"dc-native": "file:./native",
"ldapts": "8.1.3",
"lowdb": "1.0.0",
"ngx-toastr": "20.0.4",
@@ -175,7 +175,7 @@
"zone.js": "0.16.0"
},
"engines": {
"node": "~20",
"node": "~22",
"npm": "~10"
},
"lint-staged": {

View File

@@ -24,8 +24,8 @@ import { AuthService } from "./services/auth.service";
import { BatchRequestBuilder } from "./services/batch-request-builder";
import { DefaultDirectoryFactoryService } from "./services/directory-factory.service";
import { I18nService } from "./services/i18n.service";
import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service";
import { LowdbStorageService } from "./services/lowdbStorage.service";
import { NativeSecureStorageService } from "./services/nativeSecureStorage.service";
import { SingleRequestBuilder } from "./services/single-request-builder";
import { StateService } from "./services/state.service";
import { StateMigrationService } from "./services/stateMigration.service";
@@ -100,7 +100,7 @@ export class Main {
);
this.secureStorageService = plaintextSecrets
? this.storageService
: new KeytarSecureStorageService(applicationName);
: new NativeSecureStorageService(applicationName);
this.stateMigrationService = new StateMigrationService(
this.storageService,

View File

@@ -1,35 +1,11 @@
import { ipcMain } from "electron";
import { deletePassword, getPassword, setPassword } from "keytar";
// Secure storage is handled directly in the renderer process via dc-native,
// so this listener is no longer needed. Kept as a stub to avoid refactoring
// the main.ts bootstrap sequence.
export class DCCredentialStorageListener {
constructor(private serviceName: string) {}
init() {
ipcMain.on("keytar", async (event: any, message: any) => {
try {
let serviceName = this.serviceName;
message.keySuffix = "_" + (message.keySuffix ?? "");
if (message.keySuffix !== "_") {
serviceName += message.keySuffix;
}
let val: string | boolean = null;
if (message.action && message.key) {
if (message.action === "getPassword") {
val = await getPassword(serviceName, message.key);
} else if (message.action === "hasPassword") {
const result = await getPassword(serviceName, message.key);
val = result != null;
} else if (message.action === "setPassword" && message.value) {
await setPassword(serviceName, message.key, message.value);
} else if (message.action === "deletePassword") {
await deletePassword(serviceName, message.key);
}
}
event.returnValue = val;
} catch {
event.returnValue = null;
}
});
// no-op: renderer calls dc-native directly
}
}

View File

@@ -1,31 +0,0 @@
import { deletePassword, getPassword, setPassword } from "keytar";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
export class KeytarSecureStorageService implements StorageService {
constructor(private serviceName: string) {}
get<T>(key: string): Promise<T> {
return getPassword(this.serviceName, key).then((val) => {
return JSON.parse(val) as T;
});
}
async has(key: string): Promise<boolean> {
return (await this.get(key)) != null;
}
save(key: string, obj: any): Promise<any> {
// keytar throws if you try to save a falsy value: https://github.com/atom/node-keytar/issues/86
// handle this by removing the key instead
if (!obj) {
return this.remove(key);
}
return setPassword(this.serviceName, key, JSON.stringify(obj));
}
remove(key: string): Promise<any> {
return deletePassword(this.serviceName, key);
}
}

View File

@@ -0,0 +1,40 @@
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
import { passwords } from "dc-native";
export class NativeSecureStorageService implements StorageService {
constructor(private serviceName: string) {}
get<T>(key: string): Promise<T> {
return passwords
.getPassword(this.serviceName, key)
.then((val: string) => JSON.parse(val) as T)
.catch((e: Error): T => {
if (e.message === passwords.PASSWORD_NOT_FOUND) {
return null;
}
throw e;
});
}
async has(key: string): Promise<boolean> {
return (await this.get(key)) != null;
}
save(key: string, obj: any): Promise<any> {
if (!obj) {
return this.remove(key);
}
return passwords.setPassword(this.serviceName, key, JSON.stringify(obj));
}
remove(key: string): Promise<any> {
return passwords.deletePassword(this.serviceName, key).catch((e: Error) => {
if (e.message === passwords.PASSWORD_NOT_FOUND) {
return;
}
throw e;
});
}
}

View File

@@ -1,3 +1,5 @@
import { passwords } from "dc-native";
import { StateVersion } from "@/jslib/common/src/enums/stateVersion";
import { StateMigrationService as BaseStateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
@@ -61,6 +63,13 @@ export class StateMigrationService extends BaseStateMigrationService {
break;
case StateVersion.Two:
await this.migrateStateFrom2To3();
break;
case StateVersion.Three:
await this.migrateStateFrom3To4();
break;
case StateVersion.Four:
await this.migrateStateFrom4To5();
break;
}
currentStateVersion += 1;
}
@@ -168,6 +177,50 @@ export class StateMigrationService extends BaseStateMigrationService {
}
}
}
/**
* Migrates Windows credential store entries previously written by keytar (UTF-8 blob) to
* the UTF-16 format expected by desktop_core. No-ops on non-Windows platforms.
*
* This migration is needed because keytar used CredWriteA (storing blobs as raw UTF-8 bytes)
* while desktop_core uses CredWriteW (storing blobs as UTF-16). Reading old keytar credentials
* through desktop_core produces garbled output without this migration.
*/
protected async migrateStateFrom3To4(useSecureStorageForSecrets = true): Promise<void> {
if (useSecureStorageForSecrets && process.platform === "win32") {
const serviceName = "Bitwarden Directory Connector";
const authenticatedUserIds = await this.get<string[]>(StateKeys.authenticatedAccounts);
if (authenticatedUserIds?.length) {
const credentialKeys = [
SecureStorageKeys.ldap,
SecureStorageKeys.gsuite,
SecureStorageKeys.azure,
SecureStorageKeys.entra,
SecureStorageKeys.okta,
SecureStorageKeys.oneLogin,
];
await Promise.all(
authenticatedUserIds.flatMap((userId) =>
credentialKeys.map((key) =>
passwords.migrateKeytarPassword(serviceName, `${userId}_${key}`),
),
),
);
}
}
const globals = await this.getGlobals();
globals.stateVersion = StateVersion.Four;
await this.set(StateKeys.global, globals);
}
protected async migrateStateFrom4To5(): Promise<void> {
const globals = await this.getGlobals();
globals.stateVersion = StateVersion.Five;
await this.set(StateKeys.global, globals);
}
protected async migrateStateFrom2To3(useSecureStorageForSecrets = true): Promise<void> {
if (useSecureStorageForSecrets) {
const authenticatedUserIds = await this.get<string[]>(StateKeys.authenticatedAccounts);

View File

@@ -16,7 +16,8 @@
"baseUrl": ".",
"paths": {
"tldjs": ["@/jslib/src/misc/tldjs.noop"],
"@/*": ["./*"]
"@/*": ["./*"],
"dc-native": ["./native/index.d.ts"]
}
},
"include": ["src", "jslib", "scripts", "./*.ts"]

View File

@@ -20,7 +20,8 @@
"resolveJsonModule": true,
"paths": {
"tldjs": ["./jslib/common/src/misc/tldjs.noop"],
"@/*": ["./*"]
"@/*": ["./*"],
"dc-native": ["./native/index.d.ts"]
},
"useDefineForClassFields": false
},

View File

@@ -58,7 +58,6 @@ const main = {
],
externals: {
"electron-reload": "commonjs2 electron-reload",
keytar: "commonjs2 keytar",
},
};

View File

@@ -55,6 +55,9 @@ const renderer = {
node: {
__dirname: false,
},
externals: {
"dc-native": "commonjs2 dc-native",
},
entry: {
"app/main": "./src/app/main.ts",
},