mirror of
https://github.com/bitwarden/directory-connector
synced 2026-02-24 16:43:06 +00:00
Compare commits
6 Commits
renovate/n
...
ac/pm-1243
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3abd3f0496 | ||
|
|
af430157e0 | ||
|
|
db3e7aa685 | ||
|
|
9a2168c1d7 | ||
|
|
1fd8bf318f | ||
|
|
c472d5e199 |
100
.github/workflows/build.yml
vendored
100
.github/workflows/build.yml
vendored
@@ -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: |
|
||||
|
||||
46
.github/workflows/lint.yml
vendored
Normal file
46
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
lint:
|
||||
name: Run linter
|
||||
if: ${{ startsWith(github.head_ref, 'version_bump_') == false }}
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Get Node version
|
||||
id: retrieve-node-version
|
||||
run: |
|
||||
NODE_NVMRC=$(cat .nvmrc)
|
||||
NODE_VERSION=${NODE_NVMRC/v/''}
|
||||
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run ESLint and Prettier
|
||||
run: npm run lint
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -32,6 +32,10 @@ build
|
||||
build-cli
|
||||
.angular/cache
|
||||
|
||||
# Rust build artifacts
|
||||
native/target
|
||||
native/*.node
|
||||
|
||||
# Testing
|
||||
coverage*
|
||||
junit.xml*
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -23,6 +23,7 @@ export default [
|
||||
"eslint.config.mjs",
|
||||
"scripts/**/*.js",
|
||||
"**/node_modules/**",
|
||||
"native/**",
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions"
|
||||
|
||||
export class ElectronRendererSecureStorageService implements StorageService {
|
||||
async get<T>(key: string, options?: StorageOptions): Promise<T> {
|
||||
const val = ipcRenderer.sendSync("keytar", {
|
||||
const val = ipcRenderer.sendSync("nativeSecureStorage", {
|
||||
action: "getPassword",
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? "",
|
||||
@@ -14,7 +14,7 @@ export class ElectronRendererSecureStorageService implements StorageService {
|
||||
}
|
||||
|
||||
async has(key: string, options?: StorageOptions): Promise<boolean> {
|
||||
const val = ipcRenderer.sendSync("keytar", {
|
||||
const val = ipcRenderer.sendSync("nativeSecureStorage", {
|
||||
action: "hasPassword",
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? "",
|
||||
@@ -23,7 +23,7 @@ export class ElectronRendererSecureStorageService implements StorageService {
|
||||
}
|
||||
|
||||
async save(key: string, obj: any, options?: StorageOptions): Promise<any> {
|
||||
ipcRenderer.sendSync("keytar", {
|
||||
ipcRenderer.sendSync("nativeSecureStorage", {
|
||||
action: "setPassword",
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? "",
|
||||
@@ -33,7 +33,7 @@ export class ElectronRendererSecureStorageService implements StorageService {
|
||||
}
|
||||
|
||||
async remove(key: string, options?: StorageOptions): Promise<any> {
|
||||
ipcRenderer.sendSync("keytar", {
|
||||
ipcRenderer.sendSync("nativeSecureStorage", {
|
||||
action: "deletePassword",
|
||||
key: key,
|
||||
keySuffix: options?.keySuffix ?? "",
|
||||
|
||||
3498
native/Cargo.lock
generated
Normal file
3498
native/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
native/Cargo.toml
Normal file
27
native/Cargo.toml
Normal 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 = "=3.3.0", features = ["async"] }
|
||||
napi-derive = "=3.2.5"
|
||||
|
||||
[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 = "=2.2.3"
|
||||
5
native/build.rs
Normal file
5
native/build.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
extern crate napi_build;
|
||||
|
||||
fn main() {
|
||||
napi_build::setup();
|
||||
}
|
||||
34
native/index.d.ts
vendored
Normal file
34
native/index.d.ts
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
export declare namespace passwords {
|
||||
/** The error message returned when a password is not found during retrieval or deletion. */
|
||||
export const PASSWORD_NOT_FOUND: string;
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
|
||||
/**
|
||||
* Check if OS secure storage is available.
|
||||
*/
|
||||
export function isAvailable(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Migrate a credential previously stored by keytar (UTF-8 blob on Windows) to the UTF-16
|
||||
* format used by desktop_core. No-ops on non-Windows platforms.
|
||||
*
|
||||
* Returns true if a migration was performed, false otherwise.
|
||||
*/
|
||||
export function migrateKeytarPassword(service: string, account: string): Promise<boolean>;
|
||||
}
|
||||
67
native/index.js
Normal file
67
native/index.js
Normal file
@@ -0,0 +1,67 @@
|
||||
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. 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`);
|
||||
}
|
||||
|
||||
module.exports = nativeBinding;
|
||||
15
native/package.json
Normal file
15
native/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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",
|
||||
"scripts": {
|
||||
"build": "napi build --platform",
|
||||
"build:release": "napi build --platform --release"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^3.0.0"
|
||||
}
|
||||
}
|
||||
70
native/src/lib.rs
Normal file
70
native/src/lib.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
#[macro_use]
|
||||
extern crate napi_derive;
|
||||
|
||||
#[napi]
|
||||
pub mod passwords {
|
||||
/// The error message returned when a password is not found during retrieval or deletion.
|
||||
#[napi]
|
||||
pub const PASSWORD_NOT_FOUND: &str = desktop_core::password::PASSWORD_NOT_FOUND;
|
||||
|
||||
/// 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)]
|
||||
{
|
||||
crate::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
67
native/src/migration.rs
Normal 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)
|
||||
}
|
||||
2401
package-lock.json
generated
2401
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -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",
|
||||
@@ -94,7 +94,6 @@
|
||||
"@typescript-eslint/parser": "8.54.0",
|
||||
"@yao-pkg/pkg": "5.16.1",
|
||||
"babel-loader": "10.0.0",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"jest-environment-jsdom": "30.2.0",
|
||||
"concurrently": "9.2.0",
|
||||
"copy-webpack-plugin": "13.0.0",
|
||||
@@ -114,7 +113,7 @@
|
||||
"eslint-plugin-rxjs-angular-x": "0.1.0",
|
||||
"eslint-plugin-rxjs-x": "0.9.1",
|
||||
"form-data": "4.0.4",
|
||||
"glob": "13.0.0",
|
||||
"glob": "13.0.6",
|
||||
"html-loader": "5.1.0",
|
||||
"html-webpack-plugin": "5.6.3",
|
||||
"husky": "9.1.7",
|
||||
@@ -124,7 +123,6 @@
|
||||
"jest-preset-angular": "16.0.0",
|
||||
"lint-staged": "16.2.6",
|
||||
"mini-css-extract-plugin": "2.10.0",
|
||||
"minimatch": "5.1.2",
|
||||
"node-forge": "1.3.2",
|
||||
"node-loader": "2.1.0",
|
||||
"prettier": "3.8.1",
|
||||
@@ -137,7 +135,7 @@
|
||||
"tsconfig-paths-webpack-plugin": "4.2.0",
|
||||
"type-fest": "5.4.2",
|
||||
"typescript": "5.9.3",
|
||||
"webpack": "5.104.1",
|
||||
"webpack": "5.105.1",
|
||||
"webpack-cli": "6.0.1",
|
||||
"webpack-merge": "6.0.1",
|
||||
"webpack-node-externals": "3.0.0",
|
||||
@@ -164,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",
|
||||
@@ -177,7 +175,7 @@
|
||||
"zone.js": "0.16.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "~20",
|
||||
"node": "~22",
|
||||
"npm": "~10"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { passwords } from "dc-native";
|
||||
import { ipcMain } from "electron";
|
||||
import { deletePassword, getPassword, setPassword } from "keytar";
|
||||
|
||||
export class DCCredentialStorageListener {
|
||||
constructor(private serviceName: string) {}
|
||||
|
||||
init() {
|
||||
ipcMain.on("keytar", async (event: any, message: any) => {
|
||||
ipcMain.on("nativeSecureStorage", async (event: any, message: any) => {
|
||||
try {
|
||||
let serviceName = this.serviceName;
|
||||
message.keySuffix = "_" + (message.keySuffix ?? "");
|
||||
@@ -16,14 +16,14 @@ export class DCCredentialStorageListener {
|
||||
let val: string | boolean = null;
|
||||
if (message.action && message.key) {
|
||||
if (message.action === "getPassword") {
|
||||
val = await getPassword(serviceName, message.key);
|
||||
val = await passwords.getPassword(serviceName, message.key);
|
||||
} else if (message.action === "hasPassword") {
|
||||
const result = await getPassword(serviceName, message.key);
|
||||
const result = await passwords.getPassword(serviceName, message.key);
|
||||
val = result != null;
|
||||
} else if (message.action === "setPassword" && message.value) {
|
||||
await setPassword(serviceName, message.key, message.value);
|
||||
await passwords.setPassword(serviceName, message.key, message.value);
|
||||
} else if (message.action === "deletePassword") {
|
||||
await deletePassword(serviceName, message.key);
|
||||
await passwords.deletePassword(serviceName, message.key);
|
||||
}
|
||||
}
|
||||
event.returnValue = val;
|
||||
|
||||
@@ -68,10 +68,12 @@ export class LdapDirectoryService implements IDirectoryService {
|
||||
}
|
||||
groups = await this.getGroups(groupForce);
|
||||
}
|
||||
} finally {
|
||||
} catch (e) {
|
||||
await this.client.unbind();
|
||||
throw e;
|
||||
}
|
||||
|
||||
await this.client.unbind();
|
||||
return [groups, users];
|
||||
}
|
||||
|
||||
@@ -453,8 +455,9 @@ export class LdapDirectoryService implements IDirectoryService {
|
||||
|
||||
try {
|
||||
await this.client.bind(user, pass);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
await this.client.unbind();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
28
src/services/nativeSecureStorage.service.ts
Normal file
28
src/services/nativeSecureStorage.service.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { passwords } from "dc-native";
|
||||
|
||||
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
|
||||
|
||||
export class NativeSecureStorageService implements StorageService {
|
||||
constructor(private serviceName: string) {}
|
||||
|
||||
get<T>(key: string): Promise<T> {
|
||||
return passwords.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> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const path = require("path");
|
||||
|
||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
const webpack = require("webpack");
|
||||
@@ -24,7 +23,6 @@ const moduleRules = [
|
||||
];
|
||||
|
||||
const plugins = [
|
||||
new CleanWebpackPlugin(),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [{ from: "./src/locales", to: "locales" }],
|
||||
}),
|
||||
@@ -64,6 +62,7 @@ const config = {
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
path: path.resolve(__dirname, "build-cli"),
|
||||
clean: true,
|
||||
},
|
||||
module: { rules: moduleRules },
|
||||
plugins: plugins,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const path = require("path");
|
||||
const { merge } = require("webpack-merge");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
const nodeExternals = require("webpack-node-externals");
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
|
||||
@@ -23,6 +22,7 @@ const common = {
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
path: path.resolve(__dirname, "build"),
|
||||
clean: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -48,7 +48,6 @@ const main = {
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
"./package.json",
|
||||
@@ -59,7 +58,7 @@ const main = {
|
||||
],
|
||||
externals: {
|
||||
"electron-reload": "commonjs2 electron-reload",
|
||||
keytar: "commonjs2 keytar",
|
||||
"dc-native": "commonjs2 dc-native",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ const renderer = {
|
||||
node: {
|
||||
__dirname: false,
|
||||
},
|
||||
externals: {
|
||||
"dc-native": "commonjs2 dc-native",
|
||||
},
|
||||
entry: {
|
||||
"app/main": "./src/app/main.ts",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user